Landlock fix for v7.3-rc5

-----BEGIN PGP SIGNATURE-----
 
 iIYEABYKAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCarVI0xAcbWljQGRpZ2lr
 b2QubmV0AAoJEOXj0OiMgvbSEVgA+gNbC9CVCbCo0oufZbVpQwlwtuuEKEVpvxZx
 q7oFYKCsAP9svCujGCXRHOmWhAAwe+wpXNb43l8coFn+pCfA8x3fCw==
 =NOli
 -----END PGP SIGNATURE-----

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

Pull Landlock fixes from Mickaël Salaün:
 "This mainly fixes the Landlock tracepoint support merged this cycle so
  that denial and rule events report the intended policy context,
  whether through tracefs or BTF-visible callbacks.

  The size of this all is mainly from propagating the corrected contract
  through event definitions and producers, adding new tests for the
  reported context, and updating the documentation.

  Also improve annotation and fix a GCC 16 build warning"

* tag 'landlock-7.3-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux:
  landlock: Widen ruleset versions to 64 bits
  landlock: Add counted_by in landlock_domain
  landlock: Fix tracepoint contract documentation
  selftests/landlock: Test network denial context
  selftests/landlock: Test filesystem denial blockers
  landlock: Report the effective signal number
  landlock: Report the actual ptrace tracer
  landlock: Fix network denial trace context
  landlock: Fix rule tracepoint context
  landlock: Fix filesystem denial blocker reporting
  landlock: Fix tracepoint fixed-width type names
  landlock: Work around gcc-16 -Wuninitialized warning
This commit is contained in:
Linus Torvalds 2026-09-24 10:53:59 -07:00
commit 415f204422
15 changed files with 751 additions and 439 deletions

View File

