Files
Mickaël Salaün 172b6a6d84 landlock: Document tracepoints
Until now, Landlock observability documentation covered only audit
records.  The tracepoints added by the previous commits introduce a
second channel with different semantics (firing regardless of audit
configuration and domain log flags, exposing the full ruleset and domain
state to eBPF via BTF), which kernel developers, administrators, and
userspace tool authors need to discover and compare against audit.

Add a dedicated "Landlock Trace Events" reference covering the event
categories, enabling events via tracefs, ruleset versioning, eBPF access
through BPF_RAW_TRACEPOINT, and the same_exec and logged denial fields
(logged being the kernel's audit-logging decision, so a stateless ftrace
filter can select the denials audit would record with logged==1).
Cross-reference it from the administrator, kernel-internals, and
userspace API documents, contrasting tracepoints with audit: when each
channel is preferred, what each guarantees, and how NOAUDIT hooks and
audit rate limiting affect them.

Also document the relational other-party domain fields the scope and
ptrace denial tracepoints expose (tracee_domain, target_domain,
peer_domain; 0 when the other party is unsandboxed), so a consumer can
resolve them against the lifecycle events and reproduce the two-domain
verdict.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20260811094338.288094-20-mic@digikod.net
[mic: Update dates]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
2026-08-17 10:17:19 +02:00

327 lines
14 KiB
ReStructuredText

.. 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