landlock: Log truncate and IOCTL denials
authorMickaël Salaün <mic@digikod.net>
Thu, 20 Mar 2025 19:07:03 +0000 (20:07 +0100)
committerMickaël Salaün <mic@digikod.net>
Wed, 26 Mar 2025 12:59:41 +0000 (13:59 +0100)
Add audit support to the file_truncate and file_ioctl hooks.

Add a deny_masks_t type and related helpers to store the domain's layer
level per optional access rights (i.e. LANDLOCK_ACCESS_FS_TRUNCATE and
LANDLOCK_ACCESS_FS_IOCTL_DEV) when opening a file, which cannot be
inferred later.  In practice, the landlock_file_security aligned blob size is
still 16 bytes because this new one-byte deny_masks field follows the
existing two-bytes allowed_access field and precede the packed
fown_subject.

Implementing deny_masks_t with a bitfield instead of a struct enables a
generic implementation to store and extract layer levels.

Add KUnit tests to check the identification of a layer level from a
deny_masks_t, and the computation of a deny_masks_t from an access right
with its layer level or a layer_mask_t array.

Audit event sample:

  type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.ioctl_dev path="/dev/tty" dev="devtmpfs" ino=9 ioctlcmd=0x5401

Cc: Günther Noack <gnoack@google.com>
Link: https://lore.kernel.org/r/20250320190717.2287696-15-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
security/landlock/access.h
security/landlock/audit.c
security/landlock/audit.h
security/landlock/domain.c
security/landlock/domain.h
security/landlock/fs.c
security/landlock/fs.h

index 74fd8f399fbd6dd6e36ad335a91cc6c8d0bd5c11..7961c6630a2d7f5c277eff42d49b9ebc1f561ee2 100644 (file)
@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 /*
- * Landlock LSM - Access types and helpers
+ * Landlock - Access types and helpers
  *
  * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
  * Copyright © 2018-2020 ANSSI
        LANDLOCK_ACCESS_FS_REFER)
 /* clang-format on */
 
+/* clang-format off */
+#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \
+       LANDLOCK_ACCESS_FS_TRUNCATE | \
+       LANDLOCK_ACCESS_FS_IOCTL_DEV)
+/* clang-format on */
+
 typedef u16 access_mask_t;
 
 /* Makes sure all filesystem access rights can be stored. */
@@ -60,6 +66,23 @@ typedef u16 layer_mask_t;
 /* Makes sure all layers can be checked. */
 static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
 
+/*
+ * Tracks domains responsible of a denied access.  This is required to avoid
+ * storing in each object the full layer_masks[] required by update_request().
+ */
+typedef u8 deny_masks_t;
+
+/*
+ * Makes sure all optional access rights can be tied to a layer index (cf.
+ * get_deny_mask).
+ */
+static_assert(BITS_PER_TYPE(deny_masks_t) >=
+             (HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) *
+              HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)));
+
+/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */
+static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1);
+
 /* Upgrades with all initially denied by default access rights. */
 static inline struct access_masks
 landlock_upgrade_handled_access_masks(struct access_masks access_masks)
index 45e1c6ad1856114152278c684866e9521aabb1e5..0127646abe79ddf34530748562bd7bc3dfc8e926 100644 (file)
@@ -12,6 +12,7 @@
 #include <linux/pid.h>
 #include <uapi/linux/landlock.h>
 
+#include "access.h"
 #include "audit.h"
 #include "common.h"
 #include "cred.h"
@@ -249,6 +250,88 @@ static void test_get_denied_layer(struct kunit *const test)
 
 #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
 
