.. SPDX-License-Identifier: GPL-2.0 .. Copyright © 2026 Cloudflare, Inc. ===================== Landlock Trace Events ===================== :Author: Mickaël Salaün :Date: August 2026 Landlock emits trace events for sandbox lifecycle operations and access denials. These events can be consumed by ftrace (for human-readable trace output and filtering) and by eBPF programs (for programmatic introspection via BTF). User space documentation can be found here: Documentation/userspace-api/landlock.rst .. warning:: Landlock trace events, like audit records, expose sensitive information about all sandboxed processes on the system. See :ref:`landlock_observability_security` for security considerations and privilege requirements. Event overview ============== Landlock trace events are organized in four categories: **Syscall events** are emitted during Landlock system calls: - ``landlock_create_ruleset``: a new ruleset is created - ``landlock_add_rule_fs``: a filesystem rule is added to a ruleset - ``landlock_add_rule_net``: a network port rule is added to a ruleset - ``landlock_create_domain``: a new domain is created from a ruleset - ``landlock_enforce_domain``: a domain is enforced on a thread **Denial events** are emitted when an access is denied: - ``landlock_deny_access_fs``: filesystem access denied - ``landlock_deny_access_net``: network access denied - ``landlock_deny_ptrace``: ptrace access denied - ``landlock_deny_scope_signal``: signal delivery denied - ``landlock_deny_scope_abstract_unix_socket``: abstract unix socket access denied **Rule evaluation events** are emitted during rule matching: - ``landlock_check_rule_fs``: a filesystem rule is evaluated - ``landlock_check_rule_net``: a network port rule is evaluated **Lifecycle events**: - ``landlock_free_domain``: a domain is freed - ``landlock_free_ruleset``: a ruleset is freed Enabling events =============== Enable all Landlock events:: echo 1 > /sys/kernel/tracing/events/landlock/enable Enable a specific event:: echo 1 > /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/enable Read the trace output:: cat /sys/kernel/tracing/trace_pipe Event samples ============= A fully unprivileged program is sandboxed so that it can still run (its binary and shared libraries stay readable) and write only ``/tmp``, then it is denied reading ``/etc/passwd``, which lies outside its read-only set. ``/etc/passwd`` is world-readable, so the denial comes solely from Landlock, not from regular file permissions:: $ cd /sys/kernel/tracing/events/landlock/ $ echo 1 | tee landlock_{create_ruleset,create_domain,enforce_domain,deny_access_fs,free_domain}/enable >/dev/null $ LC_ALL=C LL_FS_RO=/usr:/lib:/lib64:/bin:/etc/ld.so.cache LL_FS_RW=/tmp \ ./sandboxer cat /etc/passwd $ cat /sys/kernel/tracing/trace_pipe cat-127 [...] landlock_create_ruleset: ruleset=195cc6b76.0 handled_fs=execute|write_file|read_file|read_dir|remove_dir|remove_file|make_char|make_dir|make_reg|make_sock|make_fifo|make_block|make_sym|refer|truncate|ioctl_dev|resolve_unix handled_net= scoped= cat-127 [...] landlock_create_domain: domain=195cc6b7c parent=0 ruleset=195cc6b76.6 cat-127 [...] landlock_enforce_domain: domain=195cc6b7c complete=1 process_wide=1 no_new_privs=1 cat-127 [...] landlock_deny_access_fs: domain=195cc6b7c same_exec=0 logged=0 blockers=read_file dev=0:17 ino=5901179 path=/etc/passwd kworker/0:1-11 [...] landlock_free_domain: domain=195cc6b7c denials=1 The ``[...]`` replaces the ftrace CPU, flags, and timestamp columns. The first four events share the ``cat`` command name and PID because the sandboxer replaces itself with ``cat`` via ``execve()`` before the denial, and ftrace resolves a recorded PID to its latest command name. ``landlock_free_domain`` fires later from a kworker thread, so it carries that thread's name instead. Here ``logged=0`` shows that audit would not record this cross-execution denial under the default flags, yet the ``deny_access_fs`` event still appears. Differences from audit records ============================== Tracepoints and audit records both log Landlock denials, but differ in some field formats: - **Paths**: Most filesystem tracepoints resolve the path with ``d_absolute_path()`` (namespace-independent absolute paths), while mount-topology denials that carry only a dentry use ``dentry_path_raw()``. Audit uses ``d_path()`` (relative to the process's chroot). A resolution failure is reported as ````, ````, or ````. Path-based tracepoint output is deterministic regardless of the tracer's mount namespace. - **Device names**: Tracepoints use numeric ``dev=:``. Audit uses string ``dev=""``. Numeric format is more precise for machine parsing. - **Denied access field**: The ``deny_access_fs`` and ``deny_access_net`` tracepoints use the ``blockers=`` field name (same as audit). Both render the blocked access rights as names: audit prefixes the category and separates with commas (e.g., ``blockers=fs.read_file``), while the tracepoints omit the category (carried by the event name) and separate with ``|`` (e.g., ``blockers=read_file``). Scope and ptrace tracepoints omit ``blockers`` because the event name identifies the denial type. - **Scope and ptrace target names**: Tracepoints use role-specific field names (``tracee_pid``, ``target_pid``, ``peer_pid``) that reflect the semantic of each event. Audit uses generic names (``opid``, ``ocomm``) because the audit log format is not event-type-specific. - **Process name**: The ptrace and signal denial tracepoints include the role-prefixed ``tracee_comm=`` and ``target_comm=`` labels in the printk output for stateless consumers (each matches its sibling ``tracee_pid=``/``target_pid=`` field). eBPF consumers can read ``comm`` directly from the task_struct via BTF. The ``comm`` value is treated as untrusted input and escaped in the trace text output so it cannot inject field separators or control characters. - **Other party's domain**: A scope or ptrace denial compares the subject's denying domain (``domain=``, always the enforcing domain and never the current task) with the other party's domain, so these tracepoints also report the other party's domain as a scalar ID: ``tracee_domain=`` (ptrace), ``target_domain=`` (signal), and ``peer_domain=`` (abstract unix socket). It is ``0`` when the other party is unsandboxed, and otherwise a domain ID that a consumer resolves against the ``landlock_create_ruleset`` and ``landlock_create_domain`` events it recorded. Because a scope or ptrace verdict is decided by comparing the two domains, resolving both the subject ``domain=`` and this other-party ID against those lifecycle events lets a consumer verify or reproduce the verdict by redoing the same two-domain comparison, rather than only noting which boundary was crossed. Audit records do not carry the other party's domain. Ruleset versioning ================== Syscall events include a ruleset version (``ruleset=.``) that tracks the number of rules added to the ruleset. The version is incremented on each ``landlock_add_rule()`` call and frozen at ``landlock_restrict_self()`` time. This enables trace consumers to correlate a domain with the exact set of rules it was created from. Domain enforcement ================== The whole-process-enforced guarantee (``complete=1 && process_wide=1``) is the observable outcome of a successful ``landlock_restrict_self(..., LANDLOCK_RESTRICT_SELF_TSYNC)``; see the thread synchronization section of Documentation/userspace-api/landlock.rst. The Landlock events and the generic syscall tracepoints are complementary: the Landlock events expose the *semantic effect* of an operation (the domain, its scope, the resulting ``no_new_privs`` state), while ``raw_syscalls:sys_enter``/``sys_exit`` (or the per-syscall ``syscalls:sys_{enter,exit}_landlock_*`` under ``CONFIG_FTRACE_SYSCALLS``) expose the *raw API* -- the exact ``landlock_restrict_self()`` flags, arguments, and return value. Correlate them by thread; a ``LANDLOCK_RESTRICT_SELF_TSYNC`` operation also enforces the domain on the sibling threads, whose ``landlock_enforce_domain`` events fire in each sibling's own context rather than the caller's, so correlate those to the syscall by domain ID. Interpreting check_rule events ============================== The ``check_rule_fs`` and ``check_rule_net`` events expose the per-layer rule evaluation, which is useful for understanding *why* a specific access is allowed or denied. .. warning:: These events fire on the access-check hot path, once per matching rule per check. On a busy sandboxed workload this can be very high frequency. Enable them only for targeted debugging, ideally combined with an ftrace filter (for example on ``ino`` or ``domain_id``), and expect tracing overhead while they are enabled. Two output fields carry the evaluation: - ``access_request=`` is the set of access rights being evaluated against the rule, rendered as ``|``-separated names. For most checks this is the access the operation requested. For filesystem ``rename`` and ``link`` double-checks it is the domain's full handled mask, because those operations re-evaluate every handled right. - ``grants=`` is a per-layer breakdown of the requested rights that this rule grants, in the form ``{,,...}``: - The braces wrap one comma-separated group per domain layer, ordered from the outermost (least nested) sandbox layer to the innermost. - Each group lists the requested rights the rule grants at that layer, joined by ``|``. - An empty group (for example the middle layer in ``{read_file,,read_file}``) means the rule grants none of the requested rights at that layer. A Landlock domain allows an access only when, for every requested right, every layer that handles that right has at least one matching rule granting it. A single ``check_rule`` event therefore shows one rule's contribution, not the final decision: - If a right appears in every layer's group, this rule alone is sufficient to allow that right. - If a right is missing from some layer's group, that layer must grant it through another matching rule, or the right is denied and appears in the ``blockers=`` field of the corresponding ``deny_access`` event. To reconstruct the decision for an object, aggregate the ``grants=`` groups of all ``check_rule`` events emitted for that object during the check. .. note:: Because a verdict requires aggregating ``grants=`` across all matching rules of one access check, a stateless ftrace filter on a single ``check_rule`` event cannot distinguish an allowed access from a denied one. For example, a program sandboxed with read and execute access to the whole filesystem reads ``/etc/passwd``; both the ``execve()`` and the read match the rule covering ``/`` (inode 2), so ``check_rule_fs`` fires with the requested rights intersected against what that rule grants. The ``access_request=`` mask includes ``truncate`` because the file-open hook evaluates that optional right alongside the required access, but the rule does not grant it, so ``truncate`` never appears in ``grants=``:: cat-127 [...] landlock_check_rule_fs: domain=1e40cb56f access_request=execute|read_file|truncate dev=0:17 ino=2 grants={execute|read_file} cat-127 [...] landlock_check_rule_fs: domain=1e40cb56f access_request=read_file|truncate dev=0:17 ino=2 grants={read_file} The ``[...]`` replaces the ftrace CPU, flags, and timestamp columns. A single ``grants=`` group means the enforcing domain has one layer. With two nested sandboxes that each grant the same rights, the rule spans both layers, so ``grants=`` has one group per layer:: cat-128 [...] landlock_check_rule_fs: domain=184788b52 access_request=execute|read_file|truncate dev=0:17 ino=2 grants={execute|read_file,execute|read_file} cat-128 [...] landlock_check_rule_fs: domain=184788b52 access_request=read_file|truncate dev=0:17 ino=2 grants={read_file,read_file} eBPF access =========== eBPF programs attached via ``BPF_RAW_TRACEPOINT`` can access the tracepoint arguments directly through BTF. The arguments include both standard kernel objects and Landlock-internal objects: - Standard kernel objects (``struct task_struct``, ``struct sock``, ``struct path``, ``struct dentry``) can be used with existing BPF helpers. - Landlock-internal objects (``struct landlock_domain``, ``struct landlock_ruleset``, ``struct landlock_rule``, ``struct landlock_hierarchy``) can be read via ``BPF_CORE_READ``. Internal struct layouts may change between kernel versions; use CO-RE for field relocation. A stateful eBPF program observes the full event stream and maintains per-domain state in BPF maps: 1. On ``landlock_create_domain``: record the domain ID and parent (the per-domain Landlock log flags are not event fields; read them from ``struct landlock_hierarchy`` via BTF if needed). 2. On ``landlock_enforce_domain``: record the sandboxed thread under the ``domain=`` key (join to the ``create_domain`` recorded in step 1), building the per-domain thread set; filter ``complete==1`` for a one-event-per-operation summary. 3. On ``landlock_deny_access_*``: look up the domain, decide whether to count, alert, or ignore the denial based on custom policy. 4. On ``landlock_free_domain``: clean up the per-domain state, log final statistics. This approach requires no kernel modification and no Landlock-specific BPF helpers. The Landlock IDs serve as correlation keys across events. Audit filtering equivalence =========================== The ``logged`` field reflects the domain's log policy but not the global ``audit_enabled`` toggle, so it does not change when audit is turned on or off. When audit is enabled, ``logged==1`` selects the denials the domain submits to audit (audit-side rate-limiting and exclude rules may still drop some), so a stateless ftrace filter can select them:: # Show only denials that audit would also log: echo 'logged==1' > \ /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/filter Event reference =============== .. kernel-doc:: include/trace/events/landlock.h :doc: Landlock trace events .. kernel-doc:: include/trace/events/landlock.h :internal: Additional documentation ======================== * Documentation/userspace-api/landlock.rst * Documentation/admin-guide/LSM/landlock.rst * Documentation/security/landlock.rst * https://landlock.io