Merge tag 'landlock-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux

Pull Landlock update from Mickaël Salaün:
 "This improves observability with Landlock tracepoints support, which
  required some refactoring for dedicated domain types and common
  helpers shared with audit code.

  A LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag is also added to improve
  process-wide domain enforcement consistency.

  Whiteout files are now correctly handled and tested, and a few other
  fixes"

* tag 'landlock-7.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: (34 commits)
  landlock: Document tracepoints
  selftests/landlock: Add landlock_enforce_domain trace tests
  selftests/landlock: Add scope and ptrace tracepoint tests
  selftests/landlock: Add network tracepoint tests
  selftests/landlock: Add filesystem tracepoint tests
  selftests/landlock: Add trace event test infrastructure and tests
  landlock: Add tracepoints for ptrace and scope denials
  landlock: Add landlock_deny_access_fs and landlock_deny_access_net
  landlock: Add tracepoints for rule checking
  landlock: Add landlock_enforce_domain tracepoint
  landlock: Add create_domain and free_domain tracepoints
  landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints
  landlock: Add create_ruleset and free_ruleset tracepoints
  landlock: Consolidate access-right and scope names in a shared header
  landlock: Decouple the per-denial logging decision from CONFIG_AUDIT
  landlock: Split denial logging from audit into common framework
  landlock: Split struct landlock_domain from struct landlock_ruleset
  landlock: Move domain query functions to domain.c
  landlock: Prepare ruleset and domain type split
  samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler
  ...
This commit is contained in:
Linus Torvalds
2026-08-21 12:28:35 -07:00
49 changed files with 9257 additions and 1516 deletions

View File

@@ -1,12 +1,13 @@
.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2025 Microsoft Corporation
.. Copyright © 2026 Cloudflare, Inc.
================================
Landlock: system-wide management
================================
:Author: Mickaël Salaün
:Date: June 2026
:Date: August 2026
Landlock can leverage the audit framework to log events.
@@ -52,6 +53,7 @@ AUDIT_LANDLOCK_ACCESS
- fs.refer (ABI 2+)
- fs.truncate (ABI 3+)
- fs.ioctl_dev (ABI 5+)
- fs.resolve_unix (ABI 9+)
**net.*** - Network access rights (ABI 4+):
- net.bind_tcp - TCP port binding was denied
@@ -181,11 +183,113 @@ filters to limit noise with two complementary ways:
programs,
- or with audit rules (see :manpage:`auditctl(8)`).
Tracepoints
===========
Landlock also provides tracepoints as an alternative to audit for
debugging and observability. Tracepoints fire unconditionally,
independent of audit configuration, ``audit_enabled``, and domain log
flags. This makes them suitable for always-on monitoring with eBPF or
for ad-hoc debugging with ``trace-pipe``.
See Documentation/trace/events-landlock.rst for the complete event
reference: the full event list, how to enable events and read their
output, the field formats, the ``check_rule`` interpretation guide,
worked event samples, ftrace filtering, and eBPF access.
.. _landlock_observability:
When to use tracing vs audit
-----------------------------
Audit and tracing both help diagnose Landlock policy issues:
**Audit** records denied accesses with the blockers, domain, and object
identification (path, port). Audit is the standard Linux mechanism for
security events, with a stable record format that is well established
and already supported by log management systems, SIEM platforms, and EDR
solutions. Audit is always active when the kernel is built with audit
support, filtered by the Landlock log flags to reduce noise in
production, and designed for long-term security monitoring and
compliance.
**Tracing** provides deeper introspection for policy debugging. In
addition to denied accesses, trace events cover the complete lifecycle
of Landlock objects (rulesets, domains) and intermediate rule matching
during access checks. Trace events are disabled by default (zero
overhead) and fire unconditionally. eBPF
programs attached to trace events can access the full kernel context
(ruleset rules, domain hierarchy, process credentials) via BTF, enabling
richer analysis than the flat fields in audit records. For example, an
eBPF-based live monitoring tool can correlate creation, rule-addition,
and denial events to build a real-time view of all active Landlock
domains and their policies. However, BTF-based access depends on
internal kernel struct layouts which have no stability guarantee. CO-RE
(Compile Once, Run Everywhere) provides best-effort field relocation.
The ftrace printk format is also not a stable ABI, but is
self-describing via the per-event ``format`` file, allowing tools to
adapt dynamically.
Observability guarantees and limitations
-----------------------------------------
Both audit records and trace events are emitted for every denied access,
with these exceptions:
- **Landlock log flags** (audit only): ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
``LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON``, and
``LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF`` control which denials
generate audit records. Trace events fire regardless of these flags.
- **NOAUDIT hooks**: Some LSM hooks suppress logging for speculative
permission probes (e.g., reading ``/proc/<pid>/status`` uses
``PTRACE_MODE_NOAUDIT``). When NOAUDIT is set, neither audit records
nor trace events are emitted, and the denial is not counted in
``denials``. The denial is still enforced. This avoids performance
overhead and noise from speculative probes that test permissions
without performing an actual access.
- **Audit rate limiting**: The audit subsystem may silently drop records
when the audit queue is full. Trace events are not rate-limited.
- **Tracepoint disabled**: When a trace event is disabled (the default
state), the tracepoint is a no-op with zero overhead.
When both audit and tracing are active, every denial emits a trace event,
and a denial that the domain's log policy selects additionally produces an
audit record (subject to the Landlock log flags). The ``denials`` count in
``free_domain`` events is incremented for every denial regardless of the
log flags, so it can exceed the number of audit records (which the log
flags and audit-side rate-limiting or exclude rules may suppress).
.. _landlock_observability_security:
Observability security considerations
---------------------------------------
Both audit records and trace events expose information about all
Landlock-sandboxed processes on the system, including filesystem paths
being accessed, network ports, and process identities. System
administrators must ensure that access to audit logs (controlled by the
audit subsystem configuration) and to trace events (requiring
``CAP_SYS_ADMIN`` or ``CAP_BPF`` + ``CAP_PERFMON``) is restricted to
trusted users.
eBPF programs attached to Landlock trace events have access to the full
kernel context of each event (ruleset rules, domain hierarchy, process
credentials) via BTF, exposing sensitive state about every sandboxed
process. Restrict this access to trusted users, as for the audit logs.
Audit logs and kernel trace events require elevated privileges and are
system-wide; they are not designed for per-sandbox unprivileged
monitoring.
Additional documentation
========================
* `Linux Audit Documentation`_
* Documentation/userspace-api/landlock.rst
* Documentation/trace/events-landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io

View File

@@ -1,13 +1,14 @@
.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
.. Copyright © 2019-2020 ANSSI
.. Copyright © 2026 Cloudflare, Inc.
==================================
Landlock LSM: kernel documentation
==================================
:Author: Mickaël Salaün
:Date: March 2026
:Date: August 2026
Landlock's goal is to create scoped access-control (i.e. sandboxing). To
harden a whole system, this feature should be available to any process,
@@ -177,11 +178,46 @@ makes the reasoning much easier and helps avoid pitfalls.
.. kernel-doc:: security/landlock/domain.h
:identifiers:
Denial logging
==============
Access denials are logged through two independent channels: audit
records and tracepoints. Both are managed by the common denial
framework in ``log.c``, compiled under ``CONFIG_SECURITY_LANDLOCK_LOG``
(automatically selected by ``CONFIG_AUDIT`` or ``CONFIG_TRACEPOINTS``).
Audit records respect audit configuration, the domain's Landlock log
flags, and ``LANDLOCK_LOG_DISABLED``. Tracepoints fire unconditionally,
independent of these settings. The denial counter (``num_denials``) is
always incremented regardless of logging configuration.
Each denial tracepoint carries a ``logged`` field reporting the
audit-logging verdict: whether the denial would be written to the audit
log if audit were configured and active. This verdict is the same
whether or not the kernel is built with audit support, so a
tracepoints-only build reports the selection audit would make. A quiet
rule (``LANDLOCK_ADD_RULE_QUIET`` with the access in the ``quiet_*``
fields of ``struct landlock_ruleset_attr``) suppresses logging by
setting ``logged=0`` the same way.
See Documentation/admin-guide/LSM/landlock.rst for audit record format,
tracepoint usage, and filtering examples.
.. kernel-doc:: security/landlock/log.h
:identifiers:
Trace events
------------
See Documentation/trace/events-landlock.rst for trace event usage and format
details; the full event reference lives there and is not duplicated here.
Additional documentation
========================
* Documentation/userspace-api/landlock.rst
* Documentation/admin-guide/LSM/landlock.rst
* Documentation/trace/events-landlock.rst
* https://landlock.io
.. Links

View File

@@ -0,0 +1,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

View File

@@ -54,6 +54,7 @@ applications.
events-power
events-nmi
events-msr
events-landlock
events-pci
events-pci-controller
boottime-trace

View File

@@ -8,7 +8,7 @@ Landlock: unprivileged access control
=====================================
:Author: Mickaël Salaün
:Date: June 2026
:Date: August 2026
The goal of Landlock is to enable restriction of ambient rights (e.g. global
filesystem or network access) for a set of processes. Because Landlock
@@ -250,7 +250,8 @@ similar backwards compatibility check is needed for the restrict flags
__u32 restrict_flags =
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_TSYNC;
LANDLOCK_RESTRICT_SELF_TSYNC |
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
switch (abi) {
case 1 ... 6:
/* Removes logging flags for ABI < 7 */
@@ -269,16 +270,37 @@ similar backwards compatibility check is needed for the restrict flags
* children (and not for all threads, including parents and siblings).
*/
restrict_flags &= ~LANDLOCK_RESTRICT_SELF_TSYNC;
__attribute__((fallthrough));
case 8 ... 10:
/* Removes no new privs flag for ABI < 11 */
restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
}
The next step is to restrict the current thread from gaining more privileges
(e.g. through a SUID binary). We now have a ruleset with the first rule
allowing read and execute access to ``/usr`` while denying all other handled
accesses for the filesystem, and two more rules allowing DNS queries.
(e.g. through a SUID binary). For unprivileged processes, setting the
no_new_privs attribute is required by Landlock.
Processes with ``CAP_SYS_ADMIN`` in their namespace can enforce a ruleset
without setting no_new_privs, but leaving no_new_privs unset is risky even
when Landlock does not require this attribute: sandboxed processes could
still execute set-user-ID, set-group-ID or file-capability binaries, which
would then run with elevated privileges while being restricted by a Landlock
domain they may not expect, making them potential confused deputies.
no_new_privs should only be left unset if such a privilege transition is
expected.
We now have a ruleset with the first rule allowing read and execute access to
``/usr`` while denying all other handled accesses for the filesystem, and two
more rules allowing DNS queries.
.. code-block:: c
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
/*
* If the ABI > 10, we can tie setting no_new_privs with successful ruleset
* enforcement and skip the manual prctl(PR_SET_NO_NEW_PRIVS, ...) call.
*/
if (!(restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
close(ruleset_fd);
return 1;
@@ -556,6 +578,9 @@ in the running kernel.
.. kernel-doc:: security/landlock/errata/abi-1.h
:doc: erratum_3
.. kernel-doc:: security/landlock/errata/abi-1.h
:doc: erratum_4
How to check for errata
~~~~~~~~~~~~~~~~~~~~~~~
@@ -737,6 +762,8 @@ Starting with the Landlock ABI version 6, it is possible to restrict
:manpage:`signal(7)` sending by setting ``LANDLOCK_SCOPE_SIGNAL`` to the
``scoped`` ruleset attribute.
.. _landlock_log_flags:
Logging (ABI < 7)
-----------------
@@ -744,8 +771,13 @@ Starting with the Landlock ABI version 7, it is possible to control logging of
Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
``LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON``, and
``LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF`` flags passed to
sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst
for more details on audit.
sys_landlock_restrict_self(). These flags control audit record generation.
Landlock tracepoints are not affected by these flags and always fire when
enabled, providing an alternative observability channel for debugging and
monitoring. See Documentation/admin-guide/LSM/landlock.rst for more
details on audit and tracepoints, and
Documentation/trace/events-landlock.rst for the complete trace event
reference.
Thread synchronization (ABI < 8)
--------------------------------
@@ -789,6 +821,19 @@ when at least one sys_landlock_add_rule() call is made for it with the
``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same
object without this flag do not clear it.
no_new_privs flag (ABI < 11)
----------------------------
Starting with the Landlock ABI version 11, sys_landlock_restrict_self()
accepts the ``LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS`` flag, which sets the
no_new_privs attribute of the calling thread only once the enforcement of
the ruleset succeeded: no_new_privs is set if and only if the call
succeeds. This removes the need for a prior :manpage:`prctl(2)`
``PR_SET_NO_NEW_PRIVS`` call (or ``CAP_SYS_ADMIN`` use). When combined
with ``LANDLOCK_RESTRICT_SELF_TSYNC``, no_new_privs is set on all threads
of the process. As explained in the tutorial above, leaving no_new_privs
unset is risky even when Landlock does not require it.
.. _kernel_support:
Kernel support
@@ -887,6 +932,7 @@ Additional documentation
========================
* Documentation/admin-guide/LSM/landlock.rst
* Documentation/trace/events-landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io

View File

@@ -14638,8 +14638,11 @@ W: https://landlock.io
T: git https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git
F: Documentation/admin-guide/LSM/landlock.rst
F: Documentation/security/landlock.rst
F: Documentation/trace/events-landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c
F: include/linux/landlock.h
F: include/trace/events/landlock.h
F: include/uapi/linux/landlock.h
F: samples/landlock/
F: security/landlock/

56
include/linux/landlock.h Normal file
View File

@@ -0,0 +1,56 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Public types and definitions
*
* Copyright © 2016-2026 Mickaël Salaün <mic@digikod.net>
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _LINUX_LANDLOCK_H
#define _LINUX_LANDLOCK_H
#include <linux/types.h>
#include <uapi/linux/landlock.h>
/*
* Access-right and scope names, shared between the audit records (get_blocker()
* in security/landlock/audit.c) and the trace events
* (include/trace/events/landlock.h). A consumer defines
* _LANDLOCK_NAME_ENTRY(mask, name) before expanding a list and undefines it
* afterwards: audit maps each entry to a "[bit] = name" slot for O(1) lookup,
* the trace events map it to a __print_flags() { mask, name } pair. The bit
* value lives only in the LANDLOCK_* UAPI constant each entry references.
* Names are unprefixed; audit prepends the "fs."/"net."/"scope." category.
*/
#define _LANDLOCK_ACCESS_FS_NAMES \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_EXECUTE, "execute"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_WRITE_FILE, "write_file"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_FILE, "read_file"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_DIR, "read_dir"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_DIR, "remove_dir"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_FILE, "remove_file"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_CHAR, "make_char"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_DIR, "make_dir"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_REG, "make_reg"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SOCK, "make_sock"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_FIFO, "make_fifo"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_BLOCK, "make_block"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SYM, "make_sym"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REFER, "refer"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_TRUNCATE, "truncate"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_IOCTL_DEV, "ioctl_dev"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_RESOLVE_UNIX, "resolve_unix")
#define _LANDLOCK_ACCESS_NET_NAMES \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_TCP, "bind_tcp"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_TCP, "connect_tcp"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_UDP, "bind_udp"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, \
"connect_send_udp")
#define _LANDLOCK_SCOPE_NAMES \
_LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, \
"abstract_unix_socket"), \
_LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_SIGNAL, "signal")
#endif /* _LINUX_LANDLOCK_H */

View File