+static size_t
+get_layer_from_deny_masks(access_mask_t *const access_request,
+                         const access_mask_t all_existing_optional_access,
+                         const deny_masks_t deny_masks)
+{
+       const unsigned long access_opt = all_existing_optional_access;
+       const unsigned long access_req = *access_request;
+       access_mask_t missing = 0;
+       size_t youngest_layer = 0;
+       size_t access_index = 0;
+       unsigned long access_bit;
+
+       /* This will require change with new object types. */
+       WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
+
+       for_each_set_bit(access_bit, &access_opt,
+                        BITS_PER_TYPE(access_mask_t)) {
+               if (access_req & BIT(access_bit)) {
+                       const size_t layer =
+                               (deny_masks >> (access_index * 4)) &
+                               (LANDLOCK_MAX_NUM_LAYERS - 1);
+
+                       if (layer > youngest_layer) {
+                               youngest_layer = layer;
+                               missing = BIT(access_bit);
+                       } else if (layer == youngest_layer) {
+                               missing |= BIT(access_bit);
+                       }
+               }
+               access_index++;
+       }
+
+       *access_request = missing;
+       return youngest_layer;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_layer_from_deny_masks(struct kunit *const test)
+{
+       deny_masks_t deny_mask;
+       access_mask_t access;
+
+       /* truncate:0 ioctl_dev:2 */
+       deny_mask = 0x20;
+
+       access = LANDLOCK_ACCESS_FS_TRUNCATE;
+       KUNIT_EXPECT_EQ(test, 0,
+                       get_layer_from_deny_masks(&access,
+                                                 _LANDLOCK_ACCESS_FS_OPTIONAL,
+                                                 deny_mask));
+       KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+
+       access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+       KUNIT_EXPECT_EQ(test, 2,
+                       get_layer_from_deny_masks(&access,
+                                                 _LANDLOCK_ACCESS_FS_OPTIONAL,
+                                                 deny_mask));
+       KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+
+       /* truncate:15 ioctl_dev:15 */
+       deny_mask = 0xff;
+
+       access = LANDLOCK_ACCESS_FS_TRUNCATE;
+       KUNIT_EXPECT_EQ(test, 15,
+                       get_layer_from_deny_masks(&access,
+                                                 _LANDLOCK_ACCESS_FS_OPTIONAL,
+                                                 deny_mask));
+       KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+
+       access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+       KUNIT_EXPECT_EQ(test, 15,
+                       get_layer_from_deny_masks(&access,
+                                                 _LANDLOCK_ACCESS_FS_OPTIONAL,
+                                                 deny_mask));
+       KUNIT_EXPECT_EQ(test, access,
+                       LANDLOCK_ACCESS_FS_TRUNCATE |
+                               LANDLOCK_ACCESS_FS_IOCTL_DEV);
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
 static bool is_valid_request(const struct landlock_request *const request)
 {
        if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
@@ -258,16 +341,23 @@ static bool is_valid_request(const struct landlock_request *const request)
                return false;
 
        if (request->access) {
-               if (WARN_ON_ONCE(!request->layer_masks))
+               if (WARN_ON_ONCE(!(!!request->layer_masks ^
+                                  !!request->all_existing_optional_access)))
                        return false;
        } else {
-               if (WARN_ON_ONCE(request->layer_masks))
+               if (WARN_ON_ONCE(request->layer_masks ||
+                                request->all_existing_optional_access))
                        return false;
        }
 
        if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size))
                return false;
 
+       if (request->deny_masks) {
+               if (WARN_ON_ONCE(!request->all_existing_optional_access))
+                       return false;
+       }
+
        return true;
 }
 
@@ -300,9 +390,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
                                subject->domain, &missing, request->layer_masks,
                                request->layer_masks_size);
                } else {
-                       /* This will change with the next commit. */
-                       WARN_ON_ONCE(1);
-                       youngest_layer = subject->domain->num_layers;
+                       youngest_layer = get_layer_from_deny_masks(
+                               &missing, request->all_existing_optional_access,
+                               request->deny_masks);
                }
                youngest_denied =
                        get_hierarchy(subject->domain, youngest_layer);
@@ -387,6 +477,7 @@ static struct kunit_case test_cases[] = {
        /* clang-format off */
        KUNIT_CASE(test_get_hierarchy),
        KUNIT_CASE(test_get_denied_layer),
+       KUNIT_CASE(test_get_layer_from_deny_masks),
        {}
        /* clang-format on */
 };
index 2a154116134e5037513fee710d7e00bae520593b..8130a6fcad0768f8f2b2e55c130a09f69518f96c 100644 (file)
@@ -42,6 +42,10 @@ struct landlock_request {
        /* Required fields for requests with layer masks. */
        const layer_mask_t (*layer_masks)[];
        size_t layer_masks_size;
+
+       /* Required fields for requests with deny masks. */
+       const access_mask_t all_existing_optional_access;
+       deny_masks_t deny_masks;
 };
 
 #ifdef CONFIG_AUDIT
