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 <[email protected]>
Cc: Justin Suess <[email protected]>
Cc: Masami Hiramatsu <[email protected]>
Cc: Mathieu Desnoyers <[email protected]>
Cc: Steven Rostedt <[email protected]>
Cc: Tingmao Wang <[email protected]>
Signed-off-by: Mickaël Salaün <[email protected]>
---

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 <kunit/test.h>
@@ -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 <trace/events/landlock.h>
@@ -180,6 +194,86 @@ static void audit_denial(const struct 
landlock_cred_security *const subject,
 #define CREATE_TRACE_POINTS
 #include <trace/events/landlock.h>
 #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) :
+                                             "<no_mem>";
+                               if (IS_ERR(pathname))
+                                       pathname = "<unreachable>";
+
+                               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) :
+                                                "<no_mem>";
+
+                               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


Reply via email to