@@ -0,0 +1,965 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright © 2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#undef TRACE_SYSTEM
#define TRACE_SYSTEM landlock
#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_LANDLOCK_H
#include <linux/landlock.h>
#include <linux/string.h>
#include <linux/string_helpers.h>
#include <linux/tracepoint.h>
#include <linux/trace_seq.h>
#include <net/af_unix.h>
struct dentry;
struct landlock_domain;
struct landlock_hierarchy;
struct landlock_rule;
struct landlock_ruleset;
struct path;
struct sock;
struct task_struct;
#ifdef CREATE_TRACE_POINTS
/*
* Escapes @len bytes of an untrusted string into the trace sequence @p so it
* cannot inject field separators or control characters into the ftrace text
* output, and can be unambiguously recovered. Called from the TP_printk() of
* the tracepoints that expose paths and process names. @len is passed by the
* caller (rather than derived with strlen()) so a name that is not
* NUL-terminated or carries embedded NUL bytes (an abstract socket name) is
* escaped in full instead of being truncated at the first NUL.
*
* Return: a pointer into @p's buffer, or NULL if @src is NULL or the buffer is
* exhausted (normal when the trace buffer is full).
*/
static inline const char *
__trace_print_untrusted_str(struct trace_seq *p, const char *src, size_t len)
{
int escaped_size;
char *buf;
size_t buf_size = seq_buf_get_buf(&p->seq, &buf);
const char *ret = trace_seq_buffer_ptr(p);
/* Buffer exhaustion is normal when the trace buffer is full. */
if (!src || buf_size == 0)
return NULL;
escaped_size =
string_escape_mem(src, len, buf, buf_size,
ESCAPE_SPACE | ESCAPE_SPECIAL | ESCAPE_NAP |
ESCAPE_APPEND | ESCAPE_OCTAL,
" ='\"\\");
if (unlikely(escaped_size >= buf_size)) {
/* We need some room for the final '\0'. */
seq_buf_set_overflow(&p->seq);
p->full = 1;
return NULL;
}
seq_buf_commit(&p->seq, escaped_size);
trace_seq_putc(p, 0);
return ret;
}
/*
* Fills the dense per-domain-layer array layers (one access mask per layer,
* indexed by level - 1) from rule's sparse layer stack, keeping only the
* requested rights (access_request). Layers with no matching rule entry get
* a zero mask. Shared by the check_rule_fs and check_rule_net events.
*
* rule->layers is sorted by ascending level, with levels in the domain's
* [1, num_layers] range (see landlock_merge_ruleset()), so every entry maps
* to a slot. A leftover entry would be a malformed rule; the zero-filled
* slots keep the output and the array bounds safe regardless.
*/
static inline void
__trace_landlock_fill_layers(access_mask_t *const layers,
const size_t num_layers,
const struct landlock_rule *const rule,
const access_mask_t access_request)
{
size_t i = 0;
for (size_t level = 1; level <= num_layers; level++) {
access_mask_t grants = 0;
if (i < rule->num_layers && level == rule->layers[i].level) {
grants = rule->layers[i].access & access_request;
i++;
}
layers[level - 1] = grants;
}
/* A leftover entry means an out-of-range or unsorted rule level. */
WARN_ON_ONCE(i < rule->num_layers);
}
/*
* Renders the dense per-domain-layer access array as symbolic flag names for
* the grants field: layers wrapped in "{}", flags within a layer joined by
* "|", layers separated by ",", an empty layer rendered as nothing.
* Open-codes the flag walk because trace_print_flags_seq() NUL-terminates per
* call and so cannot be chained into a single field. The shared names table
* covers every access right, so masked bits are always named. Returns the
* trace_seq position like __print_flags().
*/
static inline const char *__trace_landlock_print_layers(
struct trace_seq *p, const access_mask_t *const layers,
const size_t num_layers, const struct trace_print_flags *const names,
const size_t names_size)
{
const char *const ret = trace_seq_buffer_ptr(p);
trace_seq_putc(p, '{');
for (size_t i = 0; i < num_layers; i++) {
access_mask_t mask = layers[i];
bool first = true;
if (i)
trace_seq_putc(p, ',');
for (size_t j = 0; mask && j < names_size; j++) {
if ((mask & names[j].mask) != names[j].mask)
continue;
if (!first)
trace_seq_putc(p, '|');
trace_seq_puts(p, names[j].name);
mask &= ~names[j].mask;
first = false;
}
}
trace_seq_putc(p, '}');
trace_seq_putc(p, 0);
return ret;
}
#endif /* CREATE_TRACE_POINTS */
/* clang-format off */
/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
/**
* DOC: Landlock trace events
*
* These guarantees and constraints hold for every Landlock tracepoint.
* A new tracepoint must uphold them, and an eBPF consumer can rely on
* them.
*
* Decision context
* ~~~~~~~~~~~~~~~~
*
* A denial event, together with the lifecycle events, exposes the full
* set of inputs the verdict consumed, so a consumer that tracked domain
* creation (landlock_create_ruleset, landlock_create_domain) can verify
* or reproduce the Landlock decision rather than merely observe it
* happened. In who/what/why terms: who is the denying domain (the domain
* field, always the subject that enforced the policy, never the current
* task), what is the operation and its object, and why is every other
* input the verdict weighed.
*
* Lifecycle consistency
* ~~~~~~~~~~~~~~~~~~~~~~
*
* Lifecycle events are balanced: a creation event always has a matching
* deallocation event and vice versa, so an eBPF program can model object
* lifetimes from the trace stream without reconciliation logic. A creation
* event fires while the object is still private to the calling thread
* (landlock_create_ruleset fires before the ruleset's file descriptor is
* installed, so it cannot race a concurrent :manpage:`close(2)`); if fd
* installation later fails and the ruleset is freed, free_ruleset still
* fires, keeping the pair balanced. The domain pair (create_domain and
* free_domain) is balanced the same way: create_domain fires when the
* domain is created (under the ruleset lock, before thread-sync), and
* free_domain fires when it is freed. A rare thread-sync failure aborts
* the just-created domain, which then emits both events (its creation, then
* an immediate free). Denial events fire only for denials that actually
* happen.
*
* Pointer access
* ~~~~~~~~~~~~~~
*
* All pointer arguments in TP_PROTO are guaranteed non-NULL by the
* caller, but pointers reached through them may still be NULL (e.g.,
* hierarchy->parent at a root domain) and must be checked. eBPF programs
* read these pointers via BTF for richer introspection than the
* TP_STRUCT__entry fields, which serve TP_printk display only.
*
* Mutable object pointers are passed while the caller holds the object's
* lock, so TP_fast_assign and a BTF reader see the exact object the event
* reports, a snapshot no concurrent writer can change: add_rule holds the
* modified ruleset's lock, and create_domain holds the ruleset lock across
* the emission (before the thread-sync wait) so the inspected ruleset is
* the one merged into the domain. Objects immutable at the emission site
* (a domain after creation, a hierarchy at its last reference) need no
* lock. A few values that no held lock protects are a best-effort
* lockless snapshot instead: a task's comm, and the deny_access_net struct
* sock (whose network hook holds no socket lock), matching how the sched
* and signal trace events sample comm.
*
* Field encoding
* ~~~~~~~~~~~~~~
*
* Fields that mirror the Landlock UAPI use the same C types and endianness
* (e.g. network ports are __u64 in host endianness, like
* landlock_net_port_attr.port). Per-event details, such as where a value
* is byte-swapped, live in the field's own kdoc.
*
* Rule-check fields
* ~~~~~~~~~~~~~~~~~
*
* The check_rule events fire during an access check, once per matching
* rule, before the final allow-or-deny verdict. They share domain (the
* enforcing domain being evaluated), access_request (the access mask being
* checked), and rule (the matching rule, with per-layer access masks).
*
* Denial fields
* ~~~~~~~~~~~~~
*
* Every denial event shares three fields. domain is the ID of the
* innermost domain that blocked the access. same_exec tells whether the
* current task is the same executable that entered that domain. logged is
* the domain's audit-logging decision for this denial (its log_status is
* enabled and the per-execution flag selected by same_exec is set); a
* stateless ftrace filter can select the denials the domain submits to
* audit with logged==1, without reconstructing it from the per-execution
* log flags. Denial events order their fields as domain, same_exec,
* logged, then blockers (deny_access events only), then the type-specific
* object fields, then any variable-length field.
*
* Relational referents
* ~~~~~~~~~~~~~~~~~~~~~
*
* A scope or ptrace verdict compares two domains, so the other party's
* domain is part of the decision context. It is exposed as a scalar
* domain ID (0 when that party is unsandboxed): target_domain (signal),
* peer_domain (abstract unix socket), tracee_domain (ptrace). With both
* IDs in the stream, a consumer that tracked domain creation can relate
* the two parties without kernel-internal state. The ID is a scalar
* snapshot, not a live domain pointer that could dangle: an optional
* relational referent is a scalar (0 sentinel), not a nullable pointer.
*/
/*
* Prints a per-layer access mask array (the dynamic array @array) as symbolic
* flag names using the shared @flag_names list (a _LANDLOCK_*_NAMES macro).
* Stays outside CREATE_TRACE_POINTS: TP_printk is expanded in the print-output
* pass where that macro is undefined.
*/
#define __print_landlock_layers(array, flag_names...) \
({ \
static const struct trace_print_flags __layer_names[] = { \
flag_names \
}; \
__trace_landlock_print_layers( \
p, __get_dynamic_array(array), \
__get_dynamic_array_len(array) / \
sizeof(access_mask_t), \
__layer_names, ARRAY_SIZE(__layer_names)); \
})
/**
* landlock_create_ruleset - New ruleset created
*
* @ruleset: Newly created ruleset (never NULL); not yet shared via an fd,
* so no lock is needed.
*
* Emitted by sys_landlock_create_ruleset() while the new ruleset is still
* private to the calling thread, before its file descriptor is installed,
* so it cannot race a concurrent :manpage:`close(2)`. Balanced by a
* matching landlock_free_ruleset event.
*/
TRACE_EVENT(landlock_create_ruleset,
TP_PROTO(const struct landlock_ruleset *ruleset),
TP_ARGS(ruleset),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( access_mask_t, handled_fs )
__field( access_mask_t, handled_net )
__field( access_mask_t, scoped )
),
TP_fast_assign(
__entry->ruleset_id = ruleset->id;
__entry->ruleset_version = ruleset->version;
__entry->handled_fs = ruleset->handled_masks.fs;
__entry->handled_net = ruleset->handled_masks.net;
__entry->scoped = ruleset->handled_masks.scope;
),
TP_printk("ruleset=%llx.%u handled_fs=%s handled_net=%s scoped=%s",
__entry->ruleset_id, __entry->ruleset_version,
__print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES),
__print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES),
__print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES))
);
/**
* landlock_free_ruleset - Ruleset freed
*
* @ruleset: Ruleset being freed (never NULL); at its last reference, so no
* lock is needed.
*
* Emitted when a ruleset's last reference is dropped (typically when
* the creating process closes the ruleset file descriptor). Fires even
* when file-descriptor installation failed after creation, keeping the
* create/free pair balanced.
*/
TRACE_EVENT(landlock_free_ruleset,
TP_PROTO(const struct landlock_ruleset *ruleset),
TP_ARGS(ruleset),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
),
TP_fast_assign(
__entry->ruleset_id = ruleset->id;
__entry->ruleset_version = ruleset->version;
),
TP_printk("ruleset=%llx.%u",
__entry->ruleset_id, __entry->ruleset_version)
);
/**
* landlock_add_rule_fs - Filesystem rule added to a ruleset
*
* @ruleset: Source ruleset (never NULL).
* @access_rights: Effective access mask stored in the rule, not the raw
* sys_landlock_add_rule() argument (unhandled rights
* added).
* @path: Filesystem path for the rule (never NULL).
* @pathname: Resolved absolute path string (never NULL; error placeholder
* on resolution failure).
*
* Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
* the reported ruleset is a stable snapshot that no concurrent writer can
* change.
*/
TRACE_EVENT(landlock_add_rule_fs,
TP_PROTO(const struct landlock_ruleset *ruleset,
access_mask_t access_rights, const struct path *path,
const char *pathname),
TP_ARGS(ruleset, access_rights, path, pathname),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( access_mask_t, access_rights )
__field( dev_t, dev )
__field( ino_t, ino )
__string( pathname, pathname )
),
TP_fast_assign(
lockdep_assert_held(&ruleset->lock);
__entry->ruleset_id = ruleset->id;
__entry->ruleset_version = ruleset->version;
__entry->access_rights = access_rights;
__entry->dev = path->dentry->d_sb->s_dev;
/*
* The inode number may not be the user-visible one,
* but it will be the same used by audit.
*/
__entry->ino = d_backing_inode(path->dentry)->i_ino;
__assign_str(pathname);
),
TP_printk("ruleset=%llx.%u access_rights=%s dev=%u:%u ino=%lu path=%s",
__entry->ruleset_id, __entry->ruleset_version,
__print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_FS_NAMES),
MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
__trace_print_untrusted_str(p, __get_str(pathname),
__get_dynamic_array_len(pathname) - 1))
);
/**
* landlock_add_rule_net - Network port rule added to a ruleset
*
* @ruleset: Source ruleset (never NULL).
* @access_rights: Effective access mask stored in the rule, not the raw
* sys_landlock_add_rule() argument (unhandled rights
* added).
* @port: Network port, the landlock_net_port_attr.port UAPI value
* forwarded directly.
*
* Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
* the reported ruleset is a stable snapshot that no concurrent writer can
* change.
*/
TRACE_EVENT(landlock_add_rule_net,
TP_PROTO(const struct landlock_ruleset *ruleset,
access_mask_t access_rights, __u64 port),
TP_ARGS(ruleset, access_rights, port),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( access_mask_t, access_rights )
__field( __u64, port )
),
TP_fast_assign(
lockdep_assert_held(&ruleset->lock);
__entry->ruleset_id = ruleset->id;
__entry->ruleset_version = ruleset->version;
__entry->access_rights = access_rights;
__entry->port = port;
),
TP_printk("ruleset=%llx.%u access_rights=%s port=%llu",
__entry->ruleset_id, __entry->ruleset_version,
__print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_NET_NAMES),
__entry->port)
);
/**
* landlock_create_domain - New domain created
*
* @domain: Newly created domain (never NULL, immutable after creation).
* @domain->hierarchy->id is its unique ID, shared with the
* landlock_enforce_domain and landlock_free_domain events;
* @domain->hierarchy->details holds the requesting process.
* @ruleset: Source ruleset frozen into the domain (never NULL). The
* ruleset lock is held across the emission, so a BPF program
* reading it via BTF sees the exact merged ruleset;
* @ruleset->id / @ruleset->version identify it.
*
* Emitted by sys_landlock_restrict_self() once, in the requesting
* thread's context, right after the merge and before thread-sync. The
* flags-only path (ruleset_fd == -1) creates no domain and does not
* emit this event. Paired with the per-thread landlock_enforce_domain
* (join on @domain->hierarchy->id) and balanced by a matching
* landlock_free_domain event.
*/
TRACE_EVENT(landlock_create_domain,
TP_PROTO(const struct landlock_domain *domain,
const struct landlock_ruleset *ruleset),
TP_ARGS(domain, ruleset),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( __u64, parent_id )
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
),
TP_fast_assign(
lockdep_assert_held(&ruleset->lock);
__entry->domain_id = domain->hierarchy->id;
__entry->parent_id = domain->hierarchy->parent ?
domain->hierarchy->parent->id : 0;
__entry->ruleset_id = ruleset->id;
__entry->ruleset_version = ruleset->version;
),
TP_printk("domain=%llx parent=%llx ruleset=%llx.%u",
__entry->domain_id, __entry->parent_id,
__entry->ruleset_id, __entry->ruleset_version)
);
/**
* landlock_enforce_domain - Domain enforced on a thread
*
* @domain: Domain now enforced on the current thread (never NULL,
* immutable; read locklessly). Correlate to
* landlock_create_domain via @domain->hierarchy->id for the
* source ruleset and requesting thread, or read
* @domain->hierarchy->details for the requesting process.
* @complete: Set on the single event that concludes the operation, after
* all its other enforcements; filter on it for one event per
* operation.
* @process_wide: The enforcement covers every eligible (non-exiting)
* thread of the process: set when the caller used
* %LANDLOCK_RESTRICT_SELF_TSYNC or the process is
* single-threaded. A lone thread whose group still
* holds a zombie leader is not counted single-threaded,
* so process_wide == 0 never proves the opposite.
* @no_new_privs: The enforcing thread's no_new_privs state at
* enforcement time: 1 if set (by a prior
* :manpage:`prctl(2)` %PR_SET_NO_NEW_PRIVS or by
* %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS), 0 if the domain
* was enforced with %CAP_SYS_ADMIN instead.
*
* Emitted for each thread sys_landlock_restrict_self() enforces the
* domain on, in that thread's own context, right after its
* commit_creds(), so it fires only once the thread is irreversibly
* enforcing the domain (aborted operations emit none). Not
* balanced; every enforcement falls between the domain's
* landlock_create_domain and landlock_free_domain events.
*
* @complete == 1 && @process_wide == 1 means the whole process is
* sandboxed by @domain, durably (Landlock domains are monotonic and
* inherited on :manpage:`clone(2)`).
*/
TRACE_EVENT(landlock_enforce_domain,
TP_PROTO(const struct landlock_domain *domain, bool complete,
bool process_wide, bool no_new_privs),
TP_ARGS(domain, complete, process_wide, no_new_privs),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, complete )
__field( bool, process_wide )
__field( bool, no_new_privs )
),
TP_fast_assign(
__entry->domain_id = domain->hierarchy->id;
__entry->complete = complete;
__entry->process_wide = process_wide;
__entry->no_new_privs = no_new_privs;
),
TP_printk("domain=%llx complete=%d process_wide=%d no_new_privs=%d",
__entry->domain_id, __entry->complete, __entry->process_wide,
__entry->no_new_privs)
);
/**
* landlock_free_domain - Domain freed
*
* @hierarchy: Hierarchy node being freed (never NULL).
*
* Emitted when the hierarchy node's last reference is dropped: its
* refcount reaches zero after all child domains have released their
* parent reference. A committed domain is
* freed from a kworker via landlock_put_domain_deferred() (the credential
* free path runs in RCU context, where sleeping is forbidden), so the
* current task is not the sandboxed task that triggered the free. Balanced
* by a matching landlock_create_domain event.
*/
TRACE_EVENT(landlock_free_domain,
TP_PROTO(const struct landlock_hierarchy *hierarchy),
TP_ARGS(hierarchy),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( __u64, denials )
),
TP_fast_assign(
__entry->domain_id = hierarchy->id;
__entry->denials = atomic64_read(&hierarchy->num_denials);
),
TP_printk("domain=%llx denials=%llu",
__entry->domain_id, __entry->denials)
);
/**
* landlock_check_rule_fs - Filesystem rule evaluated during access check
*
* @domain: Enforcing domain (never NULL).
* @rule: Matching rule with per-layer access masks (never NULL).
* @access_request: Access mask evaluated against the rule (the domain's
* handled mask during rename/link double-checks).
* @dentry: Filesystem dentry being checked (never NULL).
*
* Emitted for each rule that matches during a filesystem access check.
* The grants array shows the requested rights the rule grants at each
* domain layer. See Documentation/trace/events-landlock.rst for how to
* interpret it.
*/
TRACE_EVENT(landlock_check_rule_fs,
TP_PROTO(const struct landlock_domain *domain,
const struct landlock_rule *rule,
access_mask_t access_request, const struct dentry *dentry),
TP_ARGS(domain, rule, access_request, dentry),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( access_mask_t, access_request )
__field( dev_t, dev )
__field( ino_t, ino )
__dynamic_array(access_mask_t, grants,
domain->num_layers)
),
TP_fast_assign(
__entry->domain_id = domain->hierarchy->id;
__entry->access_request = access_request;
__entry->dev = dentry->d_sb->s_dev;
__entry->ino = d_backing_inode(dentry)->i_ino;
__trace_landlock_fill_layers(__get_dynamic_array(grants),
__get_dynamic_array_len(grants) /
sizeof(access_mask_t),
rule, access_request);
),
TP_printk("domain=%llx access_request=%s dev=%u:%u ino=%lu grants=%s",
__entry->domain_id,
__print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_FS_NAMES),
MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
__print_landlock_layers(grants, _LANDLOCK_ACCESS_FS_NAMES))
);
/**
* landlock_check_rule_net - Network port rule evaluated during access check
*
* @domain: Enforcing domain (never NULL).
* @rule: Matching rule with per-layer access masks (never NULL).
* @access_request: Access mask being requested.
* @port: Network port being checked (host endianness).
*
* Emitted for each rule that matches during a network access check. The
* grants array shows the requested rights the rule grants at each domain
* layer. See Documentation/trace/events-landlock.rst for how to
* interpret it.
*/
TRACE_EVENT(landlock_check_rule_net,
TP_PROTO(const struct landlock_domain *domain,
const struct landlock_rule *rule,
access_mask_t access_request, __u64 port),
TP_ARGS(domain, rule, access_request, port),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( access_mask_t, access_request )
__field( __u64, port )
__dynamic_array(access_mask_t, grants,
domain->num_layers)
),
TP_fast_assign(
__entry->domain_id = domain->hierarchy->id;
__entry->access_request = access_request;
__entry->port = port;
__trace_landlock_fill_layers(__get_dynamic_array(grants),
__get_dynamic_array_len(grants) /
sizeof(access_mask_t),
rule, access_request);
),
TP_printk("domain=%llx access_request=%s port=%llu grants=%s",
__entry->domain_id,
__print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_NET_NAMES),
__entry->port,
__print_landlock_layers(grants, _LANDLOCK_ACCESS_NET_NAMES))
);
/**
* landlock_deny_access_fs - Filesystem access denied
*
* @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
* domain field.
* @same_exec: Whether the current task entered the denying domain itself.
* @logged: The domain's audit-logging decision for this denial.
* @blockers: Access mask that was blocked (zero for a mount-topology
* change, whose only blocker is the operation itself).
* @path: Filesystem path that was denied (never NULL).
* @pathname: Resolved path string (never NULL; an error placeholder on
* resolution failure).
*
* Emitted when a Landlock domain denies a filesystem access.
*/
TRACE_EVENT(landlock_deny_access_fs,
TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
bool logged, access_mask_t blockers, const struct path *path,
const char *pathname),
TP_ARGS(hierarchy, same_exec, logged, blockers, path, pathname),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( access_mask_t, blockers )
__field( dev_t, dev )
__field( ino_t, ino )
__string( pathname, pathname )
),
TP_fast_assign(
const struct inode *inode = d_backing_inode(path->dentry);
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->blockers = blockers;
__entry->dev = path->dentry->d_sb->s_dev;
/*
* A negative dentry has no backing inode, so mirror the
* guard in dump_common_audit_data() and report inode 0.
*/
__entry->ino = inode ? inode->i_ino : 0;
__assign_str(pathname);
),
TP_printk("domain=%llx same_exec=%d logged=%d blockers=%s dev=%u:%u ino=%lu path=%s",
__entry->domain_id, __entry->same_exec, __entry->logged,
__print_flags(__entry->blockers, "|", _LANDLOCK_ACCESS_FS_NAMES),
MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
__trace_print_untrusted_str(p, __get_str(pathname),
__get_dynamic_array_len(pathname) - 1))
);
/**
* landlock_deny_access_net - Network access denied
*
* @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
* domain field.
* @same_exec: Whether the current task entered the denying domain itself.
* @logged: The domain's audit-logging decision for this denial.
* @blockers: Access mask that was blocked.
* @sk: Socket object (never NULL), read without a socket lock, so its
* fields are a best-effort snapshot. The denied endpoint is not
* available: the hook runs before :manpage:`bind(2)` /
* :manpage:`connect(2)` sets the socket addresses.
* @sport: Source port in host endianness, set for bind denials (zero for
* an autobind/ephemeral port); zero for connect and send denials.
* @dport: Destination port in host endianness, set for connect and send
* denials; zero for bind denials, and also zero for a UDP send to
* an AF_UNSPEC address on an IPv6 socket (indistinguishable from a
* real destination port 0). The bind-vs-connect direction is
* given by @blockers, not by which port is set.
*
* Emitted when a Landlock domain denies a network operation.
*
* The port fields are converted from the socket's network byte order to
* host endianness before emitting.
*/
TRACE_EVENT(landlock_deny_access_net,
TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
bool logged, access_mask_t blockers, const struct sock *sk,
__u64 sport, __u64 dport),
TP_ARGS(hierarchy, same_exec, logged, blockers, sk, sport, dport),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( access_mask_t, blockers )
__field( __u64, sport )
__field( __u64, dport )
),
TP_fast_assign(
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->blockers = blockers;
__entry->sport = sport;
__entry->dport = dport;
),
TP_printk("domain=%llx same_exec=%d logged=%d blockers=%s sport=%llu dport=%llu",
__entry->domain_id, __entry->same_exec, __entry->logged,
__print_flags(__entry->blockers, "|", _LANDLOCK_ACCESS_NET_NAMES),
__entry->sport, __entry->dport)
);
/**
* landlock_deny_ptrace - Ptrace access denied by a Landlock domain
*
* @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
* domain field.
* @same_exec: Whether the current task entered the denying domain itself.
* @logged: The domain's audit-logging decision for this denial.
* @tracee_domain_id: The tracee's Landlock domain ID, or 0 if the tracee
* is unsandboxed.
* @tracee: The target task ptrace acted on (never NULL). tracee_pid is
* the init-namespace TGID (like audit's opid).
*
* Emitted when a Landlock domain denies a ptrace operation.
*/
TRACE_EVENT(landlock_deny_ptrace,
TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
bool logged, u64 tracee_domain_id,
const struct task_struct *tracee),
TP_ARGS(hierarchy, same_exec, logged, tracee_domain_id, tracee),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, tracee_domain_id)
__field( pid_t, tracee_pid )
__string( tracee_comm, tracee->comm )
),
TP_fast_assign(
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->tracee_domain_id = tracee_domain_id;
__entry->tracee_pid = task_tgid_nr((struct task_struct *)tracee);
__assign_str(tracee_comm);
),
TP_printk("domain=%llx same_exec=%d logged=%d tracee_domain=%llx tracee_pid=%d tracee_comm=%s",
__entry->domain_id, __entry->same_exec, __entry->logged,
__entry->tracee_domain_id, __entry->tracee_pid,
__trace_print_untrusted_str(p, __get_str(tracee_comm),
__get_dynamic_array_len(tracee_comm) - 1))
);
/**
* landlock_deny_scope_signal - Signal delivery denied by
* LANDLOCK_SCOPE_SIGNAL
*
* @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
* domain field.
* @same_exec: Whether the current task entered the denying domain itself.
* @logged: The domain's audit-logging decision for this denial.
* @target_domain_id: The target's Landlock domain ID, or 0 if the target
* is unsandboxed.
* @target: The task the signal was aimed at (never NULL). target_pid is
* the init-namespace TGID (like audit's opid).
*
* Emitted when a Landlock domain denies signal delivery to a scoped-out
* target.
*/
TRACE_EVENT(landlock_deny_scope_signal,
TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
bool logged, u64 target_domain_id,
const struct task_struct *target),
TP_ARGS(hierarchy, same_exec, logged, target_domain_id, target),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, target_domain_id)
__field( pid_t, target_pid )
__string( target_comm, target->comm )
),
TP_fast_assign(
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->target_domain_id = target_domain_id;
__entry->target_pid = task_tgid_nr((struct task_struct *)target);
__assign_str(target_comm);
),
TP_printk("domain=%llx same_exec=%d logged=%d target_domain=%llx target_pid=%d target_comm=%s",
__entry->domain_id, __entry->same_exec, __entry->logged,
__entry->target_domain_id, __entry->target_pid,
__trace_print_untrusted_str(p, __get_str(target_comm),
__get_dynamic_array_len(target_comm) - 1))
);
/**
* landlock_deny_scope_abstract_unix_socket - Abstract unix socket access
* denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
*
* @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
* domain field.
* @same_exec: Whether the current task entered the denying domain itself.
* @logged: The domain's audit-logging decision for this denial.
* @peer_domain_id: The peer's Landlock domain ID, or 0 if the peer is
* unsandboxed.
* @peer: Peer socket (never NULL). peer_pid is best-effort: it is 0 for
* a datagram peer (no SO_PEERCRED), so sun_path is the reliable
* peer identifier.
*
* Emitted when a Landlock domain denies access to a scoped-out abstract
* unix socket.
*/
TRACE_EVENT(landlock_deny_scope_abstract_unix_socket,
TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
bool logged, u64 peer_domain_id, const struct sock *peer),
TP_ARGS(hierarchy, same_exec, logged, peer_domain_id, peer),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, peer_domain_id )
__field( pid_t, peer_pid )
/*
* Abstract socket names are untrusted binary data from
* user space. Use __string_len because abstract names
* are not NUL-terminated; their length is determined by
* addr->len. unix_sk(peer)->addr is stable here because
* the caller (hook_unix_stream_connect or
* hook_unix_may_send) holds unix_state_lock(peer).
*/
__string_len( sun_path,
unix_sk(peer)->addr ?
unix_sk(peer)->addr->name->sun_path + 1 :
"",
unix_sk(peer)->addr ?
unix_sk(peer)->addr->len -
offsetof(struct sockaddr_un,
sun_path) - 1 :
0)
),
TP_fast_assign(
struct pid *peer_pid;
lockdep_assert_held(&unix_sk(peer)->lock);
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->peer_domain_id = peer_domain_id;
/*
* Best-effort (0 for a datagram peer). sk_peer_pid is
* canonically guarded by sk->sk_peer_lock, but the target
* peer's peercred is set once and not updated concurrently in
* these hooks, so this READ_ONCE() is safe; sun_path is the
* reliable identifier.
*/
peer_pid = READ_ONCE(peer->sk_peer_pid);
__entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0;
__assign_str(sun_path);
),
TP_printk("domain=%llx same_exec=%d logged=%d peer_domain=%llx peer_pid=%d sun_path=%s",
__entry->domain_id, __entry->same_exec, __entry->logged,
__entry->peer_domain_id, __entry->peer_pid,
__trace_print_untrusted_str(p, __get_str(sun_path),
__get_dynamic_array_len(sun_path) - 1))
);
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
#include <trace/define_trace.h>
/* clang-format on */

View File

@@ -191,12 +191,25 @@ struct landlock_ruleset_attr {
*
* If the calling thread is running with no_new_privs, this operation
* enables no_new_privs on the sibling threads as well.
*
* The following flag ties the no_new_privs attribute to the ruleset
* enforcement:
*
* %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
* Sets the no_new_privs attribute of the calling thread only once the
* enforcement of the ruleset succeeded: no_new_privs is set if and only
* if sys_landlock_restrict_self() succeeds. This removes the need for a
* prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS`` call (or
* %CAP_SYS_ADMIN use). This flag requires a ruleset. When
* combined with %LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the
* sibling threads as well.
*/
/* clang-format off */
#define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF (1U << 0)
#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1)
#define LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF (1U << 2)
#define LANDLOCK_RESTRICT_SELF_TSYNC (1U << 3)
#define LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS (1U << 4)
/* clang-format on */
/**
@@ -315,16 +328,16 @@ struct landlock_net_port_attr {
* :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
* explicit recipient address.
*
* This access right only applies to connections to UNIX server sockets which
* This access right applies only to connections to UNIX server sockets which
* were created outside of the newly created Landlock domain (e.g. from within
* a parent domain or from an unrestricted process). Newly created UNIX
* servers within the same Landlock domain continue to be accessible. In this
* regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
* ``LANDLOCK_SCOPE_*`` flags.
*
* If a resolve attempt is denied, the operation returns an ``EACCES`` error,
* in line with other filesystem access rights (but different to denials for
* abstract UNIX domain sockets).
* If a resolution attempt is denied, the operation returns an ``EACCES``
* error, in line with other filesystem access rights (but different to
* denials for abstract UNIX domain sockets).
*
* This access right is available since the ninth version of the Landlock ABI.
*
@@ -351,6 +364,7 @@ struct landlock_net_port_attr {
* device.
* - %LANDLOCK_ACCESS_FS_MAKE_DIR: Create (or rename) a directory.
* - %LANDLOCK_ACCESS_FS_MAKE_REG: Create (or rename or link) a regular file.
* This also guards the creation of whiteout objects as used in OverlayFS.
* - %LANDLOCK_ACCESS_FS_MAKE_SOCK: Create (or rename or link) a UNIX domain
* socket.
* - %LANDLOCK_ACCESS_FS_MAKE_FIFO: Create (or rename or link) a named pipe.

View File

@@ -369,7 +369,7 @@ static int add_quiet_access(const char *const env_var,
return 0;
}
#define LANDLOCK_ABI_LAST 10
#define LANDLOCK_ABI_LAST 11
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -453,8 +453,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
.quiet_scoped = 0,
};
bool quiet_supported = true;
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
int set_restrict_flags = 0;
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
int set_restrict_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
if (argc < 2) {
fprintf(stderr, help, argv[0]);
@@ -545,6 +546,12 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
/* Removes quiet flags for ABI < 10 later on. */
quiet_supported = false;
__attribute__((fallthrough));
case 10:
/* Removes LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS for ABI < 11 */
supported_restrict_flags &=
~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
set_restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
@@ -673,7 +680,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
}
}
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
if (!(set_restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
goto err_close_ruleset;
}

View File

@@ -21,6 +21,11 @@ config SECURITY_LANDLOCK
you should also prepend "landlock," to the content of CONFIG_LSM to
enable Landlock at boot time.
config SECURITY_LANDLOCK_LOG
bool
depends on SECURITY_LANDLOCK
default y if AUDIT || TRACEPOINTS
config SECURITY_LANDLOCK_KUNIT_TEST
bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
depends on KUNIT=y

View File

@@ -8,11 +8,15 @@ landlock-y := \
cred.o \
task.o \
fs.o \
tsync.o
tsync.o \
domain.o
landlock-$(CONFIG_INET) += net.o
landlock-$(CONFIG_AUDIT) += \
landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
id.o \
audit.o \
domain.o
log.o
landlock-$(CONFIG_AUDIT) += audit.o
landlock-$(CONFIG_TRACEPOINTS) += trace.o

View File

@@ -19,7 +19,7 @@
/*
* All access rights that are denied by default whether they are handled or not
* by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
* by a ruleset/layer. This must be ORed with all domain->handled_masks[]
* entries when we need to get the absolute handled access masks, see
* landlock_upgrade_handled_access_masks().
*/
@@ -74,13 +74,13 @@ struct layer_mask {
* @access: The unfulfilled access rights for this layer.
*/
access_mask_t access : LANDLOCK_NUM_ACCESS_MAX;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @quiet: Whether we have encountered a rule with the quiet flag for
* this layer. Used to control logging.
*/
access_mask_t quiet : 1;
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
} __packed __aligned(sizeof(access_mask_t));
/*

View File

@@ -5,9 +5,9 @@
* Copyright © 2023-2025 Microsoft Corporation
*/
#include <kunit/test.h>
#include <linux/audit.h>
#include <linux/bitops.h>
#include <linux/landlock.h>
#include <linux/lsm_audit.h>
#include <linux/pid.h>
#include <uapi/linux/landlock.h>
@@ -18,40 +18,30 @@
#include "cred.h"
#include "domain.h"
#include "limits.h"
#include "ruleset.h"
#include "log.h"
static const char *const fs_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block",
[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym",
[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
};
/*
* Access-right and scope names are built from the lists shared with the trace
* events (see <linux/landlock.h>). The designated initializer places each name
* at its bit index, so the lookup stays O(1) and does not depend on the entry
* order. log_blockers() adds the "fs."/"net."/"scope." category prefix.
*/
#define _LANDLOCK_NAME_ENTRY(mask, name) [BIT_INDEX(mask)] = name
static const char *const fs_access_strings[] = { _LANDLOCK_ACCESS_FS_NAMES };
static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
static const char *const net_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_udp",
[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)] =
"net.connect_send_udp",
};
static const char *const net_access_strings[] = { _LANDLOCK_ACCESS_NET_NAMES };
static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
static const char *const scope_strings[] = { _LANDLOCK_SCOPE_NAMES };
static_assert(ARRAY_SIZE(scope_strings) == LANDLOCK_NUM_SCOPE);
#undef _LANDLOCK_NAME_ENTRY
static __attribute_const__ const char *
get_blocker(const enum landlock_request_type type,
const unsigned long access_bit)
@@ -63,7 +53,7 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
WARN_ON_ONCE(access_bit != -1);
return "fs.change_topology";
return "change_topology";
case LANDLOCK_REQUEST_FS_ACCESS:
if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
@@ -77,32 +67,63 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
WARN_ON_ONCE(access_bit != -1);
return "scope.abstract_unix_socket";
return scope_strings[BIT_INDEX(
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)];
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
WARN_ON_ONCE(access_bit != -1);
return "scope.signal";
return scope_strings[BIT_INDEX(LANDLOCK_SCOPE_SIGNAL)];
}
WARN_ON_ONCE(1);
return "unknown";
}
/*
* Returns the audit category prefix prepended to the unprefixed blocker name
* returned by get_blocker() (filesystem and network access rights,
* change_topology, and scopes). The ptrace blocker is standalone and carries
* its full name in get_blocker(), so it uses no prefix.
*/
static __attribute_const__ const char *
blocker_prefix(const enum landlock_request_type type)
{
switch (type) {
case LANDLOCK_REQUEST_PTRACE:
return "";
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
case LANDLOCK_REQUEST_FS_ACCESS:
return "fs.";
case LANDLOCK_REQUEST_NET_ACCESS:
return "net.";
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
return "scope.";
}
WARN_ON_ONCE(1);
return "";
}
static void log_blockers(struct audit_buffer *const ab,
const enum landlock_request_type type,
const access_mask_t access)
{
const unsigned long access_mask = access;
const char *const prefix = blocker_prefix(type);
unsigned long access_bit;
bool is_first = true;
for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
audit_log_format(ab, "%s%s", is_first ? "" : ",",
audit_log_format(ab, "%s%s%s", is_first ? "" : ",", prefix,
get_blocker(type, access_bit));
is_first = false;
}
if (is_first)
audit_log_format(ab, "%s", get_blocker(type, -1));
audit_log_format(ab, "%s%s", prefix, get_blocker(type, -1));
}
static void log_domain(struct landlock_hierarchy *const hierarchy)
@@ -137,526 +158,32 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
struct landlock_hierarchy *hierarchy = domain->hierarchy;
ssize_t i;
if (WARN_ON_ONCE(layer >= domain->num_layers))
return hierarchy;
for (i = domain->num_layers - 1; i > layer; i--) {
if (WARN_ON_ONCE(!hierarchy->parent))
break;
hierarchy = hierarchy->parent;
}
return hierarchy;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_hierarchy(struct kunit *const test)
{
struct landlock_hierarchy dom0_hierarchy = {
.id = 10,
};
struct landlock_hierarchy dom1_hierarchy = {
.parent = &dom0_hierarchy,
.id = 20,
};
struct landlock_hierarchy dom2_hierarchy = {
.parent = &dom1_hierarchy,
.id = 30,
};
struct landlock_ruleset dom2 = {
.hierarchy = &dom2_hierarchy,
.num_layers = 3,
};
KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
/* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
/* Get the youngest layer that denied the access_request. */
static size_t get_denied_layer(const struct landlock_ruleset *const domain,
access_mask_t *const access_request,
const struct layer_masks *masks)
{
for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
if (masks->layers[i].access & *access_request) {
*access_request &= masks->layers[i].access;
return i;
}
}
/* Not found - fall back to default values */
*access_request = 0;
return domain->num_layers - 1;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_denied_layer(struct kunit *const test)
{
const struct landlock_ruleset dom = {
.num_layers = 5,
};
const struct layer_masks masks = {
.layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_DIR,
.layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
};
access_mask_t access;
access = LANDLOCK_ACCESS_FS_EXECUTE;
KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
access = LANDLOCK_ACCESS_FS_READ_FILE;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
access = LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_WRITE_FILE;
KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, 0);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static size_t
get_layer_from_deny_masks(access_mask_t *const access_request,
const access_mask_t all_existing_optional_access,
const deny_masks_t deny_masks,
optional_access_t quiet_optional_accesses,
bool *quiet)
{
const unsigned long access_opt = all_existing_optional_access;
const unsigned long access_req = *access_request;
access_mask_t missing = 0;
size_t youngest_layer = 0;
size_t access_index = 0;
unsigned long access_bit;
bool should_quiet = false;
/* This will require change with new object types. */
WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
for_each_set_bit(access_bit, &access_opt,
BITS_PER_TYPE(access_mask_t)) {
if (access_req & BIT(access_bit)) {
const size_t layer =
(deny_masks >>
(access_index *
HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
(LANDLOCK_MAX_NUM_LAYERS - 1);
const bool layer_has_quiet =
!!(quiet_optional_accesses & BIT(access_index));
if (layer > youngest_layer) {
youngest_layer = layer;
missing = BIT(access_bit);
should_quiet = layer_has_quiet;
} else if (layer == youngest_layer) {
missing |= BIT(access_bit);
/*
* Whether the layer has rules with quiet flag
* covering the file accessed does not depend on
* the access, and so the following
* WARN_ON_ONCE() should not fail.
*/
WARN_ON_ONCE(should_quiet && !layer_has_quiet);
should_quiet = layer_has_quiet;
}
}
access_index++;
}
*access_request = missing;
*quiet = should_quiet;
return youngest_layer;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_layer_from_deny_masks(struct kunit *const test)
{
deny_masks_t deny_mask;
access_mask_t access;
optional_access_t quiet_optional_accesses;
bool quiet;
/* truncate:0 ioctl_dev:2 */
deny_mask = 0x20;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* layer denying truncate: quiet, ioctl: not quiet */
quiet_optional_accesses = 0b01;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* Reverse order - truncate:2 ioctl_dev:0 */
deny_mask = 0x02;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
/* layer denying truncate: quiet, ioctl: not quiet */
quiet_optional_accesses = 0b01;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
/* layer denying truncate: not quiet, ioctl: quiet */
quiet_optional_accesses = 0b10;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
/* truncate:15 ioctl_dev:15 */
deny_mask = 0xff;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* Both quiet (same layer so quietness must be the same) */
quiet_optional_accesses = 0b11;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, true);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static bool is_valid_request(const struct landlock_request *const request)
{
if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
return false;
if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
return false;
if (request->access) {
if (WARN_ON_ONCE(!(!!request->layer_masks ^
!!request->all_existing_optional_access)))
return false;
} else {
if (WARN_ON_ONCE(request->layer_masks ||
request->all_existing_optional_access))
return false;
}
if (request->deny_masks) {
if (WARN_ON_ONCE(!request->all_existing_optional_access))
return false;
static_assert(sizeof(request->all_existing_optional_access) ==
sizeof(u32));
if (WARN_ON_ONCE(
request->quiet_optional_accesses >=
BIT(hweight32(
request->all_existing_optional_access))))
return false;
}
return true;
}
static access_mask_t
pick_access_mask_for_request_type(const enum landlock_request_type type,
const struct access_masks access_masks)
{
switch (type) {
case LANDLOCK_REQUEST_FS_ACCESS:
return access_masks.fs;
case LANDLOCK_REQUEST_NET_ACCESS:
return access_masks.net;
default:
WARN_ONCE(1, "Invalid request type %d passed to %s", type,
__func__);
return 0;
}
}
/**
* landlock_log_denial - Create audit records related to a denial
* landlock_audit_denial - Create an audit record for a denied access request
*
* @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
* @youngest_denied: The youngest hierarchy node that denied the access.
* @missing: The set of denied access rights.
* @logged: Whether the denial is selected for logging, as computed by
* landlock_log_denial() (domain policy and quiet rules).
*
* Emits the record when audit is enabled and the denial is selected for
* logging.
*/
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
void landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool logged)
{
struct audit_buffer *ab;
struct landlock_hierarchy *youngest_denied;
size_t youngest_layer;
access_mask_t missing;
bool object_quiet_flag = false, quiet_applicable_to_access = false;
if (WARN_ON_ONCE(!subject || !subject->domain ||
!subject->domain->hierarchy || !request))
return;
if (!is_valid_request(request))
return;
missing = request->access;
if (missing) {
/* Gets the nearest domain that denies the request. */
if (request->layer_masks) {
youngest_layer = get_denied_layer(subject->domain,
&missing,
request->layer_masks);
object_quiet_flag =
request->layer_masks->layers[youngest_layer]
.quiet;
} else {
youngest_layer = get_layer_from_deny_masks(
&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
request->deny_masks,
request->quiet_optional_accesses,
&object_quiet_flag);
}
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
} else {
youngest_layer = request->layer_plus_one - 1;
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
}
if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
return;
/*
* Consistently keeps track of the number of denied access requests
* even if audit is currently disabled, or if audit rules currently
* exclude this record type, or if landlock_restrict_self(2)'s flags
* quiet logs.
*/
atomic64_inc(&youngest_denied->num_denials);
if (!audit_enabled)
return;
/* Checks if the current exec was restricting itself. */
if (subject->domain_exec & BIT(youngest_layer)) {
/* Ignores denials for the same execution. */
if (!youngest_denied->log_same_exec)
return;
} else {
/* Ignores denials after a new execution. */
if (!youngest_denied->log_new_exec)
return;
}
/*
* Checks if the object is marked quiet by the layer that denied the
* request. If it's a different layer that marked it as quiet, but that
* layer is not the one that denied the request, we should still audit
* log the denial.
* Skips denials the domain's policy or a quiet rule excludes from
* logging (folded into @logged by landlock_log_denial()).
*/
if (object_quiet_flag) {
/*
* We now check if the denied requests are all covered by the
* layer's quiet access bits.
*/
const access_mask_t quiet_mask =
pick_access_mask_for_request_type(
request->type, youngest_denied->quiet_masks);
quiet_applicable_to_access = (quiet_mask & missing) == missing;
} else {
/*
* Either the object is not quiet, or this is a scope request.
* We check request->type to distinguish between the two cases.
*/
const access_mask_t quiet_mask =
youngest_denied->quiet_masks.scope;
switch (request->type) {
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
quiet_applicable_to_access =
!!(quiet_mask & LANDLOCK_SCOPE_SIGNAL);
break;
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
quiet_applicable_to_access =
!!(quiet_mask &
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
break;
/*
* Leave LANDLOCK_REQUEST_PTRACE and
* LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they
* are never quiet.
*/
default:
break;
}
}
if (quiet_applicable_to_access)
if (!logged)
return;
/* Uses consistent allocation flags wrt common_lsm_audit(). */
@@ -675,23 +202,19 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
}
/**
* landlock_log_drop_domain - Create an audit record on domain deallocation
* landlock_audit_free_domain - Create an audit record on domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
* Only domains which previously appeared in the audit logs are logged again.
* This is useful to know when a domain will never show again in the audit log.
*
* Called in a work queue scheduled by landlock_put_ruleset_deferred() called
* by hook_cred_free().
* Called from landlock_log_free_domain().
*/
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
void landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
if (WARN_ON_ONCE(!hierarchy))
return;
if (!audit_enabled)
return;
@@ -712,23 +235,3 @@ void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
hierarchy->id, atomic64_read(&hierarchy->num_denials));
audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
/* clang-format off */
KUNIT_CASE(test_get_hierarchy),
KUNIT_CASE(test_get_denied_layer),
KUNIT_CASE(test_get_layer_from_deny_masks),
{}
/* clang-format on */
};
static struct kunit_suite test_suite = {
.name = "landlock_audit",
.test_cases = test_cases,
};
kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */

View File

@@ -8,66 +8,33 @@
#ifndef _SECURITY_LANDLOCK_AUDIT_H
#define _SECURITY_LANDLOCK_AUDIT_H
#include <linux/audit.h>
#include <linux/lsm_audit.h>
#include <linux/types.h>
#include "access.h"
#include "cred.h"
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
LANDLOCK_REQUEST_FS_ACCESS,
LANDLOCK_REQUEST_NET_ACCESS,
LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
LANDLOCK_REQUEST_SCOPE_SIGNAL,
};
/*
* We should be careful to only use a variable of this type for
* landlock_log_denial(). This way, the compiler can remove it entirely if
* CONFIG_AUDIT is not set.
*/
struct landlock_request {
/* Mandatory fields. */
enum landlock_request_type type;
struct common_audit_data audit;
/**
* layer_plus_one: First layer level that denies the request + 1. The
* extra one is useful to detect uninitialized field.
*/
size_t layer_plus_one;
/* Required field for configurable access control. */
access_mask_t access;
/* Required fields for requests with layer masks. */
const struct layer_masks *layer_masks;
/* Required fields for requests with deny masks. */
const access_mask_t all_existing_optional_access;
deny_masks_t deny_masks;
optional_access_t quiet_optional_accesses;
};
struct landlock_hierarchy;
struct landlock_request;
#ifdef CONFIG_AUDIT
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
void landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool logged);
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
void landlock_audit_free_domain(
const struct landlock_hierarchy *const hierarchy);
#else /* CONFIG_AUDIT */
static inline void
landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool logged)
{
}
static inline void
landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
{
}

