1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
|
.. 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
|