landlock: Log truncate and IOCTL denials

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>
This commit is contained in:
Mickaël Salaün 2025-03-20 20:07:03 +01:00
parent e120b3c293
commit 20fd295494
No known key found for this signature in database
GPG Key ID: E5E3D0E88C82F6D2
7 changed files with 307 additions and 6 deletions

View 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
@ -28,6 +28,12 @@
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)

View 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 */
};

View 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

View 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 */

View 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

View 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;
}

View 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