View File

@@ -22,7 +22,7 @@ static void hook_cred_transfer(struct cred *const new,
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
landlock_get_ruleset(old_llcred->domain);
landlock_get_domain(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
@@ -35,13 +35,13 @@ static int hook_cred_prepare(struct cred *const new,
static void hook_cred_free(struct cred *const cred)
{
struct landlock_ruleset *const dom = landlock_cred(cred)->domain;
struct landlock_domain *const dom = landlock_cred(cred)->domain;
if (dom)
landlock_put_ruleset_deferred(dom);
landlock_put_domain_deferred(dom);
}
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
{
@@ -50,16 +50,16 @@ static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
return 0;
}
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
LSM_HOOK_INIT(cred_free, hook_cred_free),
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
};
__init void landlock_add_cred_hooks(void)

View File

@@ -16,6 +16,7 @@
#include <linux/rcupdate.h>
#include "access.h"
#include "domain.h"
#include "limits.h"
#include "ruleset.h"
#include "setup.h"
@@ -31,11 +32,11 @@
*/
struct landlock_cred_security {
/**
* @domain: Immutable ruleset enforced on a task.
* @domain: Immutable domain enforced on a task.
*/
struct landlock_ruleset *domain;
struct landlock_domain *domain;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
* the current task's executed file (i.e. no new execve(2) since
@@ -49,17 +50,17 @@ struct landlock_cred_security {
* not require a current domain.
*/
u8 log_subdomains_off : 1;
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
} __packed;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Makes sure all layer executions can be stored. */
static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security,
domain_exec)) >=
LANDLOCK_MAX_NUM_LAYERS);
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline struct landlock_cred_security *
landlock_cred(const struct cred *cred)
@@ -70,22 +71,20 @@ landlock_cred(const struct cred *cred)
static inline void landlock_cred_copy(struct landlock_cred_security *dst,
const struct landlock_cred_security *src)
{
landlock_put_ruleset(dst->domain);
landlock_put_domain(dst->domain);
*dst = *src;
landlock_get_ruleset(src->domain);
landlock_get_domain(src->domain);
}
static inline struct landlock_ruleset *landlock_get_current_domain(void)
static inline struct landlock_domain *landlock_get_current_domain(void)
{
return landlock_cred(current_cred())->domain;
}
/*
* The call needs to come from an RCU read-side critical section.
*/
static inline const struct landlock_ruleset *
/* The call needs to come from an RCU read-side critical section. */
static inline const struct landlock_domain *
landlock_get_task_domain(const struct task_struct *const task)
{
return landlock_cred(__task_cred(task))->domain;
@@ -126,7 +125,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
const union access_masks_all masks_all = {
.masks = masks,
};
const struct landlock_ruleset *domain;
const struct landlock_domain *domain;
ssize_t layer_level;
if (!cred)
@@ -139,7 +138,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
for (layer_level = domain->num_layers - 1; layer_level >= 0;
layer_level--) {
union access_masks_all layer = {
.masks = domain->access_masks[layer_level],
.masks = domain->handled_masks[layer_level],
};
if (layer.all & masks_all.all) {

View File

@@ -5,26 +5,486 @@
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#include <kunit/test.h>
#include <linux/bitops.h>
#include <linux/bits.h>
#include <linux/cleanup.h>
#include <linux/cred.h>
#include <linux/err.h>
#include <linux/file.h>
#include <linux/lockdep.h>
#include <linux/mm.h>
#include <linux/mutex.h>
#include <linux/overflow.h>
#include <linux/path.h>
#include <linux/pid.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
#include <linux/slab.h>
#include <linux/uidgid.h>
#include <linux/workqueue.h>
#include "access.h"
#include "common.h"
#include "domain.h"
#include "id.h"
#include "limits.h"
#include "ruleset.h"
#ifdef CONFIG_AUDIT
static void build_check_domain(void)
{
const struct landlock_domain domain = {
.num_layers = ~0,
};
BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS);
}
static struct landlock_domain *create_domain(const u32 num_layers)
{
struct landlock_domain *new_domain;
build_check_domain();
new_domain = kzalloc_flex(*new_domain, handled_masks, num_layers,
GFP_KERNEL_ACCOUNT);
if (!new_domain)
return ERR_PTR(-ENOMEM);
refcount_set(&new_domain->usage, 1);
new_domain->rules.root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
new_domain->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
new_domain->num_layers = num_layers;
return new_domain;
}
static void free_domain(struct landlock_domain *const domain)
{
might_sleep();
landlock_free_rules(&domain->rules);
landlock_put_hierarchy(domain->hierarchy);
kfree(domain);
}
void landlock_put_domain(struct landlock_domain *const domain)
{
might_sleep();
if (domain && refcount_dec_and_test(&domain->usage))
free_domain(domain);
}
static void free_domain_work(struct work_struct *const work)
{
struct landlock_domain *domain;
domain = container_of(work, struct landlock_domain, work_free);
free_domain(domain);
}
void landlock_put_domain_deferred(struct landlock_domain *const domain)
{
if (domain && refcount_dec_and_test(&domain->usage)) {
INIT_WORK(&domain->work_free, free_domain_work);
schedule_work(&domain->work_free);
}
}
/* The returned access has the same lifetime as the domain. */
static const struct landlock_rule *
find_rule(const struct landlock_domain *const domain,
const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
root = landlock_get_rule_root((struct landlock_rules *)&domain->rules,
id.type);
if (IS_ERR(root))
return NULL;
node = root->rb_node;
while (node) {
struct landlock_rule *this =
rb_entry(node, struct landlock_rule, node);
if (this->key.data == id.key.data)
return this;
if (this->key.data < id.key.data)
node = node->rb_right;
else
node = node->rb_left;
}
return NULL;
}
/**
* landlock_unmask_layers - Remove the access rights in @masks which are
* granted by a matching rule
*
* Looks up the rule matching @id in @domain, then updates the set of
* (per-layer) unfulfilled access rights @masks so that all the access rights
* granted by that rule are removed (because they are now fulfilled).
*
* @domain: The Landlock domain to search for a matching rule.
* @id: Identifier for the rule target (e.g. inode, port).
* @masks: A matrix of unfulfilled access rights for each layer.
* @matched_rule: Optional output for the matched rule (for tracing); set to
* the matching rule when non-NULL, unchanged otherwise.
*
* Return: True if the request is allowed (i.e. the access rights granted all
* remaining unfulfilled access rights and masks has no leftover set bits).
*/
bool landlock_unmask_layers(const struct landlock_domain *const domain,
const struct landlock_id id,
struct layer_masks *masks,
const struct landlock_rule **matched_rule)
{
const struct landlock_rule *rule;
if (!masks)
return true;
rule = find_rule(domain, id);
if (!rule)
return false;
if (matched_rule)
*matched_rule = rule;
/*
* An access is granted if, for each policy layer, at least one rule
* encountered on the pathwalk grants the requested access, regardless
* of its position in the layer stack. We must then check the remaining
* layers for each inode, from the first added layer to the last one.
* When there are multiple requested accesses, for each policy layer,
* the full set of requested accesses may not be granted by only one
* rule, but by the union (binary OR) of multiple rules. For example,
* /a/b <execute> + /a <read> grants /a/b <execute + read>.
*
* This function is called once per matching rule during the pathwalk,
* progressively clearing bits in @masks. The overall access decision
* is per-layer: access is granted iff masks->layers[l].access == 0 for
* all layers l. When two independent mechanisms can each grant access
* within a layer (e.g. a path rule OR a scope exception), the
* composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not
* (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when
* different layers grant via different mechanisms.
*/
for (size_t i = 0; i < rule->num_layers; i++) {
const struct landlock_layer *const layer = &rule->layers[i];
/* Clear the bits where the layer in the rule grants access. */
masks->layers[layer->level - 1].access &= ~layer->access;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Collect rule flags for each layer. */
if (layer->flags.quiet)
masks->layers[layer->level - 1].quiet = true;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
if (masks->layers[i].access)
return false;
}
return true;
}
typedef access_mask_t
get_access_mask_t(const struct landlock_domain *const domain,
const u16 layer_level);
/**
* landlock_init_layer_masks - Initialize layer masks from an access request
*
* Populates @masks such that for each access right in @access_request, the bits
* for all the layers are set where this access right is handled. Rule flags
* are also zeroed.
*
* @domain: The domain that defines the current restrictions.
* @access_request: The requested access rights to check.
* @masks: Layer access masks to populate.
* @key_type: The key type to switch between access masks of different types.
*
* Return: An access mask where each access right bit is set which is handled in
* any of the active layers in @domain.
*/
access_mask_t
landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_masks *const masks,
const enum landlock_key_type key_type)
{
access_mask_t handled_accesses = 0;
get_access_mask_t *get_access_mask;
switch (key_type) {
case LANDLOCK_KEY_INODE:
get_access_mask = landlock_get_fs_access_mask;
break;
#if IS_ENABLED(CONFIG_INET)
case LANDLOCK_KEY_NET_PORT:
get_access_mask = landlock_get_net_access_mask;
break;
#endif /* IS_ENABLED(CONFIG_INET) */
default:
WARN_ON_ONCE(1);
return 0;
}
/* An empty access request can happen because of O_WRONLY | O_RDWR. */
if (!access_request)
return 0;
for (size_t i = 0; i < domain->num_layers; i++) {
const access_mask_t handled = get_access_mask(domain, i);
masks->layers[i].access = access_request & handled;
handled_accesses |= masks->layers[i].access;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
masks->layers[i].quiet = false;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
i++) {
masks->layers[i].access = 0;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
masks->layers[i].quiet = false;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
return handled_accesses;
}
static int merge_tree(struct landlock_domain *const dst,
struct landlock_ruleset *const src,
const enum landlock_key_type key_type)
{
struct landlock_rule *walker_rule, *next_rule;
struct rb_root *src_root;
int err = 0;
might_sleep();
lockdep_assert_held(&src->lock);
src_root = landlock_get_rule_root(&src->rules, key_type);
if (IS_ERR(src_root))
return PTR_ERR(src_root);
/* Merges the @src tree. */
rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
node) {
struct landlock_layer layers[] = { {
.level = dst->num_layers,
} };
const struct landlock_id id = {
.key = walker_rule->key,
.type = key_type,
};
if (WARN_ON_ONCE(walker_rule->num_layers != 1))
return -EINVAL;
if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
return -EINVAL;
layers[0].access = walker_rule->layers[0].access;
layers[0].flags = walker_rule->layers[0].flags;
err = landlock_store_rule(&dst->rules, id, &layers,
ARRAY_SIZE(layers));
if (err)
return err;
}
return err;
}
static int merge_ruleset(struct landlock_domain *const dst,
struct landlock_ruleset *const src)
{
int err = 0;
might_sleep();
/* Should already be checked by landlock_merge_ruleset() */
if (WARN_ON_ONCE(!src))
return 0;
/* Only merge into a domain. */
if (WARN_ON_ONCE(!dst || !dst->hierarchy))
return -EINVAL;
lockdep_assert_held(&src->lock);
/* Stacks the new layer. */
if (WARN_ON_ONCE(dst->num_layers < 1))
return -EINVAL;
dst->handled_masks[dst->num_layers - 1] =
landlock_upgrade_handled_access_masks(src->handled_masks);
/* Merges the @src inode tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
if (err)
return err;
#if IS_ENABLED(CONFIG_INET)
/* Merges the @src network port tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
if (err)
return err;
#endif /* IS_ENABLED(CONFIG_INET) */
return 0;
}
static int inherit_tree(struct landlock_domain *const parent,
struct landlock_domain *const child,
const enum landlock_key_type key_type)
{
struct landlock_rule *walker_rule, *next_rule;
struct rb_root *parent_root;
int err = 0;
might_sleep();
parent_root = landlock_get_rule_root(&parent->rules, key_type);
if (IS_ERR(parent_root))
return PTR_ERR(parent_root);
/* Copies the @parent inode or network tree. */
rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
parent_root, node) {
const struct landlock_id id = {
.key = walker_rule->key,
.type = key_type,
};
err = landlock_store_rule(&child->rules, id,
&walker_rule->layers,
walker_rule->num_layers);
if (err)
return err;
}
return err;
}
static int inherit_ruleset(struct landlock_domain *const parent,
struct landlock_domain *const child)
{
int err = 0;
might_sleep();
if (!parent)
return 0;
/* Copies the @parent inode tree. */
err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
if (err)
return err;
#if IS_ENABLED(CONFIG_INET)
/* Copies the @parent network port tree. */
err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
if (err)
return err;
#endif /* IS_ENABLED(CONFIG_INET) */
if (WARN_ON_ONCE(child->num_layers <= parent->num_layers))
return -EINVAL;
/*
* Copies the parent layer stack and leaves a space for the new layer.
*/
memcpy(child->handled_masks, parent->handled_masks,
flex_array_size(parent, handled_masks, parent->num_layers));
if (WARN_ON_ONCE(!parent->hierarchy))
return -EINVAL;
landlock_get_hierarchy(parent->hierarchy);
child->hierarchy->parent = parent->hierarchy;
return 0;
}
/**
* landlock_merge_ruleset - Merge a ruleset with a domain
*
* @parent: Parent domain.
* @ruleset: New ruleset to be merged.
*
* The current task is requesting to be restricted. The subjective credentials
* must not be in an overridden state. cf. landlock_init_hierarchy_log().
*
* The caller must hold @ruleset->lock.
*
* Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
* failure. If @parent is NULL, the new domain duplicates @ruleset.
*/
struct landlock_domain *
landlock_merge_ruleset(struct landlock_domain *const parent,
struct landlock_ruleset *const ruleset)
{
struct landlock_domain *new_dom __free(landlock_put_domain) = NULL;
u32 num_layers;
int err;
might_sleep();
lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!ruleset))
return ERR_PTR(-EINVAL);
if (parent) {
if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
return ERR_PTR(-E2BIG);
num_layers = parent->num_layers + 1;
} else {
num_layers = 1;
}
/* Creates a new domain... */
new_dom = create_domain(num_layers);
if (IS_ERR(new_dom))
return new_dom;
new_dom->hierarchy =
kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
if (!new_dom->hierarchy)
return ERR_PTR(-ENOMEM);
refcount_set(&new_dom->hierarchy->usage, 1);
/* ...as a child of @parent... */
err = inherit_ruleset(parent, new_dom);
if (err)
return ERR_PTR(err);
/* ...and including @ruleset. */
err = merge_ruleset(new_dom, ruleset);
if (err)
return ERR_PTR(err);
err = landlock_init_hierarchy_log(new_dom->hierarchy);
if (err)
return ERR_PTR(err);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return no_free_ptr(new_dom);
}
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* get_current_exe - Get the current's executable path, if any
@@ -128,7 +588,13 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
hierarchy->details = details;
hierarchy->id = landlock_get_id_range(1);
hierarchy->log_status = LANDLOCK_LOG_PENDING;
/*
* The hierarchy is born unobservable: landlock_restrict_self() moves it
* out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation
* event, so the matching free_domain event fires for it and not for a
* hierarchy whose creation was never observed.
*/
hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED;
hierarchy->log_same_exec = true;
hierarchy->log_new_exec = false;
atomic64_set(&hierarchy->num_denials, 0);
@@ -306,4 +772,4 @@ kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */

View File

@@ -5,11 +5,13 @@
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
#include <linux/cleanup.h>
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/path.h>
@@ -17,12 +19,28 @@
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include "access.h"
#include "audit.h"
#include "log.h"
#include "ruleset.h"
enum landlock_log_status {
LANDLOCK_LOG_PENDING = 0,
/*
* Hierarchy whose creation event has not been emitted, so it is not yet
* observable from user space. A hierarchy is born in this state (the
* zero value, so a partially initialized hierarchy defaults to "not
* observable") and leaves it when landlock_restrict_self() emits its
* creation event, right after the merge and before the thread-sync
* wait. No trace free_domain event (and no audit deallocation record)
* fires while a hierarchy is in this state, so a hierarchy that never
* became observable (e.g. its initialization failed) is freed silently.
* A domain aborted by a thread-sync failure already emitted its
* creation event, so it is no longer UNCOMMITTED and does fire
* free_domain.
*/
LANDLOCK_LOG_UNCOMMITTED = 0,
LANDLOCK_LOG_PENDING,
LANDLOCK_LOG_RECORDED,
LANDLOCK_LOG_DISABLED,
};
@@ -81,7 +99,7 @@ struct landlock_hierarchy {
*/
refcount_t usage;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @log_status: Whether this domain should be logged or not. Because
* concurrent log entries may be created at the same time, it is still
@@ -116,10 +134,10 @@ struct landlock_hierarchy {
* logged) if the related object is marked as quiet.
*/
struct access_masks quiet_masks;
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
};
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
deny_masks_t
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
@@ -142,7 +160,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
kfree(hierarchy->details);
}
#else /* CONFIG_AUDIT */
#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline int
landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
@@ -155,7 +173,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
{
}
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void
landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy)
@@ -169,11 +187,140 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
landlock_log_drop_domain(hierarchy);
landlock_log_free_domain(hierarchy);
landlock_free_hierarchy_details(hierarchy);
hierarchy = hierarchy->parent;
kfree(freeme);
}
}
/**
* struct landlock_domain - Immutable Landlock domain
*
* A domain is created from a ruleset by landlock_merge_ruleset() and enforced
* on a task. Once created, its rules and access masks are immutable. Unlike
* &struct landlock_ruleset, a domain has no lock field.
*/
struct landlock_domain {
/**
* @rules: Red-black tree storage for rules.
*/
struct landlock_rules rules;
/**
* @hierarchy: Enables hierarchy identification even when a parent
* domain vanishes. This is needed for the ptrace and scope
* restrictions.
*/
struct landlock_hierarchy *hierarchy;
union {
/**
* @work_free: Enables to free a domain within a lockless
* section. This is only used by landlock_put_domain_deferred()
* when @usage reaches zero. The fields @usage, @num_layers and
* @handled_masks are then unused.
*/
struct work_struct work_free;
struct {
/**
* @usage: Number of credentials referencing this
* domain.
*/
refcount_t usage;
/**
* @num_layers: Number of layers that are used in this
* domain. This enables to check that all the layers
* allow an access request.
*/
u32 num_layers;
/**
* @handled_masks: Contains the subset of filesystem and
* network actions that are restricted by a domain. A
* domain saves all layers of merged rulesets in a stack
* (FAM), starting from the first layer to the last one.
* These layers are used when merging rulesets, for user
* space backward compatibility (i.e. future-proof), and
* to properly handle merged rulesets without
* overlapping access rights. These layers are set once
* and never changed for the lifetime of the domain.
*/
struct access_masks handled_masks[];
};
};
};
static inline access_mask_t
landlock_get_fs_access_mask(const struct landlock_domain *const domain,
const u16 layer_level)
{
/* Handles all initially denied by default access rights. */
return domain->handled_masks[layer_level].fs |
_LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
}
static inline access_mask_t
landlock_get_net_access_mask(const struct landlock_domain *const domain,
const u16 layer_level)
{
return domain->handled_masks[layer_level].net;
}
static inline access_mask_t
landlock_get_scope_mask(const struct landlock_domain *const domain,
const u16 layer_level)
{
return domain->handled_masks[layer_level].scope;
}
/**
* landlock_union_access_masks - Return all access rights handled in the
* domain
*
* @domain: Landlock domain
*
* Return: An access_masks result of the OR of all the domain's access masks.
*/
static inline struct access_masks
landlock_union_access_masks(const struct landlock_domain *const domain)
{
union access_masks_all matches = {};
size_t layer_level;
for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
union access_masks_all layer = {
.masks = domain->handled_masks[layer_level],
};
matches.all |= layer.all;
}
return matches.masks;
}
void landlock_put_domain(struct landlock_domain *const domain);
void landlock_put_domain_deferred(struct landlock_domain *const domain);
DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
struct landlock_domain *
landlock_merge_ruleset(struct landlock_domain *const parent,
struct landlock_ruleset *const ruleset);
bool landlock_unmask_layers(const struct landlock_domain *const domain,
const struct landlock_id id,
struct layer_masks *masks,
const struct landlock_rule **matched_rule);
access_mask_t
landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_masks *masks,
const enum landlock_key_type key_type);
static inline void landlock_get_domain(struct landlock_domain *const domain)
{
if (domain)
refcount_inc(&domain->usage);
}
#endif /* _SECURITY_LANDLOCK_DOMAIN_H */

View File

@@ -22,3 +22,26 @@
* from their original mount points.
*/
LANDLOCK_ERRATUM(3)
/**
* DOC: erratum_4
*
* Erratum 4: Creation of whiteout objects
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* This fix changes the access rights required for the creation of whiteout
* objects through :manpage:`mknod(2)`, :manpage:`renameat2(2)`, or
* :manpage:`link(2)`. Creating whiteout objects is now guarded by
* ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
*
* Whiteout objects are used in OverlayFS to mark the absence of a file in an
* upper file system. Despite being created with ``S_IFCHR``, whiteout objects
* do not count as character devices.
*
* Impact:
*
* Sandboxed programs that create OverlayFS whiteouts (such as fuse-overlayfs)
* now require ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of
* ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
*/
LANDLOCK_ERRATUM(4)

View File