@ -6,7 +6,7 @@ Landlock Trace Events
=====================
:Author: Mickaël Salaün
:Date: August 2026
:Date: September 2026
Landlock emits trace events for sandbox lifecycle operations and access
denials. These events can be consumed by ftrace (for human-readable
@ -31,8 +31,8 @@ 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_add_rule_path_beneath``: a filesystem rule is added to a ruleset
- ``landlock_add_rule_net_port``: 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
@ -47,8 +47,8 @@ Landlock trace events are organized in four categories:
**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
- ``landlock_check_rule_inode``: an inode-keyed rule is evaluated
- ``landlock_check_rule_net_port``: a network-port-keyed rule is evaluated
**Lifecycle events**:
@ -141,10 +141,10 @@ in some field formats:
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:
- **Other party's domain**: A scope or ptrace denial compares the subject's
denying domain (``domain=``), which is the enforcing domain and not
necessarily the current task's domain, with the other party's domain.
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
@ -189,7 +189,7 @@ 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
The ``check_rule_inode`` and ``check_rule_net_port`` events expose the per-layer
rule evaluation, which is useful for understanding *why* a specific
access is allowed or denied.
@ -231,9 +231,10 @@ contribution, not the final decision:
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.
For an access check that a consumer can delimit, aggregate the ``grants=``
groups of all matching ``check_rule`` events. These events do not carry a
request ID; use their execution context and generic tracepoints to separate
concurrent or successive checks of the same object.
.. note::
@ -244,29 +245,31 @@ check.
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
read match the rule covering ``/`` (inode 2), so ``check_rule_inode`` 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}
cat-127 [...] landlock_check_rule_inode: domain=1e40cb56f access_request=execute|read_file|truncate dev=0:17 ino=2 grants={execute|read_file}
cat-127 [...] landlock_check_rule_inode: 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}
cat-128 [...] landlock_check_rule_inode: 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_inode: 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:
BTF-enabled raw tracepoint programs attached through libbpf
``SEC("tp_btf/...")`` sections receive typed callback arguments. The
event prototypes in `Event reference`_ document their argument layouts.
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
@ -277,7 +280,7 @@ standard kernel objects and Landlock-internal objects:
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
A stateful eBPF program attached before sandbox construction can maintain
per-domain state in BPF maps:
1. On ``landlock_create_domain``: record the domain ID and parent (the
@ -293,7 +296,10 @@ per-domain state in BPF maps:
final statistics.
This approach requires no kernel modification and no Landlock-specific
BPF helpers. The Landlock IDs serve as correlation keys across events.
BPF helpers. Landlock IDs serve as correlation keys within one boot.
Records exported through tracing or BPF buffers can be lost, and records
from different CPUs are not globally ordered, so consumers must detect and
reconcile incomplete state.
Audit filtering equivalence
===========================

View File

@ -10,14 +10,19 @@
#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_LANDLOCK_H
#include <linux/in.h>
#include <linux/in6.h>
#include <linux/landlock.h>
#include <linux/socket.h>
#include <linux/string.h>
#include <linux/string_helpers.h>
#include <linux/tracepoint.h>
#include <linux/trace_seq.h>
#include <net/af_unix.h>
enum landlock_request_type;
struct dentry;
struct landlock_blockers;
struct landlock_domain;
struct landlock_hierarchy;
struct landlock_rule;
@ -26,6 +31,12 @@ struct path;
struct sock;
struct task_struct;
static_assert(sizeof(access_mask_t) <= sizeof(u64));
TRACE_DEFINE_ENUM(LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY);
TRACE_DEFINE_ENUM(LANDLOCK_REQUEST_FS_ACCESS);
TRACE_DEFINE_ENUM(LANDLOCK_REQUEST_NET_ACCESS);
#ifdef CREATE_TRACE_POINTS
/* About 6 KiB, leaving about 2 KiB for sibling helpers and fixed fields. */
@ -108,7 +119,7 @@ __trace_print_untrusted_str(struct trace_seq *p, const char *src, size_t len)
* 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.
* a zero mask. Shared by the check_rule_inode and check_rule_net_port 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
@ -182,6 +193,9 @@ static inline const char *__trace_landlock_print_layers(
/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
#define _LANDLOCK_FS_BLOCKER_TYPE_NAMES \
{ LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, "change_topology" }
/**
* DOC: Landlock trace events
*
@ -192,22 +206,22 @@ static inline const char *__trace_landlock_print_layers(
* 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.
* A denial event identifies the domain whose policy denied the request, the
* Landlock operation and policy object that were checked, and the blocker or
* domain relationship responsible for the denial. When tracing starts with
* sandbox construction, ruleset and domain events provide the policy history
* needed to interpret these identifiers. The denying domain is the subject
* that enforced the policy, not necessarily current. Generic tracepoints can
* provide additional operational context.
*
* 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
* Lifecycle emission is balanced: a creation event always has a matching
* deallocation event and vice versa. A consumer that observes an object's
* complete lifetime can model it from this pair; one that attaches late or
* loses exported records must reconcile incomplete state. 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
@ -243,8 +257,8 @@ static inline const char *__trace_landlock_print_layers(
* 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
* Fields that mirror the Landlock UAPI preserve their widths 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.
*
@ -281,6 +295,16 @@ static inline const char *__trace_landlock_print_layers(
* 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.
* Nonzero IDs are unique within one boot. For ptrace, same_exec instead
* describes the tracer, even for
* PTRACE_TRACEME, and may differ from the current task.
*
* Blocker fields
* ~~~~~~~~~~~~~~
*
* The filesystem and network blocker arguments identify the request type
* and carry its final missing access subset when applicable. The type
* determines how to interpret the access value.
*/
/*
@ -319,8 +343,8 @@ TRACE_EVENT(landlock_create_ruleset,
TP_ARGS(ruleset),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( u64, ruleset_id )
__field( u64, ruleset_version )
__field( access_mask_t, handled_fs )
__field( access_mask_t, handled_net )
__field( access_mask_t, scoped )
@ -334,7 +358,7 @@ TRACE_EVENT(landlock_create_ruleset,
__entry->scoped = ruleset->handled_masks.scope;
),
TP_printk("ruleset=%llx.%u handled_fs=%s handled_net=%s scoped=%s",
TP_printk("ruleset=%llx.%llu 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),
@ -359,8 +383,8 @@ TRACE_EVENT(landlock_free_ruleset,
TP_ARGS(ruleset),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( u64, ruleset_id )
__field( u64, ruleset_version )
),
TP_fast_assign(
@ -368,17 +392,19 @@ TRACE_EVENT(landlock_free_ruleset,
__entry->ruleset_version = ruleset->version;
),
TP_printk("ruleset=%llx.%u",
TP_printk("ruleset=%llx.%llu",
__entry->ruleset_id, __entry->ruleset_version)
);
/**
* landlock_add_rule_fs - Filesystem rule added to a ruleset
* landlock_add_rule_path_beneath - Path-beneath 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).
* @flags: Complete validated landlock_add_rule_flags value supplied by this
* successful call, not the rule's accumulated quiet state.
* @access_rights: Canonical per-call access mask passed to
* landlock_insert_rule() after normalization, not the raw
* sys_landlock_add_rule() argument or accumulated rule.
* @path: Filesystem path for the rule (never NULL).
* @pathname: Resolved absolute path string (never NULL; error placeholder
* on resolution failure).
@ -387,17 +413,17 @@ TRACE_EVENT(landlock_free_ruleset,
* the reported ruleset is a stable snapshot that no concurrent writer can
* change.
*/
TRACE_EVENT(landlock_add_rule_fs,
TRACE_EVENT(landlock_add_rule_path_beneath,
TP_PROTO(const struct landlock_ruleset *ruleset,
access_mask_t access_rights, const struct path *path,
TP_PROTO(const struct landlock_ruleset *ruleset, u32 flags,
u64 access_rights, const struct path *path,
const char *pathname),
TP_ARGS(ruleset, access_rights, path, pathname),
TP_ARGS(ruleset, flags, access_rights, path, pathname),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( u64, ruleset_id )
__field( u64, ruleset_version )
__field( access_mask_t, access_rights )
__field( dev_t, dev )
__field( ino_t, ino )
@ -418,7 +444,7 @@ TRACE_EVENT(landlock_add_rule_fs,
__assign_str(pathname);
),
TP_printk("ruleset=%llx.%u access_rights=%s dev=%u:%u ino=%lu path=%s",
TP_printk("ruleset=%llx.%llu 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,
@ -427,31 +453,33 @@ TRACE_EVENT(landlock_add_rule_fs,
);
/**
* landlock_add_rule_net - Network port rule added to a ruleset
* landlock_add_rule_net_port - 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.
* @flags: Complete validated landlock_add_rule_flags value supplied by this
* successful call, not the rule's accumulated quiet state.
* @access_rights: Canonical per-call access mask passed to
* landlock_insert_rule() after normalization, not the raw
* sys_landlock_add_rule() argument or accumulated rule.
* @port: Network port in host endianness, forwarded directly from
* &landlock_net_port_attr.port.
*
* 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,
TRACE_EVENT(landlock_add_rule_net_port,
TP_PROTO(const struct landlock_ruleset *ruleset,
access_mask_t access_rights, __u64 port),
TP_PROTO(const struct landlock_ruleset *ruleset, u32 flags,
u64 access_rights, u64 port),
TP_ARGS(ruleset, access_rights, port),
TP_ARGS(ruleset, flags, access_rights, port),
TP_STRUCT__entry(
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( u64, ruleset_id )
__field( u64, ruleset_version )
__field( access_mask_t, access_rights )
__field( __u64, port )
__field( u64, port )
),
TP_fast_assign(
@ -462,7 +490,7 @@ TRACE_EVENT(landlock_add_rule_net,
__entry->port = port;
),
TP_printk("ruleset=%llx.%u access_rights=%s port=%llu",
TP_printk("ruleset=%llx.%llu access_rights=%s port=%llu",
__entry->ruleset_id, __entry->ruleset_version,
__print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_NET_NAMES),
__entry->port)
@ -495,10 +523,10 @@ TRACE_EVENT(landlock_create_domain,
TP_ARGS(domain, ruleset),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( __u64, parent_id )
__field( __u64, ruleset_id )
__field( __u32, ruleset_version )
__field( u64, domain_id )
__field( u64, parent_id )
__field( u64, ruleset_id )
__field( u64, ruleset_version )
),
TP_fast_assign(
@ -510,7 +538,7 @@ TRACE_EVENT(landlock_create_domain,
__entry->ruleset_version = ruleset->version;
),
TP_printk("domain=%llx parent=%llx ruleset=%llx.%u",
TP_printk("domain=%llx parent=%llx ruleset=%llx.%llu",
__entry->domain_id, __entry->parent_id,
__entry->ruleset_id, __entry->ruleset_version)
);
@ -557,7 +585,7 @@ TRACE_EVENT(landlock_enforce_domain,
TP_ARGS(domain, complete, process_wide, no_new_privs),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( bool, complete )
__field( bool, process_wide )
__field( bool, no_new_privs )
@ -595,8 +623,8 @@ TRACE_EVENT(landlock_free_domain,
TP_ARGS(hierarchy),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( __u64, denials )
__field( u64, domain_id )
__field( u64, denials )
),
TP_fast_assign(
@ -609,7 +637,7 @@ TRACE_EVENT(landlock_free_domain,
);
/**
* landlock_check_rule_fs - Filesystem rule evaluated during access check
* landlock_check_rule_inode - Inode rule evaluated during access check
*
* @domain: Enforcing domain (never NULL).
* @rule: Matching rule with per-layer access masks (never NULL).
@ -622,16 +650,16 @@ TRACE_EVENT(landlock_free_domain,
* domain layer. See Documentation/trace/events-landlock.rst for how to
* interpret it.
*/
TRACE_EVENT(landlock_check_rule_fs,
TRACE_EVENT(landlock_check_rule_inode,
TP_PROTO(const struct landlock_domain *domain,
const struct landlock_rule *rule,
access_mask_t access_request, const struct dentry *dentry),
u64 access_request, const struct dentry *dentry),
TP_ARGS(domain, rule, access_request, dentry),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( access_mask_t, access_request )
__field( dev_t, dev )
__field( ino_t, ino )
@ -648,7 +676,8 @@ TRACE_EVENT(landlock_check_rule_fs,
__trace_landlock_fill_layers(__get_dynamic_array(grants),
__get_dynamic_array_len(grants) /
sizeof(access_mask_t),
rule, access_request);
rule,
(access_mask_t)access_request);
),
TP_printk("domain=%llx access_request=%s dev=%u:%u ino=%lu grants=%s",
@ -659,7 +688,7 @@ TRACE_EVENT(landlock_check_rule_fs,
);
/**
* landlock_check_rule_net - Network port rule evaluated during access check
* landlock_check_rule_net_port - Network port rule evaluated
*
* @domain: Enforcing domain (never NULL).
* @rule: Matching rule with per-layer access masks (never NULL).
@ -671,18 +700,18 @@ TRACE_EVENT(landlock_check_rule_fs,
* layer. See Documentation/trace/events-landlock.rst for how to
* interpret it.
*/
TRACE_EVENT(landlock_check_rule_net,
TRACE_EVENT(landlock_check_rule_net_port,
TP_PROTO(const struct landlock_domain *domain,
const struct landlock_rule *rule,
access_mask_t access_request, __u64 port),
u64 access_request, u64 port),
TP_ARGS(domain, rule, access_request, port),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( access_mask_t, access_request )
__field( __u64, port )
__field( u64, port )
__dynamic_array(access_mask_t, grants,
domain->num_layers)
),
@ -695,7 +724,8 @@ TRACE_EVENT(landlock_check_rule_net,
__trace_landlock_fill_layers(__get_dynamic_array(grants),
__get_dynamic_array_len(grants) /
sizeof(access_mask_t),
rule, access_request);
rule,
(access_mask_t)access_request);
),
TP_printk("domain=%llx access_request=%s port=%llu grants=%s",
@ -712,8 +742,7 @@ TRACE_EVENT(landlock_check_rule_net,
* 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).
* @blockers: Request type and final missing access subset (never NULL).
* @path: Filesystem path that was denied (never NULL).
* @pathname: Resolved path string (never NULL; an error placeholder on
* resolution failure).
@ -723,16 +752,17 @@ TRACE_EVENT(landlock_check_rule_net,
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),
bool logged, const struct landlock_blockers *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( u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( access_mask_t, blockers )
__field( enum landlock_request_type, blockers_type )
__field( access_mask_t, blockers_access )
__field( dev_t, dev )
__field( ino_t, ino )
__string( pathname, pathname )
@ -744,7 +774,8 @@ TRACE_EVENT(landlock_deny_access_fs,
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->blockers = blockers;
__entry->blockers_type = blockers->type;
__entry->blockers_access = blockers->access;
__entry->dev = path->dentry->d_sb->s_dev;
/*
* A negative dentry has no backing inode, so mirror the
@ -756,12 +787,20 @@ TRACE_EVENT(landlock_deny_access_fs,
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),
__entry->blockers_type == LANDLOCK_REQUEST_FS_ACCESS ?
__print_flags(__entry->blockers_access, "|", _LANDLOCK_ACCESS_FS_NAMES) :
__print_symbolic(__entry->blockers_type,
_LANDLOCK_FS_BLOCKER_TYPE_NAMES),
MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
__trace_print_untrusted_str(p, __get_str(pathname),
__get_dynamic_array_len(pathname) - 1))
);
static_assert(offsetof(struct sockaddr_in, sin_port) ==
offsetof(struct sockaddr_in6, sin6_port));
static_assert(sizeof_field(struct sockaddr_in, sin_port) ==
sizeof_field(struct sockaddr_in6, sin6_port));
/**
* landlock_deny_access_net - Network access denied
*
@ -769,54 +808,69 @@ TRACE_EVENT(landlock_deny_access_fs,
* 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.
* @blockers: Request type and final missing access subset (never NULL).
* @sk: Socket object (never NULL), read without a socket lock, so its fields
* are a best-effort snapshot.
* @socket_family: Socket-family snapshot used by the verdict.
* @address: Authoritative address checked by the verdict (never NULL).
* The producer copies @addrlen bytes from the checked address and
* zeroes the remaining storage before emission. The
* &sockaddr_in.sin_port or &sockaddr_in6.sin6_port member, when
* present, remains in network endianness.
* @addrlen: Validated signed length of @address.
*
* 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.
* Emitted when a Landlock domain denies a network operation. The blocker
* identifies whether the address is a bind or connect/send policy object.
* The flattened port field is converted from the checked address to host
* endianness, or is -1 when no port was checked. Zero is a valid checked
* port.
*/
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),
bool logged, const struct landlock_blockers *blockers,
const struct sock *sk, u16 socket_family,
const struct sockaddr_storage *address, int addrlen),
TP_ARGS(hierarchy, same_exec, logged, blockers, sk, sport, dport),
TP_ARGS(hierarchy, same_exec, logged, blockers, sk, socket_family,
address, addrlen),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( access_mask_t, blockers )
__field( __u64, sport )
__field( __u64, dport )
__field( enum landlock_request_type, blockers_type )
__field( access_mask_t, blockers_access )
__field( s64, port )
),
TP_fast_assign(
const struct sockaddr *const addr =
(const struct sockaddr *)address;
const bool has_port =
addrlen >= (int)offsetofend(struct sockaddr_in, sin_port) &&
(addr->sa_family == AF_INET ||
addr->sa_family == AF_INET6 ||
(addr->sa_family == AF_UNSPEC &&
socket_family == AF_INET));
__entry->domain_id = hierarchy->id;
__entry->same_exec = same_exec;
__entry->logged = logged;
__entry->blockers = blockers;
__entry->sport = sport;
__entry->dport = dport;
__entry->blockers_type = blockers->type;
__entry->blockers_access = blockers->access;
__entry->port =
has_port ?
ntohs(((const struct sockaddr_in *)addr)->sin_port) :
-1;
),
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)
TP_printk("domain=%llx same_exec=%d logged=%d blockers=%s port=%lld",
__entry->domain_id, __entry->same_exec, __entry->logged,
__entry->blockers_type == LANDLOCK_REQUEST_NET_ACCESS ?
__print_flags(__entry->blockers_access, "|", _LANDLOCK_ACCESS_NET_NAMES) :
"unknown",
__entry->port)
);
/**
@ -824,12 +878,14 @@ TRACE_EVENT(landlock_deny_access_net,
*
* @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.
* @same_exec: Whether the tracer 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).
* @tracer: The tracer or proposed tracer (never NULL); for PTRACE_TRACEME
* this is the parent, not the syscall caller.
*
* Emitted when a Landlock domain denies a ptrace operation.
*/
@ -837,15 +893,16 @@ 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),
const struct task_struct *tracee,
const struct task_struct *tracer),
TP_ARGS(hierarchy, same_exec, logged, tracee_domain_id, tracee),
TP_ARGS(hierarchy, same_exec, logged, tracee_domain_id, tracee, tracer),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, tracee_domain_id)
__field( u64, tracee_domain_id)
__field( pid_t, tracee_pid )
__string( tracee_comm, tracee->comm )
),
@ -872,12 +929,14 @@ TRACE_EVENT(landlock_deny_ptrace,
*
* @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.
* @same_exec: Whether the policy subject 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).
* @signal: The signal selected by the denied check. Zero is a permission
* probe, not an absent value.
*
* Emitted when a Landlock domain denies signal delivery to a scoped-out
* target.
@ -886,15 +945,15 @@ 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),
const struct task_struct *target, int signal),
TP_ARGS(hierarchy, same_exec, logged, target_domain_id, target),
TP_ARGS(hierarchy, same_exec, logged, target_domain_id, target, signal),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, target_domain_id)
__field( u64, target_domain_id)
__field( pid_t, target_pid )
__string( target_comm, target->comm )
),
@ -940,10 +999,10 @@ TRACE_EVENT(landlock_deny_scope_abstract_unix_socket,
TP_ARGS(hierarchy, same_exec, logged, peer_domain_id, peer),
TP_STRUCT__entry(
__field( __u64, domain_id )
__field( u64, domain_id )
__field( bool, same_exec )
__field( bool, logged )
__field( __u64, peer_domain_id )
__field( u64, peer_domain_id )
__field( pid_t, peer_pid )
/*
* Abstract socket names are untrusted binary data from
@ -973,11 +1032,10 @@ TRACE_EVENT(landlock_deny_scope_abstract_unix_socket,
__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.
* Best-effort (0 for a datagram peer). The caller holds the
* peer's AF_UNIX state lock, serializing published peercred
* updates. The peer socket keeps a reference to sk_peer_pid
* through pid_nr(); sun_path is the reliable identifier.
*/
peer_pid = READ_ONCE(peer->sk_peer_pid);
__entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0;
@ -991,6 +1049,7 @@ TRACE_EVENT(landlock_deny_scope_abstract_unix_socket,
__get_dynamic_array_len(sun_path) - 1))
);
#undef _LANDLOCK_FS_BLOCKER_TYPE_NAMES
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */

