Merge landlock_find_rule() into landlock_unmask_layers() to consolidate
rule finding into unmask checking.  landlock_unmask_layers() now takes a
landlock_id and the domain instead of a rule pointer.

This enables us to not deal with Landlock rule pointers outside of the
domain implementation, to avoid two calls, and to get all required
information available to landlock_unmask_layers().

Use the per-type tracepoint wrappers unmask_layers_fs() and
unmask_layers_net() to emit tracepoints recording which rules matched
and what access masks were fulfilled.

Setting allowed_parent2 to true for non-dom-check requests when
get_inode_id() returns false preserves the pre-refactoring behavior: a
negative dentry (no backing inode) has no matching rule, so the access
is allowed at this path component.  Before the refactoring,
landlock_unmask_layers() with a NULL rule produced this result as a side
effect; now the caller must set it explicitly.

The check_rule tracepoints add up to 80 bytes of stack in the access
check path (dynamic layers array in TP_STRUCT__entry).  This cost is
only paid when a tracer is attached; the static branch is not taken
otherwise.

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:
https://lore.kernel.org/r/[email protected]
- Merged find-rule consolidation (v1 2/5) into this patch.
- Added check_rule_net tracepoint for network rules.
- Added get_inode_id() helper with rcu_access_pointer().
- Added allowed_parent2 behavioral fix.
---
 include/trace/events/landlock.h |  99 ++++++++++++++++++++++
 security/landlock/domain.c      |  32 ++++---
 security/landlock/domain.h      |  10 +--
 security/landlock/fs.c          | 145 +++++++++++++++++++++++---------
 security/landlock/net.c         |  21 ++++-
 5 files changed, 246 insertions(+), 61 deletions(-)

diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 533aea6152e1..e7bb8fa802bf 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -12,8 +12,10 @@
 
 #include <linux/tracepoint.h>
 
+struct dentry;
 struct landlock_domain;
 struct landlock_hierarchy;
+struct landlock_rule;
 struct landlock_ruleset;
 struct path;
 
@@ -234,6 +236,103 @@ TRACE_EVENT(landlock_free_domain,
            TP_printk("domain=%llx denials=%llu", __entry->domain_id,
                      __entry->denials));
 
+/**
+ * landlock_check_rule_fs - filesystem rule evaluated during access check
+ * @domain: Enforcing domain (never NULL)
+ * @dentry: Filesystem dentry being checked (never NULL)
+ * @access_request: Access mask being requested
+ * @rule: Matching rule with per-layer access masks (never NULL)
+ *
+ * Emitted for each rule that matches during a filesystem access check.
+ * The layers array shows the allowed access mask at each domain layer.
+ */
+TRACE_EVENT(landlock_check_rule_fs,
+
+           TP_PROTO(const struct landlock_domain *domain,
+                    const struct dentry *dentry, access_mask_t access_request,
+                    const struct landlock_rule *rule),
+
+           TP_ARGS(domain, dentry, access_request, rule),
+
+           TP_STRUCT__entry(__field(__u64, domain_id) __field(
+                   access_mask_t,
+                   access_request) __field(dev_t, dev) __field(ino_t, ino)
+                                    __dynamic_array(access_mask_t, layers,
+                                                    domain->num_layers)),
+
+           TP_fast_assign(__entry->domain_id = domain->hierarchy->id;
+                          __entry->access_request = access_request;
+                          __entry->dev = dentry->d_sb->s_dev;
+                          __entry->ino = d_backing_inode(dentry)->i_ino;
+
+                          for (size_t level = 1, i = 0;
+                               level <= __get_dynamic_array_len(layers) /
+                                                sizeof(access_mask_t);
+                               level++) {
+                                  access_mask_t allowed;
+
+                                  if (i < rule->num_layers &&
+                                      level == rule->layers[i].level) {
+                                          allowed = rule->layers[i].access;
+                                          i++;
+                                  } else {
+                                          allowed = 0;
+                                  }
+                                  ((access_mask_t *)__get_dynamic_array(
+                                          layers))[level - 1] = allowed;
+                          }),
+
+           TP_printk("domain=%llx request=0x%x dev=%u:%u ino=%lu allowed=%s",
+                     __entry->domain_id, __entry->access_request,
+                     MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+                     __print_dynamic_array(layers, sizeof(access_mask_t))));
+
+/**
+ * landlock_check_rule_net - network port rule evaluated during access check
+ * @domain: Enforcing domain (never NULL)
+ * @port: Network port being checked (host endianness)
+ * @access_request: Access mask being requested
+ * @rule: Matching rule with per-layer access masks (never NULL)
+ */
+TRACE_EVENT(landlock_check_rule_net,
+
+           TP_PROTO(const struct landlock_domain *domain, __u64 port,
+                    access_mask_t access_request,
+                    const struct landlock_rule *rule),
+
+           TP_ARGS(domain, port, access_request, rule),
+
+           TP_STRUCT__entry(__field(__u64, domain_id) __field(
+                   access_mask_t, access_request) __field(__u64, port)
+                                    __dynamic_array(access_mask_t, layers,
+                                                    domain->num_layers)),
+
+           TP_fast_assign(__entry->domain_id = domain->hierarchy->id;
+                          __entry->access_request = access_request;
+                          __entry->port = port;
+
+                          for (size_t level = 1, i = 0;
+                               level <= __get_dynamic_array_len(layers) /
+                                                sizeof(access_mask_t);
+                               level++) {
+                                  access_mask_t allowed;
+
+                                  if (i < rule->num_layers &&
+                                      level == rule->layers[i].level) {
+                                          allowed = rule->layers[i].access;
+                                          i++;
+                                  } else {
+                                          allowed = 0;
+                                  }
+                                  ((access_mask_t *)__get_dynamic_array(
+                                          layers))[level - 1] = allowed;
+                          }),
+
+           TP_printk("domain=%llx request=0x%x port=%llu allowed=%s",
+                     __entry->domain_id, __entry->access_request,
+                     __entry->port,
+                     __print_dynamic_array(layers, sizeof(access_mask_t))));
+
 #endif /* _TRACE_LANDLOCK_H */
 
 /* This part must be outside protection */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 45ee7ec87957..e8d82b8a14a3 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -98,9 +98,9 @@ void landlock_put_domain_deferred(struct landlock_domain 
*const domain)
 }
 
 /* The returned access has the same lifetime as @domain. */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