index 3c1715e4b1c99308a4cade26da6b8edd41799509..f61fa5ae8e56b0faedea1d823c3c49817cfd8c96 100644 (file)
@@ -7,6 +7,9 @@
  * Copyright © 2024-2025 Microsoft Corporation
  */
 
+#include <kunit/test.h>
+#include <linux/bitops.h>
+#include <linux/bits.h>
 #include <linux/cred.h>
 #include <linux/file.h>
 #include <linux/mm.h>
@@ -15,6 +18,8 @@
 #include <linux/sched.h>
 #include <linux/uidgid.h>
 
+#include "access.h"
+#include "common.h"
 #include "domain.h"
 #include "id.h"
 
@@ -126,4 +131,132 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
        return 0;
 }
 
+static deny_masks_t
+get_layer_deny_mask(const access_mask_t all_existing_optional_access,
+                   const unsigned long access_bit, const size_t layer)
+{
+       unsigned long access_weight;
+
+       /* This may require change with new object types. */
+       WARN_ON_ONCE(all_existing_optional_access !=
+                    _LANDLOCK_ACCESS_FS_OPTIONAL);
+
+       if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS))
+               return 0;
+
+       access_weight = hweight_long(all_existing_optional_access &
+                                    GENMASK(access_bit, 0));
+       if (WARN_ON_ONCE(access_weight < 1))
+               return 0;
+
+       return layer
+              << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1));
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_layer_deny_mask(struct kunit *const test)
+{
+       const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE);
+       const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV);
+
+       KUNIT_EXPECT_EQ(test, 0,
+                       get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                           truncate, 0));
+       KUNIT_EXPECT_EQ(test, 0x3,
+                       get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                           truncate, 3));
+
+       KUNIT_EXPECT_EQ(test, 0,
+                       get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                           ioctl_dev, 0));
+       KUNIT_EXPECT_EQ(test, 0xf0,
+                       get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                           ioctl_dev, 15));
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+deny_masks_t
+landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
+                       const access_mask_t optional_access,
+                       const layer_mask_t (*const layer_masks)[],
+                       const size_t layer_masks_size)
+{
+       const unsigned long access_opt = optional_access;
+       unsigned long access_bit;
+       deny_masks_t deny_masks = 0;
+
+       /* This may require change with new object types. */
+       WARN_ON_ONCE(access_opt !=
+                    (optional_access & all_existing_optional_access));
+
+       if (WARN_ON_ONCE(!layer_masks))
+               return 0;
+
+       if (WARN_ON_ONCE(!access_opt))
+               return 0;
+
+       for_each_set_bit(access_bit, &access_opt, layer_masks_size) {
+               const layer_mask_t mask = (*layer_masks)[access_bit];
+
+               if (!mask)
+                       continue;
+
+               /* __fls(1) == 0 */
+               deny_masks |= get_layer_deny_mask(all_existing_optional_access,
+                                                 access_bit, __fls(mask));
+       }
+       return deny_masks;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_landlock_get_deny_masks(struct kunit *const test)
+{
+       const layer_mask_t layers1[BITS_PER_TYPE(access_mask_t)] = {
+               [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) |
+                                                         BIT_ULL(9),
+               [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1),
+               [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = BIT_ULL(2) |
+                                                           BIT_ULL(0),
+       };
+
+       KUNIT_EXPECT_EQ(test, 0x1,
+                       landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                               LANDLOCK_ACCESS_FS_TRUNCATE,
+                                               &layers1, ARRAY_SIZE(layers1)));
+       KUNIT_EXPECT_EQ(test, 0x20,
+                       landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                               LANDLOCK_ACCESS_FS_IOCTL_DEV,
+                                               &layers1, ARRAY_SIZE(layers1)));
+       KUNIT_EXPECT_EQ(
+               test, 0x21,
+               landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
+                                       LANDLOCK_ACCESS_FS_TRUNCATE |
+                                               LANDLOCK_ACCESS_FS_IOCTL_DEV,
+                                       &layers1, ARRAY_SIZE(layers1)));
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static struct kunit_case test_cases[] = {
+       /* clang-format off */
+       KUNIT_CASE(test_get_layer_deny_mask),
+       KUNIT_CASE(test_landlock_get_deny_masks),
+       {}
+       /* clang-format on */
+};
+
+static struct kunit_suite test_suite = {
+       .name = "landlock_domain",
+       .test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
 #endif /* CONFIG_AUDIT */
index 2c6f04da3628780a1afb1eae6c01a13fcd1bc64b..96642dd87b335d7d7904f0b848577825f6c20c7f 100644 (file)
@@ -18,6 +18,7 @@
 #include <linux/sched.h>
 #include <linux/slab.h>
 
+#include "access.h"
 #include "audit.h"
 
 enum landlock_log_status {
@@ -107,6 +108,12 @@ struct landlock_hierarchy {
 
 #ifdef CONFIG_AUDIT
 
+deny_masks_t
+landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
+                       const access_mask_t optional_access,
+                       const layer_mask_t (*const layer_masks)[],
+                       size_t layer_masks_size);
+
 int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
 
 static inline void
index c24fcc70262d2f2bfdc80388070c9eee1dfad206..0a58962cf61d552ea8fbbd033621bc067a653350 100644 (file)
@@ -43,6 +43,7 @@
 #include "audit.h"
 #include "common.h"
 #include "cred.h"
+#include "domain.h"
 #include "fs.h"
 #include "limits.h"
 #include "object.h"
@@ -1671,6 +1672,11 @@ static int hook_file_open(struct file *const file)
         * file access rights in the opened struct file.
         */
        landlock_file(file)->allowed_access = allowed_access;
+#ifdef CONFIG_AUDIT
+       landlock_file(file)->deny_masks = landlock_get_deny_masks(
+               _LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks,
+               ARRAY_SIZE(layer_masks));
+#endif /* CONFIG_AUDIT */
 
        if ((open_access_request & allowed_access) == open_access_request)
                return 0;
@@ -1695,6 +1701,19 @@ static int hook_file_truncate(struct file *const file)
         */
        if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE)
                return 0;
+
+       landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) {
+               .type = LANDLOCK_REQUEST_FS_ACCESS,
+               .audit = {
+                       .type = LSM_AUDIT_DATA_FILE,
+                       .u.file = file,
+               },
+               .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
+               .access = LANDLOCK_ACCESS_FS_TRUNCATE,
+#ifdef CONFIG_AUDIT
+               .deny_masks = landlock_file(file)->deny_masks,
+#endif /* CONFIG_AUDIT */
+       });
        return -EACCES;
 }
 