View File

@ -61,6 +61,10 @@ union access_masks_all {
static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
sizeof(typeof_member(union access_masks_all, all)));
#define _LANDLOCK_LAYER_MASK_PADDING \
(BITS_PER_TYPE(access_mask_t) - LANDLOCK_NUM_ACCESS_MAX - \
IS_ENABLED(CONFIG_SECURITY_LANDLOCK_LOG))
/**
* struct layer_mask - The access rights and rule flags for a layer.
*
@ -81,6 +85,10 @@ struct layer_mask {
*/
access_mask_t quiet : 1;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/**
* @__pad: Padding for the compiler's bitfield initialization.
*/
access_mask_t __pad : _LANDLOCK_LAYER_MASK_PADDING;
} __packed __aligned(sizeof(access_mask_t));
/*

View File

@ -243,7 +243,8 @@ struct landlock_domain {
* overlapping access rights. These layers are set once
* and never changed for the lifetime of the domain.
*/
struct access_masks handled_masks[];
struct access_masks
handled_masks[] __counted_by(num_layers);
};
};
};

View File

@ -356,14 +356,14 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
* held for BTF consistency (enforced by lockdep_assert_held in
* TP_fast_assign).
*/
if (!err && trace_landlock_add_rule_fs_enabled()) {
if (!err && trace_landlock_add_rule_path_beneath_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);
trace_landlock_add_rule_path_beneath(
ruleset, flags, access_rights, path, pathname);
}
mutex_unlock(&ruleset->lock);
@ -423,8 +423,8 @@ static bool unmask_layers_fs(const struct landlock_domain *const domain,
ret = landlock_unmask_layers(domain, id, masks, &rule);
if (rule)
trace_landlock_check_rule_fs(domain, rule, access_request,
dentry);
trace_landlock_check_rule_inode(domain, rule, access_request,
dentry);
return ret;
}

