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. The events use the "deny_" prefix (rather than "check_") to make clear that they fire only on denial, not on every access check. These complement the check_rule tracepoints by showing the final denial verdict, including the denial-by-absence case (when no rule matches along the pathwalk, no check_rule events fire, but the deny_access event makes the denial explicit). Trace events fire unconditionally, independent of audit configuration and user-specified log flags (LANDLOCK_LOG_DISABLED). The user's "disable logging" intent applies to audit records, not to kernel tracing. The LANDLOCK_LOG_DISABLED check is moved into the audit-specific path; num_denials and trace emission execute regardless. The deny_access events pass the denying hierarchy node (const struct landlock_hierarchy *hierarchy) in TP_PROTO, not the task's current domain. The domain_id entry field shows the ID of the specific hierarchy node that blocked the access, matching audit record semantics. This differs from check_rule events which pass the task's current domain (needed for the dynamic per-layer array sizing). The same_exec field is passed in TP_PROTO because it is computed from the credential bitmask, not derivable from the hierarchy pointer alone. The events include same_exec, log_same_exec, and log_new_exec fields for stateless ftrace filtering that replicates audit's suppression logic. The denial field is named "blockers" (matching the audit record field) rather than "blocked", to enable consistent field-name correlation between audit and trace output. Network denial sport and dport fields use __u64 host-endian, matching the landlock_net_port_attr.port UAPI convention. The caller converts from the lsm_network_audit __be16 fields via ntohs() before emitting the event. The filesystem path is resolved via d_absolute_path() (the same helper used by landlock_add_rule_fs), producing namespace-independent absolute paths. Audit uses d_path() which resolves relative to the process's chroot; the difference is documented but acceptable for tracepoints which are designed for deterministic output regardless of the tracer's namespace state. Device numbers use numeric major:minor format (unlike audit's string s_id) for machine parseability. For FS_CHANGE_TOPOLOGY hooks that provide only a dentry, the path is resolved via dentry_path_raw() instead of d_absolute_path(). The denial tracepoint allocates PATH_MAX bytes from the heap via __getname() for path resolution. This cost is only paid when a tracer is attached. Cc: Günther Noack Cc: Justin Suess Cc: Masami Hiramatsu Cc: Mathieu Desnoyers Cc: Steven Rostedt Cc: Tingmao Wang Signed-off-by: Mickaël Salaün --- Changes since v1: - New patch. --- include/trace/events/landlock.h | 100 +++++++++++++++++++++ security/landlock/log.c | 149 ++++++++++++++++++++++++++------ security/landlock/log.h | 9 +- 3 files changed, 227 insertions(+), 31 deletions(-) diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index e7bb8fa802bf..1afab091efba 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -18,6 +18,7 @@ struct landlock_hierarchy; struct landlock_rule; struct landlock_ruleset; struct path; +struct sock; /** * DOC: Landlock trace events @@ -50,6 +51,15 @@ struct path; * Network port fields use __u64 in host endianness, matching the * landlock_net_port_attr.port UAPI convention. Callers convert from * network byte order before emitting the event. + * + * Field ordering convention for denial events: domain ID, same_exec, + * log_same_exec, log_new_exec, then blockers (deny_access events only), + * then type-specific object identification fields, then variable-length + * fields. + * + * The deny_access denial events include same_exec and log_same_exec / + * log_new_exec fields so that both stateless (ftrace filter) and stateful + * (eBPF) consumers can replicate the audit subsystem's filtering logic. */ /** @@ -333,6 +343,96 @@ TRACE_EVENT(landlock_check_rule_net, __entry->port, __print_dynamic_array(layers, sizeof(access_mask_t)))); +/** + * landlock_deny_access_fs - filesystem access denied + * @hierarchy: Hierarchy node that blocked the access (never NULL). + * Identifies the specific domain in the hierarchy whose + * rules caused the denial. eBPF can read hierarchy->id, + * hierarchy->log_same_exec, hierarchy->log_new_exec, and + * walk hierarchy->parent for the domain chain. + * @same_exec: Whether the current task is the same executable that + * called landlock_restrict_self() for the denying hierarchy + * node. Computed from the credential bitmask, not derivable + * from the hierarchy alone. + * @blockers: Access mask that was blocked + * @path: Filesystem path that was denied (never NULL) + * @pathname: Resolved absolute path string (never NULL) + */ +TRACE_EVENT( + landlock_deny_access_fs, + + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec, + access_mask_t blockers, const struct path *path, + const char *pathname), + + TP_ARGS(hierarchy, same_exec, blockers, path, pathname), + + TP_STRUCT__entry( + __field(__u64, domain_id) __field(bool, same_exec) + __field(u32, log_same_exec) __field(u32, log_new_exec) + __field(access_mask_t, blockers) + __field(dev_t, dev) __field(ino_t, ino) + __string(pathname, pathname)), + + TP_fast_assign(__entry->domain_id = hierarchy->id; + __entry->same_exec = same_exec; + __entry->log_same_exec = hierarchy->log_same_exec; + __entry->log_new_exec = hierarchy->log_new_exec; + __entry->blockers = blockers; + __entry->dev = path->dentry->d_sb->s_dev; + __entry->ino = d_backing_inode(path->dentry)->i_ino; + __assign_str(pathname);), + + TP_printk( + "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u blockers=0x%x dev=%u:%u ino=%lu path=%s", + __entry->domain_id, __entry->same_exec, __entry->log_same_exec, + __entry->log_new_exec, __entry->blockers, MAJOR(__entry->dev), + MINOR(__entry->dev), __entry->ino, + __print_untrusted_str(pathname))); + +/** + * landlock_deny_access_net - network access denied + * @hierarchy: Hierarchy node that blocked the access (never NULL) + * @same_exec: Whether the current task is the same executable that + * called landlock_restrict_self() for the denying hierarchy + * node + * @blockers: Access mask that was blocked + * @sk: Socket object (never NULL); eBPF can read socket family, state, + * local/remote addresses, and options via BTF + * @sport: Source port in host endianness (non-zero for bind denials, + * zero for connect denials) + * @dport: Destination port in host endianness (non-zero for connect + * denials, zero for bind denials) + */ +TRACE_EVENT( + landlock_deny_access_net, + + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec, + access_mask_t blockers, const struct sock *sk, __u64 sport, + __u64 dport), + + TP_ARGS(hierarchy, same_exec, blockers, sk, sport, dport), + + TP_STRUCT__entry( + __field(__u64, domain_id) __field(bool, same_exec) + __field(u32, log_same_exec) __field(u32, log_new_exec) + __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->log_same_exec = hierarchy->log_same_exec; + __entry->log_new_exec = hierarchy->log_new_exec; + __entry->blockers = blockers; __entry->sport = sport; + __entry->dport = dport;), + + TP_printk( + "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u blockers=0x%x sport=%llu dport=%llu", + __entry->domain_id, __entry->same_exec, __entry->log_same_exec, + __entry->log_new_exec, __entry->blockers, __entry->sport, + __entry->dport)); + #endif /* _TRACE_LANDLOCK_H */ /* This part must be outside protection */ diff --git a/security/landlock/log.c b/security/landlock/log.c index ab4f982f8184..c81cb7c1c448 100644 --- a/security/landlock/log.c +++ b/security/landlock/log.c @@ -3,6 +3,7 @@ * Landlock - Log helpers * * Copyright © 2023-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare */ #include @@ -143,6 +144,9 @@ static void audit_denial(const struct landlock_cred_security *const subject, { struct audit_buffer *ab; + if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED) + return; + if (!audit_enabled) return; @@ -172,6 +176,16 @@ static void audit_denial(const struct landlock_cred_security *const subject, log_domain(youngest_denied); } +#else /* CONFIG_AUDIT */ + +static inline void +audit_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request, + struct landlock_hierarchy *const youngest_denied, + const size_t youngest_layer, const access_mask_t missing) +{ +} + #endif /* CONFIG_AUDIT */ #include @@ -180,6 +194,86 @@ static void audit_denial(const struct landlock_cred_security *const subject, #define CREATE_TRACE_POINTS #include #undef CREATE_TRACE_POINTS + +#include "fs.h" + +static void trace_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const size_t youngest_layer, + const access_mask_t missing) +{ + const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer)); + + 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(); + const char *pathname; + const struct path *path; + + /* + * FS_CHANGE_TOPOLOGY uses either LSM_AUDIT_DATA_PATH or + * LSM_AUDIT_DATA_DENTRY depending on the hook. For the + * dentry case, build a path on the stack with the real + * dentry so TP_fast_assign can extract dev and ino. + * The mnt field is unused by TP_fast_assign. + */ + if (request->audit.type == LSM_AUDIT_DATA_DENTRY) { + struct path dentry_path = { + .dentry = request->audit.u.dentry, + }; + + path = &dentry_path; + pathname = + buf ? dentry_path_raw( + request->audit.u.dentry, + buf, PATH_MAX) : + ""; + if (IS_ERR(pathname)) + pathname = ""; + + trace_landlock_deny_access_fs(youngest_denied, + same_exec, + missing, path, + pathname); + } else { + path = &request->audit.u.path; + pathname = buf ? resolve_path_for_trace(path, + buf) : + ""; + + trace_landlock_deny_access_fs(youngest_denied, + same_exec, + 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, missing, + request->audit.u.net->sk, + ntohs(request->audit.u.net->sport), + ntohs(request->audit.u.net->dport)); + break; + default: + break; + } +} + +#else /* CONFIG_TRACEPOINTS */ + +static inline void +trace_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const size_t youngest_layer, const access_mask_t missing) +{ +} + #endif /* CONFIG_TRACEPOINTS */ static struct landlock_hierarchy * @@ -439,9 +533,6 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, 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 @@ -450,45 +541,25 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, */ atomic64_inc(&youngest_denied->num_denials); -#ifdef CONFIG_AUDIT + trace_denial(subject, request, youngest_denied, youngest_layer, + missing); audit_denial(subject, request, youngest_denied, youngest_layer, missing); -#endif /* CONFIG_AUDIT */ } #ifdef CONFIG_AUDIT -/** - * landlock_log_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_domain_deferred() called by - * hook_cred_free(). - */ -void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy) +static void audit_drop_domain(const struct landlock_hierarchy *const hierarchy) { struct audit_buffer *ab; - if (WARN_ON_ONCE(!hierarchy)) - return; - - trace_landlock_free_domain(hierarchy); - if (!audit_enabled) return; - /* Ignores domains that were not logged. */ + /* Ignores domains that were not logged. */ if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED) return; - /* - * If logging of domain allocation succeeded, warns about failure to log - * domain deallocation to highlight unbalanced domain lifetime logs. - */ ab = audit_log_start(audit_context(), GFP_KERNEL, AUDIT_LANDLOCK_DOMAIN); if (!ab) @@ -499,8 +570,32 @@ void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy) audit_log_end(ab); } +#else /* CONFIG_AUDIT */ + +static inline void +audit_drop_domain(const struct landlock_hierarchy *const hierarchy) +{ +} + #endif /* CONFIG_AUDIT */ +/** + * 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; + + trace_landlock_free_domain(hierarchy); + audit_drop_domain(hierarchy); +} + #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST static struct kunit_case test_cases[] = { diff --git a/security/landlock/log.h b/security/landlock/log.h index 4370fff86e45..5615a776c29a 100644 --- a/security/landlock/log.h +++ b/security/landlock/log.h @@ -3,6 +3,7 @@ * Landlock - Log helpers * * Copyright © 2023-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare */ #ifndef _SECURITY_LANDLOCK_LOG_H @@ -28,7 +29,7 @@ enum landlock_request_type { /* * 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. + * CONFIG_SECURITY_LANDLOCK_LOG is not set. */ struct landlock_request { /* Mandatory fields. */ @@ -52,14 +53,14 @@ struct landlock_request { deny_masks_t deny_masks; }; -#ifdef CONFIG_AUDIT +#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_AUDIT */ +#else /* CONFIG_SECURITY_LANDLOCK_LOG */ static inline void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy) @@ -72,6 +73,6 @@ landlock_log_denial(const struct landlock_cred_security *const subject, { } -#endif /* CONFIG_AUDIT */ +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ #endif /* _SECURITY_LANDLOCK_LOG_H */ -- 2.53.0