.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2026 Cloudflare, Inc.

=====================
Landlock Trace Events
=====================

:Author: Mickaël Salaün
:Date: September 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_path_beneath``: a filesystem rule is added to a ruleset
- ``landlock_add_rule_net_port``: 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_inode``: an inode-keyed rule is evaluated
- ``landlock_check_rule_net_port``: a network-port-keyed 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=``), which is the enforcing domain and not
  necessarily the current task's domain, with the other party's domain.
  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_inode`` and ``check_rule_net_port`` 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.

For an access check that a consumer can delimit, aggregate the ``grants=``
groups of all matching ``check_rule`` events.  These events do not carry a
request ID; use their execution context and generic tracepoints to separate
concurrent or successive checks of the same object.

.. 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_inode`` 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_inode: domain=1e40cb56f access_request=execute|read_file|truncate dev=0:17 ino=2 grants={execute|read_file}
  cat-127 [...] landlock_check_rule_inode: 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_inode: 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_inode: domain=184788b52 access_request=read_file|truncate dev=0:17 ino=2 grants={read_file,read_file}

eBPF access
===========

BTF-enabled raw tracepoint programs attached through libbpf
``SEC("tp_btf/...")`` sections receive typed callback arguments.  The
event prototypes in `Event reference`_ document their argument layouts.
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 attached before sandbox construction can maintain
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.  Landlock IDs serve as correlation keys within one boot.
Records exported through tracing or BPF buffers can be lost, and records
from different CPUs are not globally ordered, so consumers must detect and
reconcile incomplete state.

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
