mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-09-18 23:19:34 +02:00
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>
327 lines
14 KiB
ReStructuredText
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
|