View File

@ -15,6 +15,8 @@
struct landlock_cred_security;
struct landlock_hierarchy;
struct sockaddr;
struct task_struct;
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
@ -25,6 +27,31 @@ enum landlock_request_type {
LANDLOCK_REQUEST_SCOPE_SIGNAL,
};
struct landlock_blockers {
access_mask_t access;
enum landlock_request_type type;
};
#ifdef CONFIG_TRACEPOINTS
struct landlock_net_trace {
const struct sockaddr *address;
int addrlen;
u16 socket_family;
};
struct landlock_ptrace_trace {
u64 tracee_domain_id;
const struct task_struct *tracer;
};
struct landlock_signal_trace {
u64 target_domain_id;
int signal;
};
#endif /* CONFIG_TRACEPOINTS */
/*
* 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
@ -52,13 +79,24 @@ struct landlock_request {
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_TRACEPOINTS
union {
/*
* Other-party domain ID for an abstract UNIX socket scope
* denial, or 0 if that party is unsandboxed. Store an ID, not
* a pointer: the other task can replace its credential and free
* the domain it referenced.
*/
u64 other_domain_id;
/* Synchronous context for a network denial. */
const struct landlock_net_trace *trace_net;
/* Synchronous context for a ptrace denial. */
const struct landlock_ptrace_trace *trace_ptrace;
/* Synchronous context for a signal denial. */
const struct landlock_signal_trace *trace_signal;
};
#endif /* CONFIG_TRACEPOINTS */
};
#ifdef CONFIG_SECURITY_LANDLOCK_LOG

View File

@ -47,7 +47,8 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
* TP_fast_assign).
*/
if (!err)
trace_landlock_add_rule_net(ruleset, access_rights, port);
trace_landlock_add_rule_net_port(ruleset, flags, access_rights,
port);
mutex_unlock(&ruleset->lock);
return err;
@ -63,7 +64,7 @@ static bool unmask_layers_net(const struct landlock_domain *const domain,
ret = landlock_unmask_layers(domain, id, masks, &rule);
if (rule)
trace_landlock_check_rule_net(
trace_landlock_check_rule_net_port(
domain, rule, access_request,
ntohs((__force __be16)id.key.data));
return ret;
@ -92,7 +93,7 @@ static int current_check_access_socket(struct socket *const sock,
return 0;
/* Checks for minimal header length to safely read sa_family. */
if (addrlen < offsetofend(typeof(*address), sa_family))
if (addrlen < (int)offsetofend(typeof(*address), sa_family))
return -EINVAL;
/*
@ -144,6 +145,17 @@ static int current_check_access_socket(struct socket *const sock,
.audit.u.net = &audit_net,
.access = access_request,
.layer_masks = &layer_masks,
#ifdef CONFIG_TRACEPOINTS
.trace_net =
&(struct landlock_net_trace){
.address =
address,
.addrlen =
addrlen,
.socket_family =
sock_family,
},
#endif /* CONFIG_TRACEPOINTS */
});
return -EACCES;
}
@ -275,14 +287,22 @@ static int current_check_access_socket(struct socket *const sock,
audit_net.family = address->sa_family;
audit_net.sk = sock->sk;
landlock_log_denial(subject,
&(struct landlock_request){
.type = LANDLOCK_REQUEST_NET_ACCESS,
.audit.type = LSM_AUDIT_DATA_NET,
.audit.u.net = &audit_net,
.access = access_request,
.layer_masks = &layer_masks,
});
landlock_log_denial(
subject, &(struct landlock_request){
.type = LANDLOCK_REQUEST_NET_ACCESS,
.audit.type = LSM_AUDIT_DATA_NET,
.audit.u.net = &audit_net,
.access = access_request,
.layer_masks = &layer_masks,
#ifdef CONFIG_TRACEPOINTS
.trace_net =
&(struct landlock_net_trace){
.address = address,
.addrlen = addrlen,
.socket_family = sock_family,
},
#endif /* CONFIG_TRACEPOINTS */
});
return -EACCES;
}