-                  const struct landlock_id id)
+static const struct landlock_rule *
+find_rule(const struct landlock_domain *const domain,
+         const struct landlock_id id)
 {
        const struct rb_root *root;
        const struct rb_node *node;
@@ -127,26 +127,38 @@ landlock_find_rule(const struct landlock_domain *const 
domain,
 
 /**
  * landlock_unmask_layers - Remove the access rights in @masks which are
- *                          granted in @rule
+ *                          granted by a matching rule
  *
- * Updates the set of (per-layer) unfulfilled access rights @masks so that all
- * the access rights granted in @rule are removed from it (because they are now
- * fulfilled).
+ * Looks up the rule matching @id in @domain, then updates the set of
+ * (per-layer) unfulfilled access rights @masks so that all the access rights
+ * granted by that rule are removed (because they are now fulfilled).
  *
- * @rule: A rule that grants a set of access rights for each layer.
+ * @domain: The Landlock domain to search for a matching rule.
+ * @id: Identifier for the rule target (e.g. inode, port).
  * @masks: A matrix of unfulfilled access rights for each layer.
+ * @matched_rule: Optional output for the matched rule (for tracing); set to
+ *                the matching rule when non-NULL, unchanged otherwise.
  *
  * Return: True if the request is allowed (i.e. the access rights granted all
  * remaining unfulfilled access rights and masks has no leftover set bits).
  */
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-                           struct layer_access_masks *masks)
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+                           const struct landlock_id id,
+                           struct layer_access_masks *masks,
+                           const struct landlock_rule **matched_rule)
 {
+       const struct landlock_rule *rule;
+
        if (!masks)
                return true;
+
+       rule = find_rule(domain, id);
        if (!rule)
                return false;
 
+       if (matched_rule)
+               *matched_rule = rule;
+
        /*
         * An access is granted if, for each policy layer, at least one rule
         * encountered on the pathwalk grants the requested access, regardless
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 56f54efb65d1..35abae29677c 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -289,12 +289,10 @@ struct landlock_domain *
 landlock_merge_ruleset(struct landlock_domain *const parent,
                       struct landlock_ruleset *const ruleset);
 
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
-                  const struct landlock_id id);
-
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-                           struct layer_access_masks *masks);
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+                           const struct landlock_id id,
+                           struct layer_access_masks *masks,
+                           const struct landlock_rule **matched_rule);
 
 access_mask_t
 landlock_init_layer_masks(const struct landlock_domain *const domain,
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index f627ecc537a5..fe211656f6d9 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -375,31 +375,55 @@ int landlock_append_fs_rule(struct landlock_ruleset 
*const ruleset,
 
 /* Access-control management */
 
-/*
- * The lifetime of the returned rule is tied to @domain.
+/**
+ * get_inode_id - Look up the Landlock object for a dentry
+ * @dentry: The dentry to look up.
+ * @id: Filled with the inode's Landlock object pointer on success.
+ *
+ * Extracts the Landlock object pointer from @dentry's inode security blob and
+ * stores it in @id for use as a rule-tree lookup key.
+ *
+ * When this returns false (negative dentry or no Landlock object), no rule can
+ * match this inode, so landlock_unmask_layers() need not be called.  Callers
+ * that gate landlock_unmask_layers() on this function must handle the NULL
+ * @masks case independently, since the !masks-returns-true early-return in
+ * landlock_unmask_layers() will not be reached.  See the allowed_parent2
+ * initialization in is_access_to_paths_allowed().
  *
- * Returns NULL if no rule is found or if @dentry is negative.
+ * Return: True if a Landlock object exists for @dentry, false otherwise.
  */
-static const struct landlock_rule *
-find_rule(const struct landlock_domain *const domain,
-         const struct dentry *const dentry)
+static bool get_inode_id(const struct dentry *const dentry,
+                        struct landlock_id *id)
 {
-       const struct landlock_rule *rule;
-       const struct inode *inode;
-       struct landlock_id id = {
-               .type = LANDLOCK_KEY_INODE,
-       };
-
        /* Ignores nonexistent leafs. */
        if (d_is_negative(dentry))
-               return NULL;
+               return false;
 
-       inode = d_backing_inode(dentry);
-       rcu_read_lock();
-       id.key.object = rcu_dereference(landlock_inode(inode)->object);
-       rule = landlock_find_rule(domain, id);
-       rcu_read_unlock();
-       return rule;
+       /*
+        * rcu_access_pointer() is sufficient: the pointer is used only
+        * as a numeric comparison key for rule lookup, not dereferenced.
+        * The object cannot be freed while the domain exists because the
+        * domain's rule tree holds its own reference to it.
+        */
+       id->key.object = rcu_access_pointer(
+               landlock_inode(d_backing_inode(dentry))->object);
+       return !!id->key.object;
+}
+
+static bool unmask_layers_fs(const struct landlock_domain *const domain,
+                            const struct landlock_id id,
+                            const access_mask_t access_request,
+                            struct layer_access_masks *masks,
+                            const struct dentry *const dentry)
+{
+       const struct landlock_rule *rule = NULL;
+       bool ret;
+
+       ret = landlock_unmask_layers(domain, id, masks, &rule);
+       if (rule)
+               trace_landlock_check_rule_fs(domain, dentry, access_request,
+                                            rule);
+       return ret;
 }
 
 /*
@@ -771,6 +795,9 @@ is_access_to_paths_allowed(const struct landlock_domain 
*const domain,
        bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
             child1_is_directory = true, child2_is_directory = true;
        struct path walker_path;
+       struct landlock_id id = {
+               .type = LANDLOCK_KEY_INODE,
+       };
        access_mask_t access_masked_parent1, access_masked_parent2;
        struct layer_access_masks _layer_masks_child1, _layer_masks_child2;
        struct layer_access_masks *layer_masks_child1 = NULL,
@@ -810,24 +837,46 @@ is_access_to_paths_allowed(const struct landlock_domain 
*const domain,
                /* For a simple request, only check for requested accesses. */
                access_masked_parent1 = access_request_parent1;
                access_masked_parent2 = access_request_parent2;
+               /*
+                * Simple requests have no parent2 to check, so parent2 is
+                * trivially allowed.  This must be set explicitly because the
+                * get_inode_id() gate in the pathwalk loop may prevent
+                * landlock_unmask_layers() from being called (which would
+                * otherwise return true for NULL masks as a side effect).
+                */
+               allowed_parent2 = true;
                is_dom_check = false;
        }
 
        if (unlikely(dentry_child1)) {
-               if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-                                             &_layer_masks_child1,
-                                             LANDLOCK_KEY_INODE))
-                       landlock_unmask_layers(find_rule(domain, dentry_child1),
-                                              &_layer_masks_child1);
+               struct landlock_id id = {
+                       .type = LANDLOCK_KEY_INODE,
+               };
+               access_mask_t handled;
+
+               handled = landlock_init_layer_masks(domain,
+                                                   LANDLOCK_MASK_ACCESS_FS,
+                                                   &_layer_masks_child1,
+                                                   LANDLOCK_KEY_INODE);
+               if (handled && get_inode_id(dentry_child1, &id))
+                       unmask_layers_fs(domain, id, handled,
+                                        &_layer_masks_child1, dentry_child1);
                layer_masks_child1 = &_layer_masks_child1;
                child1_is_directory = d_is_dir(dentry_child1);
        }
        if (unlikely(dentry_child2)) {
-               if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-                                             &_layer_masks_child2,
-                                             LANDLOCK_KEY_INODE))
-                       landlock_unmask_layers(find_rule(domain, dentry_child2),
-                                              &_layer_masks_child2);
+               struct landlock_id id = {
+                       .type = LANDLOCK_KEY_INODE,
+               };
+               access_mask_t handled;
+
+               handled = landlock_init_layer_masks(domain,
+                                                   LANDLOCK_MASK_ACCESS_FS,
+                                                   &_layer_masks_child2,
+                                                   LANDLOCK_KEY_INODE);
+               if (handled && get_inode_id(dentry_child2, &id))
+                       unmask_layers_fs(domain, id, handled,
+                                        &_layer_masks_child2, dentry_child2);
                layer_masks_child2 = &_layer_masks_child2;
                child2_is_directory = d_is_dir(dentry_child2);
        }