@@ -1719,6 +1738,21 @@ static int hook_file_ioctl_common(const struct file *const file,
                                  is_masked_device_ioctl(cmd))
                return 0;
 
+       landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) {
+               .type = LANDLOCK_REQUEST_FS_ACCESS,
+               .audit = {
+                       .type = LSM_AUDIT_DATA_IOCTL_OP,
+                       .u.op = &(struct lsm_ioctlop_audit) {
+                               .path = file->f_path,
+                               .cmd = cmd,
+                       },
+               },
+               .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
+               .access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
+#ifdef CONFIG_AUDIT
+               .deny_masks = landlock_file(file)->deny_masks,
+#endif /* CONFIG_AUDIT */
+       });
        return -EACCES;
 }
 
index b29972eb9224f4cd3205a0cc2e38270c0c5b2b9d..8c48fad4e123ea242c041a3eb477dacd8c7679a7 100644 (file)
@@ -55,6 +55,15 @@ struct landlock_file_security {
         * needed to authorize later operations on the open file.
         */
        access_mask_t allowed_access;
+
+#ifdef CONFIG_AUDIT
+       /**
+        * @deny_masks: Domain layer levels that deny an optional access (see
+        * _LANDLOCK_ACCESS_FS_OPTIONAL).
+        */
+       deny_masks_t deny_masks;
+#endif /* CONFIG_AUDIT */
+
        /**
         * @fown_subject: Landlock credential of the task that set the PID that
         * may receive a signal e.g., SIGURG when writing MSG_OOB to the