View File

@ -171,9 +171,10 @@ struct landlock_ruleset {
* @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.
* the exact successful rule history it was created from. Protected by
* @lock.
*/
u32 version;
u64 version;
/**
* @id: Unique identifier for this ruleset, used for tracing.
*/

View File

@ -88,7 +88,9 @@ static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
const struct landlock_cred_security *parent_subject;
#ifdef CONFIG_TRACEPOINTS
u64 tracee_domain_id = 0;
#endif /* CONFIG_TRACEPOINTS */
int err;
/* Quick return for non-landlocked tasks. */
@ -100,10 +102,10 @@ static int hook_ptrace_access_check(struct task_struct *const child,
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
#ifdef CONFIG_TRACEPOINTS
if (child_dom)
tracee_domain_id = child_dom->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* CONFIG_TRACEPOINTS */
}
if (!err)
@ -121,7 +123,12 @@ 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,
#ifdef CONFIG_TRACEPOINTS
.trace_ptrace = &(struct landlock_ptrace_trace) {
.tracee_domain_id = tracee_domain_id,
.tracer = current,
},
#endif /* CONFIG_TRACEPOINTS */
});
return err;
@ -142,7 +149,6 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
const struct landlock_domain *child_dom;
u64 tracee_domain_id = 0;
int err;
child_dom = landlock_get_current_domain();
@ -154,12 +160,6 @@ 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
@ -174,7 +174,13 @@ 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,
#ifdef CONFIG_TRACEPOINTS
.trace_ptrace = &(struct landlock_ptrace_trace) {
/* The current task's domain is stable here. */
.tracee_domain_id = child_dom ? child_dom->hierarchy->id : 0,
.tracer = parent,
},
#endif /* CONFIG_TRACEPOINTS */
});
return err;
}
@ -250,8 +256,7 @@ static bool domain_is_scoped(const struct landlock_domain *const client,
}
static bool sock_is_scoped(struct sock *const other,
const struct landlock_domain *const domain,
u64 *const peer_domain_id)
const struct landlock_domain *const domain)
{
const struct landlock_domain *dom_other;
@ -269,13 +274,23 @@ static bool sock_is_scoped(struct sock *const other,
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);
}
#ifdef CONFIG_TRACEPOINTS
static u64 get_socket_domain_id(const struct sock *const other)
{
const struct landlock_domain *domain;
lockdep_assert_held(&unix_sk(other)->lock);
domain = landlock_cred(other->sk_socket->file->f_cred)->domain;
return domain ? domain->hierarchy->id : 0;
}
#endif /* CONFIG_TRACEPOINTS */
static bool is_abstract_socket(struct sock *const sock)
{
struct unix_address *addr = unix_sk(sock)->addr;
@ -299,7 +314,6 @@ 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);
@ -311,7 +325,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, &peer_domain_id))
if (!sock_is_scoped(other, subject->domain))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@ -323,7 +337,9 @@ static int hook_unix_stream_connect(struct sock *const sock,
},
},
.layer_plus_one = handle_layer + 1,
.other_domain_id = peer_domain_id,
#ifdef CONFIG_TRACEPOINTS
.other_domain_id = get_socket_domain_id(other),
#endif /* CONFIG_TRACEPOINTS */
});
return -EPERM;
}
@ -332,7 +348,6 @@ 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);
@ -350,7 +365,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, &peer_domain_id))
if (!sock_is_scoped(other->sk, subject->domain))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@ -362,7 +377,9 @@ static int hook_unix_may_send(struct socket *const sock,
},
},
.layer_plus_one = handle_layer + 1,
.other_domain_id = peer_domain_id,
#ifdef CONFIG_TRACEPOINTS
.other_domain_id = get_socket_domain_id(other->sk),
#endif /* CONFIG_TRACEPOINTS */
});
return -EPERM;
}
@ -377,7 +394,9 @@ static int hook_task_kill(struct task_struct *const p,
{
bool is_scoped;
size_t handle_layer;
#ifdef CONFIG_TRACEPOINTS
u64 target_domain_id = 0;
#endif /* CONFIG_TRACEPOINTS */
const struct landlock_cred_security *subject;
if (!cred) {
@ -409,10 +428,10 @@ static int hook_task_kill(struct task_struct *const p,
is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
#ifdef CONFIG_TRACEPOINTS
if (other)
target_domain_id = other->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* CONFIG_TRACEPOINTS */
}
if (!is_scoped)
@ -425,7 +444,12 @@ 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,
#ifdef CONFIG_TRACEPOINTS
.trace_signal = &(struct landlock_signal_trace) {
.target_domain_id = target_domain_id,
.signal = sig,
},
#endif /* CONFIG_TRACEPOINTS */
});
return -EPERM;
}
@ -435,7 +459,9 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
{
const struct landlock_cred_security *subject;
bool is_scoped = false;
#ifdef CONFIG_TRACEPOINTS
u64 target_domain_id = 0;
#endif /* CONFIG_TRACEPOINTS */
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
@ -468,10 +494,10 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
#ifdef CONFIG_TRACEPOINTS
if (other)
target_domain_id = other->hierarchy->id;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* CONFIG_TRACEPOINTS */
}
if (!is_scoped)
@ -486,7 +512,12 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
.other_domain_id = target_domain_id,
#ifdef CONFIG_TRACEPOINTS
.trace_signal = &(struct landlock_signal_trace) {
.target_domain_id = target_domain_id,
.signal = signum ? signum : SIGIO,
},
#endif /* CONFIG_TRACEPOINTS */
});
return -EPERM;
}

View File

