mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/
synced 2025-04-19 20:58:31 +09:00
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:
parent
e120b3c293
commit
20fd295494
@ -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)
|
||||
|
@ -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 */
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user