@@ -20,6 +20,7 @@
#include <linux/falloc.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kdev_t.h>
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/list.h>
@@ -42,16 +43,18 @@
#include <uapi/linux/landlock.h>
#include "access.h"
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "limits.h"
#include "log.h"
#include "object.h"
#include "ruleset.h"
#include "setup.h"
#include <trace/events/landlock.h>
/* Underlying object management */
static void release_inode(struct landlock_object *const object)
@@ -336,18 +339,34 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (!d_is_dir(path->dentry) &&
!access_mask_subset(access_rights, ACCESS_FILE))
return -EINVAL;
if (WARN_ON_ONCE(ruleset->num_layers != 1))
return -EINVAL;
/* Transforms relative access rights to absolute ones. */
access_rights |= LANDLOCK_MASK_ACCESS_FS &
~landlock_get_fs_access_mask(ruleset, 0);
~(ruleset->handled_masks.fs |
_LANDLOCK_ACCESS_FS_INITIALLY_DENIED);
id.key.object = get_inode_object(d_backing_inode(path->dentry));
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
/*
* Emit after the rule insertion succeeds, so every event corresponds to
* a rule that is actually in the ruleset. The ruleset lock is still
* held for BTF consistency (enforced by lockdep_assert_held in
* TP_fast_assign).
*/
if (!err && trace_landlock_add_rule_fs_enabled()) {
char *buffer __free(__putname) = __getname();
const char *pathname =
buffer ? resolve_path_for_trace(path, buffer) :
"<no_mem>";
trace_landlock_add_rule_fs(ruleset, access_rights, path,
pathname);
}
mutex_unlock(&ruleset->lock);
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
@@ -358,31 +377,55 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
/* Access-control management */
/*
* The lifetime of the returned rule is tied to @domain.
/**
* get_inode_id - Look up the Landlock object for a dentry
* @dentry: The dentry to look up.
* @id: Filled with the inode's Landlock object pointer on success.
*
* Returns NULL if no rule is found or if @dentry is negative.
* Extracts the Landlock object pointer from @dentry's inode security blob and
* stores it in @id for use as a rule-tree lookup key.
*
* When this returns false (negative dentry or no Landlock object), no rule can
* match this inode, so landlock_unmask_layers() need not be called. Callers
* that gate landlock_unmask_layers() on this function must handle the NULL
* masks case independently, since the !masks-returns-true early-return in
* landlock_unmask_layers() will not be reached. See the allowed_parent2
* initialization in is_access_to_paths_allowed().
*
* Return: True if a Landlock object exists for @dentry, false otherwise.
*/
static const struct landlock_rule *
find_rule(const struct landlock_ruleset *const domain,
const struct dentry *const dentry)
static bool get_inode_id(const struct dentry *const dentry,
struct landlock_id *id)
{
const struct landlock_rule *rule;
const struct inode *inode;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
/* Ignores nonexistent leafs. */
if (d_is_negative(dentry))
return NULL;
return false;
inode = d_backing_inode(dentry);
rcu_read_lock();
id.key.object = rcu_dereference(landlock_inode(inode)->object);
rule = landlock_find_rule(domain, id);
rcu_read_unlock();
return rule;
/*
* rcu_access_pointer() is sufficient: the pointer is used only as a
* numeric comparison key for rule lookup, not dereferenced. The object
* cannot be freed while the domain exists because the domain's rule
* tree holds its own reference to it.
*/
id->key.object = rcu_access_pointer(
landlock_inode(d_backing_inode(dentry))->object);
return !!id->key.object;
}
static bool unmask_layers_fs(const struct landlock_domain *const domain,
const struct landlock_id id,
const access_mask_t access_request,
struct layer_masks *masks,
const struct dentry *const dentry)
{
const struct landlock_rule *rule = NULL;
bool ret;
ret = landlock_unmask_layers(domain, id, masks, &rule);
if (rule)
trace_landlock_check_rule_fs(domain, rule, access_request,
dentry);
return ret;
}
/*
@@ -749,7 +792,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* Return: True if the access request is granted, false otherwise.
*/
static bool
is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
is_access_to_paths_allowed(const struct landlock_domain *const domain,
const struct path *const path,
const access_mask_t access_request_parent1,
struct layer_masks *layer_masks_parent1,
@@ -763,6 +806,9 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
child1_is_directory = true, child2_is_directory = true;
struct path walker_path;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
access_mask_t access_masked_parent1, access_masked_parent2;
struct layer_masks _layer_masks_child1, _layer_masks_child2;
struct layer_masks *layer_masks_child1 = NULL,
@@ -802,28 +848,46 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
/* For a simple request, only check for requested accesses. */
access_masked_parent1 = access_request_parent1;
access_masked_parent2 = access_request_parent2;
/*
* Simple requests have no parent2 to check, so parent2 is
* trivially allowed. This must be set explicitly because the
* get_inode_id() gate in the pathwalk loop may prevent
* landlock_unmask_layers() from being called (which would
* otherwise return true for NULL masks as a side effect).
*/
allowed_parent2 = true;
is_dom_check = false;
}
if (unlikely(dentry_child1)) {
/*
* Get the layer masks for the child dentries for use by domain
* check later.
*/
if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
&_layer_masks_child1,
LANDLOCK_KEY_INODE))
landlock_unmask_layers(find_rule(domain, dentry_child1),
&_layer_masks_child1);
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
access_mask_t handled;
handled = landlock_init_layer_masks(domain,
LANDLOCK_MASK_ACCESS_FS,
&_layer_masks_child1,
LANDLOCK_KEY_INODE);
if (handled && get_inode_id(dentry_child1, &id))
unmask_layers_fs(domain, id, handled,
&_layer_masks_child1, dentry_child1);
layer_masks_child1 = &_layer_masks_child1;
child1_is_directory = d_is_dir(dentry_child1);
}
if (unlikely(dentry_child2)) {
if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
&_layer_masks_child2,
LANDLOCK_KEY_INODE))
landlock_unmask_layers(find_rule(domain, dentry_child2),
&_layer_masks_child2);
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
access_mask_t handled;
handled = landlock_init_layer_masks(domain,
LANDLOCK_MASK_ACCESS_FS,
&_layer_masks_child2,
LANDLOCK_KEY_INODE);
if (handled && get_inode_id(dentry_child2, &id))
unmask_layers_fs(domain, id, handled,
&_layer_masks_child2, dentry_child2);
layer_masks_child2 = &_layer_masks_child2;
child2_is_directory = d_is_dir(dentry_child2);
}
@@ -835,8 +899,6 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
* restriction.
*/
while (true) {
const struct landlock_rule *rule;
/*
* If at least all accesses allowed on the destination are
* already allowed on the source, respectively if there is at
@@ -877,13 +939,20 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
break;
}
rule = find_rule(domain, walker_path.dentry);
allowed_parent1 =
allowed_parent1 ||
landlock_unmask_layers(rule, layer_masks_parent1);
allowed_parent2 =
allowed_parent2 ||
landlock_unmask_layers(rule, layer_masks_parent2);
if (get_inode_id(walker_path.dentry, &id)) {
allowed_parent1 =
allowed_parent1 ||
unmask_layers_fs(domain, id,
access_masked_parent1,
layer_masks_parent1,
walker_path.dentry);
allowed_parent2 =
allowed_parent2 ||
unmask_layers_fs(domain, id,
access_masked_parent2,
layer_masks_parent2,
walker_path.dentry);
}
/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2)
@@ -933,10 +1002,11 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
path_put(&walker_path);
/*
* Check CONFIG_AUDIT to enable elision of log_request_parent* and
* associated caller's stack variables thanks to dead code elimination.
* Check CONFIG_SECURITY_LANDLOCK_LOG to enable elision of
* log_request_parent* and associated caller's stack variables thanks to
* dead code elimination.
*/
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (!allowed_parent1 && log_request_parent1) {
log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS;
log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH;
@@ -952,7 +1022,7 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
log_request_parent2->access = access_masked_parent2;
log_request_parent2->layer_masks = layer_masks_parent2;
}
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return allowed_parent1 && allowed_parent2;
}
@@ -983,7 +1053,8 @@ static int current_check_access_path(const struct path *const path,
return -EACCES;
}
static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
static __attribute_const__ access_mask_t get_mode_access(const umode_t mode,
const dev_t dev)
{
switch (mode & S_IFMT) {
case S_IFLNK:
@@ -991,6 +1062,9 @@ static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
case S_IFDIR:
return LANDLOCK_ACCESS_FS_MAKE_DIR;
case S_IFCHR:
/* Whiteout objects are guarded with MAKE_REG. */
if (dev == WHITEOUT_DEV)
return LANDLOCK_ACCESS_FS_MAKE_REG;
return LANDLOCK_ACCESS_FS_MAKE_CHAR;
case S_IFBLK:
return LANDLOCK_ACCESS_FS_MAKE_BLOCK;
@@ -1007,6 +1081,13 @@ static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
}
}
static access_mask_t get_dentry_access(const struct dentry *const dentry)
{
const struct inode *const inode = d_backing_inode(dentry);
return get_mode_access(inode->i_mode, inode->i_rdev);
}
static access_mask_t maybe_remove(const struct dentry *const dentry)
{
if (d_is_negative(dentry))
@@ -1039,29 +1120,36 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
* Return: True if all the domain access rights are allowed for @dir, false if
* the walk reached @mnt_root.
*/
static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
static bool collect_domain_accesses(const struct landlock_domain *const domain,
const struct dentry *const mnt_root,
struct dentry *dir,
struct layer_masks *layer_masks_dom)
{
bool ret = false;
access_mask_t access_masked_dom;
if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
return true;
if (is_nouser_or_private(dir))
return true;
if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
layer_masks_dom, LANDLOCK_KEY_INODE))
access_masked_dom =
landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
layer_masks_dom, LANDLOCK_KEY_INODE);
if (!access_masked_dom)
return true;
dget(dir);
while (true) {
struct dentry *parent_dentry;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
/* Gets all layers allowing all domain accesses. */
if (landlock_unmask_layers(find_rule(domain, dir),
layer_masks_dom)) {
if (get_inode_id(dir, &id) &&
unmask_layers_fs(domain, id, access_masked_dom,
layer_masks_dom, dir)) {
/*
* Stops when all handled accesses are allowed by at
* least one rule in each layer.
@@ -1093,6 +1181,7 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
* @new_dentry: Destination file or directory.
* @removable: Sets to true if it is a rename operation.
* @exchange: Sets to true if it is a rename operation with RENAME_EXCHANGE.
* @whiteout: Sets to true if it is a rename operation with RENAME_WHITEOUT.
*
* Because of its unprivileged constraints, Landlock relies on file hierarchies
* (and not only inodes) to tie access rights to files. Being able to link or
@@ -1140,7 +1229,8 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
static int current_check_refer_path(struct dentry *const old_dentry,
const struct path *const new_dir,
struct dentry *const new_dentry,
const bool removable, const bool exchange)
const bool removable, const bool exchange,
const bool whiteout)
{
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), any_fs, NULL);
@@ -1159,18 +1249,25 @@ static int current_check_refer_path(struct dentry *const old_dentry,
if (exchange) {
if (unlikely(d_is_negative(new_dentry)))
return -ENOENT;
access_request_parent1 =
get_mode_access(d_backing_inode(new_dentry)->i_mode);
access_request_parent1 = get_dentry_access(new_dentry);
} else {
access_request_parent1 = 0;
}
access_request_parent2 =
get_mode_access(d_backing_inode(old_dentry)->i_mode);
access_request_parent2 = get_dentry_access(old_dentry);
if (removable) {
access_request_parent1 |= maybe_remove(old_dentry);
access_request_parent2 |= maybe_remove(new_dentry);
}
/*
* In case of renameat2(2) with RENAME_WHITEOUT, a whiteout object is
* created in the source location, so we require an additional access
* right there.
*/
if (whiteout)
access_request_parent1 |=
get_mode_access(S_IFCHR | WHITEOUT_MODE, WHITEOUT_DEV);
/* The mount points are the same for old and new paths, cf. EXDEV. */
if (old_dentry->d_parent == new_dir->dentry) {
/*
@@ -1520,7 +1617,7 @@ static int hook_path_link(struct dentry *const old_dentry,
struct dentry *const new_dentry)
{
return current_check_refer_path(old_dentry, new_dir, new_dentry, false,
false);
false, false);
}
static int hook_path_rename(const struct path *const old_dir,
@@ -1531,7 +1628,8 @@ static int hook_path_rename(const struct path *const old_dir,
{
/* old_dir refers to old_dentry->d_parent and new_dir->mnt */
return current_check_refer_path(old_dentry, new_dir, new_dentry, true,
!!(flags & RENAME_EXCHANGE));
!!(flags & RENAME_EXCHANGE),
!!(flags & RENAME_WHITEOUT));
}
static int hook_path_mkdir(const struct path *const dir,
@@ -1544,7 +1642,8 @@ static int hook_path_mknod(const struct path *const dir,
struct dentry *const dentry, const umode_t mode,
const unsigned int dev)
{
return current_check_access_path(dir, get_mode_access(mode));
return current_check_access_path(
dir, get_mode_access(mode, new_decode_dev(dev)));
}
static int hook_path_symlink(const struct path *const dir,
@@ -1589,8 +1688,8 @@ static int hook_path_truncate(const struct path *const path)
* @masks: Layer access masks to unmask
* @access: Access bits that control scoping
*/
static void unmask_scoped_access(const struct landlock_ruleset *const client,
const struct landlock_ruleset *const server,
static void unmask_scoped_access(const struct landlock_domain *const client,
const struct landlock_domain *const server,
struct layer_masks *const masks,
const access_mask_t access)
{
@@ -1644,7 +1743,7 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
static int hook_unix_find(const struct path *const path, struct sock *other,
int flags)
{
const struct landlock_ruleset *dom_other;
const struct landlock_domain *dom_other;
const struct landlock_cred_security *subject;
struct layer_masks layer_masks;
struct landlock_request request = {};
@@ -1802,14 +1901,14 @@ static int hook_file_open(struct file *const file)
* file access rights in the opened struct file.
*/
landlock_file(file)->allowed_access = allowed_access;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
landlock_file(file)->deny_masks = landlock_get_deny_masks(
_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks);
landlock_file(file)->quiet_optional_accesses =
landlock_get_quiet_optional_accesses(
_LANDLOCK_ACCESS_FS_OPTIONAL,
landlock_file(file)->deny_masks, &layer_masks);
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
if (access_mask_subset(open_access_request, allowed_access))
return 0;
@@ -1843,10 +1942,10 @@ static int hook_file_truncate(struct file *const file)
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_TRUNCATE,
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.deny_masks = landlock_file(file)->deny_masks,
.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
});
return -EACCES;
}
@@ -1883,10 +1982,10 @@ static int hook_file_ioctl_common(const struct file *const file,
},
.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.deny_masks = landlock_file(file)->deny_masks,
.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
});
return -EACCES;
}
@@ -1939,7 +2038,7 @@ static bool control_current_fowner(struct fown_struct *const fown)
static void hook_file_set_fowner(struct file *file)
{
struct landlock_ruleset *prev_dom;
struct landlock_domain *prev_dom;
struct landlock_cred_security fown_subject = {};
struct pid *prev_tg, *fown_tg = NULL;
size_t fown_layer = 0;
@@ -1952,7 +2051,7 @@ static void hook_file_set_fowner(struct file *file)
landlock_get_applicable_subject(
current_cred(), signal_scope, &fown_layer);
if (new_subject) {
landlock_get_ruleset(new_subject->domain);
landlock_get_domain(new_subject->domain);
fown_subject = *new_subject;
fown_tg = get_pid(task_tgid(current));
}
@@ -1962,19 +2061,19 @@ static void hook_file_set_fowner(struct file *file)
prev_tg = landlock_file(file)->fown_tg;
landlock_file(file)->fown_subject = fown_subject;
landlock_file(file)->fown_tg = fown_tg;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
landlock_file(file)->fown_layer = fown_layer;
#endif /* CONFIG_AUDIT*/
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/* May be called in an RCU read-side critical section. */
landlock_put_ruleset_deferred(prev_dom);
landlock_put_domain_deferred(prev_dom);
put_pid(prev_tg);
}
static void hook_file_free_security(struct file *file)
{
put_pid(landlock_file(file)->fown_tg);
landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
landlock_put_domain_deferred(landlock_file(file)->fown_subject.domain);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {

View File

@@ -11,6 +11,7 @@
#define _SECURITY_LANDLOCK_FS_H
#include <linux/build_bug.h>
#include <linux/cleanup.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
@@ -20,6 +21,8 @@
#include "ruleset.h"
#include "setup.h"
DEFINE_FREE(__putname, char *, if (_T) __putname(_T))
/**
* struct landlock_inode_security - Inode security blob
*
@@ -57,7 +60,7 @@ struct landlock_file_security {
*/
access_mask_t allowed_access;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @deny_masks: Domain layer levels that deny an optional access (see
* _LANDLOCK_ACCESS_FS_OPTIONAL).
@@ -75,7 +78,7 @@ struct landlock_file_security {
* LANDLOCK_SCOPE_SIGNAL.
*/
u8 fown_layer;
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/**
* @fown_subject: Landlock credential of the task that set the PID that
@@ -97,7 +100,7 @@ struct landlock_file_security {
struct pid *fown_tg;
};
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Makes sure all layers can be identified. */
/* clang-format off */
@@ -113,7 +116,7 @@ static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security,
quiet_optional_accesses)) >=
HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/**
* struct landlock_superblock_security - Superblock security blob
@@ -153,4 +156,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_hierarchy, const u32 flags);
/**
* resolve_path_for_trace - Resolve a path for tracepoint display
*
* @path: The path to resolve.
* @buf: A buffer of at least PATH_MAX bytes for the resolved path.
*
* Uses d_absolute_path() to produce a namespace-independent absolute path,
* unlike d_path() which resolves relative to the process's chroot. This
* ensures trace output is deterministic regardless of the tracer's mount
* namespace.
*
* Return: A pointer into @buf with the resolved path, or an error string
* ("<too_long>", "<unreachable>").
*/
static inline const char *resolve_path_for_trace(const struct path *path,
char *buf)
{
const char *p;
p = d_absolute_path(path, buf, PATH_MAX);
if (!IS_ERR_OR_NULL(p))
return p;
if (PTR_ERR(p) == -ENAMETOOLONG)
return "<too_long>";
return "<unreachable>";
}
#endif /* _SECURITY_LANDLOCK_FS_H */

View File

@@ -8,18 +8,18 @@
#ifndef _SECURITY_LANDLOCK_ID_H
#define _SECURITY_LANDLOCK_ID_H
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
void __init landlock_init_id(void);
u64 landlock_get_id_range(size_t number_of_ids);
#else /* CONFIG_AUDIT */
#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void __init landlock_init_id(void)
{
}
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* _SECURITY_LANDLOCK_ID_H */

View File

@@ -34,7 +34,7 @@
#define LANDLOCK_NUM_ACCESS_MAX \
MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE)
#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC
#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
/* clang-format on */

587
security/landlock/log.c Normal file
View File

@@ -0,0 +1,587 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#include <kunit/test.h>
#include <linux/bitops.h>
#include <uapi/linux/landlock.h>
#include "access.h"
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "limits.h"
#include "log.h"
#include "ruleset.h"
#include "trace.h"
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
{
struct landlock_hierarchy *hierarchy = domain->hierarchy;
ssize_t i;
if (WARN_ON_ONCE(layer >= domain->num_layers))
return hierarchy;
for (i = domain->num_layers - 1; i > layer; i--) {
if (WARN_ON_ONCE(!hierarchy->parent))
break;
hierarchy = hierarchy->parent;
}
return hierarchy;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_hierarchy(struct kunit *const test)
{
struct landlock_hierarchy dom0_hierarchy = {
.id = 10,
};
struct landlock_hierarchy dom1_hierarchy = {
.parent = &dom0_hierarchy,
.id = 20,
};
struct landlock_hierarchy dom2_hierarchy = {
.parent = &dom1_hierarchy,
.id = 30,
};
struct landlock_domain dom2 = {
.hierarchy = &dom2_hierarchy,
.num_layers = 3,
};
KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
/* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
/* Get the youngest layer that denied the access_request. */
static size_t get_denied_layer(const struct landlock_domain *const domain,
access_mask_t *const access_request,
const struct layer_masks *masks)
{
for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
if (masks->layers[i].access & *access_request) {
*access_request &= masks->layers[i].access;
return i;
}
}
/* Not found - fall back to default values */
*access_request = 0;
return domain->num_layers - 1;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_denied_layer(struct kunit *const test)
{
const struct landlock_domain dom = {
.num_layers = 5,
};
const struct layer_masks masks = {
.layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_DIR,
.layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
};
access_mask_t access;
access = LANDLOCK_ACCESS_FS_EXECUTE;
KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
access = LANDLOCK_ACCESS_FS_READ_FILE;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
access = LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
access = LANDLOCK_ACCESS_FS_WRITE_FILE;
KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
KUNIT_EXPECT_EQ(test, access, 0);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static size_t
get_layer_from_deny_masks(access_mask_t *const access_request,
const access_mask_t all_existing_optional_access,
const deny_masks_t deny_masks,
optional_access_t quiet_optional_accesses,
bool *quiet)
{
const unsigned long access_opt = all_existing_optional_access;
const unsigned long access_req = *access_request;
access_mask_t missing = 0;
size_t youngest_layer = 0;
size_t access_index = 0;
unsigned long access_bit;
bool should_quiet = false;
/* This will require change with new object types. */
WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
for_each_set_bit(access_bit, &access_opt,
BITS_PER_TYPE(access_mask_t)) {
if (access_req & BIT(access_bit)) {
const size_t layer =
(deny_masks >>
(access_index *
HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
(LANDLOCK_MAX_NUM_LAYERS - 1);
const bool layer_has_quiet =
!!(quiet_optional_accesses & BIT(access_index));
if (layer > youngest_layer) {
youngest_layer = layer;
missing = BIT(access_bit);
should_quiet = layer_has_quiet;
} else if (layer == youngest_layer) {
missing |= BIT(access_bit);
/*
* Whether the layer has rules with quiet flag
* covering the file accessed does not depend on
* the access, and so the following
* WARN_ON_ONCE() should not fail.
*/
WARN_ON_ONCE(should_quiet && !layer_has_quiet);
should_quiet = layer_has_quiet;
}
}
access_index++;
}
*access_request = missing;
*quiet = should_quiet;
return youngest_layer;
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static void test_get_layer_from_deny_masks(struct kunit *const test)
{
deny_masks_t deny_mask;
access_mask_t access;
optional_access_t quiet_optional_accesses;
bool quiet;
/* truncate:0 ioctl_dev:2 */
deny_mask = 0x20;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* layer denying truncate: quiet, ioctl: not quiet */
quiet_optional_accesses = 0b01;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* Reverse order - truncate:2 ioctl_dev:0 */
deny_mask = 0x02;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
/* layer denying truncate: quiet, ioctl: not quiet */
quiet_optional_accesses = 0b01;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
/* layer denying truncate: not quiet, ioctl: quiet */
quiet_optional_accesses = 0b10;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 0,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 2,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
/* truncate:15 ioctl_dev:15 */
deny_mask = 0xff;
quiet_optional_accesses = 0;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, false);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, false);
/* Both quiet (same layer so quietness must be the same) */
quiet_optional_accesses = 0b11;
access = LANDLOCK_ACCESS_FS_TRUNCATE;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
KUNIT_EXPECT_EQ(test, quiet, true);
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
KUNIT_EXPECT_EQ(test, 15,
get_layer_from_deny_masks(
&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
deny_mask, quiet_optional_accesses, &quiet));
KUNIT_EXPECT_EQ(test, access,
LANDLOCK_ACCESS_FS_TRUNCATE |
LANDLOCK_ACCESS_FS_IOCTL_DEV);
KUNIT_EXPECT_EQ(test, quiet, true);
}
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
static bool is_valid_request(const struct landlock_request *const request)
{
if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
return false;
if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
return false;
if (request->access) {
if (WARN_ON_ONCE(!(!!request->layer_masks ^
!!request->all_existing_optional_access)))
return false;
} else {
if (WARN_ON_ONCE(request->layer_masks ||
request->all_existing_optional_access))
return false;
}
if (request->deny_masks) {
if (WARN_ON_ONCE(!request->all_existing_optional_access))
return false;
static_assert(sizeof(request->all_existing_optional_access) ==
sizeof(u32));
if (WARN_ON_ONCE(
request->quiet_optional_accesses >=
BIT(hweight32(
request->all_existing_optional_access))))
return false;
}
return true;
}
static access_mask_t
pick_access_mask_for_request_type(const enum landlock_request_type type,
const struct access_masks access_masks)
{
switch (type) {
case LANDLOCK_REQUEST_FS_ACCESS:
return access_masks.fs;
case LANDLOCK_REQUEST_NET_ACCESS:
return access_masks.net;
default:
WARN_ONCE(1, "Invalid request type %d passed to %s", type,
__func__);
return 0;
}
}
/*
* Whether a quiet rule silences the denial: the rule must cover the whole
* denied access in the layer that denied it (a quiet rule in a non-denying
* layer does not suppress the denial).
*/
static bool
is_denial_quieted(const struct landlock_request *const request,
const struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool object_quiet_flag)
{
if (object_quiet_flag) {
const access_mask_t quiet_mask =
pick_access_mask_for_request_type(
request->type, youngest_denied->quiet_masks);
return (quiet_mask & missing) == missing;
}
/*
* Either the object is not quiet, or this is a scope request. We check
* request->type to distinguish between the two cases.
*/
switch (request->type) {
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
return !!(youngest_denied->quiet_masks.scope &
LANDLOCK_SCOPE_SIGNAL);
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
return !!(youngest_denied->quiet_masks.scope &
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
/*
* Leave LANDLOCK_REQUEST_PTRACE and LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY
* unhandled for now - they are never quiet.
*/
default:
return false;
}
}
/*
* Computes whether a denial from youngest_denied is selected for logging by the
* domain's policy: its logging must not be disabled (by both per-execution
* flags being off, or by an ancestor's
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF), the per-execution flag matching
* same_exec must be set, and no quiet rule may cover the denied access.
* landlock_log_denial() computes this once and passes it to
* landlock_audit_denial(), which additionally requires audit_enabled.
*/
static bool
is_denial_logged(const struct landlock_request *const request,
const struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool same_exec,
const bool object_quiet_flag)
{
if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
return false;
if (!(same_exec ? youngest_denied->log_same_exec :
youngest_denied->log_new_exec))
return false;
return !is_denial_quieted(request, youngest_denied, missing,
object_quiet_flag);
}
/**
* landlock_log_denial - Log a denied access
*
* @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
*/
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
{
struct landlock_hierarchy *youngest_denied;
size_t youngest_layer;
access_mask_t missing;
bool object_quiet_flag = false;
if (WARN_ON_ONCE(!subject || !subject->domain ||
!subject->domain->hierarchy || !request))
return;
if (!is_valid_request(request))
return;
missing = request->access;
if (missing) {
/* Gets the nearest domain that denies the request. */
if (request->layer_masks) {
youngest_layer = get_denied_layer(subject->domain,
&missing,
request->layer_masks);
object_quiet_flag =
request->layer_masks->layers[youngest_layer]
.quiet;
} else {
youngest_layer = get_layer_from_deny_masks(
&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
request->deny_masks,
request->quiet_optional_accesses,
&object_quiet_flag);
}
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
} else {
youngest_layer = request->layer_plus_one - 1;
youngest_denied =
get_hierarchy(subject->domain, youngest_layer);
}
const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer));
const bool logged = is_denial_logged(request, youngest_denied, missing,
same_exec, object_quiet_flag);
/*
* Consistently keeps track of the number of denied access requests even
* if audit is currently disabled, or if audit rules currently exclude
* this record type, or if landlock_restrict_self(2)'s flags quiet logs.
*/
atomic64_inc(&youngest_denied->num_denials);
landlock_trace_denial(request, youngest_denied, missing, same_exec,
logged);
landlock_audit_denial(request, youngest_denied, missing, logged);
}
/**
* landlock_log_free_domain - Log domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
* Called from landlock_put_domain_deferred() (via a work queue scheduled by
* hook_cred_free()) or directly from landlock_put_domain().
*/
void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
{
if (WARN_ON_ONCE(!hierarchy))
return;
landlock_trace_free_domain(hierarchy);
landlock_audit_free_domain(hierarchy);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
/* clang-format off */
KUNIT_CASE(test_get_hierarchy),
KUNIT_CASE(test_get_denied_layer),
KUNIT_CASE(test_get_layer_from_deny_masks),
{}
/* clang-format on */
};
static struct kunit_suite test_suite = {
.name = "landlock_log",
.test_cases = test_cases,
};
kunit_test_suite(test_suite);
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */

86
security/landlock/log.h Normal file
View File

@@ -0,0 +1,86 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_LOG_H
#define _SECURITY_LANDLOCK_LOG_H
#include <linux/lsm_audit.h>
#include "access.h"
struct landlock_cred_security;
struct landlock_hierarchy;
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
LANDLOCK_REQUEST_FS_ACCESS,
LANDLOCK_REQUEST_NET_ACCESS,
LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
LANDLOCK_REQUEST_SCOPE_SIGNAL,
};
/*
* We should be careful to only use a variable of this type for
* landlock_log_denial(). This way, the compiler can remove it entirely if
* CONFIG_SECURITY_LANDLOCK_LOG is not set.
*/
struct landlock_request {
/* Mandatory fields. */
enum landlock_request_type type;
struct common_audit_data audit;
/**
* layer_plus_one: First layer level that denies the request + 1. The
* extra one is useful to detect uninitialized field.
*/
size_t layer_plus_one;
/* Required field for configurable access control. */
access_mask_t access;
/* Required fields for requests with layer masks. */
const struct layer_masks *layer_masks;
/* Required fields for requests with deny masks. */
const access_mask_t all_existing_optional_access;
deny_masks_t deny_masks;
optional_access_t quiet_optional_accesses;
/*
* Other-party domain ID for a relational (scope/ptrace) denial, or 0 if
* that party is unsandboxed. An ID, not a pointer: the other task can
* replace its credential and free the domain it referenced. Trace path
* only; audit ignores it.
*/
u64 other_domain_id;
};
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy);
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void
landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
{
}
static inline void
landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
{
}
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* _SECURITY_LANDLOCK_LOG_H */

View File

@@ -12,13 +12,16 @@
#include <linux/socket.h>
#include <net/ipv6.h>
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "limits.h"
#include "log.h"
#include "net.h"
#include "ruleset.h"
#include <trace/events/landlock.h>
int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights,
const u32 flags)
@@ -32,16 +35,40 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
/* Transforms relative access rights to absolute ones. */
access_rights |= LANDLOCK_MASK_ACCESS_NET &
~landlock_get_net_access_mask(ruleset, 0);
access_rights |= LANDLOCK_MASK_ACCESS_NET & ~ruleset->handled_masks.net;
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
/*
* Emit after the rule insertion succeeds, so every event corresponds to
* a rule that is actually in the ruleset. The ruleset lock is still
* held for BTF consistency (enforced by lockdep_assert_held in
* TP_fast_assign).
*/
if (!err)
trace_landlock_add_rule_net(ruleset, access_rights, port);
mutex_unlock(&ruleset->lock);
return err;
}
static bool unmask_layers_net(const struct landlock_domain *const domain,
const struct landlock_id id,
struct layer_masks *masks,
access_mask_t access_request)
{
const struct landlock_rule *rule = NULL;
bool ret;
ret = landlock_unmask_layers(domain, id, masks, &rule);
if (rule)
trace_landlock_check_rule_net(
domain, rule, access_request,
ntohs((__force __be16)id.key.data));
return ret;
}
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
@@ -51,7 +78,6 @@ static int current_check_access_socket(struct socket *const sock,
unsigned short sock_family;
__be16 port;
struct layer_masks layer_masks = {};
const struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
@@ -237,14 +263,14 @@ static int current_check_access_socket(struct socket *const sock,
id.key.data = (__force uintptr_t)port;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
rule = landlock_find_rule(subject->domain, id);
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_NET_PORT);
if (!access_request)
return 0;
if (landlock_unmask_layers(rule, &layer_masks))
if (unmask_layers_net(subject->domain, id, &layer_masks,
access_request))
return 0;
audit_net.family = address->sa_family;

View File

@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2026 Cloudflare, Inc.
*/
#include <linux/bits.h>
@@ -20,39 +21,15 @@
#include <linux/refcount.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
#include "access.h"
#include "domain.h"
#include "id.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
static struct landlock_ruleset *create_ruleset(const u32 num_layers)
{
struct landlock_ruleset *new_ruleset;
new_ruleset = kzalloc_flex(*new_ruleset, access_masks, num_layers,
GFP_KERNEL_ACCOUNT);
if (!new_ruleset)
return ERR_PTR(-ENOMEM);
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
new_ruleset->root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
new_ruleset->root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
new_ruleset->num_layers = num_layers;
/*
* hierarchy = NULL
* num_rules = 0
* access_masks[] = 0
*/
return new_ruleset;
}
#include <trace/events/landlock.h>
struct landlock_ruleset *
landlock_create_ruleset(const access_mask_t fs_access_mask,
@@ -64,15 +41,44 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
/* Informs about useless ruleset. */
if (!fs_access_mask && !net_access_mask && !scope_mask)
return ERR_PTR(-ENOMSG);
new_ruleset = create_ruleset(1);
if (IS_ERR(new_ruleset))
return new_ruleset;
if (fs_access_mask)
landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
if (net_access_mask)
landlock_add_net_access_mask(new_ruleset, net_access_mask, 0);
if (scope_mask)
landlock_add_scope_mask(new_ruleset, scope_mask, 0);
new_ruleset = kzalloc_obj(*new_ruleset, GFP_KERNEL_ACCOUNT);
if (!new_ruleset)
return ERR_PTR(-ENOMEM);
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
new_ruleset->rules.root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
#ifdef CONFIG_TRACEPOINTS
new_ruleset->id = landlock_get_id_range(1);
#endif /* CONFIG_TRACEPOINTS */
/* Should already be checked in landlock_create_ruleset(). */
if (fs_access_mask) {
const access_mask_t mask = fs_access_mask &
LANDLOCK_MASK_ACCESS_FS;
WARN_ON_ONCE(fs_access_mask != mask);
new_ruleset->handled_masks.fs |= mask;
}
if (net_access_mask) {
const access_mask_t mask = net_access_mask &
LANDLOCK_MASK_ACCESS_NET;
WARN_ON_ONCE(net_access_mask != mask);
new_ruleset->handled_masks.net |= mask;
}
if (scope_mask) {
const access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
WARN_ON_ONCE(scope_mask != mask);
new_ruleset->handled_masks.scope |= mask;
}
return new_ruleset;
}
@@ -129,7 +135,7 @@ create_rule(const struct landlock_id id,
return ERR_PTR(-ENOMEM);
RB_CLEAR_NODE(&new_rule->node);
if (is_object_pointer(id.type)) {
/* This should have been caught by insert_rule(). */
/* This should have been caught by landlock_store_rule(). */
WARN_ON_ONCE(!id.key.object);
landlock_get_object(id.key.object);
}
@@ -145,24 +151,6 @@ create_rule(const struct landlock_id id,
return new_rule;
}
static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
const enum landlock_key_type key_type)
{
switch (key_type) {
case LANDLOCK_KEY_INODE:
return &ruleset->root_inode;
#if IS_ENABLED(CONFIG_INET)
case LANDLOCK_KEY_NET_PORT:
return &ruleset->root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
default:
WARN_ON_ONCE(1);
return ERR_PTR(-EINVAL);
}
}
static void free_rule(struct landlock_rule *const rule,
const enum landlock_key_type key_type)
{
@@ -176,19 +164,20 @@ static void free_rule(struct landlock_rule *const rule,
static void build_check_ruleset(void)
{
const struct landlock_ruleset ruleset = {
const struct landlock_rules rules = {
.num_rules = ~0,
.num_layers = ~0,
};
BUILD_BUG_ON(ruleset.num_rules < LANDLOCK_MAX_NUM_RULES);
BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
}
/**
* insert_rule - Create and insert a rule in a ruleset
* landlock_store_rule - Create and insert a rule into the rule storage
*
* @ruleset: The ruleset to be updated.
* @rules: The rule storage to be updated. The caller is responsible for
* any required locking. For rulesets, this means holding
* &landlock_ruleset.lock. For domains under construction, no lock is
* needed because the domain is not yet visible to other tasks.
* @id: The ID to build the new rule with. The underlying kernel object, if
* any, must be held by the caller.
* @layers: One or multiple layers to be copied into the new rule.
@@ -196,19 +185,19 @@ static void build_check_ruleset(void)
*
* When user space requests to add a new rule to a ruleset, @layers only
* contains one entry and this entry is not assigned to any level. In this
* case, the new rule will extend @ruleset, similarly to a boolean OR between
* case, the new rule will extend @rules, similarly to a boolean OR between
* access rights.
*
* When merging a ruleset in a domain, or copying a domain, @layers will be
* added to @ruleset as new constraints, similarly to a boolean AND between
* access rights.
* added to @rules as new constraints, similarly to a boolean AND between access
* rights.
*
* Return: 0 on success, -errno on failure.
*/
static int insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const struct landlock_layer (*layers)[],
const size_t num_layers)
int landlock_store_rule(struct landlock_rules *const rules,
const struct landlock_id id,
const struct landlock_layer (*layers)[],
const size_t num_layers)
{
struct rb_node **walker_node;
struct rb_node *parent_node = NULL;
@@ -216,14 +205,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
struct rb_root *root;
might_sleep();
lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
return -ENOENT;
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
return -ENOENT;
root = get_root(ruleset, id.type);
root = landlock_get_rule_root(rules, id.type);
if (IS_ERR(root))
return PTR_ERR(root);
@@ -249,7 +237,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
* landlock_add_rule(2), i.e. @ruleset is not a domain.
* landlock_add_rule(2), i.e. @rules is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -278,14 +266,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* There is no match for @id. */
build_check_ruleset();
if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES)
return -E2BIG;
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
return PTR_ERR(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
ruleset->num_rules++;
rules->num_rules++;
return 0;
}
@@ -311,197 +299,50 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
{
struct landlock_layer layers[] = { {
.access = access,
/* When @level is zero, insert_rule() extends @ruleset. */
/*
* When @level is zero, landlock_store_rule() extends @ruleset.
*/
.level = 0,
.flags = {
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
},
} };
int err;
build_check_layer();
return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
}
lockdep_assert_held(&ruleset->lock);
err = landlock_store_rule(&ruleset->rules, id, &layers,
ARRAY_SIZE(layers));
static int merge_tree(struct landlock_ruleset *const dst,
struct landlock_ruleset *const src,
const enum landlock_key_type key_type)
{
struct landlock_rule *walker_rule, *next_rule;
struct rb_root *src_root;
int err = 0;
#ifdef CONFIG_TRACEPOINTS
if (!err)
ruleset->version++;
#endif /* CONFIG_TRACEPOINTS */
might_sleep();
lockdep_assert_held(&dst->lock);
lockdep_assert_held(&src->lock);
src_root = get_root(src, key_type);
if (IS_ERR(src_root))
return PTR_ERR(src_root);
/* Merges the @src tree. */
rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
node) {
struct landlock_layer layers[] = { {
.level = dst->num_layers,
} };
const struct landlock_id id = {
.key = walker_rule->key,
.type = key_type,
};
if (WARN_ON_ONCE(walker_rule->num_layers != 1))
return -EINVAL;
if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
return -EINVAL;
layers[0].access = walker_rule->layers[0].access;
layers[0].flags = walker_rule->layers[0].flags;
err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
if (err)
return err;
}
return err;
}
static int merge_ruleset(struct landlock_ruleset *const dst,
struct landlock_ruleset *const src)
{
int err = 0;
might_sleep();
/* Should already be checked by landlock_merge_ruleset() */
if (WARN_ON_ONCE(!src))
return 0;
/* Only merge into a domain. */
if (WARN_ON_ONCE(!dst || !dst->hierarchy))
return -EINVAL;
/* Locks @dst first because we are its only owner. */
mutex_lock(&dst->lock);
mutex_lock_nested(&src->lock, SINGLE_DEPTH_NESTING);
/* Stacks the new layer. */
if (WARN_ON_ONCE(src->num_layers != 1 || dst->num_layers < 1)) {
err = -EINVAL;
goto out_unlock;
}
dst->access_masks[dst->num_layers - 1] =
landlock_upgrade_handled_access_masks(src->access_masks[0]);
/* Merges the @src inode tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
if (err)
goto out_unlock;
#if IS_ENABLED(CONFIG_INET)
/* Merges the @src network port tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
if (err)
goto out_unlock;
#endif /* IS_ENABLED(CONFIG_INET) */
out_unlock:
mutex_unlock(&src->lock);
mutex_unlock(&dst->lock);
return err;
}
static int inherit_tree(struct landlock_ruleset *const parent,
struct landlock_ruleset *const child,
const enum landlock_key_type key_type)
{
struct landlock_rule *walker_rule, *next_rule;
struct rb_root *parent_root;
int err = 0;
might_sleep();
lockdep_assert_held(&parent->lock);
lockdep_assert_held(&child->lock);
parent_root = get_root(parent, key_type);
if (IS_ERR(parent_root))
return PTR_ERR(parent_root);
/* Copies the @parent inode or network tree. */
rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
parent_root, node) {
const struct landlock_id id = {
.key = walker_rule->key,
.type = key_type,
};
err = insert_rule(child, id, &walker_rule->layers,
walker_rule->num_layers);
if (err)
return err;
}
return err;
}
static int inherit_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const child)
{
int err = 0;
might_sleep();
if (!parent)
return 0;
/* Locks @child first because we are its only owner. */
mutex_lock(&child->lock);
mutex_lock_nested(&parent->lock, SINGLE_DEPTH_NESTING);
/* Copies the @parent inode tree. */
err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
if (err)
goto out_unlock;
#if IS_ENABLED(CONFIG_INET)
/* Copies the @parent network port tree. */
err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
if (err)
goto out_unlock;
#endif /* IS_ENABLED(CONFIG_INET) */
if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) {
err = -EINVAL;
goto out_unlock;
}
/* Copies the parent layer stack and leaves a space for the new layer. */
memcpy(child->access_masks, parent->access_masks,
flex_array_size(parent, access_masks, parent->num_layers));
if (WARN_ON_ONCE(!parent->hierarchy)) {
err = -EINVAL;
goto out_unlock;
}
landlock_get_hierarchy(parent->hierarchy);
child->hierarchy->parent = parent->hierarchy;
out_unlock:
mutex_unlock(&parent->lock);
mutex_unlock(&child->lock);
return err;
}
static void free_ruleset(struct landlock_ruleset *const ruleset)
void landlock_free_rules(struct landlock_rules *const rules)
{
struct landlock_rule *freeme, *next;
might_sleep();
rbtree_postorder_for_each_entry_safe(freeme, next, &ruleset->root_inode,
rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode,
node)
free_rule(freeme, LANDLOCK_KEY_INODE);
#if IS_ENABLED(CONFIG_INET)
rbtree_postorder_for_each_entry_safe(freeme, next,
&ruleset->root_net_port, node)
&rules->root_net_port, node)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
}
landlock_put_hierarchy(ruleset->hierarchy);
static void free_ruleset(struct landlock_ruleset *const ruleset)
{
might_sleep();
trace_landlock_free_ruleset(ruleset);
landlock_free_rules(&ruleset->rules);
kfree(ruleset);
}
@@ -511,234 +352,3 @@ void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
if (ruleset && refcount_dec_and_test(&ruleset->usage))
free_ruleset(ruleset);
}
static void free_ruleset_work(struct work_struct *const work)
{
struct landlock_ruleset *ruleset;
ruleset = container_of(work, struct landlock_ruleset, work_free);
free_ruleset(ruleset);
}
/* Only called by hook_cred_free(). */
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
{
if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
INIT_WORK(&ruleset->work_free, free_ruleset_work);
schedule_work(&ruleset->work_free);
}
}
/**
* landlock_merge_ruleset - Merge a ruleset with a domain
*
* @parent: Parent domain.
* @ruleset: New ruleset to be merged.
*
* The current task is requesting to be restricted. The subjective credentials
* must not be in an overridden state. cf. landlock_init_hierarchy_log().
*
* Return: A new domain merging @parent and @ruleset on success, or ERR_PTR()
* on failure. If @parent is NULL, the new domain duplicates @ruleset.
*/
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset)
{
struct landlock_ruleset *new_dom __free(landlock_put_ruleset) = NULL;
u32 num_layers;
int err;
might_sleep();
if (WARN_ON_ONCE(!ruleset || parent == ruleset))
return ERR_PTR(-EINVAL);
if (parent) {
if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
return ERR_PTR(-E2BIG);
num_layers = parent->num_layers + 1;
} else {
num_layers = 1;
}
/* Creates a new domain... */
new_dom = create_ruleset(num_layers);
if (IS_ERR(new_dom))
return new_dom;
new_dom->hierarchy =
kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
if (!new_dom->hierarchy)
return ERR_PTR(-ENOMEM);
refcount_set(&new_dom->hierarchy->usage, 1);
/* ...as a child of @parent... */
err = inherit_ruleset(parent, new_dom);
if (err)
return ERR_PTR(err);
/* ...and including @ruleset. */
err = merge_ruleset(new_dom, ruleset);
if (err)
return ERR_PTR(err);
err = landlock_init_hierarchy_log(new_dom->hierarchy);
if (err)
return ERR_PTR(err);
#ifdef CONFIG_AUDIT
new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
#endif /* CONFIG_AUDIT */
return no_free_ptr(new_dom);
}
/*
* The returned access has the same lifetime as @ruleset.
*/
const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
root = get_root((struct landlock_ruleset *)ruleset, id.type);
if (IS_ERR(root))
return NULL;
node = root->rb_node;
while (node) {
struct landlock_rule *this =
rb_entry(node, struct landlock_rule, node);
if (this->key.data == id.key.data)
return this;
if (this->key.data < id.key.data)
node = node->rb_right;
else
node = node->rb_left;
}
return NULL;
}
/**
* landlock_unmask_layers - Remove the access rights in @masks
* which are granted in @rule
*
* Updates the set of (per-layer) unfulfilled access rights @masks
* so that all the access rights granted in @rule are removed from it
* (because they are now fulfilled).
*
* @rule: A rule that grants a set of access rights for each layer
* @masks: A matrix of unfulfilled access rights for each layer
*
* Return: True if the request is allowed (i.e. the access rights granted all
* remaining unfulfilled access rights and masks has no leftover set bits).
*/
bool landlock_unmask_layers(const struct landlock_rule *const rule,
struct layer_masks *masks)
{
if (!masks)
return true;
if (!rule)
return false;
/*
* An access is granted if, for each policy layer, at least one rule
* encountered on the pathwalk grants the requested access,
* regardless of its position in the layer stack. We must then check
* the remaining layers for each inode, from the first added layer to
* the last one. When there is multiple requested accesses, for each
* policy layer, the full set of requested accesses may not be granted
* by only one rule, but by the union (binary OR) of multiple rules.
* E.g. /a/b <execute> + /a <read> => /a/b <execute + read>
*/
for (size_t i = 0; i < rule->num_layers; i++) {
const struct landlock_layer *const layer = &rule->layers[i];
/* Clear the bits where the layer in the rule grants access. */
masks->layers[layer->level - 1].access &= ~layer->access;
#ifdef CONFIG_AUDIT
/* Collect rule flags for each layer. */
if (layer->flags.quiet)
masks->layers[layer->level - 1].quiet = true;
#endif /* CONFIG_AUDIT */
}
for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
if (masks->layers[i].access)
return false;
}
return true;
}
typedef access_mask_t
get_access_mask_t(const struct landlock_ruleset *const ruleset,
const u16 layer_level);
/**
* landlock_init_layer_masks - Initialize layer masks from an access request
*
* Populates @masks such that for each access right in @access_request, the bits
* for all the layers are set where this access right is handled. Rule flags
* are also zeroed.
*
* @domain: The domain that defines the current restrictions.
* @access_request: The requested access rights to check.
* @masks: Layer access masks to populate.
* @key_type: The key type to switch between access masks of different types.
*
* Return: An access mask where each access right bit is set which is handled
* in any of the active layers in @domain.
*/
access_mask_t
landlock_init_layer_masks(const struct landlock_ruleset *const domain,
const access_mask_t access_request,
struct layer_masks *const masks,
const enum landlock_key_type key_type)
{
access_mask_t handled_accesses = 0;
get_access_mask_t *get_access_mask;
switch (key_type) {
case LANDLOCK_KEY_INODE:
get_access_mask = landlock_get_fs_access_mask;
break;
#if IS_ENABLED(CONFIG_INET)
case LANDLOCK_KEY_NET_PORT:
get_access_mask = landlock_get_net_access_mask;
break;
#endif /* IS_ENABLED(CONFIG_INET) */
default:
WARN_ON_ONCE(1);
return 0;
}
/* An empty access request can happen because of O_WRONLY | O_RDWR. */
if (!access_request)
return 0;
for (size_t i = 0; i < domain->num_layers; i++) {
const access_mask_t handled = get_access_mask(domain, i);
masks->layers[i].access = access_request & handled;
handled_accesses |= masks->layers[i].access;
#ifdef CONFIG_AUDIT
masks->layers[i].quiet = false;
#endif /* CONFIG_AUDIT */
}
for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
i++) {
masks->layers[i].access = 0;
#ifdef CONFIG_AUDIT
masks->layers[i].quiet = false;
#endif /* CONFIG_AUDIT */
}
return handled_accesses;
}

