mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-31 10:31:33 -04:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
326
Documentation/trace/events-landlock.rst
Normal file
326
Documentation/trace/events-landlock.rst
Normal 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
|
||||
@@ -54,6 +54,7 @@ applications.
|
||||
events-power
|
||||
events-nmi
|
||||
events-msr
|
||||
events-landlock
|
||||
events-pci
|
||||
events-pci-controller
|
||||
boottime-trace
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
56
include/linux/landlock.h
Normal 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 */
|
||||
965
include/trace/events/landlock.h
Normal file
965
include/trace/events/landlock.h
Normal 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 */
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
587
security/landlock/log.c
Normal 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
86
security/landlock/log.h
Normal 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 */
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
185
security/landlock/trace.c
Normal 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
44
security/landlock/trace.h
Normal 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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
639
tools/testing/selftests/landlock/trace.h
Normal file
639
tools/testing/selftests/landlock/trace.h
Normal 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(®ex, 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(®ex, tmp, 0, NULL, 0) == 0)
|
||||
count++;
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
if (*end == '\n')
|
||||
line = end + 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
regfree(®ex);
|
||||
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(®ex, 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(®ex, 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(®ex);
|
||||
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(®ex);
|
||||
return 0;
|
||||
}
|
||||
next:
|
||||
if (*end == '\n')
|
||||
line = end + 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
regfree(®ex);
|
||||
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));
|
||||
}
|
||||
496
tools/testing/selftests/landlock/trace_fs_test.c
Normal file
496
tools/testing/selftests/landlock/trace_fs_test.c
Normal 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
|
||||
1620
tools/testing/selftests/landlock/trace_test.c
Normal file
1620
tools/testing/selftests/landlock/trace_test.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user