@@ -839,8 +888,6 @@ is_access_to_paths_allowed(const struct landlock_domain 
*const domain,
         * restriction.
         */
        while (true) {
-               const struct landlock_rule *rule;
-
                /*
                 * If at least all accesses allowed on the destination are
                 * already allowed on the source, respectively if there is at
@@ -881,13 +928,20 @@ is_access_to_paths_allowed(const struct landlock_domain 
*const domain,
                                break;
                }
 
-               rule = find_rule(domain, walker_path.dentry);
-               allowed_parent1 =
-                       allowed_parent1 ||
-                       landlock_unmask_layers(rule, layer_masks_parent1);
-               allowed_parent2 =
-                       allowed_parent2 ||
-                       landlock_unmask_layers(rule, layer_masks_parent2);
+               if (get_inode_id(walker_path.dentry, &id)) {
+                       allowed_parent1 =
+                               allowed_parent1 ||
+                               unmask_layers_fs(domain, id,
+                                                access_masked_parent1,
+                                                layer_masks_parent1,
+                                                walker_path.dentry);
+                       allowed_parent2 =
+                               allowed_parent2 ||
+                               unmask_layers_fs(domain, id,
+                                                access_masked_parent2,
+                                                layer_masks_parent2,
+                                                walker_path.dentry);
+               }
 
                /* Stops when a rule from each layer grants access. */
                if (allowed_parent1 && allowed_parent2)
@@ -1050,23 +1104,30 @@ static bool collect_domain_accesses(const struct 
landlock_domain *const domain,
                                    struct layer_access_masks *layer_masks_dom)
 {
        bool ret = false;
+       access_mask_t access_masked_dom;
 
        if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
                return true;
        if (is_nouser_or_private(dir))
                return true;
 
-       if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-                                      layer_masks_dom, LANDLOCK_KEY_INODE))
+       access_masked_dom =
+               landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
+                                         layer_masks_dom, LANDLOCK_KEY_INODE);
+       if (!access_masked_dom)
                return true;
 
        dget(dir);
        while (true) {
                struct dentry *parent_dentry;
+               struct landlock_id id = {
+                       .type = LANDLOCK_KEY_INODE,
+               };
 
                /* Gets all layers allowing all domain accesses. */
-               if (landlock_unmask_layers(find_rule(domain, dir),
-                                          layer_masks_dom)) {
+               if (get_inode_id(dir, &id) &&
+                   unmask_layers_fs(domain, id, access_masked_dom,
+                                    layer_masks_dom, dir)) {
                        /*
                         * Stops when all handled accesses are allowed by at
                         * least one rule in each layer.
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 1e893123e787..a2aefc7967a1 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -53,6 +53,22 @@ int landlock_append_net_rule(struct landlock_ruleset *const 
ruleset,
        return err;
 }
 
+static bool unmask_layers_net(const struct landlock_domain *const domain,
+                             const struct landlock_id id,
+                             struct layer_access_masks *masks,
+                             access_mask_t access_request)
+{
+       const struct landlock_rule *rule = NULL;
+       bool ret;
+
+       ret = landlock_unmask_layers(domain, id, masks, &rule);
+       if (rule)
+               trace_landlock_check_rule_net(
+                       domain, ntohs((__force __be16)id.key.data),
+                       access_request, rule);
+       return ret;
+}
+
 static int current_check_access_socket(struct socket *const sock,
                                       struct sockaddr *const address,
                                       const int addrlen,
@@ -60,7 +76,6 @@ static int current_check_access_socket(struct socket *const 
sock,
 {
        __be16 port;
        struct layer_access_masks layer_masks = {};
-       const struct landlock_rule *rule;
        struct landlock_id id = {
                .type = LANDLOCK_KEY_NET_PORT,
        };
@@ -199,14 +214,14 @@ static int current_check_access_socket(struct socket 
*const sock,
        id.key.data = (__force uintptr_t)port;
        BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
 
-       rule = landlock_find_rule(subject->domain, id);
        access_request = landlock_init_layer_masks(subject->domain,
                                                   access_request, &layer_masks,
                                                   LANDLOCK_KEY_NET_PORT);
        if (!access_request)
                return 0;
 
-       if (landlock_unmask_layers(rule, &layer_masks))
+       if (unmask_layers_net(subject->domain, id, &layer_masks,
+                             access_request))
                return 0;
 
        audit_net.family = address->sa_family;
-- 
2.53.0


Reply via email to