View File

@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -14,14 +15,11 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
#include "object.h"
struct landlock_hierarchy;
/**
* struct landlock_layer - Access rights for a given layer
*/
@@ -68,13 +66,12 @@ union landlock_key {
*/
enum landlock_key_type {
/**
* @LANDLOCK_KEY_INODE: Type of &landlock_ruleset.root_inode's node
* keys.
* @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys.
*/
LANDLOCK_KEY_INODE = 1,
/**
* @LANDLOCK_KEY_NET_PORT: Type of &landlock_ruleset.root_net_port's
* node keys.
* @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node
* keys.
*/
LANDLOCK_KEY_NET_PORT,
};
@@ -121,6 +118,33 @@ struct landlock_rule {
struct landlock_layer layers[] __counted_by(num_layers);
};
/**
* struct landlock_rules - Red-black tree storage for Landlock rules
*
* This structure holds the rule trees shared by both rulesets and domains.
*/
struct landlock_rules {
/**
* @root_inode: Root of a red-black tree containing &struct
* landlock_rule nodes with inode object. Immutable for domains.
*/
struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET)
/**
* @root_net_port: Root of a red-black tree containing &struct
* landlock_rule nodes with network port. Immutable for domains.
*/
struct rb_root root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
/**
* @num_rules: Number of non-overlapping (i.e. not for the same object)
* rules in this tree storage.
*/
u32 num_rules;
};
/**
* struct landlock_ruleset - Landlock ruleset
*
@@ -129,81 +153,44 @@ struct landlock_rule {
*/
struct landlock_ruleset {
/**
* @root_inode: Root of a red-black tree containing &struct
* landlock_rule nodes with inode object. Once a ruleset is tied to a
* process (i.e. as a domain), this tree is immutable until @usage
* reaches zero.
* @rules: Red-black tree storage for rules.
*/
struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET)
struct landlock_rules rules;
/**
* @root_net_port: Root of a red-black tree containing &struct
* landlock_rule nodes with network port. Once a ruleset is tied to a
* process (i.e. as a domain), this tree is immutable until @usage
* reaches zero.
* @lock: Protects against concurrent modifications of @rules, if @usage
* is greater than zero.
*/
struct rb_root root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
struct mutex lock;
/**
* @usage: Number of file descriptors referencing this ruleset.
*/
refcount_t usage;
#ifdef CONFIG_TRACEPOINTS
/**
* @version: Counter incremented on each successful
* landlock_add_rule(2), including when it only extends an existing
* rule's access rights. Used by tracepoints to correlate a domain with
* the exact ruleset state it was created from. Protected by @lock.
*/
u32 version;
/**
* @id: Unique identifier for this ruleset, used for tracing.
*/
u64 id;
#endif /* CONFIG_TRACEPOINTS */
/**
* @hierarchy: Enables hierarchy identification even when a parent
* domain vanishes. This is needed for the ptrace protection.
* @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
* merged domain, this is stored in each layer's struct
* landlock_hierarchy instead.
*/
struct landlock_hierarchy *hierarchy;
union {
/**
* @work_free: Enables to free a ruleset within a lockless
* section. This is only used by
* landlock_put_ruleset_deferred() when @usage reaches zero.
* The fields @lock, @usage, @num_rules, @num_layers,
* @quiet_masks and @access_masks are then unused.
*/
struct work_struct work_free;
struct {
/**
* @lock: Protects against concurrent modifications of
* @root, if @usage is greater than zero.
*/
struct mutex lock;
/**
* @usage: Number of processes (i.e. domains) or file
* descriptors referencing this ruleset.
*/
refcount_t usage;
/**
* @num_rules: Number of non-overlapping (i.e. not for
* the same object) rules in this ruleset.
*/
u32 num_rules;
/**
* @num_layers: Number of layers that are used in this
* ruleset. This enables to check that all the layers
* allow an access request. A value of 0 identifies a
* non-merged ruleset (i.e. not a domain).
*/
u32 num_layers;
/**
* @quiet_masks: Stores the quiet flags for an unmerged
* ruleset. For a merged domain, this is stored in each
* layer's struct landlock_hierarchy instead.
*/
struct access_masks quiet_masks;
/**
* @access_masks: Contains the subset of filesystem and
* network actions that are restricted by a ruleset.
* A domain saves all layers of merged rulesets in a
* stack (FAM), starting from the first layer to the
* last one. These layers are used when merging
* rulesets, for user space backward compatibility
* (i.e. future-proof), and to properly handle merged
* rulesets without overlapping access rights. These
* layers are set once and never changed for the
* lifetime of the ruleset.
*/
struct access_masks access_masks[];
};
};
struct access_masks quiet_masks;
/**
* @handled_masks: Contains the subset of filesystem and network actions
* that are handled by this ruleset.
*/
struct access_masks handled_masks;
};
struct landlock_ruleset *
@@ -212,7 +199,6 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
const access_mask_t scope_mask);
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
@@ -221,13 +207,39 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access, const u32 flags);
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
int landlock_store_rule(struct landlock_rules *const rules,
const struct landlock_id id,
const struct landlock_layer (*layers)[],
const size_t num_layers);
const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct landlock_id id);
void landlock_free_rules(struct landlock_rules *const rules);
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
*
* @rules: The rules storage to look up.
* @key_type: The type of key to select the tree for.
*
* Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type.
*/
static inline struct rb_root *
landlock_get_rule_root(struct landlock_rules *const rules,
const enum landlock_key_type key_type)
{
switch (key_type) {
case LANDLOCK_KEY_INODE:
return &rules->root_inode;
#if IS_ENABLED(CONFIG_INET)
case LANDLOCK_KEY_NET_PORT:
return &rules->root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
default:
WARN_ON_ONCE(1);
return ERR_PTR(-EINVAL);
}
}
static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
{
@@ -235,96 +247,4 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
refcount_inc(&ruleset->usage);
}
/**
* landlock_union_access_masks - Return all access rights handled in the
* domain
*
* @domain: Landlock ruleset (used as a domain)
*
* Return: An access_masks result of the OR of all the domain's access masks.
*/
static inline struct access_masks
landlock_union_access_masks(const struct landlock_ruleset *const domain)
{
union access_masks_all matches = {};
size_t layer_level;
for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
union access_masks_all layer = {
.masks = domain->access_masks[layer_level],
};
matches.all |= layer.all;
}
return matches.masks;
}
static inline void
landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
const access_mask_t fs_access_mask,
const u16 layer_level)
{
access_mask_t fs_mask = fs_access_mask & LANDLOCK_MASK_ACCESS_FS;
/* Should already be checked in sys_landlock_create_ruleset(). */
WARN_ON_ONCE(fs_access_mask != fs_mask);
ruleset->access_masks[layer_level].fs |= fs_mask;
}
static inline void
landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
const access_mask_t net_access_mask,
const u16 layer_level)
{
access_mask_t net_mask = net_access_mask & LANDLOCK_MASK_ACCESS_NET;
/* Should already be checked in sys_landlock_create_ruleset(). */
WARN_ON_ONCE(net_access_mask != net_mask);
ruleset->access_masks[layer_level].net |= net_mask;
}
static inline void
landlock_add_scope_mask(struct landlock_ruleset *const ruleset,
const access_mask_t scope_mask, const u16 layer_level)
{
access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
/* Should already be checked in sys_landlock_create_ruleset(). */
WARN_ON_ONCE(scope_mask != mask);
ruleset->access_masks[layer_level].scope |= mask;
}
static inline access_mask_t
landlock_get_fs_access_mask(const struct landlock_ruleset *const ruleset,
const u16 layer_level)
{
/* Handles all initially denied by default access rights. */
return ruleset->access_masks[layer_level].fs |
_LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
}
static inline access_mask_t
landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
const u16 layer_level)
{
return ruleset->access_masks[layer_level].net;
}
static inline access_mask_t
landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
const u16 layer_level)
{
return ruleset->access_masks[layer_level].scope;
}
bool landlock_unmask_layers(const struct landlock_rule *const rule,
struct layer_masks *masks);
access_mask_t
landlock_init_layer_masks(const struct landlock_ruleset *const domain,
const access_mask_t access_request,
struct layer_masks *masks,
const enum landlock_key_type key_type);
#endif /* _SECURITY_LANDLOCK_RULESET_H */

View File

