| .. 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 ``<no_mem>``, ``<too_long>``, or ``<unreachable>``. |
| Path-based tracepoint output is deterministic regardless of the tracer's |
| mount namespace. |
| |
| - **Device names**: Tracepoints use numeric ``dev=<major>:<minor>``. |
| Audit uses string ``dev="<s_id>"``. 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=<hex_id>.<version>``) |
| 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 ``{<layer>,<layer>,...}``: |
| |
| - 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 |