From 01ce260f5ccf0fe7e38d2fd548e776f594409cf6 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 11 Aug 2026 11:43:26 +0200 Subject: landlock: Add landlock_deny_access_fs and landlock_deny_access_net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add per-type tracepoints emitted from landlock_log_denial() when an access is denied: landlock_deny_access_fs for filesystem denials and landlock_deny_access_net for network denials. They use the "deny_" prefix (rather than "check_") to mark that they fire only on a denial, and they complement the check_rule events by making the denial-by-absence case explicit (when no rule matches, no check_rule event fires). Unlike the audit records, these events fire regardless of the audit configuration and the domain's log flags: the user's "disable logging" intent applies to audit records, not to kernel tracing. The logged field records whether the domain's log policy would submit the denial to audit; it is the decision computed once by landlock_log_denial() and passed to both the audit and the tracing emitter, so a stateless ftrace filter can select the audit-visible denials with logged==1. TP_PROTO passes the denying hierarchy node, not the task's current domain, so domain_id reports the specific node that blocked the access, matching audit record semantics. (check_rule instead passes the current domain, which it needs to size its per-layer array.) same_exec is also passed explicitly because it is computed from the credential bitmask and is not derivable from the hierarchy pointer alone. The denial field is named blockers to match the audit record field. The filesystem path comes from the request's audit data. Its type selects which union member holds the object, exactly as dump_common_audit_data() selects it (a path, a file's path, an ioctl op's path, or a bare dentry); reading the wrong member would dereference garbage, so every reachable type has an explicit case and an unexpected one is flagged with WARN_ONCE() instead of misread. Path-backed types resolve via d_absolute_path() (as landlock_add_rule_fs does) and the bare-dentry case via dentry_path_raw(). The inode number is read defensively. A filesystem denial can carry a negative dentry (no backing inode), for example a denied creation, so the event mirrors the guard in dump_common_audit_data() and reports inode 0 rather than dereferencing a NULL inode. The sibling fs tracepoints do not need the guard: a dentry that matches a rule during an access check, or one opened to add a rule, always has a backing inode. Landlock tracepoints are reachable by unprivileged sandboxees, so a denial on a negative dentry with the event enabled must not fault the kernel. Cc: Günther Noack Cc: Justin Suess Cc: Masami Hiramatsu Cc: Mathieu Desnoyers Cc: Steven Rostedt Cc: Tingmao Wang Link: https://patch.msgid.link/20260811094338.288094-13-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/log.c | 2 + security/landlock/trace.c | 119 +++++++++++++++++++++++++++++++++++++++++++++- security/landlock/trace.h | 16 +++++++ 3 files changed, 136 insertions(+), 1 deletion(-) (limited to 'security') diff --git a/security/landlock/log.c b/security/landlock/log.c index ad4e3ae99d3a..a8578a6f2ce9 100644 --- a/security/landlock/log.c +++ b/security/landlock/log.c @@ -544,6 +544,8 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, */ atomic64_inc(&youngest_denied->num_denials); + landlock_trace_denial(request, youngest_denied, missing, same_exec, + logged); landlock_audit_denial(request, youngest_denied, missing, logged); } diff --git a/security/landlock/trace.c b/security/landlock/trace.c index 5e6df313c7d2..4c4229d4ffdf 100644 --- a/security/landlock/trace.c +++ b/security/landlock/trace.c @@ -6,9 +6,19 @@ * Copyright © 2026 Cloudflare, Inc. */ -#include "trace.h" +#include +#include +#include +#include +#include +#include + +#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 @@ -44,3 +54,110 @@ void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy 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 = ""; + } 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 ? + "" : + ""; + } 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; + default: + break; + } +} diff --git a/security/landlock/trace.h b/security/landlock/trace.h index 59c8ea348625..7be98e748855 100644 --- a/security/landlock/trace.h +++ b/security/landlock/trace.h @@ -9,13 +9,21 @@ #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 @@ -23,6 +31,14 @@ 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 */ -- cgit