@@ -22,6 +22,7 @@
#include <linux/mount.h>
#include <linux/path.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/security.h>
#include <linux/stddef.h>
#include <linux/syscalls.h>
@@ -38,6 +39,8 @@
#include "setup.h"
#include "tsync.h"
#include <trace/events/landlock.h>
static bool is_initialized(void)
{
if (likely(landlock_initialized))
@@ -169,7 +172,7 @@ static const struct file_operations ruleset_fops = {
* If the change involves a fix that requires userspace awareness, also update
* the errata documentation in Documentation/userspace-api/landlock.rst .
*/
const int landlock_abi_version = 10;
const int landlock_abi_version = 11;
/**
* sys_landlock_create_ruleset - Create a new ruleset
@@ -281,6 +284,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
/*
* Emits before anon_inode_getfd() installs the file descriptor, while
* the ruleset is still private to this thread: no lock is needed, and
* the event cannot race a concurrent close() freeing the ruleset under
* the tracepoint's BTF read. This is the last point at which the
* ruleset is guaranteed alive and unshared.
*/
trace_landlock_create_ruleset(ruleset);
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
@@ -308,8 +320,6 @@ static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
if (!(fd_file(ruleset_f)->f_mode & mode))
return ERR_PTR(-EPERM);
ruleset = fd_file(ruleset_f)->private_data;
if (WARN_ON_ONCE(ruleset->num_layers != 1))
return ERR_PTR(-EINVAL);
landlock_get_ruleset(ruleset);
return ruleset;
}
@@ -367,7 +377,7 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
mask = ruleset->access_masks[0].fs;
mask = ruleset->handled_masks.fs;
if ((path_beneath_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -408,7 +418,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
mask = landlock_get_net_access_mask(ruleset, 0);
mask = ruleset->handled_masks.net;
if ((net_port_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -502,21 +512,28 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
* - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
* - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
* - %LANDLOCK_RESTRICT_SELF_TSYNC
* - %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
*
* This system call enforces a Landlock ruleset on the current thread.
* Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
* namespace or is running with no_new_privs. This avoids scenarios where
* unprivileged tasks can affect the behavior of privileged children.
*
* With %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS, the no_new_privs attribute of the
* calling thread is set only once the enforcement of the ruleset succeeded,
* which fulfills the above requirement: no_new_privs is set if and only if the
* call succeeds.
*
* Return: 0 on success, or -errno on failure. Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: @flags contains an unknown bit.
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
* - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
* - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
* current thread is not running with no_new_privs, or it doesn't have
* %CAP_SYS_ADMIN in its namespace.
* - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
* %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
* is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
* namespace.
* - %E2BIG: The maximum number of stacked rulesets is reached for the current
* thread.
*
@@ -527,26 +544,30 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
bool process_wide;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
prev_log_subdomains;
if (!is_initialized())
return -EOPNOTSUPP;
/*
* Similar checks as for seccomp(2), except that an -EPERM may be
* returned.
*/
if (!task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
LANDLOCK_MASK_RESTRICT_SELF)
return -EINVAL;
/*
* Similar checks as for seccomp(2), except that an -EPERM may be
* returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
* requirement.
*/
if (!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
!task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
/* Translates "off" flag to boolean. */
log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
/* Translates "on" flag to boolean. */
@@ -576,11 +597,11 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
new_llcred = landlock_cred(new_cred);
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
prev_log_subdomains = !new_llcred->log_subdomains_off;
new_llcred->log_subdomains_off = !prev_log_subdomains ||
!log_subdomains;
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/*
* The only case when a ruleset may not be set is if
@@ -595,37 +616,91 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* manipulating the current credentials because they are
* dedicated per thread.
*/
struct landlock_ruleset *const new_dom =
landlock_merge_ruleset(new_llcred->domain, ruleset);
mutex_lock(&ruleset->lock);
new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
mutex_unlock(&ruleset->lock);
abort_creds(new_cred);
return PTR_ERR(new_dom);
}
/*
* Emits the domain-creation event while @ruleset->lock is still
* held, right after the merge, so an eBPF program attached to
* the tracepoint reads the exact ruleset that was merged into
* the domain: a consistent snapshot that a concurrent
* landlock_add_rule() (which holds the same lock) cannot
* modify.
*
* This must come before the thread-sync wait below. Holding
* @ruleset->lock across landlock_restrict_sibling_threads()
* would hang: a sibling thread blocked in landlock_add_rule()
* on the same @ruleset->lock cannot run the task_work that
* thread-sync waits for (the lock wait is uninterruptible).
* Emitting here keeps the lock off the thread-sync path.
*
* The trade-off is that the event fires for a domain that a
* later (rare) thread-sync failure aborts. That path emits the
* matching free_domain event so the create/free pair stays
* balanced (see the thread-sync error path below).
*/
trace_landlock_create_domain(new_dom, ruleset);
mutex_unlock(&ruleset->lock);
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_dom->hierarchy->log_same_exec = log_same_exec;
new_dom->hierarchy->log_new_exec = log_new_exec;
/*
* The creation event fired above, so move the domain out of
* LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
* too, even if a thread-sync failure aborts it below. Audit
* logging may still be disabled (DISABLED); tracing observes it
* anyway.
*/
if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
#endif /* CONFIG_AUDIT */
else
new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/* Replaces the old (prepared) domain. */
landlock_put_ruleset(new_llcred->domain);
landlock_put_domain(new_llcred->domain);
new_llcred->domain = new_dom;
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
const int err = landlock_restrict_sibling_threads(
current_cred(), new_cred);
current_cred(), new_cred, flags);
if (err) {
/*
* Thread-sync failed (rare), so the new domain is
* aborted instead of committed. Its creation event
* already fired above, so the imminent free must emit
* the matching free_domain event to keep the
* create/free pair balanced; no special log_status is
* set here.
*/
abort_creds(new_cred);
return err;
}
}
return commit_creds(new_cred);
/* Sets no_new_privs past the last point of failure. */
if (flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS)
task_set_no_new_privs(current);
/* Whole process: thread-sync swept siblings, or single-threaded. */
process_wide = (flags & LANDLOCK_RESTRICT_SELF_TSYNC) ||
get_nr_threads(current) == 1;
commit_creds(new_cred);
/* The caller commits last, so its event concludes the operation. */
if (ruleset)
trace_landlock_enforce_domain(new_dom, true, process_wide,
task_no_new_privs(current));
return 0;
}

View File

@@ -20,11 +20,11 @@
#include <net/af_unix.h>
#include <net/sock.h>
#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "log.h"
#include "ruleset.h"
#include "setup.h"
#include "task.h"
@@ -41,8 +41,8 @@
* Return: True if @parent is an ancestor of or equal to @child, false
* otherwise.
*/
static bool domain_scope_le(const struct landlock_ruleset *const parent,
const struct landlock_ruleset *const child)
static bool domain_scope_le(const struct landlock_domain *const parent,
const struct landlock_domain *const child)
{
const struct landlock_hierarchy *walker;
@@ -63,8 +63,8 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
return false;
}
static int domain_ptrace(const struct landlock_ruleset *const parent,
const struct landlock_ruleset *const child)
static int domain_ptrace(const struct landlock_domain *const parent,
const struct landlock_domain *const child)
{
if (domain_scope_le(parent, child))
return 0;
@@ -88,6 +88,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
const struct landlock_cred_security *parent_subject;
u64 tracee_domain_id = 0;
int err;
/* Quick return for non-landlocked tasks. */
@@ -96,9 +97,13 @@ static int hook_ptrace_access_check(struct task_struct *const child,
return 0;
scoped_guard(rcu) {
const struct landlock_ruleset *const child_dom =
const struct landlock_domain *const child_dom =
landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (child_dom)
tracee_domain_id = child_dom->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!err)
@@ -116,6 +121,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
.u.tsk = child,
},
.layer_plus_one = parent_subject->domain->num_layers,
.other_domain_id = tracee_domain_id,
});
return err;
@@ -135,7 +141,8 @@ static int hook_ptrace_access_check(struct task_struct *const child,
static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
const struct landlock_ruleset *child_dom;
const struct landlock_domain *child_dom;
u64 tracee_domain_id = 0;
int err;
child_dom = landlock_get_current_domain();
@@ -147,6 +154,12 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
if (!err)
return 0;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* The tracee is the current task; its domain is stable here. */
if (child_dom)
tracee_domain_id = child_dom->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/*
* For the ptrace_traceme case, we log the domain which is the cause of
* the denial, which means the parent domain instead of the current
@@ -161,6 +174,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
.u.tsk = current,
},
.layer_plus_one = parent_subject->domain->num_layers,
.other_domain_id = tracee_domain_id,
});
return err;
}
@@ -176,8 +190,8 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
* Return: True if @server is in a different domain from @client and @client
* is scoped to access @server (i.e. access should be denied), false otherwise.
*/
static bool domain_is_scoped(const struct landlock_ruleset *const client,
const struct landlock_ruleset *const server,
static bool domain_is_scoped(const struct landlock_domain *const client,
const struct landlock_domain *const server,
access_mask_t scope)
{
int client_layer, server_layer;
@@ -236,13 +250,28 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
}
static bool sock_is_scoped(struct sock *const other,
const struct landlock_ruleset *const domain)
const struct landlock_domain *const domain,
u64 *const peer_domain_id)
{
const struct landlock_ruleset *dom_other;
const struct landlock_domain *dom_other;
/* The credentials will not change. */
lockdep_assert_held(&unix_sk(other)->lock);
/*
* A live kernel socket (e.g. from sock_create_kern()) has no backing
* file, hence no Landlock domain, so treat it as unscoped. The
* sk_socket check only guards that dereference; sk_socket is NULL
* solely for a dead peer, which the caller already excludes under the
* held lock, so no separate SOCK_DEAD check is needed.
*/
if (unlikely(!other->sk_socket || !other->sk_socket->file))
return false;
dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
*peer_domain_id = dom_other ? dom_other->hierarchy->id : 0;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return domain_is_scoped(domain, dom_other,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
}
@@ -270,6 +299,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
struct sock *const newsk)
{
size_t handle_layer;
u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -281,7 +311,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
if (!is_abstract_socket(other))
return 0;
if (!sock_is_scoped(other, subject->domain))
if (!sock_is_scoped(other, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -293,6 +323,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
},
},
.layer_plus_one = handle_layer + 1,
.other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -301,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock,
struct socket *const other)
{
size_t handle_layer;
u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -318,7 +350,7 @@ static int hook_unix_may_send(struct socket *const sock,
if (!is_abstract_socket(other->sk))
return 0;
if (!sock_is_scoped(other->sk, subject->domain))
if (!sock_is_scoped(other->sk, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -330,6 +362,7 @@ static int hook_unix_may_send(struct socket *const sock,
},
},
.layer_plus_one = handle_layer + 1,
.other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -344,6 +377,7 @@ static int hook_task_kill(struct task_struct *const p,
{
bool is_scoped;
size_t handle_layer;
u64 target_domain_id = 0;
const struct landlock_cred_security *subject;
if (!cred) {
@@ -370,9 +404,15 @@ static int hook_task_kill(struct task_struct *const p,
return 0;
scoped_guard(rcu) {
is_scoped = domain_is_scoped(subject->domain,
landlock_get_task_domain(p),
const struct landlock_domain *const other =
landlock_get_task_domain(p);
is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (other)
target_domain_id = other->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -385,6 +425,7 @@ static int hook_task_kill(struct task_struct *const p,
.u.tsk = p,
},
.layer_plus_one = handle_layer + 1,
.other_domain_id = target_domain_id,
});
return -EPERM;
}
@@ -394,6 +435,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
{
const struct landlock_cred_security *subject;
bool is_scoped = false;
u64 target_domain_id = 0;
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
@@ -421,9 +463,15 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
return 0;
scoped_guard(rcu) {
is_scoped = domain_is_scoped(subject->domain,
landlock_get_task_domain(tsk),
const struct landlock_domain *const other =
landlock_get_task_domain(tsk);
is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (other)
target_domain_id = other->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -435,9 +483,10 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
.type = LSM_AUDIT_DATA_TASK,
.u.tsk = tsk,
},
#ifdef CONFIG_AUDIT
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
#endif /* CONFIG_AUDIT */
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
.other_domain_id = target_domain_id,
});
return -EPERM;
}

185
security/landlock/trace.c Normal file
View File

@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Tracepoint helpers
*
* Copyright © 2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#include <linux/cleanup.h>
#include <linux/dcache.h>
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/lsm_audit.h>
#include <net/sock.h>
#include "access.h"
#include "domain.h"
#include "fs.h"
#include "log.h"
#include "ruleset.h"
#include "trace.h"
/*
* Generates the tracepoint definitions in this translation unit. The trace
* event header dereferences the traced objects in TP_fast_assign, so the full
* struct definitions (e.g. ruleset.h, domain.h) must be included before it.
*/
#define CREATE_TRACE_POINTS
#include <trace/events/landlock.h>
/**
* landlock_trace_free_domain - Emit a tracepoint on domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
* Fires only for a hierarchy whose creation event was emitted, i.e. one that
* left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the
* create/free pair balanced: a hierarchy that never became observable is freed
* silently, while a domain that landlock_restrict_self() created and a
* thread-sync failure then aborted still fires free_domain, because its
* creation event already fired.
*
* Called from landlock_log_free_domain().
*/
void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
{
/*
* The log_status read is a correctness guard (keep the create/free pair
* balanced), not a cost guard, so this cold path needs no
* trace_..._enabled() check: the tracepoint is a static-branch no-op
* when disabled. The denial path guards trace_..._enabled() instead
* because it does expensive __getname()/path work before emitting.
*/
if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
trace_landlock_free_domain(hierarchy);
}
/**
* landlock_trace_denial - Emit a tracepoint for a denied access request
*
* @request: Detail of the user space request.
* @youngest_denied: The youngest hierarchy node that denied the access.
* @missing: The set of denied access rights.
* @same_exec: Whether the current task is the same executable that called
* landlock_restrict_self() for the denying domain, as computed
* by landlock_log_denial().
* @logged: Whether the domain's policy selects this denial for logging, as
* computed by landlock_log_denial().
*
* Emits the tracepoint matching @request->type when its event is enabled.
* Unlike audit, fires regardless of @logged; the value is recorded in the event
* so consumers can filter on it.
*
* Called from landlock_log_denial().
*/
void landlock_trace_denial(
const struct landlock_request *const request,
const struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool same_exec, const bool logged)
{
switch (request->type) {
case LANDLOCK_REQUEST_FS_ACCESS:
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
if (trace_landlock_deny_access_fs_enabled()) {
char *buf __free(__putname) = __getname();
struct path dentry_path;
const char *pathname;
const struct path *path = NULL;
/*
* Selects the path from the audit data type, as
* dump_common_audit_data() does. A FS_ACCESS denial
* carries a file (hook_file_truncate) or an ioctl op
* (hook_file_ioctl) rather than a path;
* FS_CHANGE_TOPOLOGY carries a path or a bare dentry.
* Reading the wrong union member would dereference
* garbage, so every reachable type is handled here.
*/
switch (request->audit.type) {
case LSM_AUDIT_DATA_FILE:
path = &request->audit.u.file->f_path;
break;
case LSM_AUDIT_DATA_IOCTL_OP:
path = &request->audit.u.op->path;
break;
case LSM_AUDIT_DATA_DENTRY:
/*
* Build a path on the stack with the real
* dentry so TP_fast_assign can extract dev and
* ino; the mnt field is unused there.
*/
dentry_path = (struct path){
.dentry = request->audit.u.dentry,
};
path = &dentry_path;
break;
case LSM_AUDIT_DATA_PATH:
path = &request->audit.u.path;
break;
default:
WARN_ONCE(1,
"Unhandled Landlock FS audit type %d",
request->audit.type);
break;
}
if (!path)
break;
if (!buf) {
pathname = "<no_mem>";
} else if (request->audit.type ==
LSM_AUDIT_DATA_DENTRY) {
/* No vfsmount: render the dentry path alone. */
pathname = dentry_path_raw(
request->audit.u.dentry, buf, PATH_MAX);
if (IS_ERR(pathname))
pathname =
PTR_ERR(pathname) ==
-ENAMETOOLONG ?
"<too_long>" :
"<unreachable>";
} else {
pathname = resolve_path_for_trace(path, buf);
}
trace_landlock_deny_access_fs(youngest_denied,
same_exec, logged,
missing, path, pathname);
}
break;
case LANDLOCK_REQUEST_NET_ACCESS:
if (trace_landlock_deny_access_net_enabled())
trace_landlock_deny_access_net(
youngest_denied, same_exec, logged, missing,
request->audit.u.net->sk,
ntohs(request->audit.u.net->sport),
ntohs(request->audit.u.net->dport));
break;
case LANDLOCK_REQUEST_PTRACE:
if (trace_landlock_deny_ptrace_enabled())
trace_landlock_deny_ptrace(youngest_denied, same_exec,
logged,
request->other_domain_id,
request->audit.u.tsk);
break;
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
if (trace_landlock_deny_scope_signal_enabled())
trace_landlock_deny_scope_signal(
youngest_denied, same_exec, logged,
request->other_domain_id, request->audit.u.tsk);
break;
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
trace_landlock_deny_scope_abstract_unix_socket(
youngest_denied, same_exec, logged,
request->other_domain_id,
request->audit.u.net->sk);
break;
default:
WARN_ONCE(1, "Unhandled Landlock request type %d",
request->type);
break;
}
}

44
security/landlock/trace.h Normal file
View File

@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock - Tracepoint helpers
*
* Copyright © 2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_TRACE_H
#define _SECURITY_LANDLOCK_TRACE_H
#include "access.h"
struct landlock_hierarchy;
struct landlock_request;
#ifdef CONFIG_TRACEPOINTS
void landlock_trace_free_domain(
const struct landlock_hierarchy *const hierarchy);
void landlock_trace_denial(
const struct landlock_request *const request,
const struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool same_exec, const bool logged);
#else /* CONFIG_TRACEPOINTS */
static inline void
landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
{
}
static inline void
landlock_trace_denial(const struct landlock_request *const request,
const struct landlock_hierarchy *const youngest_denied,
const access_mask_t missing, const bool same_exec,
const bool logged)
{
}
#endif /* CONFIG_TRACEPOINTS */
#endif /* _SECURITY_LANDLOCK_TRACE_H */

View File

@@ -17,10 +17,13 @@
#include <linux/sched/task.h>
#include <linux/slab.h>
#include <linux/task_work.h>
#include <uapi/linux/landlock.h>
#include "cred.h"
#include "tsync.h"
#include <trace/events/landlock.h>
/*
* Shared state between multiple threads which are enforcing Landlock rulesets
* in lockstep with each other.
@@ -78,6 +81,8 @@ struct tsync_work {
*/
static void restrict_one_thread(struct tsync_shared_context *ctx)
{
const struct landlock_domain *new_dom =
landlock_cred(ctx->new_cred)->domain;
int err;
struct cred *cred = NULL;
@@ -146,6 +151,18 @@ static void restrict_one_thread(struct tsync_shared_context *ctx)
commit_creds(cred);
/*
* Emitted strictly after commit_creds() and before the out: label, so
* it fires only for a thread now enforcing new_dom, and every
* non-concluding (complete == false) event happens-before the
* operation's single concluding one. Skipped on the flags-only path,
* where old_cred and new_cred carry the same domain. A sibling never
* concludes the operation and its enforcement is always process-wide.
*/
if (new_dom != landlock_cred(ctx->old_cred)->domain)
trace_landlock_enforce_domain(new_dom, false, true,
task_no_new_privs(current));
out:
/* Notify the calling thread once all threads are done */
if (atomic_dec_return(&ctx->num_unfinished) == 0)
@@ -466,7 +483,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
* restrict_sibling_threads - enables a Landlock policy for all sibling threads
*/
int landlock_restrict_sibling_threads(const struct cred *old_cred,
const struct cred *new_cred)
const struct cred *new_cred,
const u32 restrict_flags)
{
int err;
struct tsync_shared_context shared_ctx;
@@ -481,7 +499,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
init_completion(&shared_ctx.all_finished);
shared_ctx.old_cred = old_cred;
shared_ctx.new_cred = new_cred;
shared_ctx.set_no_new_privs = task_no_new_privs(current);
shared_ctx.set_no_new_privs =
(restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
task_no_new_privs(current);
/*
* Serialize concurrent TSYNC operations to prevent deadlocks when

View File

@@ -9,8 +9,10 @@
#define _SECURITY_LANDLOCK_TSYNC_H
#include <linux/cred.h>
#include <linux/types.h>
int landlock_restrict_sibling_threads(const struct cred *old_cred,
const struct cred *new_cred);
const struct cred *new_cred,
u32 restrict_flags);
#endif /* _SECURITY_LANDLOCK_TSYNC_H */

View File

@@ -214,41 +214,6 @@ static int audit_set_status(int fd, __u32 key, __u32 val)
return audit_request(fd, &msg, NULL);
}
/* Returns a pointer to the last filled character of @dst, which is `\0`. */
static __maybe_unused char *regex_escape(const char *const src, char *dst,
size_t dst_size)
{
char *d = dst;
for (const char *s = src; *s; s++) {
switch (*s) {
case '$':
case '*':
case '.':
case '[':
case '\\':
case ']':
case '^':
if (d >= dst + dst_size - 2)
return (char *)-ENOMEM;
*d++ = '\\';
*d++ = *s;
break;
default:
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d++ = *s;
}
}
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d = '\0';
return d;
}
/*
* @domain_id: The domain ID extracted from the audit message (if the first part
* of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is

View File

@@ -76,7 +76,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
@@ -255,12 +255,24 @@ TEST(restrict_self_checks_ordering)
/* Checks unprivileged enforcement without no_new_privs. */
drop_caps(_metadata);
/*
* The flags validity is checked before the no_new_privs /
* CAP_SYS_ADMIN requirement.
*/
ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
ASSERT_EQ(EPERM, errno);
/*
* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills the no_new_privs /
* CAP_SYS_ADMIN requirement but requires a ruleset, so the FD is
* checked next.
*/
ASSERT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
ASSERT_EQ(EBADF, errno);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
@@ -277,6 +289,41 @@ TEST(restrict_self_checks_ordering)
ASSERT_EQ(0, close(ruleset_fd));
}
TEST(restrict_self_max_layers)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = -1,
};
const int ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
path_beneath_attr.parent_fd =
open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, path_beneath_attr.parent_fd);
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr, 0));
ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
/* Enforces the maximum number of allowed layers. */
for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
/* Enforces one too many rulesets. */
drop_caps(_metadata);
ASSERT_EQ(-1, landlock_restrict_self(
ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
ASSERT_EQ(E2BIG, errno);
/* Checks that the failed call did not set no_new_privs. */
ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
ASSERT_EQ(0, close(ruleset_fd));
}
TEST(restrict_self_fd)
{
int fd;
@@ -288,7 +335,7 @@ TEST(restrict_self_fd)
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_fd_logging_flags)
TEST(restrict_self_fd_flags)
{
int fd;
@@ -302,11 +349,16 @@ TEST(restrict_self_fd_logging_flags)
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADFD, errno);
/* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_logging_flags)
TEST(restrict_self_flags)
{
const __u32 last_flag = LANDLOCK_RESTRICT_SELF_TSYNC;
const __u32 last_flag = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
/* Tests invalid flag combinations. */
@@ -349,6 +401,17 @@ TEST(restrict_self_logging_flags)
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
/* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
EXPECT_EQ(EBADF, errno);
/* Tests with an invalid ruleset_fd. */
EXPECT_EQ(-1, landlock_restrict_self(
@@ -359,6 +422,37 @@ TEST(restrict_self_logging_flags)
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
}
TEST(restrict_self_no_new_privs)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
const int ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
/*
* The calling thread does not need CAP_SYS_ADMIN nor an explicit
* prctl(2) PR_SET_NO_NEW_PRIVS call.
*/
drop_caps(_metadata);
ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
/* Checks that a failed call does not set no_new_privs. */
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
/* Checks that a successful call sets no_new_privs. */
ASSERT_EQ(0, landlock_restrict_self(
ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
EXPECT_EQ(0, close(ruleset_fd));
}
TEST(ruleset_fd_io)
{
struct landlock_ruleset_attr ruleset_attr = {

View File

@@ -253,3 +253,50 @@ static void __maybe_unused set_unix_address(struct service_fixture *const srv,
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}
/**
* regex_escape - Escape BRE metacharacters in a string
*
* @src: Source string to escape.
* @dst: Destination buffer for the escaped string.
* @dst_size: Size of the destination buffer.
*
* Escapes characters that have special meaning in POSIX Basic Regular
* Expressions: $ * . [ \ ] ^
*
* Returns a pointer to the NUL terminator in @dst (cursor-style API for
* chaining), or (char *)-ENOMEM if the buffer is too small.
*/
static __maybe_unused char *regex_escape(const char *const src, char *dst,
size_t dst_size)
{
char *d = dst;
for (const char *s = src; *s; s++) {
switch (*s) {
case '$':
case '*':
case '.':
case '[':
case '\\':
case ']':
case '^':
if (d >= dst + dst_size - 2)
return (char *)-ENOMEM;
*d++ = '\\';
*d++ = *s;
break;
default:
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d++ = *s;
}
}
if (d >= dst + dst_size - 1)
return (char *)-ENOMEM;
*d = '\0';
return d;
}

View File

@@ -2,6 +2,8 @@ CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_ENABLE_DEFAULT_TRACERS=y
CONFIG_FTRACE=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_KEYS=y

View File

@@ -44,6 +44,9 @@
#include "audit.h"
#include "common.h"
#include "trace.h"
#define TRACE_TASK "fs_test"
#ifndef renameat2
int renameat2(int olddirfd, const char *oldpath, int newdirfd,
@@ -2247,6 +2250,177 @@ TEST_F_FORK(layout1, rename_file)
RENAME_EXCHANGE));
}
TEST_F_FORK(layout1, rename_whiteout_denied)
{
/* The affected file is a FIFO. */
ASSERT_EQ(0, unlink(file1_s3d3));
ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0));
/* Deny MAKE_REG, but allow MAKE_FIFO. */
enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL);
/*
* Try to rename a file with RENAME_WHITEOUT.
* file1_s3d3 is in dir_s3d2 (tmpfs), so it supports RENAME_WHITEOUT.
* Denied, because whiteout creation is guarded with MAKE_REG.
*/
EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD,
TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT));
EXPECT_EQ(EACCES, errno);
}
static bool is_whiteout(const char *const path)
{
struct stat st;
if (stat(path, &st) == -1)
return false;
return S_ISCHR(st.st_mode) && st.st_rdev == makedev(0, 0);
}
static bool is_fifo(const char *const path)
{
struct stat st;
return stat(path, &st) == 0 && S_ISFIFO(st.st_mode);
}
static bool is_missing(const char *const path)
{
struct stat st;
return stat(path, &st) == -1 && errno == ENOENT;
}
TEST_F_FORK(layout1, rename_whiteout_allowed)
{
const struct rule rules[] = {
{
.path = dir_s3d3,
.access = LANDLOCK_ACCESS_FS_MAKE_REG,
},
{},
};
/* The affected file is a FIFO. */
ASSERT_EQ(0, unlink(file1_s3d3));
ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0));
/* Allow MAKE_REG below dir_s3d3. */
enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, rules);
/*
* Rename a file with RENAME_WHITEOUT within the same directory.
* Allowed, because MAKE_REG is granted for the whiteout object which
* gets created in the source location.
*/
EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD,
TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT));
/* A whiteout object took the place of the moved FIFO. */
EXPECT_TRUE(is_whiteout(file1_s3d3));
EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d3/f2"));
}
TEST_F_FORK(layout1, rename_whiteout_reparenting)
{
const struct rule rules[] = {
{
.path = dir_s3d2,
.access = LANDLOCK_ACCESS_FS_REFER,
},
{
.path = dir_s3d3,
.access = LANDLOCK_ACCESS_FS_MAKE_REG,
},
{},
};
/* The moved files are FIFOs. */
ASSERT_EQ(0, unlink(file1_s3d3));
ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0));
ASSERT_EQ(0, unlink(file1_s3d4));
ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0));
/* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */
enforce_fs(_metadata,
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
rules);
/*
* The whiteout object is created in the source directory: Moving the
* FIFO out of dir_s3d4 is denied because MAKE_REG is not granted
* there, even though it is granted in the destination directory
* dir_s3d3.
*/
EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD,
TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT));
EXPECT_EQ(EACCES, errno);
/*
* Moving the FIFO out of dir_s3d3 is allowed, because MAKE_REG is
* granted there for the created whiteout object.
*/
EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD,
TMP_DIR "/s3d1/s3d2/s3d4/f2", RENAME_WHITEOUT));
/* A whiteout object took the place of the moved FIFO. */
EXPECT_TRUE(is_whiteout(file1_s3d3));
EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d4/f2"));
}
TEST_F_FORK(layout1, rename_whiteout_exchange)
{
const char *const whiteout_s3d3 = TMP_DIR "/s3d1/s3d2/s3d3/f2";
const struct rule rules[] = {
{
.path = dir_s3d2,
.access = LANDLOCK_ACCESS_FS_REFER,
},
{
.path = dir_s3d3,
.access = LANDLOCK_ACCESS_FS_MAKE_REG,
},
{},
};
/* The exchanged files are FIFOs and an existing whiteout object. */
ASSERT_EQ(0, unlink(file1_s3d3));
ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0));
ASSERT_EQ(0, mknod(whiteout_s3d3, S_IFCHR | 0600, makedev(0, 0)));
ASSERT_EQ(0, unlink(file1_s3d4));
ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0));
/* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */
enforce_fs(_metadata,
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
rules);
/*
* With RENAME_EXCHANGE, the whiteout object moves into the source
* directory of the rename: Exchanging the FIFO in dir_s3d4 with the
* whiteout object is denied because MAKE_REG is not granted in
* dir_s3d4, even though it is granted in the whiteout object's own
* directory dir_s3d3.
*/
EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD, whiteout_s3d3,
RENAME_EXCHANGE));
EXPECT_EQ(EACCES, errno);
/*
* Exchanging the FIFO in dir_s3d3 with the whiteout object is
* allowed, because MAKE_REG is granted in the directory into which
* the whiteout object moves.
*/
EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, whiteout_s3d3,
RENAME_EXCHANGE));
/* The FIFO and the whiteout object swapped places. */
EXPECT_TRUE(is_whiteout(file1_s3d3));
EXPECT_TRUE(is_fifo(whiteout_s3d3));
}
TEST_F_FORK(layout1, rename_dir)
{
const struct rule rules[] = {
@@ -3270,6 +3444,18 @@ TEST_F_FORK(layout1, make_char)
makedev(1, 3));
}
TEST_F_FORK(layout1, make_whiteout)
{
/*
* Creates a whiteout object (creation guarded by MAKE_REG).
*
* Contrary to the other character devices, this does not require
* CAP_MKNOD, cf. vfs_mknod().
*/
test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, S_IFCHR,
makedev(0, 0));
}
TEST_F_FORK(layout1, make_block)
{
/* Creates a /dev/loop0 device. */
@@ -6459,6 +6645,8 @@ static const char lower_fo1[] = LOWER_DATA "/fo1";
static const char lower_do1[] = LOWER_DATA "/do1";
static const char lower_do1_fo2[] = LOWER_DATA "/do1/fo2";
static const char lower_do1_fl3[] = LOWER_DATA "/do1/fl3";
/* lower_pl1 is a FIFO and is deliberately not in the lists below. */
static const char lower_pl1[] = LOWER_DATA "/pl1";
static const char (*lower_base_files[])[] = {
&lower_fl1,
@@ -6508,6 +6696,8 @@ static const char (*upper_sub_files[])[] = {
#define MERGE_BASE TMP_DIR "/merge"
#define MERGE_DATA MERGE_BASE "/data"
static const char merge_fl1[] = MERGE_DATA "/fl1";
/* merge_pl1 is a FIFO and is deliberately not in the lists below. */
static const char merge_pl1[] = MERGE_DATA "/pl1";
static const char merge_dl1[] = MERGE_DATA "/dl1";
static const char merge_dl1_fl2[] = MERGE_DATA "/dl1/fl2";
static const char merge_fu1[] = MERGE_DATA "/fu1";
@@ -6548,7 +6738,8 @@ static const char (*merge_sub_files[])[] = {
*       fl3
*       fo2
*    fl1
*    fo1
*    fo1
*    pl1 [FIFO]
* merge
*    data
*    dl1
@@ -6561,7 +6752,8 @@ static const char (*merge_sub_files[])[] = {
*       fu2
*    fl1
*    fo1
*    fu1
*    fu1
*    pl1 [FIFO]
* upper
* data
*    do1
@@ -6599,6 +6791,7 @@ FIXTURE_SETUP(layout2_overlay)
create_file(_metadata, lower_fo1);
create_file(_metadata, lower_do1_fo2);
create_file(_metadata, lower_do1_fl3);
ASSERT_EQ(0, mknod(lower_pl1, S_IFIFO | 0600, 0));
create_directory(_metadata, UPPER_BASE);
set_cap(_metadata, CAP_SYS_ADMIN);
@@ -6631,6 +6824,7 @@ FIXTURE_TEARDOWN_PARENT(layout2_overlay)
EXPECT_EQ(0, remove_path(lower_fl1));
EXPECT_EQ(0, remove_path(lower_do1_fo2));
EXPECT_EQ(0, remove_path(lower_fo1));
EXPECT_EQ(0, remove_path(lower_pl1));
/* umount(LOWER_BASE)) is handled by namespace lifetime. */
EXPECT_EQ(0, remove_path(LOWER_BASE));
@@ -6927,7 +7121,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
ASSERT_EQ(0, test_open(path_entry, O_RDWR));
}
/* Only allowes access to the merge hierarchy. */
/* Only allows access to the merge hierarchy. */
enforce_fs(_metadata, ACCESS_RW, layer5_merge_only);
/* Checks new accesses on lower layer. */
@@ -6951,6 +7145,43 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
}
}
TEST_F_FORK(layout2_overlay, rename_in_overlay_without_make_reg)
{
const char *const merge_pl1_renamed = MERGE_DATA "/pl1_renamed";
if (self->skip_test)
SKIP(return, "overlayfs is not supported (test)");
/*
* merge_pl1 is a FIFO which only exists in the lower layer. Before
* the rename, the upper layer has no entry under this name.
*/
ASSERT_TRUE(is_fifo(merge_pl1));
ASSERT_TRUE(is_missing(UPPER_DATA "/pl1"));
/* MAKE_REG is restricted, but MAKE_FIFO is not. */
enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL);
/*
* Rename the FIFO through OverlayFS. merge_pl1 originates from the
* lower layer, so this triggers a copy-up and creates the whiteout in
* the upper layer to hide the lower layer FIFO file. Even though
* MAKE_REG is restricted, the rename on the OverlayFS works.
*/
EXPECT_EQ(0, rename(merge_pl1, merge_pl1_renamed));
/* Check that the rename worked. */
EXPECT_TRUE(is_fifo(merge_pl1_renamed));
EXPECT_TRUE(is_missing(merge_pl1));
/*
* Check that the whiteout object was created on the underlying "upper"
* filesystem during the rename. This is OK because the whiteout object
* was created by OverlayFS, not by the calling task.
*/
EXPECT_TRUE(is_whiteout(UPPER_DATA "/pl1"));
}
FIXTURE(layout3_fs)
{
bool has_created_dir;
@@ -7436,7 +7667,7 @@ TEST_F(audit_layout1, make_char)
enforce_fs(_metadata, ACCESS_ALL, NULL);
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0));
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, makedev(7, 0)));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_char",
dir_s1d3));
@@ -7446,6 +7677,25 @@ TEST_F(audit_layout1, make_char)
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_whiteout)
{
struct audit_records records;
EXPECT_EQ(0, unlink(file1_s1d3));
enforce_fs(_metadata, ACCESS_ALL, NULL);
/* Whiteout creation is denied and logged as fs.make_reg. */
EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, makedev(0, 0)));
EXPECT_EQ(EACCES, errno);
EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_reg",
dir_s1d3));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
}
TEST_F(audit_layout1, make_dir)
{
struct audit_records records;
@@ -10189,4 +10439,484 @@ TEST_F(audit_quiet_rename, quiet_behind_mountpoint_disconnected)
ASSERT_EQ(0, records.access);
}
/* clang-format off */
FIXTURE(trace_layout1) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_layout1)
{
struct stat st;
/*
* Check tracefs availability before creating the layout, following the
* layout3_fs pattern: skip before any layout creation to avoid leaving
* stale TMP_DIR on skip.
*/
if (stat(TRACEFS_LANDLOCK_DIR, &st)) {
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
/* Isolate tracefs state (PID filter, event enables). */
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
clear_cap(_metadata, CAP_SYS_ADMIN);
prepare_layout(_metadata);
create_layout1(_metadata);
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_fixture_setup());
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
clear_cap(_metadata, CAP_DAC_OVERRIDE);
}
FIXTURE_TEARDOWN_PARENT(trace_layout1)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_DAC_OVERRIDE);
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
tracefs_clear_pid_filter();
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
remove_layout1(_metadata);
cleanup_layout(_metadata);
}
/*
* Verifies that check_rule_fs events include correct field values: domain, dev,
* ino, access_request, and grants. All values are verified against stat() of
* the rule path on a deterministic tmpfs layout.
*/
TEST_F(trace_layout1, check_rule_fs_fields)
{
struct stat dir_stat;
char expected_dev[32];
char expected_ino[32];
char *buf;
char field[64];
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
ASSERT_EQ(0, stat(dir_s1d1, &dir_stat));
snprintf(expected_dev, sizeof(expected_dev), "%u:%u",
major(dir_stat.st_dev), minor(dir_stat.st_dev));
snprintf(expected_ino, sizeof(expected_ino), "%lu", dir_stat.st_ino);
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_DAC_OVERRIDE);
sandbox_child_fs_access(_metadata, dir_s1d1,
LANDLOCK_ACCESS_FS_READ_DIR,
LANDLOCK_ACCESS_FS_READ_DIR, dir_s1d1);
set_cap(_metadata, CAP_DAC_OVERRIDE);
buf = tracefs_read_trace();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_NE(NULL, buf);
EXPECT_EQ(1,
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)))
{
TH_LOG("Expected 1 check_rule_fs event\n%s", buf);
}
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"dev", field, sizeof(field)));
EXPECT_STREQ(expected_dev, field)
{
TH_LOG("Expected dev=%s, got %s", expected_dev, field);
}
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"ino", field, sizeof(field)));
EXPECT_STREQ(expected_ino, field)
{
TH_LOG("Expected ino=%s, got %s", expected_ino, field);
}
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"access_request", field,
sizeof(field)));
EXPECT_STREQ("read_dir", field)
{
TH_LOG("Expected access_request=read_dir, got %s", field);
}
/*
* The domain handles only READ_DIR, so the rule carries the
* unhandled-rights padding; intersecting with the request leaves just
* the requested read_dir (no padding, no hex).
*/
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"grants", field, sizeof(field)));
EXPECT_STREQ("{read_dir}", field)
{
TH_LOG("Expected grants={read_dir}, got %s", field);
}
free(buf);
}
/*
* Verifies check_rule_fs behavior with multiple rules. With rules at s1d1 and
* s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the
* pathwalk short-circuits after the first rule fully unmasks the single layer.
*/
TEST_F(trace_layout1, check_rule_fs_multiple_rules)
{
pid_t pid;
int status;
char *buf;
int count;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_DAC_OVERRIDE);
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0))
_exit(1);
close(path_beneath.parent_fd);
path_beneath.parent_fd =
open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0))
_exit(1);
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0))
_exit(1);
close(ruleset_fd);
fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
set_cap(_metadata, CAP_DAC_OVERRIDE);
buf = tracefs_read_trace();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_NE(NULL, buf);
/*
* Only 1 check_rule_fs event: the rule on dir_s1d2 fully unmasked the
* single layer, so the pathwalk short-circuits before reaching the
* dir_s1d1 rule.
*/
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count,
buf);
}
free(buf);
}
/*
* Verifies the grants array is intersected with the request: a handled,
* granted, but unrequested right (execute) is filtered out, leaving only the
* requested read_dir.
*/
TEST_F(trace_layout1, check_rule_fs_request_subset)
{
char *buf;
char field[64];
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_DAC_OVERRIDE);
/*
* Handle and grant READ_DIR|EXECUTE; the open only requests read_dir.
*/
sandbox_child_fs_access(
_metadata, dir_s1d1,
LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
dir_s1d1);
set_cap(_metadata, CAP_DAC_OVERRIDE);
buf = tracefs_read_trace();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_NE(NULL, buf);
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"access_request", field,
sizeof(field)));
EXPECT_STREQ("read_dir", field);
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"grants", field, sizeof(field)));
EXPECT_STREQ("{read_dir}", field);
free(buf);
}
/*
* Verifies that the optional TRUNCATE access right, which hook_file_open()
* speculatively evaluates on every open, appears in the access_request= and
* grants= fields. Opening file1_s1d1 read-only needs only read_file, but the
* open hook also evaluates truncate; the domain handles and the rule grants
* both, so the event reports access_request=read_file|truncate and
* grants={read_file|truncate}, and the open is allowed.
*/
TEST_F(trace_layout1, check_rule_fs_optional_access)
{
pid_t pid;
int status;
char *buf;
char field[64];
int count;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_DAC_OVERRIDE);
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_TRUNCATE,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_TRUNCATE,
};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0))
_exit(1);
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0))
_exit(1);
close(ruleset_fd);
/* Read-only open needs only read_file; truncate is optional. */
fd = open(file1_s1d1, O_RDONLY | O_CLOEXEC);
if (fd < 0)
_exit(1);
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
/* The open is allowed: the required read_file is granted. */
EXPECT_EQ(0, WEXITSTATUS(status));
set_cap(_metadata, CAP_DAC_OVERRIDE);
buf = tracefs_read_trace();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_NE(NULL, buf);
/* The rule at dir_s1d1 matches when opening file1_s1d1. */
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count,
buf);
}
/* The open hook adds the optional truncate to the request. */
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"access_request", field,
sizeof(field)));
EXPECT_STREQ("read_file|truncate", field);
/* The rule grants both, so truncate appears in the grants array. */
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"grants", field, sizeof(field)));
EXPECT_STREQ("{read_file|truncate}", field);
free(buf);
}
/*
* Verifies that check_rule_fs fires for a rule that matches the inode even when
* it grants none of the requested rights, so the grants set is empty. Landlock
* cannot know a rule ignores the request before reading it, so the event is
* still emitted (grants={}), which lets a tracer see that the rule matched.
* The domain handles READ_DIR|EXECUTE, dir_s1d2 grants only EXECUTE and its
* parent dir_s1d1 grants only READ_DIR. Reading dir_s1d2 (requesting read_dir)
* first matches the dir_s1d2 rule, which grants nothing requested (grants={});
* walking up to dir_s1d1 then grants read_dir (grants={read_dir}) and allows
* the access.
*/
TEST_F(trace_layout1, check_rule_fs_empty_grant)
{
pid_t pid;
int status;
char *buf;
int count;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
set_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_DAC_OVERRIDE);
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath = {};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0)
_exit(1);
/* Parent dir_s1d1 grants only READ_DIR. */
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR;
path_beneath.parent_fd =
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0))
_exit(1);
close(path_beneath.parent_fd);
/* Child dir_s1d2 grants only EXECUTE. */
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
path_beneath.parent_fd =
open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0))
_exit(1);
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0))
_exit(1);
close(ruleset_fd);
fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0)
_exit(1);
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
set_cap(_metadata, CAP_DAC_OVERRIDE);
buf = tracefs_read_trace();
clear_cap(_metadata, CAP_DAC_OVERRIDE);
ASSERT_NE(NULL, buf);
/*
* dir_s1d2 (grants nothing requested) then dir_s1d1 (grants read_dir).
*/
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_EQ(2, count)
{
TH_LOG("Expected 2 check_rule_fs events, got %d\n%s", count,
buf);
}
/* The dir_s1d2 rule matches the inode but grants none of read_dir. */
EXPECT_EQ(
1,
tracefs_count_matches(
buf,
TRACE_PREFIX(
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
"access_request=read_dir "
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
"grants={}$"))
{
TH_LOG("Expected a grants={} event\n%s", buf);
}
/* Walking up to dir_s1d1 grants the requested read_dir. */
EXPECT_EQ(
1,
tracefs_count_matches(
buf,
TRACE_PREFIX(
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
"access_request=read_dir "
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
"grants={read_dir}$"))
{
TH_LOG("Expected a grants={read_dir} event\n%s", buf);
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@@ -10,11 +10,12 @@
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <linux/in.h>
#include <linux/landlock.h>
#include <sched.h>
#include <stdint.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
@@ -22,6 +23,9 @@
#include "audit.h"
#include "common.h"
#include "trace.h"
#define TRACE_TASK "net_test"
const short sock_port_start = (1 << 10);
@@ -3285,4 +3289,588 @@ TEST_F(audit, sendmsg)
EXPECT_EQ(0, close(sock_fd));
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_net) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_net)
{
int ret;
/* Isolate the network namespace so the bound port cannot collide. */
setup_loopback(_metadata);
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_net)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/*
* Baseline: verifies that without Landlock, the bind succeeds and no
* deny_access_net trace event fires.
*/
/* clang-format off */
FIXTURE_VARIANT(trace_net)
{
/* clang-format on */
bool sandbox;
int bind_port_offset; /* 0 = allowed port, 1 = denied port */
int expect_denied;
};
/* Unsandboxed: no Landlock, bind should succeed with no events. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net, unsandboxed) {
/* clang-format on */
.sandbox = false,
.bind_port_offset = 0,
.expect_denied = 0,
};
/* Denied: sandboxed, bind to port not in ruleset. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net, bind_denied) {
/* clang-format on */
.sandbox = true,
.bind_port_offset = 1,
.expect_denied = 1,
};
/* Allowed: sandboxed, bind to port in ruleset. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net, bind_allowed) {
/* clang-format on */
.sandbox = true,
.bind_port_offset = 0,
.expect_denied = 0,
};
TEST_F(trace_net, deny_access_net_bind)
{
char *buf;
int count, status;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
ASSERT_EQ(0, tracefs_clear_buf());
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
int sock_fd;
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP,
};
struct landlock_net_port_attr port_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.port = sock_port_start,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_NET_PORT,
&port_attr, 0)) {
close(ruleset_fd);
_exit(1);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
}
sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sock_fd < 0)
_exit(1);
addr.sin_port =
htons(sock_port_start + variant->bind_port_offset);
if (variant->expect_denied) {
/* Bind should be denied. */
if (bind(sock_fd, (struct sockaddr *)&addr,
sizeof(addr)) == 0) {
close(sock_fd);
_exit(2);
}
if (errno != EACCES) {
close(sock_fd);
_exit(3);
}
} else {
/* Bind should succeed. */
if (bind(sock_fd, (struct sockaddr *)&addr,
sizeof(addr))) {
close(sock_fd);
_exit(2);
}
}
close(sock_fd);
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_access_net event, got %d\n%s",
count, buf);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_access_net events, "
"got %d\n%s",
count, buf);
}
}
free(buf);
}
/*
* Anchors the denial fields shared by every deny_access_net event so a field
* test proves more than sport/dport: the denying domain, the same-exec bit, the
* audit-logging verdict, and the blocked access all stay populated.
*/
static void
expect_net_deny_common_fields(struct __test_metadata *const _metadata,
const char *const buf)
{
char field[64];
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"domain", field, sizeof(field)));
EXPECT_STRNE("0", field);
/* Same exec that restricted itself, no exec in between. */
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"same_exec", field, sizeof(field)));
EXPECT_STREQ("1", field);
/* Default flags, same exec: audit would log this denial. */
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"logged", field, sizeof(field)));
EXPECT_STREQ("1", field);
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"blockers", field, sizeof(field)));
EXPECT_STRNE("", field);
}
/* Connect and field-check tests use a separate fixture without variants. */
/* clang-format off */
FIXTURE(trace_net_connect) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_net_connect)
{
int ret;
/* Isolate the network namespace so the bound port cannot collide. */
setup_loopback(_metadata);
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_net_connect)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_net_connect) {
/* clang-format on */
/* handled_access_net, also the access allowed on the base port. */
__u64 handled;
/* Bind the allowed base port before the denied operation. */
bool bind_base_first;
/* Denied operation on the next port: connect (true) or bind (false). */
bool deny_connect;
};
/* clang-format off */
/* Denied connect(): sport=0, dport=<denied port>. */
FIXTURE_VARIANT_ADD(trace_net_connect, connect_denied) {
.handled = LANDLOCK_ACCESS_NET_CONNECT_TCP,
.bind_base_first = false,
.deny_connect = true,
};
/* Denied bind(): sport=<denied port>, dport=0. */
FIXTURE_VARIANT_ADD(trace_net_connect, bind_fields) {
.handled = LANDLOCK_ACCESS_NET_BIND_TCP,
.bind_base_first = false,
.deny_connect = false,
};
/* Denied connect() after an allowed bind(): the connect fields (sport=0). */
FIXTURE_VARIANT_ADD(trace_net_connect, connect_after_bind) {
.handled = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP,
.bind_base_first = true,
.deny_connect = true,
};
/* clang-format on */
/*
* A denied TCP bind(2) or connect(2) emits one deny_access_net event. The port
* is reported in the field matching the denied operation, in host endianness
* (the UAPI landlock_net_port_attr.port convention): a connect denial reports
* sport=0 dport=<port>, a bind denial reports sport=<port> dport=0, so a
* byte-order or field-swap bug is caught. A prior allowed bind
* (connect_after_bind) does not change the connect denial's fields.
*/
TEST_F(trace_net_connect, deny_access_net)
{
pid_t child;
int status;
char *buf;
char field[64], expected[16];
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = variant->handled,
};
struct landlock_net_port_attr port_attr = {
.allowed_access = variant->handled,
.port = sock_port_start,
};
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
int ruleset_fd, sock_fd, optval = 1, ret;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
&port_attr, 0)) {
close(ruleset_fd);
_exit(1);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sock_fd < 0)
_exit(1);
/* Bind the allowed base port first (succeeds, no event). */
if (variant->bind_base_first) {
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval));
addr.sin_port = htons(sock_port_start);
if (bind(sock_fd, (struct sockaddr *)&addr,
sizeof(addr))) {
close(sock_fd);
_exit(1);
}
}
/* Denied operation on the next port. */
addr.sin_port = htons(sock_port_start + 1);
if (variant->deny_connect)
ret = connect(sock_fd, (struct sockaddr *)&addr,
sizeof(addr));
else
ret = bind(sock_fd, (struct sockaddr *)&addr,
sizeof(addr));
if (ret == 0) {
close(sock_fd);
_exit(2);
}
if (errno != EACCES) {
close(sock_fd);
_exit(3);
}
close(sock_fd);
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
EXPECT_EQ(1, tracefs_count_matches(buf,
REGEX_DENY_ACCESS_NET(TRACE_TASK)));
expect_net_deny_common_fields(_metadata, buf);
/*
* The denied operation's port field carries the port; the other is 0.
*/
snprintf(expected, sizeof(expected), "%llu",
(unsigned long long)(sock_port_start + 1));
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"sport", field, sizeof(field)));
EXPECT_STREQ(variant->deny_connect ? "0" : expected, field);
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"dport", field, sizeof(field)));
EXPECT_STREQ(variant->deny_connect ? expected : "0", field);
free(buf);
}
/* Field verification for the check_rule_net event on an allowed access. */
/* clang-format off */
FIXTURE(trace_net_check_rule) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_net_check_rule)
{
int ret;
/* Isolate the network namespace so the bound port cannot collide. */
setup_loopback(_metadata);
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_net_check_rule)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/*
* Verifies that an allowed bind matching a net-port rule emits exactly one
* landlock_check_rule_net event with the enforcing domain, the requested
* access, the checked port (host endianness), and the per-layer grants. The
* whole event is anchored to exact values so a revert of the check_rule_net
* emit (or a byte-order or field-plumbing regression) fails the test.
*/
TEST_F(trace_net_check_rule, check_rule_net_fields)
{
pid_t child;
int status;
char *buf;
char field[64], expected[16];
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
};
struct landlock_net_port_attr port_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.port = sock_port_start,
};
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
int ruleset_fd, sock_fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
&port_attr, 0)) {
close(ruleset_fd);
_exit(1);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
/* Bind to the allowed port: succeeds and matches the rule. */
sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sock_fd < 0)
_exit(1);
addr.sin_port = htons(sock_port_start);
if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr))) {
close(sock_fd);
_exit(2);
}
close(sock_fd);
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
/* A single-layer domain matching one port rule emits one event. */
EXPECT_EQ(1,
tracefs_count_matches(buf, REGEX_CHECK_RULE_NET(TRACE_TASK)))
{
TH_LOG("Expected 1 check_rule_net event\n%s", buf);
}
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_CHECK_RULE_NET(TRACE_TASK),
"domain", field, sizeof(field)));
EXPECT_STRNE("0", field);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_CHECK_RULE_NET(TRACE_TASK),
"access_request", field, sizeof(field)));
EXPECT_STREQ("bind_tcp", field);
/*
* The port is reported in host endianness (UAPI convention), so on
* little-endian htons(sock_port_start) would print a different value:
* the exact match also catches byte-order regressions.
*/
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_CHECK_RULE_NET(TRACE_TASK),
"port", field, sizeof(field)));
snprintf(expected, sizeof(expected), "%llu",
(unsigned long long)sock_port_start);
EXPECT_STREQ(expected, field);
/* One layer that fully grants the request: grants={bind_tcp}. */
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_CHECK_RULE_NET(TRACE_TASK),
"grants", field, sizeof(field)));
EXPECT_STREQ("{bind_tcp}", field);
free(buf);
}
/*
* IPv6 network trace tests are intentionally elided. IPv6 hook dispatch uses
* the same current_check_access_socket() code path as IPv4, validated by the
* audit tests in this file. The trace events use the same blockers/sport/dport
* fields regardless of address family.
*/
TEST_HARNESS_MAIN