@ -12,6 +12,7 @@
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/lsm_audit.h>
#include <linux/socket.h>
#include <net/sock.h>
#include "access.h"
@ -61,8 +62,8 @@ void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy
*
* @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
* @missing: The final missing access subset, when applicable.
* @same_exec: Whether the policy subject 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
@ -83,6 +84,10 @@ void landlock_trace_denial(
case LANDLOCK_REQUEST_FS_ACCESS:
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
if (trace_landlock_deny_access_fs_enabled()) {
const struct landlock_blockers blockers = {
.access = missing,
.type = request->type,
};
char *buf __free(__putname) = __getname();
struct path dentry_path;
const char *pathname;
@ -147,29 +152,67 @@ void landlock_trace_denial(
trace_landlock_deny_access_fs(youngest_denied,
same_exec, logged,
missing, path, pathname);
&blockers, path,
pathname);
}
break;
case LANDLOCK_REQUEST_NET_ACCESS:
if (trace_landlock_deny_access_net_enabled())
if (trace_landlock_deny_access_net_enabled()) {
const struct landlock_net_trace *const trace_net =
request->trace_net;
const struct landlock_blockers blockers = {
.access = missing,
.type = request->type,
};
struct sockaddr_storage address = {};
if (WARN_ON_ONCE(!trace_net || !trace_net->address))
return;
if (WARN_ON_ONCE(
trace_net->addrlen <
(int)offsetofend(struct sockaddr,
sa_family) ||
trace_net->addrlen > (int)sizeof(address)))
return;
memcpy(&address, trace_net->address,
trace_net->addrlen);
trace_landlock_deny_access_net(
youngest_denied, same_exec, logged, missing,
youngest_denied, same_exec, logged, &blockers,
request->audit.u.net->sk,
ntohs(request->audit.u.net->sport),
ntohs(request->audit.u.net->dport));
trace_net->socket_family, &address,
trace_net->addrlen);
}
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);
if (trace_landlock_deny_ptrace_enabled()) {
const struct landlock_ptrace_trace *const trace_ptrace =
request->trace_ptrace;
if (WARN_ON_ONCE(!trace_ptrace ||
!trace_ptrace->tracer))
return;
trace_landlock_deny_ptrace(
youngest_denied, same_exec, logged,
trace_ptrace->tracee_domain_id,
request->audit.u.tsk, trace_ptrace->tracer);
}
break;
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
if (trace_landlock_deny_scope_signal_enabled())
if (trace_landlock_deny_scope_signal_enabled()) {
const struct landlock_signal_trace *const trace_signal =
request->trace_signal;
if (WARN_ON_ONCE(!trace_signal))
return;
trace_landlock_deny_scope_signal(
youngest_denied, same_exec, logged,
request->other_domain_id, request->audit.u.tsk);
trace_signal->target_domain_id,
request->audit.u.tsk, trace_signal->signal);
}
break;
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
if (trace_landlock_deny_scope_abstract_unix_socket_enabled())

View File

