blob: af9267cca47d9dbf3a421a296a398b3b13b59b98 [file] [edit]
.. 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