View File

@@ -11,7 +11,9 @@
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <sched.h>
#include <signal.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/types.h>
@@ -20,6 +22,7 @@
#include "audit.h"
#include "common.h"
#include "trace.h"
/* Copied from security/yama/yama_lsm.c */
#define YAMA_SCOPE_DISABLED 0
@@ -430,4 +433,403 @@ TEST_F(audit, trace)
EXPECT_EQ(0, records.domain);
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_ptrace) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_ptrace)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_ptrace)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_ptrace)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/* Denied: sandboxed child ptraces unsandboxed parent (tracee_domain=0). */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: sandboxed child ptraces a sandboxed parent, so the tracee is in a
* domain and tracee_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child uses PTRACE_TRACEME. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_ptrace, deny_ptrace)
{
char *buf, field[64], expected_pid[16];
int count, status;
pid_t child, parent;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
parent = getpid();
/*
* Set a known comm so the denied variant can verify both the trace line
* task name and the tracee_comm= field.
*/
prctl(PR_SET_NAME, "ll_trace_test");
/*
* For the non-zero tracee_domain case, sandbox the parent (the tracee)
* before forking. The child inherits that domain and adds its own
* layer, so the child (tracer) is not an ancestor of the tracee and the
* ptrace is still denied, with tracee_domain= naming the parent's
* domain.
*/
if (variant->sandbox_target)
create_domain(_metadata);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int ruleset_fd;
/*
* Any scope creates a domain. Ptrace denial checks
* domain ancestry, not specific flags.
*/
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
/* PTRACE_ATTACH on unsandboxed parent: denied. */
if (ptrace(PTRACE_ATTACH, parent, NULL, NULL) == 0) {
ptrace(PTRACE_DETACH, parent, NULL, NULL);
_exit(2);
}
if (errno != EPERM)
_exit(3);
} else {
/* No sandbox: ptrace should succeed. */
if (ptrace(PTRACE_TRACEME) != 0)
_exit(1);
}
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test"));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_ptrace event, got %d\n%s", count,
buf);
}
/* Verify tracee_pid is the parent's TGID. */
snprintf(expected_pid, sizeof(expected_pid), "%d", parent);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/* Verify tracee_comm matches prctl(PR_SET_NAME). */
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_comm", field, sizeof(field)));
EXPECT_STREQ("ll_trace_test", field);
/*
* Verify tracee_domain: 0 when the tracee is unsandboxed,
* non-zero when the tracee is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected tracee_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_ptrace events, got %d\n%s",
count, buf);
}
}
free(buf);
}
/* clang-format off */
FIXTURE(trace_ptrace_traceme) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_ptrace_traceme)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_ptrace_traceme)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_ptrace_traceme)
{
/* clang-format on */
bool sandbox_tracer;
bool sandbox_tracee;
int expect_denied;
};
/*
* Denied: a sandboxed tracer cannot trace the unsandboxed child that asked to
* be traced with PTRACE_TRACEME (tracee_domain=0).
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied) {
/* clang-format on */
.sandbox_tracer = true,
.sandbox_tracee = false,
.expect_denied = 1,
};
/*
* Denied: a sandboxed child in its own domain asks to be traced by a tracer in
* an unrelated domain, so the tracee is in a domain and tracee_domain= is
* non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied_scoped_tracee) {
/* clang-format on */
.sandbox_tracer = true,
.sandbox_tracee = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child uses PTRACE_TRACEME with an unsandboxed tracer. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, allowed) {
/* clang-format on */
.sandbox_tracer = false,
.sandbox_tracee = false,
.expect_denied = 0,
};
TEST_F(trace_ptrace_traceme, deny_ptrace)
{
char *buf, field[64], expected_pid[16];
int count, status, sync_pipe[2];
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* Set a known comm so the denied variant can verify both the trace line
* task name and the tracee_comm= field. The tracee is the current
* (child) task for PTRACE_TRACEME, so the child inherits this name.
*/
prctl(PR_SET_NAME, "ll_trace_test");
ASSERT_EQ(0, pipe2(sync_pipe, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char c;
close(sync_pipe[1]);
/*
* The tracee is the current task; for the non-zero
* tracee_domain case it sandboxes itself in its own domain,
* unrelated to the tracer's domain, so PTRACE_TRACEME is still
* denied and tracee_domain= names the child's own domain.
*/
if (variant->sandbox_tracee)
create_domain(_metadata);
/* Waits for the tracer (parent) to enter its domain, if any. */
if (read(sync_pipe[0], &c, 1) != 1)
_exit(1);
close(sync_pipe[0]);
if (variant->expect_denied) {
if (ptrace(PTRACE_TRACEME) == 0)
_exit(2);
if (errno != EPERM)
_exit(3);
} else {
if (ptrace(PTRACE_TRACEME) != 0)
_exit(4);
/* Lets the tracer reap the trace-stop and detach. */
raise(SIGSTOP);
}
_exit(0);
}
close(sync_pipe[0]);
/*
* For a denial, the proposed tracer must be in a domain that is not an
* ancestor of the tracee's domain. Sandboxing the parent after the
* fork gives it a domain unrelated to the child.
*/
if (variant->sandbox_tracer)
create_domain(_metadata);
/* Signals the child that the tracer is in its domain, if any. */
ASSERT_EQ(1, write(sync_pipe[1], ".", 1));
close(sync_pipe[1]);
if (!variant->expect_denied) {
/* PTRACE_TRACEME succeeded: reap the SIGSTOP and detach. */
ASSERT_EQ(child, waitpid(child, &status, WUNTRACED));
ASSERT_TRUE(WIFSTOPPED(status));
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test"));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_ptrace event, got %d\n%s", count,
buf);
}
/* Verify tracee_pid is the child's TGID (the traced task). */
snprintf(expected_pid, sizeof(expected_pid), "%d", child);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/*
* Verify tracee_domain: 0 when the tracee is unsandboxed,
* non-zero when the tracee is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_tracee, strcmp("0", field) != 0)
{
TH_LOG("Unexpected tracee_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_ptrace events, got %d\n%s",
count, buf);
}
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@@ -12,6 +12,7 @@
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -23,6 +24,9 @@
#include "audit.h"
#include "common.h"
#include "scoped_common.h"
#include "trace.h"
#define TRACE_TASK "scoped_abstract"
/* Number of pending connections queue to be hold. */
const short backlog = 10;
@@ -1205,4 +1209,264 @@ TEST(self_connect)
_metadata->exit_code = KSFT_FAIL;
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_unix) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_unix)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(
TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_unix)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_unix) {
/* clang-format on */
int sock_type; /* SOCK_STREAM (connect) or SOCK_DGRAM (sendto). */
bool sandbox;
bool sandbox_target; /* Peer owned by a domain: peer_domain != 0. */
int expect_denied;
};
/* clang-format off */
/* Stream: sandboxed client connect() to an unsandboxed peer (peer_domain=0). */
FIXTURE_VARIANT_ADD(trace_unix, stream_denied) {
.sock_type = SOCK_STREAM, .sandbox = true,
.sandbox_target = false, .expect_denied = 1,
};
/* Stream: peer socket owned by a domain, so peer_domain != 0. */
FIXTURE_VARIANT_ADD(trace_unix, stream_denied_scoped_peer) {
.sock_type = SOCK_STREAM, .sandbox = true,
.sandbox_target = true, .expect_denied = 1,
};
/* Stream: unsandboxed client, connect() succeeds, no event. */
FIXTURE_VARIANT_ADD(trace_unix, stream_allowed) {
.sock_type = SOCK_STREAM, .sandbox = false,
.sandbox_target = false, .expect_denied = 0,
};
/* Datagram: sandboxed client sendto() an unsandboxed peer (peer_domain=0). */
FIXTURE_VARIANT_ADD(trace_unix, dgram_denied) {
.sock_type = SOCK_DGRAM, .sandbox = true,
.sandbox_target = false, .expect_denied = 1,
};
/* Datagram: peer socket owned by a domain, so peer_domain != 0. */
FIXTURE_VARIANT_ADD(trace_unix, dgram_denied_scoped_peer) {
.sock_type = SOCK_DGRAM, .sandbox = true,
.sandbox_target = true, .expect_denied = 1,
};
/* Datagram: unsandboxed client, sendto() succeeds, no event. */
FIXTURE_VARIANT_ADD(trace_unix, dgram_allowed) {
.sock_type = SOCK_DGRAM, .sandbox = false,
.sandbox_target = false, .expect_denied = 0,
};
/* clang-format on */
/*
* A sandboxed thread reaching an abstract unix socket peer through connect(2)
* (stream) or sendto(2) (datagram) is denied and emits
* landlock_deny_scope_abstract_unix_socket. The abstract name is crafted with
* a space and an embedded NUL followed by an "END" marker to check the
* tracepoint escaping and its length handling (a raw space would break the
* sun_path field regex; strlen() would truncate at the NUL and drop "END").
* peer_pid is only meaningful for a stream peer (a datagram peer has no
* SO_PEERCRED), so it is asserted only there.
*/
TEST_F(trace_unix, deny_scope_unix)
{
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
};
char *buf, field[128], expected_pid[16];
int server_fd, count, status, name_len, addr_len;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* For the non-zero peer_domain case, sandbox the parent before it
* creates the server socket, so the socket carries the parent's domain
* and peer_domain= is non-zero.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
server_fd = socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0);
ASSERT_LE(0, server_fd);
addr.sun_path[0] = '\0';
name_len = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1,
"landlock_trace_test_%d ", getpid());
addr.sun_path[1 + name_len] = '\0';
memcpy(addr.sun_path + 1 + name_len + 1, "END", 3);
addr_len =
offsetof(struct sockaddr_un, sun_path) + 1 + name_len + 1 + 3;
ASSERT_EQ(0, bind(server_fd, (struct sockaddr *)&addr, addr_len));
if (variant->sock_type == SOCK_STREAM)
ASSERT_EQ(0, listen(server_fd, 1));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_fd, ret;
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
}
client_fd =
socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0);
if (client_fd < 0)
_exit(1);
if (variant->sock_type == SOCK_STREAM)
ret = connect(client_fd, (struct sockaddr *)&addr,
addr_len);
else
ret = sendto(client_fd, ".", 1, 0,
(struct sockaddr *)&addr, addr_len);
if (variant->sandbox) {
/* Reaching the peer should be denied. */
if (ret != -1 || errno != EPERM) {
close(client_fd);
_exit(2);
}
} else {
/* No sandbox: stream connect() == 0, sendto() == 1. */
int ok = variant->sock_type == SOCK_STREAM ? 0 : 1;
if (ret != ok) {
close(client_fd);
_exit(2);
}
}
close(client_fd);
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
close(server_fd);
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(
buf, REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK));
if (!variant->expect_denied) {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope events, got %d\n%s",
count, buf);
}
free(buf);
return;
}
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_abstract_unix_socket event, "
"got %d\n%s",
count, buf);
}
/*
* sun_path is escaped: a raw space would break this field's [^ ]*$
* regex, so a successful extract proves the space was escaped, and its
* full length is honored: the "END" marker after the embedded NUL must
* survive (strlen() would truncate it at the NUL).
*/
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK),
"sun_path", field, sizeof(field)));
EXPECT_NE(NULL, strstr(field, "END"))
{
TH_LOG("sun_path truncated or unescaped: %s", field);
}
/* peer_pid is the parent's PID for a stream peer (0 for datagram). */
if (variant->sock_type == SOCK_STREAM) {
snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(
TRACE_TASK),
"peer_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
}
/* peer_domain: 0 when the peer is unsandboxed, non-zero otherwise. */
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK),
"peer_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected peer_domain=%s", field);
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@@ -10,7 +10,9 @@
#include <fcntl.h>
#include <linux/landlock.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -18,6 +20,9 @@
#include "common.h"
#include "scoped_common.h"
#include "trace.h"
#define TRACE_TASK "scoped_signal_t"
/* This variable is used for handling several signals. */
static volatile sig_atomic_t is_signaled;
@@ -762,4 +767,403 @@ TEST(sigio_to_pgid_self)
EXPECT_EQ(0, close(trigger[1]));
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_signal) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_signal)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_signal)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_signal)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/* Denied: sandboxed child signals unsandboxed parent (target_domain=0). */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: sandboxed child signals a sandboxed parent, so the target is in a
* domain and target_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child signals unsandboxed parent. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_signal, deny_scope_signal)
{
char *buf, field[64], expected_pid[16];
int count, status;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* For the non-zero target_domain case, sandbox the parent (the signal
* target) before forking. The child inherits that domain and adds its
* own scoped layer, so the signal is still denied and target_domain=
* names the parent's domain.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
}
if (variant->sandbox) {
/* Signal to unsandboxed parent should be denied. */
if (kill(getppid(), 0) == 0)
_exit(2);
if (errno != EPERM)
_exit(3);
} else {
/* No sandbox: kill should succeed. */
if (kill(getppid(), 0) != 0)
_exit(1);
}
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_signal event, got %d\n%s",
count, buf);
}
/* Verify target_pid is the parent's PID. */
snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/*
* Verify target_domain: 0 when the target is unsandboxed,
* non-zero when the target is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected target_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope_signal events, "
"got %d\n%s",
count, buf);
}
}
free(buf);
}
/*
* Trace test for the asynchronous SIGIO/SIGURG delivery path
* (hook_file_send_sigiotask), which reaches the same landlock_deny_scope_signal
* tracepoint as a synchronous kill(2) but through fcntl(F_SETOWN).
*/
/* clang-format off */
FIXTURE(trace_fown) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_fown)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_fown)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_fown)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/*
* Denied: a sandboxed file owner's SIGURG is delivered to an unsandboxed target
* process (target_domain=0).
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: the SIGURG target sandboxes itself in its own domain, so the target
* is in a domain and target_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: an unsandboxed file owner delivers SIGURG. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_fown, deny_scope_fown)
{
int server_socket, recv_socket;
struct service_fixture server_address;
char buffer_parent, field[64], *buf;
int status, count;
int pipe_parent[2], pipe_child[2];
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
memset(&server_address, 0, sizeof(server_address));
set_unix_address(&server_address, 0);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_socket;
char buffer_child;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(0, setup_signal_handler(SIGURG));
client_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, client_socket);
/*
* The SIGURG target is this child; for the non-zero
* target_domain case it sandboxes itself in its own domain,
* unrelated to the file owner's domain.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/* Waits for the parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr,
server_address.unix_addr_len));
/*
* Waits for the parent to accept the connection, sandbox
* itself, and call fcntl(F_SETOWN).
*/
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
/* Triggers the asynchronous SIGURG to this file owner. */
ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB));
EXPECT_EQ(0, close(client_socket));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
_exit(0);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
server_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, server_socket);
ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr,
server_address.unix_addr_len));
ASSERT_EQ(0, listen(server_socket, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
recv_socket = accept(server_socket, NULL, NULL);
ASSERT_LE(0, recv_socket);
/*
* The file owner is the denying subject; its domain is captured at
* fcntl(F_SETOWN) time, so sandbox it before setting the owner.
*/
if (variant->sandbox)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* Sets the child to receive SIGURG for MSG_OOB. This uncommon use is a
* valid attack scenario which also simplifies this test.
*/
ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for the child to send MSG_OOB. */
ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB));
EXPECT_EQ(0, close(recv_socket));
EXPECT_EQ(0, close(server_socket));
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_signal event, got %d\n%s",
count, buf);
}
/*
* Verify target_domain: 0 when the target is unsandboxed,
* non-zero when the target is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected target_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope_signal events, "
"got %d\n%s",
count, buf);
}
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@@ -0,0 +1,639 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock trace test helpers
*
* Copyright © 2026 Cloudflare, Inc.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "kselftest_harness.h"
#define TRACEFS_ROOT "/sys/kernel/tracing"
#define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
#define TRACEFS_CREATE_RULESET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
#define TRACEFS_CREATE_DOMAIN_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
#define TRACEFS_ENFORCE_DOMAIN_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable"
#define TRACEFS_ADD_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
#define TRACEFS_ADD_RULE_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
#define TRACEFS_CHECK_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
#define TRACEFS_CHECK_RULE_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
#define TRACEFS_DENY_ACCESS_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
#define TRACEFS_DENY_ACCESS_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
#define TRACEFS_DENY_PTRACE_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
#define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
TRACEFS_LANDLOCK_DIR \
"/landlock_deny_scope_abstract_unix_socket/enable"
#define TRACEFS_FREE_DOMAIN_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
#define TRACEFS_FREE_RULESET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
#define TRACEFS_TRACE TRACEFS_ROOT "/trace"
#define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
#define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
#define TRACE_BUFFER_SIZE (64 * 1024)
/*
* Trace line prefix: matches the ftrace "trace" file format. Format: "
* <task>-<pid> [<cpu>] <flags> <timestamp>: "
*
* The task parameter must be a string literal truncated to 15 chars
* (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The
* pattern accepts either the expected task name or "<...>" because the ftrace
* comm cache may evict short-lived processes (e.g., forked children that exit
* before the trace buffer is read).
*
* No unescaped '.' in any REGEX macro; literal dots use '\\.'.
*/
#define TRACE_PREFIX(task) \
"^ *\\(<\\.\\.\\.>" \
"\\|" task "\\)" \
"-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
/*
* Task name for events emitted by kworker threads (e.g., free_domain fires from
* a work queue, not from the test process).
*/
#define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
#define REGEX_ADD_RULE_FS(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_fs: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"access_rights=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"path=[^ ]\\+$"
#define REGEX_ADD_RULE_NET(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_net: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"access_rights=[a-z_|]* " \
"port=[0-9]\\+$"
#define REGEX_CREATE_RULESET(task) \
TRACE_PREFIX(task) \
"landlock_create_ruleset: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"handled_fs=[a-z_|]* " \
"handled_net=[a-z_|]* " \
"scoped=[a-z_|]*$"
#define REGEX_CREATE_DOMAIN(task) \
TRACE_PREFIX(task) \
"landlock_create_domain: " \
"domain=[0-9a-f]\\+ " \
"parent=[0-9a-f]\\+ " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
#define REGEX_CHECK_RULE_FS(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_fs: " \
"domain=[0-9a-f]\\+ " \
"access_request=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"grants={[a-z_|,]*}$"
#define REGEX_CHECK_RULE_NET(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_net: " \
"domain=[0-9a-f]\\+ " \
"access_request=[a-z_|]* " \
"port=[0-9]\\+ " \
"grants={[a-z_|,]*}$"
#define REGEX_DENY_ACCESS_FS(task) \
TRACE_PREFIX(task) \
"landlock_deny_access_fs: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"blockers=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"path=[^ ]*$"
#define REGEX_DENY_ACCESS_NET(task) \
TRACE_PREFIX(task) \
"landlock_deny_access_net: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"blockers=[a-z_|]* " \
"sport=[0-9]\\+ " \
"dport=[0-9]\\+$"
#define REGEX_DENY_PTRACE(task) \
TRACE_PREFIX(task) \
"landlock_deny_ptrace: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"tracee_domain=[0-9a-f]\\+ " \
"tracee_pid=[0-9]\\+ " \
"tracee_comm=[^ ]*$"
#define REGEX_DENY_SCOPE_SIGNAL(task) \
TRACE_PREFIX(task) \
"landlock_deny_scope_signal: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"target_domain=[0-9a-f]\\+ " \
"target_pid=[0-9]\\+ " \
"target_comm=[^ ]*$"
#define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \
TRACE_PREFIX(task) \
"landlock_deny_scope_abstract_unix_socket: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"peer_domain=[0-9a-f]\\+ " \
"peer_pid=[0-9]\\+ " \
"sun_path=[^ ]*$"
#define REGEX_FREE_DOMAIN(task) \
TRACE_PREFIX(task) \
"landlock_free_domain: " \
"domain=[0-9a-f]\\+ " \
"denials=[0-9]\\+$"
#define REGEX_FREE_RULESET(task) \
TRACE_PREFIX(task) \
"landlock_free_ruleset: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
static int __maybe_unused tracefs_write(const char *path, const char *value)
{
int fd;
ssize_t ret;
size_t len = strlen(value);
fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
if (fd < 0)
return -errno;
ret = write(fd, value, len);
close(fd);
if (ret < 0)
return -errno;
if ((size_t)ret != len)
return -EIO;
return 0;
}
static int __maybe_unused tracefs_write_int(const char *path, int value)
{
char buf[32];
snprintf(buf, sizeof(buf), "%d", value);
return tracefs_write(path, buf);
}
static int __maybe_unused tracefs_setup(void)
{
struct stat st;
/* Mount tracefs if not already mounted. */
if (stat(TRACEFS_ROOT, &st) != 0) {
int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
if (ret)
return -errno;
}
/* Verify landlock events are available. */
if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
return -ENOENT;
return 0;
}
/*
* Set up PID-based event filtering so only events from the current process and
* its children are recorded. This is analogous to audit's AUDIT_EXE filter: it
* prevents events from unrelated processes from polluting the trace buffer.
*/
static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
{
int ret;
/* Enable event-fork so children inherit the PID filter. */
ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
if (ret)
return ret;
return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
}
/* Clear the PID filter to stop filtering by PID. */
static int __maybe_unused tracefs_clear_pid_filter(void)
{
return tracefs_write(TRACEFS_SET_EVENT_PID, "");
}
static int __maybe_unused tracefs_enable_event(const char *enable_path,
bool enable)
{
return tracefs_write(enable_path, enable ? "1" : "0");
}
static int __maybe_unused tracefs_clear(void)
{
return tracefs_write(TRACEFS_TRACE, "");
}
/*
* Reads the trace buffer content into a newly allocated buffer. The caller is
* responsible for freeing the returned buffer. Returns NULL on error.
*/
static char __maybe_unused *tracefs_read_trace(void)
{
char *buf;
int fd;
ssize_t total = 0, ret;
buf = malloc(TRACE_BUFFER_SIZE);
if (!buf)
return NULL;
fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
free(buf);
return NULL;
}
while (total < TRACE_BUFFER_SIZE - 1) {
ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
}
close(fd);
buf[total] = '\0';
return buf;
}
/* Counts the number of lines in @buf matching the basic regex @pattern. */
static int __maybe_unused tracefs_count_matches(const char *buf,
const char *pattern)
{
regex_t regex;
int count = 0;
const char *line, *end;
if (regcomp(&regex, pattern, 0) != 0)
return -EINVAL;
line = buf;
while (*line) {
end = strchr(line, '\n');
if (!end)
end = line + strlen(line);
/* Create a temporary NUL-terminated line. */
size_t len = end - line;
char *tmp = malloc(len + 1);
if (tmp) {
memcpy(tmp, line, len);
tmp[len] = '\0';
if (regexec(&regex, tmp, 0, NULL, 0) == 0)
count++;
free(tmp);
}
if (*end == '\n')
line = end + 1;
else
break;
}
regfree(&regex);
return count;
}
/*
* Extracts the value of a named field from a trace line in @buf. Searches for
* the first line matching @line_pattern, then extracts the value after
* "@field_name=" into @out. Stops at space or newline.
*
* Returns 0 on success, -ENOENT if no match.
*/
static int __maybe_unused tracefs_extract_field(const char *buf,
const char *line_pattern,
const char *field_name,
char *out, size_t out_size)
{
regex_t regex;
const char *line, *end;
if (regcomp(&regex, line_pattern, 0) != 0)
return -EINVAL;
line = buf;
while (*line) {
end = strchr(line, '\n');
if (!end)
end = line + strlen(line);
size_t len = end - line;
char *tmp = malloc(len + 1);
if (tmp) {
const char *field, *val_start;
size_t field_len, val_len;
memcpy(tmp, line, len);
tmp[len] = '\0';
if (regexec(&regex, tmp, 0, NULL, 0) != 0) {
free(tmp);
goto next;
}
/*
* Find "field_name=" in the line, ensuring a word
* boundary before the field name to avoid substring
* matches (e.g., "port" in "sport").
*/
field_len = strlen(field_name);
field = tmp;
while ((field = strstr(field, field_name))) {
if (field[field_len] == '=' &&
(field == tmp || field[-1] == ' '))
break;
field++;
}
if (!field) {
free(tmp);
regfree(&regex);
return -ENOENT;
}
val_start = field + field_len + 1;
val_len = 0;
while (val_start[val_len] &&
val_start[val_len] != ' ' &&
val_start[val_len] != '\n')
val_len++;
if (val_len >= out_size)
val_len = out_size - 1;
memcpy(out, val_start, val_len);
out[val_len] = '\0';
free(tmp);
regfree(&regex);
return 0;
}
next:
if (*end == '\n')
line = end + 1;
else
break;
}
regfree(&regex);
return -ENOENT;
}
/*
* Common fixture setup for trace tests. Mounts tracefs if needed and sets a
* PID filter. The caller must create a mount namespace first
* (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
* mount; the trace buffer, per-event enable flags, and PID filter are global
* kernel state, scoped to the test by the PID filter.
*
* Returns 0 on success, -errno on failure (caller should SKIP).
*/
static int __maybe_unused tracefs_fixture_setup(void)
{
int ret;
ret = tracefs_setup();
if (ret)
return ret;
return tracefs_set_pid_filter(getpid());
}
static void __maybe_unused tracefs_fixture_teardown(void)
{
tracefs_clear_pid_filter();
}
/*
* Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
* drops the capability. Returns the value from @func, or -EPERM if the
* capability manipulation fails.
*/
static int __maybe_unused tracefs_priv_call(int (*func)(void))
{
const cap_value_t admin = CAP_SYS_ADMIN;
cap_t cap_p;
int ret;
cap_p = cap_get_proc();
if (!cap_p)
return -EPERM;
if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
cap_set_proc(cap_p)) {
cap_free(cap_p);
return -EPERM;
}
ret = func();
cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
cap_set_proc(cap_p);
cap_free(cap_p);
return ret;
}
/* Read the trace buffer with elevated privileges. Returns NULL on failure. */
static char __maybe_unused *tracefs_read_buf(void)
{
/* Cannot use tracefs_priv_call() because the return type is char *. */
cap_t cap_p;
char *buf;
const cap_value_t admin = CAP_SYS_ADMIN;
cap_p = cap_get_proc();
if (!cap_p)
return NULL;
if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
cap_set_proc(cap_p)) {
cap_free(cap_p);
return NULL;
}
buf = tracefs_read_trace();
cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
cap_set_proc(cap_p);
cap_free(cap_p);
return buf;
}
/* Clear the trace buffer with elevated privileges. Returns 0 on success. */
static int __maybe_unused tracefs_clear_buf(void)
{
return tracefs_priv_call(tracefs_clear);
}
/*
* Forks a child that creates a Landlock sandbox and performs an FS access. The
* parent waits for the child, then reads the trace buffer.
*
* Requires common.h and wrappers.h to be included before trace.h.
*/
static void __maybe_unused sandbox_child_fs_access(
struct __test_metadata *const _metadata, const char *rule_path,
__u64 handled_access, __u64 allowed_access, const char *access_path)
{
pid_t pid;
int status;
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = handled_access,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
close(ruleset_fd);
_exit(1);
}
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0)) {
close(path_beneath.parent_fd);
close(ruleset_fd);
_exit(1);
}
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
}
/*
* Forks a child that creates a Landlock sandbox allowing execute+read_dir for
* /usr and execute-only for ".", then execs ./true. The true binary opens "."
* on startup, triggering a read_dir denial with same_exec=0. The parent waits
* for the child to exit.
*/
static void __maybe_unused sandbox_child_exec_true(
struct __test_metadata *const _metadata, __u32 restrict_flags)
{
pid_t pid;
int status;
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd >= 0) {
landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0);
close(path_beneath.parent_fd);
}
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
path_beneath.parent_fd =
open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd >= 0) {
landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0);
close(path_beneath.parent_fd);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, restrict_flags))
_exit(1);
close(ruleset_fd);
execl("./true", "./true", NULL);
_exit(1);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
}