@ -10493,9 +10493,9 @@ FIXTURE_TEARDOWN_PARENT(trace_layout1)
}
/*
* 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.
* Verifies that check_rule_inode 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)
{
@ -10529,7 +10529,7 @@ TEST_F(trace_layout1, check_rule_fs_fields)
EXPECT_EQ(1,
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)))
{
TH_LOG("Expected 1 check_rule_fs event\n%s", buf);
TH_LOG("Expected 1 check_rule_inode event\n%s", buf);
}
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
@ -10570,8 +10570,8 @@ TEST_F(trace_layout1, check_rule_fs_fields)
}
/*
* 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
* Verifies check_rule_inode 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)
@ -10643,14 +10643,14 @@ TEST_F(trace_layout1, check_rule_fs_multiple_rules)
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
* Only one check_rule_inode event: the rule on dir_s1d2 fully unmasks
* 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,
TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count,
buf);
}
@ -10777,7 +10777,7 @@ TEST_F(trace_layout1, check_rule_fs_optional_access)
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,
TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count,
buf);
}
@ -10796,7 +10796,7 @@ TEST_F(trace_layout1, check_rule_fs_optional_access)
}
/*
* Verifies that check_rule_fs fires for a rule that matches the inode even when
* Verifies that check_rule_inode fires for a rule matching 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.
@ -10884,7 +10884,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant)
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,
TH_LOG("Expected 2 check_rule_inode events, got %d\n%s", count,
buf);
}
@ -10894,7 +10894,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant)
tracefs_count_matches(
buf,
TRACE_PREFIX(
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ "
"access_request=read_dir "
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
"grants={}$"))
@ -10908,7 +10908,7 @@ TEST_F(trace_layout1, check_rule_fs_empty_grant)
tracefs_count_matches(
buf,
TRACE_PREFIX(
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ "
"access_request=read_dir "
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
"grants={read_dir}$"))

View File

@ -3481,13 +3481,14 @@ TEST_F(trace_net, deny_access_net_bind)
}
/*
* 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.
* Anchors the denial fields shared by every deny_access_net event so a port
* test also proves the denying domain, execution status, logging verdict, and
* exact blocked access.
*/
static void
expect_net_deny_common_fields(struct __test_metadata *const _metadata,
const char *const buf)
const char *const buf,
const char *const expected_blockers)
{
char field[64];
@ -3511,18 +3512,21 @@ expect_net_deny_common_fields(struct __test_metadata *const _metadata,
ASSERT_EQ(0,
tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
"blockers", field, sizeof(field)));
EXPECT_STRNE("", field);
EXPECT_STREQ(expected_blockers, field);
}
/* Connect and field-check tests use a separate fixture without variants. */
enum trace_net_operation {
TRACE_NET_BIND,
TRACE_NET_SEND,
};
/* clang-format off */
FIXTURE(trace_net_connect) {
FIXTURE(trace_net_address) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_net_connect)
FIXTURE_SETUP(trace_net_address)
{
int ret;
@ -3547,7 +3551,7 @@ FIXTURE_SETUP(trace_net_connect)
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_net_connect)
FIXTURE_TEARDOWN(trace_net_address)
{
if (!self->tracefs_ok)
return;
@ -3559,160 +3563,183 @@ FIXTURE_TEARDOWN(trace_net_connect)
}
/* clang-format off */
FIXTURE_VARIANT(trace_net_connect) {
FIXTURE_VARIANT(trace_net_address) {
/* 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;
int socket_family;
int socket_type;
enum trace_net_operation operation;
int address_family;
socklen_t addrlen;
__u64 handled_access;
const char *expected_blockers;
bool address_port_zero;
bool expected_address_port;
int expected_port;
};
/* 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,
FIXTURE_VARIANT_ADD(trace_net_address, ipv4_tcp_bind) {
/* clang-format on */
.socket_family = AF_INET,
.socket_type = SOCK_STREAM,
.operation = TRACE_NET_BIND,
.address_family = AF_INET,
.addrlen = sizeof(struct sockaddr_in),
.handled_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.expected_blockers = "bind_tcp",
.expected_address_port = 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,
/* Explicit bind(0) has a checked zero port. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net_address, ipv4_udp_bind_zero) {
/* clang-format on */
.socket_family = AF_INET,
.socket_type = SOCK_DGRAM,
.operation = TRACE_NET_BIND,
.address_family = AF_INET,
.addrlen = sizeof(struct sockaddr_in),
.handled_access = LANDLOCK_ACCESS_NET_BIND_UDP,
.expected_blockers = "bind_udp",
.address_port_zero = true,
.expected_port = 0,
};
/* 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,
/* A UDP send can deny its synthetic unspecified bind endpoint. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net_address, ipv6_udp_autobind) {
/* clang-format on */
.socket_family = AF_INET6,
.socket_type = SOCK_DGRAM,
.operation = TRACE_NET_SEND,
.address_family = AF_INET6,
.addrlen = sizeof(struct sockaddr_in6),
.handled_access = LANDLOCK_ACCESS_NET_BIND_UDP,
.expected_blockers = "bind_udp",
.expected_port = 0,
};
/* clang-format on */
/* A family-only address has no checked port. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_net_address, ipv6_unspec_udp_send_min) {
/* clang-format on */
.socket_family = AF_INET6,
.socket_type = SOCK_DGRAM,
.operation = TRACE_NET_SEND,
.address_family = AF_UNSPEC,
.addrlen = sizeof(sa_family_t),
.handled_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.expected_blockers = "connect_send_udp",
.expected_port = -1,
};
/*
* 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)
static void set_trace_net_address(struct sockaddr_storage *const storage,
const int socket_family,
const int address_family,
const unsigned short port)
{
pid_t child;
int status;
char *buf;
memset(storage, 0, sizeof(*storage));
if (socket_family == AF_INET) {
struct sockaddr_in *const addr4 = (struct sockaddr_in *)storage;
addr4->sin_family = address_family;
addr4->sin_port = htons(port);
addr4->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
} else {
struct sockaddr_in6 *const addr6 =
(struct sockaddr_in6 *)storage;
addr6->sin6_family = address_family;
addr6->sin6_port = htons(port);
addr6->sin6_addr = in6addr_loopback;
}
}
/* Verifies the actionable signed port for representative checked shapes. */
TEST_F(trace_net_address, deny_access_net)
{
const char *const event_regex = REGEX_DENY_ACCESS_NET(TRACE_TASK);
const unsigned short address_port =
variant->address_port_zero ? 0 : sock_port_start + 1;
const int expected_port = variant->expected_address_port ?
address_port :
variant->expected_port;
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = variant->handled_access,
};
struct sockaddr_storage address;
char field[64], expected[16];
char *buf;
int count, ret, ruleset_fd, socket_fd, status;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
set_trace_net_address(&address, variant->socket_family,
variant->address_family, address_port);
socket_fd = socket(variant->socket_family,
variant->socket_type | SOCK_CLOEXEC, 0);
ASSERT_LE(0, socket_fd);
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(0, tracefs_clear_buf());
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)
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 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);
}
if (landlock_restrict_self(ruleset_fd, 0))
_exit(2);
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);
switch (variant->operation) {
case TRACE_NET_BIND:
ret = bind(socket_fd, (const struct sockaddr *)&address,
variant->addrlen);
break;
case TRACE_NET_SEND:
ret = sendto(socket_fd, "A", 1, MSG_NOSIGNAL,
(const struct sockaddr *)&address,
variant->addrlen);
break;
default:
_exit(3);
}
close(sock_fd);
if (ret >= 0 || errno != EACCES)
_exit(4);
close(socket_fd);
_exit(0);
}
close(ruleset_fd);
close(socket_fd);
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
ASSERT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, event_regex);
if (count != 1)
TH_LOG("Expected 1 denial event, got %d\n%s", count, buf);
ASSERT_EQ(1, count);
expect_net_deny_common_fields(_metadata, buf,
variant->expected_blockers);
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);
ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "port", field,
sizeof(field)));
snprintf(expected, sizeof(expected), "%d", expected_port);
EXPECT_STREQ(expected, field);
free(buf);
}
/* Field verification for the check_rule_net event on an allowed access. */
/* Field verification for the check_rule_net_port event on an allowed access. */
/* clang-format off */
FIXTURE(trace_net_check_rule) {
@ -3757,10 +3784,11 @@ FIXTURE_TEARDOWN(trace_net_check_rule)
/*
* Verifies that an allowed bind matching a net-port rule emits exactly one
* landlock_check_rule_net event with the enforcing domain, the requested
* landlock_check_rule_net_port 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.
* whole event is anchored to exact values so removing the check_rule_net_port
* emission or introducing a byte-order or field-plumbing regression fails the
* test.
*/
TEST_F(trace_net_check_rule, check_rule_net_fields)
{
@ -3832,7 +3860,7 @@ TEST_F(trace_net_check_rule, check_rule_net_fields)
EXPECT_EQ(1,
tracefs_count_matches(buf, REGEX_CHECK_RULE_NET(TRACE_TASK)))
{
TH_LOG("Expected 1 check_rule_net event\n%s", buf);
TH_LOG("Expected 1 check_rule_net_port event\n%s", buf);
}
ASSERT_EQ(0,
@ -3866,11 +3894,4 @@ TEST_F(trace_net_check_rule, check_rule_net_fields)
free(buf);
}
/*
* IPv6 network trace tests are intentionally elided. IPv6 hook dispatch uses
* the same current_check_access_socket() code path as IPv4, validated by the
* audit tests in this file. The trace events use the same blockers/sport/dport
* fields regardless of address family.
*/
TEST_HARNESS_MAIN

View File

@ -27,14 +27,14 @@
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_ADD_RULE_PATH_BENEATH_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_path_beneath/enable"
#define TRACEFS_ADD_RULE_NET_PORT_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net_port/enable"
#define TRACEFS_CHECK_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_inode/enable"
#define TRACEFS_CHECK_RULE_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net_port/enable"
#define TRACEFS_DENY_ACCESS_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
#define TRACEFS_DENY_ACCESS_NET_ENABLE \
@ -79,18 +79,18 @@
*/
#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]\\+ " \
#define REGEX_ADD_RULE_PATH_BENEATH(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_path_beneath: " \
"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) \
#define REGEX_ADD_RULE_NET_PORT(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_net: " \
"landlock_add_rule_net_port: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"access_rights=[a-z_|]* " \
"port=[0-9]\\+$"
@ -110,21 +110,21 @@
"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]\\+ " \
#define REGEX_CHECK_RULE_FS(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_inode: " \
"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]\\+ " \
#define REGEX_CHECK_RULE_NET(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_net_port: " \
"domain=[0-9a-f]\\+ " \
"access_request=[a-z_|]* " \
"port=[0-9]\\+ " \
"grants={[a-z_|,]*}$"
#define REGEX_DENY_ACCESS_FS(task) \
@ -145,8 +145,7 @@
"same_exec=[01] " \
"logged=[01] " \
"blockers=[a-z_|]* " \
"sport=[0-9]\\+ " \
"dport=[0-9]\\+$"
"port=-\\?[0-9]\\+$"
#define REGEX_DENY_PTRACE(task) \
TRACE_PREFIX(task) \

View File

@ -121,7 +121,8 @@ FIXTURE_SETUP(trace_fs)
}
self->tracefs_ok = 1;
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_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());
@ -134,7 +135,7 @@ FIXTURE_TEARDOWN(trace_fs)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false);
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
tracefs_fixture_teardown();
@ -183,11 +184,11 @@ TEST_F(trace_fs, unsandboxed)
}
/*
* 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.
* Verifies that adding a filesystem rule emits a landlock_add_rule_path_beneath
* event with the expected path and field values: the ruleset ID and
* access_rights are non-zero, and the path matches.
*/
TEST_F(trace_fs, add_rule_fs)
TEST_F(trace_fs, add_rule_path_beneath)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
@ -215,28 +216,30 @@ TEST_F(trace_fs, add_rule_fs)
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
count = tracefs_count_matches(buf,
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
TH_LOG("Expected 1 add_rule_path_beneath 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)));
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_ADD_RULE_PATH_BENEATH(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)));
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_ADD_RULE_PATH_BENEATH(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)));
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
"path", field_buf, sizeof(field_buf)));
EXPECT_STREQ("/usr", field_buf);
free(buf);
@ -246,7 +249,7 @@ TEST_F(trace_fs, add_rule_fs)
* Verifies that a path whose escaping exceeds the trace scratch sequence does
* not corrupt a sibling symbolic field.
*/
TEST_F(trace_fs, add_rule_fs_escaped_path_overflow)
TEST_F(trace_fs, add_rule_path_beneath_escaped_path_overflow)
{
static const char access_prefix[] = "execute|write_file|read_file|";
static const char access_suffix[] = "|ioctl_dev|resolve_unix";
@ -277,10 +280,12 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow)
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
count = tracefs_count_matches(buf,
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s",
count, buf);
}
/*
@ -288,9 +293,9 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow)
* field also catches scratch-sequence poisoning when the compiler
* evaluates the overflowing path first, as GCC currently does.
*/
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
"access_rights", field_buf,
sizeof(field_buf)));
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
"access_rights", field_buf, sizeof(field_buf)));
EXPECT_EQ(0,
strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1));
EXPECT_EQ(NULL, strstr(field_buf, "|refer|"));
@ -298,7 +303,8 @@ TEST_F(trace_fs, add_rule_fs_escaped_path_overflow)
ASSERT_LE(sizeof(access_suffix) - 1, field_len);
EXPECT_STREQ(access_suffix,
field_buf + field_len - (sizeof(access_suffix) - 1));
expect_truncated_path(_metadata, buf, REGEX_ADD_RULE_FS(TRACE_TASK));
expect_truncated_path(_metadata, buf,
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
free(buf);
}
@ -542,7 +548,8 @@ TEST_F(trace_fs, check_rule_nested)
*/
TEST_F(trace_fs, deny_access_fs_denied)
{
char *buf;
const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK);
char *buf, blockers[64];
int count;
ASSERT_EQ(0, tracefs_clear_buf());
@ -558,8 +565,77 @@ TEST_F(trace_fs, deny_access_fs_denied)
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
EXPECT_LE(1, count);
count = tracefs_count_matches(buf, event_regex);
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 access denial, got %d\n%s", count, buf);
}
ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers",
blockers, sizeof(blockers)));
EXPECT_STREQ("read_dir", blockers);
free(buf);
}
/*
* Verifies that a denied mount reports the singleton topology blocker rather
* than an empty access mask.
*/
TEST_F(trace_fs, deny_change_topology)
{
const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK);
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_REFER,
};
char *buf, blockers[64];
int count, ruleset_fd, status;
pid_t pid;
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(0, tracefs_clear_buf());
/* Ensure that Landlock is the only expected mount denial. */
set_cap(_metadata, CAP_SYS_ADMIN);
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
close(ruleset_fd);
_exit(1);
}
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(2);
}
close(ruleset_fd);
if (mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) != -1)
_exit(3);
if (errno != EPERM)
_exit(4);
_exit(0);
}
close(ruleset_fd);
clear_cap(_metadata, CAP_SYS_ADMIN);
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, event_regex);
EXPECT_EQ(1, count)
{
TH_LOG("Expected 1 topology denial, got %d\n%s", count, buf);
}
ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers",
blockers, sizeof(blockers)));
EXPECT_STREQ("change_topology", blockers);
free(buf);
}

