diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index 86ad04a01ed4..f82588f6f90e 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -15,6 +15,7 @@ #include #include #include +#include struct dentry; struct landlock_domain; @@ -23,6 +24,7 @@ struct landlock_rule; struct landlock_ruleset; struct path; struct sock; +struct task_struct; #ifdef CREATE_TRACE_POINTS @@ -151,6 +153,18 @@ static inline const char *__trace_landlock_print_layers( * A new tracepoint must uphold them, and an eBPF consumer can rely on * them. * + * Decision context + * ~~~~~~~~~~~~~~~~ + * + * A denial event, together with the lifecycle events, exposes the full + * set of inputs the verdict consumed, so a consumer that tracked domain + * creation (landlock_create_ruleset, landlock_create_domain) can verify + * or reproduce the Landlock decision rather than merely observe it + * happened. In who/what/why terms: who is the denying domain (the domain + * field, always the subject that enforced the policy, never the current + * task), what is the operation and its object, and why is every other + * input the verdict weighed. + * * Lifecycle consistency * ~~~~~~~~~~~~~~~~~~~~~~ * @@ -219,6 +233,18 @@ static inline const char *__trace_landlock_print_layers( * log flags. Denial events order their fields as domain, same_exec, * logged, then blockers (deny_access events only), then the type-specific * object fields, then any variable-length field. + * + * Relational referents + * ~~~~~~~~~~~~~~~~~~~~~ + * + * A scope or ptrace verdict compares two domains, so the other party's + * domain is part of the decision context. It is exposed as a scalar + * domain ID (0 when that party is unsandboxed): target_domain (signal), + * peer_domain (abstract unix socket), tracee_domain (ptrace). With both + * IDs in the stream, a consumer that tracked domain creation can relate + * the two parties without kernel-internal state. The ID is a scalar + * snapshot, not a live domain pointer that could dangle: an optional + * relational referent is a scalar (0 sentinel), not a nullable pointer. */ /* @@ -757,6 +783,178 @@ TRACE_EVENT(landlock_deny_access_net, __entry->sport, __entry->dport) ); +/** + * landlock_deny_ptrace - Ptrace access denied by a Landlock domain + * + * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the + * domain field. + * @same_exec: Whether the current task entered the denying domain itself. + * @logged: The domain's audit-logging decision for this denial. + * @tracee_domain_id: The tracee's Landlock domain ID, or 0 if the tracee + * is unsandboxed. + * @tracee: The target task ptrace acted on (never NULL). tracee_pid is + * the init-namespace TGID (like audit's opid). + * + * Emitted when a Landlock domain denies a ptrace operation. + */ +TRACE_EVENT(landlock_deny_ptrace, + + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec, + bool logged, u64 tracee_domain_id, + const struct task_struct *tracee), + + TP_ARGS(hierarchy, same_exec, logged, tracee_domain_id, tracee), + + TP_STRUCT__entry( + __field( __u64, domain_id ) + __field( bool, same_exec ) + __field( bool, logged ) + __field( __u64, tracee_domain_id) + __field( pid_t, tracee_pid ) + __string( tracee_comm, tracee->comm ) + ), + + TP_fast_assign( + __entry->domain_id = hierarchy->id; + __entry->same_exec = same_exec; + __entry->logged = logged; + __entry->tracee_domain_id = tracee_domain_id; + __entry->tracee_pid = task_tgid_nr((struct task_struct *)tracee); + __assign_str(tracee_comm); + ), + + TP_printk("domain=%llx same_exec=%d logged=%d tracee_domain=%llx tracee_pid=%d tracee_comm=%s", + __entry->domain_id, __entry->same_exec, __entry->logged, + __entry->tracee_domain_id, __entry->tracee_pid, + __trace_print_untrusted_str(p, __get_str(tracee_comm), + __get_dynamic_array_len(tracee_comm) - 1)) +); + +/** + * landlock_deny_scope_signal - Signal delivery denied by + * LANDLOCK_SCOPE_SIGNAL + * + * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the + * domain field. + * @same_exec: Whether the current task entered the denying domain itself. + * @logged: The domain's audit-logging decision for this denial. + * @target_domain_id: The target's Landlock domain ID, or 0 if the target + * is unsandboxed. + * @target: The task the signal was aimed at (never NULL). target_pid is + * the init-namespace TGID (like audit's opid). + * + * Emitted when a Landlock domain denies signal delivery to a scoped-out + * target. + */ +TRACE_EVENT(landlock_deny_scope_signal, + + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec, + bool logged, u64 target_domain_id, + const struct task_struct *target), + + TP_ARGS(hierarchy, same_exec, logged, target_domain_id, target), + + TP_STRUCT__entry( + __field( __u64, domain_id ) + __field( bool, same_exec ) + __field( bool, logged ) + __field( __u64, target_domain_id) + __field( pid_t, target_pid ) + __string( target_comm, target->comm ) + ), + + TP_fast_assign( + __entry->domain_id = hierarchy->id; + __entry->same_exec = same_exec; + __entry->logged = logged; + __entry->target_domain_id = target_domain_id; + __entry->target_pid = task_tgid_nr((struct task_struct *)target); + __assign_str(target_comm); + ), + + TP_printk("domain=%llx same_exec=%d logged=%d target_domain=%llx target_pid=%d target_comm=%s", + __entry->domain_id, __entry->same_exec, __entry->logged, + __entry->target_domain_id, __entry->target_pid, + __trace_print_untrusted_str(p, __get_str(target_comm), + __get_dynamic_array_len(target_comm) - 1)) +); + +/** + * landlock_deny_scope_abstract_unix_socket - Abstract unix socket access + * denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET + * + * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the + * domain field. + * @same_exec: Whether the current task entered the denying domain itself. + * @logged: The domain's audit-logging decision for this denial. + * @peer_domain_id: The peer's Landlock domain ID, or 0 if the peer is + * unsandboxed. + * @peer: Peer socket (never NULL). peer_pid is best-effort: it is 0 for + * a datagram peer (no SO_PEERCRED), so sun_path is the reliable + * peer identifier. + * + * Emitted when a Landlock domain denies access to a scoped-out abstract + * unix socket. + */ +TRACE_EVENT(landlock_deny_scope_abstract_unix_socket, + + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec, + bool logged, u64 peer_domain_id, const struct sock *peer), + + TP_ARGS(hierarchy, same_exec, logged, peer_domain_id, peer), + + TP_STRUCT__entry( + __field( __u64, domain_id ) + __field( bool, same_exec ) + __field( bool, logged ) + __field( __u64, peer_domain_id ) + __field( pid_t, peer_pid ) + /* + * Abstract socket names are untrusted binary data from + * user space. Use __string_len because abstract names + * are not NUL-terminated; their length is determined by + * addr->len. unix_sk(peer)->addr is stable here because + * the caller (hook_unix_stream_connect or + * hook_unix_may_send) holds unix_state_lock(peer). + */ + __string_len( sun_path, + unix_sk(peer)->addr ? + unix_sk(peer)->addr->name->sun_path + 1 : + "", + unix_sk(peer)->addr ? + unix_sk(peer)->addr->len - + offsetof(struct sockaddr_un, + sun_path) - 1 : + 0) + ), + + TP_fast_assign( + struct pid *peer_pid; + + lockdep_assert_held(&unix_sk(peer)->lock); + __entry->domain_id = hierarchy->id; + __entry->same_exec = same_exec; + __entry->logged = logged; + __entry->peer_domain_id = peer_domain_id; + /* + * Best-effort (0 for a datagram peer). sk_peer_pid is + * canonically guarded by sk->sk_peer_lock, but the target + * peer's peercred is set once and not updated concurrently in + * these hooks, so this READ_ONCE() is safe; sun_path is the + * reliable identifier. + */ + peer_pid = READ_ONCE(peer->sk_peer_pid); + __entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0; + __assign_str(sun_path); + ), + + TP_printk("domain=%llx same_exec=%d logged=%d peer_domain=%llx peer_pid=%d sun_path=%s", + __entry->domain_id, __entry->same_exec, __entry->logged, + __entry->peer_domain_id, __entry->peer_pid, + __trace_print_untrusted_str(p, __get_str(sun_path), + __get_dynamic_array_len(sun_path) - 1)) +); + #undef _LANDLOCK_NAME_ENTRY #endif /* _TRACE_LANDLOCK_H */ diff --git a/security/landlock/log.h b/security/landlock/log.h index 25afc17cf055..e0a6e44f3ddd 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, Inc. */ #ifndef _SECURITY_LANDLOCK_LOG_H @@ -50,6 +51,14 @@ struct landlock_request { const access_mask_t all_existing_optional_access; deny_masks_t deny_masks; optional_access_t quiet_optional_accesses; + + /* + * Other-party domain ID for a relational (scope/ptrace) denial, or 0 if + * that party is unsandboxed. An ID, not a pointer: the other task can + * replace its credential and free the domain it referenced. Trace path + * only; audit ignores it. + */ + u64 other_domain_id; }; #ifdef CONFIG_SECURITY_LANDLOCK_LOG diff --git a/security/landlock/task.c b/security/landlock/task.c index bb119f198f72..4491ce31ae04 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -88,6 +88,7 @@ static int hook_ptrace_access_check(struct task_struct *const child, const unsigned int mode) { const struct landlock_cred_security *parent_subject; + u64 tracee_domain_id = 0; int err; /* Quick return for non-landlocked tasks. */ @@ -99,6 +100,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 + if (child_dom) + tracee_domain_id = child_dom->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ } if (!err) @@ -116,6 +121,7 @@ static int hook_ptrace_access_check(struct task_struct *const child, .u.tsk = child, }, .layer_plus_one = parent_subject->domain->num_layers, + .other_domain_id = tracee_domain_id, }); return err; @@ -136,6 +142,7 @@ 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(); @@ -147,6 +154,12 @@ static int hook_ptrace_traceme(struct task_struct *const parent) if (!err) return 0; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /* The tracee is the current task; its domain is stable here. */ + if (child_dom) + tracee_domain_id = child_dom->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + /* * For the ptrace_traceme case, we log the domain which is the cause of * the denial, which means the parent domain instead of the current @@ -161,6 +174,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent) .u.tsk = current, }, .layer_plus_one = parent_subject->domain->num_layers, + .other_domain_id = tracee_domain_id, }); return err; } @@ -236,7 +250,8 @@ 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) + const struct landlock_domain *const domain, + u64 *const peer_domain_id) { const struct landlock_domain *dom_other; @@ -254,6 +269,9 @@ 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); } @@ -281,6 +299,7 @@ static int hook_unix_stream_connect(struct sock *const sock, struct sock *const newsk) { size_t handle_layer; + u64 peer_domain_id = 0; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, &handle_layer); @@ -292,7 +311,7 @@ static int hook_unix_stream_connect(struct sock *const sock, if (!is_abstract_socket(other)) return 0; - if (!sock_is_scoped(other, subject->domain)) + if (!sock_is_scoped(other, subject->domain, &peer_domain_id)) return 0; landlock_log_denial(subject, &(struct landlock_request) { @@ -304,6 +323,7 @@ static int hook_unix_stream_connect(struct sock *const sock, }, }, .layer_plus_one = handle_layer + 1, + .other_domain_id = peer_domain_id, }); return -EPERM; } @@ -312,6 +332,7 @@ static int hook_unix_may_send(struct socket *const sock, struct socket *const other) { size_t handle_layer; + u64 peer_domain_id = 0; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, &handle_layer); @@ -329,7 +350,7 @@ static int hook_unix_may_send(struct socket *const sock, if (!is_abstract_socket(other->sk)) return 0; - if (!sock_is_scoped(other->sk, subject->domain)) + if (!sock_is_scoped(other->sk, subject->domain, &peer_domain_id)) return 0; landlock_log_denial(subject, &(struct landlock_request) { @@ -341,6 +362,7 @@ static int hook_unix_may_send(struct socket *const sock, }, }, .layer_plus_one = handle_layer + 1, + .other_domain_id = peer_domain_id, }); return -EPERM; } @@ -355,6 +377,7 @@ static int hook_task_kill(struct task_struct *const p, { bool is_scoped; size_t handle_layer; + u64 target_domain_id = 0; const struct landlock_cred_security *subject; if (!cred) { @@ -381,9 +404,15 @@ static int hook_task_kill(struct task_struct *const p, return 0; scoped_guard(rcu) { - is_scoped = domain_is_scoped(subject->domain, - landlock_get_task_domain(p), + const struct landlock_domain *const other = + landlock_get_task_domain(p); + + is_scoped = domain_is_scoped(subject->domain, other, signal_scope.scope); +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (other) + target_domain_id = other->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ } if (!is_scoped) @@ -396,6 +425,7 @@ static int hook_task_kill(struct task_struct *const p, .u.tsk = p, }, .layer_plus_one = handle_layer + 1, + .other_domain_id = target_domain_id, }); return -EPERM; } @@ -405,6 +435,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk, { const struct landlock_cred_security *subject; bool is_scoped = false; + u64 target_domain_id = 0; /* Lock already held by send_sigio() and send_sigurg(). */ lockdep_assert_held(&fown->lock); @@ -432,9 +463,15 @@ static int hook_file_send_sigiotask(struct task_struct *tsk, return 0; scoped_guard(rcu) { - is_scoped = domain_is_scoped(subject->domain, - landlock_get_task_domain(tsk), + const struct landlock_domain *const other = + landlock_get_task_domain(tsk); + + is_scoped = domain_is_scoped(subject->domain, other, signal_scope.scope); +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (other) + target_domain_id = other->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ } if (!is_scoped) @@ -449,6 +486,7 @@ 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, }); return -EPERM; } diff --git a/security/landlock/trace.c b/security/landlock/trace.c index 4c4229d4ffdf..2ea7aac8d75d 100644 --- a/security/landlock/trace.c +++ b/security/landlock/trace.c @@ -157,7 +157,29 @@ void landlock_trace_denial( ntohs(request->audit.u.net->sport), ntohs(request->audit.u.net->dport)); break; + case LANDLOCK_REQUEST_PTRACE: + if (trace_landlock_deny_ptrace_enabled()) + trace_landlock_deny_ptrace(youngest_denied, same_exec, + logged, + request->other_domain_id, + request->audit.u.tsk); + break; + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + if (trace_landlock_deny_scope_signal_enabled()) + trace_landlock_deny_scope_signal( + youngest_denied, same_exec, logged, + request->other_domain_id, request->audit.u.tsk); + break; + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + if (trace_landlock_deny_scope_abstract_unix_socket_enabled()) + trace_landlock_deny_scope_abstract_unix_socket( + youngest_denied, same_exec, logged, + request->other_domain_id, + request->audit.u.net->sk); + break; default: + WARN_ONCE(1, "Unhandled Landlock request type %d", + request->type); break; } }