View File

@@ -0,0 +1,496 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Filesystem tracepoints
*
* Copyright © 2026 Cloudflare, Inc.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "common.h"
#include "trace.h"
#define TRACE_TASK "trace_fs_test"
/*
* Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value
* ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged
* one. The tracepoint fires for every denial; logged carries the audit
* verdict.
*/
#define REGEX_DENY_ACCESS_FS_LOGGED(task, log) \
TRACE_PREFIX(task) \
"landlock_deny_access_fs: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=" log " " \
"blockers=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"path=[^ ]*$"
/* clang-format off */
FIXTURE(trace_fs) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_fs)
{
int ret;
set_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, unshare(CLONE_NEWNS));
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
ret = tracefs_fixture_setup();
if (ret) {
clear_cap(_metadata, CAP_SYS_ADMIN);
self->tracefs_ok = 0;
SKIP(return, "tracefs not available");
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_fs)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/*
* Baseline: verifies that without Landlock, the operation succeeds and no
* check_rule or deny_access trace events fire.
*/
TEST_F(trace_fs, unsandboxed)
{
char *buf;
int count, status, fd;
pid_t pid;
ASSERT_EQ(0, tracefs_clear_buf());
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
/*
* No sandbox: verify that a normal FS access does not produce
* Landlock trace events.
*/
fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_EQ(0, count);
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
EXPECT_EQ(0, count);
free(buf);
}
/*
* Verifies that adding a filesystem rule emits a landlock_add_rule_fs trace
* event with the expected path and field values: ruleset ID is non-zero,
* access_rights is non-zero, and path matches.
*/
TEST_F(trace_fs, add_rule_fs)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
};
char *buf, field_buf[64];
int ruleset_fd, count;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, path_beneath.parent_fd);
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0));
ASSERT_EQ(0, close(path_beneath.parent_fd));
ASSERT_EQ(0, close(ruleset_fd));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
}
/* Ruleset ID should be non-zero. */
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
"ruleset", field_buf,
sizeof(field_buf)));
EXPECT_STRNE("0", field_buf);
/* Access rights should be non-zero. */
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
"access_rights", field_buf,
sizeof(field_buf)));
EXPECT_STRNE("", field_buf);
/* Path should be /usr. */
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
"path", field_buf, sizeof(field_buf)));
EXPECT_STREQ("/usr", field_buf);
free(buf);
}
/*
* Verifies that an allowed access emits check_rule events (rule matched during
* pathwalk) but does NOT emit deny_access events (no denial).
*/
TEST_F(trace_fs, allowed_access)
{
char *buf, field_buf[64];
int count;
ASSERT_EQ(0, tracefs_clear_buf());
/* Rule allows READ_DIR for /usr, access /usr which is allowed. */
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
LANDLOCK_ACCESS_FS_READ_DIR, "/usr");
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_LE(1, count);
/* Single-layer grants array, intersected with the request. */
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"grants", field_buf,
sizeof(field_buf)));
EXPECT_STREQ("{read_dir}", field_buf);
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
EXPECT_EQ(0, count);
free(buf);
}
/*
* Verifies that accessing a path whose access type is not in the handled set
* does not emit landlock_check_rule events. The ruleset handles READ_FILE, but
* the directory open checks READ_DIR which is unhandled; Landlock has no
* opinion and no rule evaluation occurs.
*/
TEST_F(trace_fs, check_rule_unhandled)
{
char *buf;
int count;
ASSERT_EQ(0, tracefs_clear_buf());
/* Handles READ_FILE only; READ_DIR is unhandled. */
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE,
LANDLOCK_ACCESS_FS_READ_FILE, "/tmp");
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
/* No check_rule events because READ_DIR is not in the handled set. */
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_EQ(0, count);
free(buf);
}
/*
* Verifies that nested domains (child sandboxed under a parent domain) emit
* check_rule events from both layers and produce a deny_access event when the
* inner domain's rule does not cover the access.
*/
TEST_F(trace_fs, check_rule_nested)
{
char *buf, field_buf[64], *comma;
size_t first_len, second_len;
int count_rule, count_access, status;
pid_t pid;
ASSERT_EQ(0, tracefs_clear_buf());
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd, fd;
/* First layer: allow /usr. */
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
close(ruleset_fd);
_exit(1);
}
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0)) {
close(path_beneath.parent_fd);
close(ruleset_fd);
_exit(1);
}
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
/* Second layer: also allow /usr. */
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
close(ruleset_fd);
_exit(1);
}
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0)) {
close(path_beneath.parent_fd);
close(ruleset_fd);
_exit(1);
}
close(path_beneath.parent_fd);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
/* Access /usr which is allowed by both layers. */
fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
/* Access /tmp which has no rule in either layer. */
fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count_rule =
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
EXPECT_LE(1, count_rule);
/*
* Both layers have the same rule, so the grants array must have two
* identical symbolic entries, e.g. {read_dir,read_dir}.
*/
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
"grants", field_buf,
sizeof(field_buf)));
comma = strchr(field_buf, ',');
EXPECT_NE(0, !!comma);
if (comma) {
/*
* Verify both entries are identical: compare the substring
* before the comma with the substring after it (stripping the
* braces).
*/
first_len = comma - field_buf - 1;
second_len = strlen(comma + 1) - 1;
EXPECT_EQ(first_len, second_len);
EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len));
}
count_access =
tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
EXPECT_LE(1, count_access);
free(buf);
}
/*
* Verifies that a denied FS access emits a landlock_deny_access_fs trace event
* with the blocked access and path.
*/
TEST_F(trace_fs, deny_access_fs_denied)
{
char *buf;
int count;
ASSERT_EQ(0, tracefs_clear_buf());
/*
* Rule allows READ_DIR for /usr, but access /tmp which has no rule.
* READ_DIR access to /tmp is denied by absence and should emit a
* deny_access_fs event.
*/
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
EXPECT_LE(1, count);
free(buf);
}
/*
* A denied FS access covered by a quiet rule (LANDLOCK_ADD_RULE_QUIET with the
* access listed in quiet_access_fs) still emits a landlock_deny_access_fs
* event, but with logged=0, the same audit-logging verdict audit would apply to
* suppress the record.
*/
TEST_F(trace_fs, deny_access_fs_quiet)
{
char *buf, field[64];
pid_t pid;
int status;
ASSERT_EQ(0, tracefs_clear_buf());
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
.quiet_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = 0,
};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
/* Marks /tmp quiet without granting any access. */
path_beneath.parent_fd =
open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
close(ruleset_fd);
_exit(1);
}
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, LANDLOCK_ADD_RULE_QUIET)) {
close(path_beneath.parent_fd);
close(ruleset_fd);
_exit(1);
}
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
/* Denied READ_DIR on the quiet /tmp: suppressed, logged=0. */
fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
/* The event fires with the suppressed verdict. */
EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
TRACE_TASK, "0")));
/* The quiet rule must not leave the denial logged. */
EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
TRACE_TASK, "1")));
/*
* Quiet suppresses only the logged verdict: the rest of the denial
* event stays populated (non-zero domain, non-empty blockers).
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
"domain", field, sizeof(field)));
EXPECT_STRNE("0", field);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
"blockers", field, sizeof(field)));
EXPECT_STRNE("", field);
free(buf);
}
TEST_HARNESS_MAIN

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Minimal helper for Landlock selftests. Opens its own working directory
* before exiting, which may trigger access denials depending on the sandbox
* configuration.
*/
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
close(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
return 0;
}

View File

@@ -62,32 +62,104 @@ static void *idle(void *data)
pthread_cleanup_pop(1);
}
TEST(multi_threaded_success)
FIXTURE(multi_threaded)
{
int ruleset_fd;
};
FIXTURE_VARIANT(multi_threaded)
{
const __u32 restrict_flags;
/* Sets no_new_privs with prctl(2) before the enforcement. */
const bool prior_no_new_privs;
/* Enforces the maximum number of allowed layers beforehand. */
const bool max_layers;
const int expected_errno;
/* Expected no_new_privs state of all threads after the call. */
const bool expected_no_new_privs;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(multi_threaded, success) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_TSYNC,
.prior_no_new_privs = true,
.expected_no_new_privs = true,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(multi_threaded, no_new_privs) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_TSYNC |
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
.expected_no_new_privs = true,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(multi_threaded, no_new_privs_max_layers) {
/* clang-format on */
.restrict_flags = LANDLOCK_RESTRICT_SELF_TSYNC |
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
.max_layers = true,
.expected_errno = E2BIG,
.expected_no_new_privs = false,
};
FIXTURE_SETUP(multi_threaded)
{
self->ruleset_fd = create_ruleset(_metadata);
if (variant->max_layers) {
/* Enforces the maximum number of allowed layers. */
for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
ASSERT_EQ(0,
landlock_restrict_self(self->ruleset_fd, 0));
}
disable_caps(_metadata);
}
FIXTURE_TEARDOWN(multi_threaded)
{
EXPECT_EQ(0, close(self->ruleset_fd));
}
TEST_F(multi_threaded, restrict)
{
pthread_t t1, t2;
bool no_new_privs1, no_new_privs2;
const int ruleset_fd = create_ruleset(_metadata);
disable_caps(_metadata);
ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
if (variant->prior_no_new_privs) {
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
} else {
/* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
}
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC));
if (variant->expected_errno) {
EXPECT_EQ(-1, landlock_restrict_self(self->ruleset_fd,
variant->restrict_flags));
EXPECT_EQ(variant->expected_errno, errno);
} else {
EXPECT_EQ(0, landlock_restrict_self(self->ruleset_fd,
variant->restrict_flags));
}
/* Checks the no_new_privs state of the calling thread. */
EXPECT_EQ(variant->expected_no_new_privs,
prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
ASSERT_EQ(0, pthread_cancel(t1));
ASSERT_EQ(0, pthread_cancel(t2));
ASSERT_EQ(0, pthread_join(t1, NULL));
ASSERT_EQ(0, pthread_join(t2, NULL));
/* The no_new_privs flag was implicitly enabled on all threads. */
EXPECT_TRUE(no_new_privs1);
EXPECT_TRUE(no_new_privs2);
EXPECT_EQ(0, close(ruleset_fd));
/* Checks the no_new_privs state of the sibling threads. */
EXPECT_EQ(variant->expected_no_new_privs, no_new_privs1);
EXPECT_EQ(variant->expected_no_new_privs, no_new_privs2);
}
TEST(multi_threaded_success_despite_diverging_domains)