View File

@ -49,8 +49,10 @@ FIXTURE_SETUP(trace)
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE,
true));
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
@ -72,8 +74,8 @@ FIXTURE_TEARDOWN(trace)
tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false);
tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false);
tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false);
tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE, false);
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
@ -103,8 +105,10 @@ TEST_F(trace, no_trace_when_disabled)
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false));
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_ENFORCE_DOMAIN_ENABLE, false));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE,
false));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_PORT_ENABLE,
false));
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
ASSERT_EQ(0,
tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false));
@ -265,10 +269,11 @@ TEST_F(trace, ruleset_version)
ASSERT_NE(0, !!dot);
EXPECT_STREQ("0", dot + 1);
/* Verify 2 add_rule_fs events were emitted. */
EXPECT_EQ(2, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
/* Verify two add_rule_path_beneath events were emitted. */
EXPECT_EQ(2, tracefs_count_matches(
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)))
{
TH_LOG("Expected 2 add_rule_fs events\n%s", buf);
TH_LOG("Expected 2 add_rule_path_beneath events\n%s", buf);
}
/*
@ -373,7 +378,7 @@ TEST_F(trace, create_domain)
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
ASSERT_LE(1, check_count)
{
TH_LOG("Expected check_rule_fs events\n%s", buf);
TH_LOG("Expected check_rule_inode events\n%s", buf);
}
EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
@ -508,9 +513,11 @@ TEST_F(trace, add_rule_invalid_fd)
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
EXPECT_EQ(0, tracefs_count_matches(
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK)))
{
TH_LOG("No add_rule_fs event expected on invalid fd\n%s", buf);
TH_LOG("No add_rule_path_beneath event expected on invalid fd\n%s",
buf);
}
free(buf);
@ -902,10 +909,10 @@ TEST_F(trace, non_audit_visible_denial_counting)
}
/*
* Verifies that landlock_add_rule_net emits a trace event with the correct port
* and allowed access mask fields.
* Verifies that landlock_add_rule_net_port emits a trace event with the correct
* port and allowed access mask fields.
*/
TEST_F(trace, add_rule_net_fields)
TEST_F(trace, add_rule_net_port_fields)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
@ -931,9 +938,10 @@ TEST_F(trace, add_rule_net_fields)
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_ADD_RULE_NET(TRACE_TASK)))
EXPECT_EQ(1, tracefs_count_matches(buf,
REGEX_ADD_RULE_NET_PORT(TRACE_TASK)))
{
TH_LOG("Expected 1 add_rule_net event\n%s", buf);
TH_LOG("Expected 1 add_rule_net_port event\n%s", buf);
}
/*
@ -941,7 +949,8 @@ TEST_F(trace, add_rule_net_fields)
* (landlock_net_port_attr.port). On little-endian, htons(8080) is
* 36895, so this comparison catches byte-order bugs.
*/
EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
EXPECT_EQ(0, tracefs_extract_field(buf,
REGEX_ADD_RULE_NET_PORT(TRACE_TASK),
"port", field, sizeof(field)));
EXPECT_STREQ("8080", field);
/*
@ -950,9 +959,9 @@ TEST_F(trace, add_rule_net_fields)
* net access bits are unhandled because the ruleset only handles
* BIND_TCP).
*/
EXPECT_EQ(0,
tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
"access_rights", field, sizeof(field)));
EXPECT_EQ(0, tracefs_extract_field(
buf, REGEX_ADD_RULE_NET_PORT(TRACE_TASK),
"access_rights", field, sizeof(field)));
EXPECT_STREQ("bind_tcp|connect_tcp|bind_udp|connect_send_udp", field);
free(buf);