Skip to content

Commit 20fd295

Browse files
committed
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>
1 parent e120b3c commit 20fd295

7 files changed

Lines changed: 307 additions & 6 deletions

File tree

security/landlock/access.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* SPDX-License-Identifier: GPL-2.0-only */
22
/*
3-
* Landlock LSM - Access types and helpers
3+
* Landlock - Access types and helpers
44
*
55
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
66
* Copyright © 2018-2020 ANSSI
@@ -28,6 +28,12 @@
2828
LANDLOCK_ACCESS_FS_REFER)
2929
/* clang-format on */
3030

31+
/* clang-format off */
32+
#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \
33+
LANDLOCK_ACCESS_FS_TRUNCATE | \
34+
LANDLOCK_ACCESS_FS_IOCTL_DEV)
35+
/* clang-format on */
36+
3137
typedef u16 access_mask_t;
3238

3339
/* Makes sure all filesystem access rights can be stored. */
@@ -60,6 +66,23 @@ typedef u16 layer_mask_t;
6066
/* Makes sure all layers can be checked. */
6167
static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
6268

69+
/*
70+
* Tracks domains responsible of a denied access. This is required to avoid
71+
* storing in each object the full layer_masks[] required by update_request().
72+
*/
73+
typedef u8 deny_masks_t;
74+
75+
/*
76+
* Makes sure all optional access rights can be tied to a layer index (cf.
77+
* get_deny_mask).
78+
*/
79+
static_assert(BITS_PER_TYPE(deny_masks_t) >=
80+
(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) *
81+
HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)));
82+
83+
/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */
84+
static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1);
85+
6386
/* Upgrades with all initially denied by default access rights. */
6487
static inline struct access_masks
6588
landlock_upgrade_handled_access_masks(struct access_masks access_masks)

security/landlock/audit.c

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <linux/pid.h>
1313
#include <uapi/linux/landlock.h>
1414

15+
#include "access.h"
1516
#include "audit.h"
1617
#include "common.h"
1718
#include "cred.h"
@@ -249,6 +250,88 @@ static void test_get_denied_layer(struct kunit *const test)
249250

250251
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
251252

253+
static size_t
254+
get_layer_from_deny_masks(access_mask_t *const access_request,
255+
const access_mask_t all_existing_optional_access,
256+
const deny_masks_t deny_masks)
257+
{
258+
const unsigned long access_opt = all_existing_optional_access;
259+
const unsigned long access_req = *access_request;
260+
access_mask_t missing = 0;
261+
size_t youngest_layer = 0;
262+
size_t access_index = 0;
263+
unsigned long access_bit;
264+
265+
/* This will require change with new object types. */
266+
WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
267+
268+
for_each_set_bit(access_bit, &access_opt,
269+
BITS_PER_TYPE(access_mask_t)) {
270+
if (access_req & BIT(access_bit)) {
271+
const size_t layer =
272+
(deny_masks >> (access_index * 4)) &
273+
(LANDLOCK_MAX_NUM_LAYERS - 1);
274+
275+
if (layer > youngest_layer) {
276+
youngest_layer = layer;
277+
missing = BIT(access_bit);
278+
} else if (layer == youngest_layer) {
279+
missing |= BIT(access_bit);
280+
}
281+
}
282+
access_index++;
283+
}
284+
285+
*access_request = missing;
286+
return youngest_layer;
287+
}
288+
289+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
290+
291+
static void test_get_layer_from_deny_masks(struct kunit *const test)
292+
{
293+
deny_masks_t deny_mask;
294+
access_mask_t access;
295+
296+
/* truncate:0 ioctl_dev:2 */
297+
deny_mask = 0x20;
298+
299+
access = LANDLOCK_ACCESS_FS_TRUNCATE;
300+
KUNIT_EXPECT_EQ(test, 0,
301+
get_layer_from_deny_masks(&access,
302+
_LANDLOCK_ACCESS_FS_OPTIONAL,
303+
deny_mask));
304+
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
305+
306+
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
307+
KUNIT_EXPECT_EQ(test, 2,
308+
get_layer_from_deny_masks(&access,
309+
_LANDLOCK_ACCESS_FS_OPTIONAL,
310+
deny_mask));
311+
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
312+
313+
/* truncate:15 ioctl_dev:15 */
314+
deny_mask = 0xff;
315+
316+
access = LANDLOCK_ACCESS_FS_TRUNCATE;
317+
KUNIT_EXPECT_EQ(test, 15,
318+
get_layer_from_deny_masks(&access,
319+
_LANDLOCK_ACCESS_FS_OPTIONAL,
320+
deny_mask));
321+
KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
322+
323+
access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
324+
KUNIT_EXPECT_EQ(test, 15,
325+
get_layer_from_deny_masks(&access,
326+
_LANDLOCK_ACCESS_FS_OPTIONAL,
327+
deny_mask));
328+
KUNIT_EXPECT_EQ(test, access,
329+
LANDLOCK_ACCESS_FS_TRUNCATE |
330+
LANDLOCK_ACCESS_FS_IOCTL_DEV);
331+
}
332+
333+
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
334+
252335
static bool is_valid_request(const struct landlock_request *const request)
253336
{
254337
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)
258341
return false;
259342

260343
if (request->access) {
261-
if (WARN_ON_ONCE(!request->layer_masks))
344+
if (WARN_ON_ONCE(!(!!request->layer_masks ^
345+
!!request->all_existing_optional_access)))
262346
return false;
263347
} else {
264-
if (WARN_ON_ONCE(request->layer_masks))
348+
if (WARN_ON_ONCE(request->layer_masks ||
349+
request->all_existing_optional_access))
265350
return false;
266351
}
267352

268353
if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size))
269354
return false;
270355

356+
if (request->deny_masks) {
357+
if (WARN_ON_ONCE(!request->all_existing_optional_access))
358+
return false;
359+
}
360+
271361
return true;
272362
}
273363

@@ -300,9 +390,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
300390
subject->domain, &missing, request->layer_masks,
301391
request->layer_masks_size);
302392
} else {
303-
/* This will change with the next commit. */
304-
WARN_ON_ONCE(1);
305-
youngest_layer = subject->domain->num_layers;
393+
youngest_layer = get_layer_from_deny_masks(
394+
&missing, request->all_existing_optional_access,
395+
request->deny_masks);
306396
}
307397
youngest_denied =
308398
get_hierarchy(subject->domain, youngest_layer);
@@ -387,6 +477,7 @@ static struct kunit_case test_cases[] = {
387477
/* clang-format off */
388478
KUNIT_CASE(test_get_hierarchy),
389479
KUNIT_CASE(test_get_denied_layer),
480+
KUNIT_CASE(test_get_layer_from_deny_masks),
390481
{}
391482
/* clang-format on */
392483
};

security/landlock/audit.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ struct landlock_request {
4242
/* Required fields for requests with layer masks. */
4343
const layer_mask_t (*layer_masks)[];
4444
size_t layer_masks_size;
45+
46+
/* Required fields for requests with deny masks. */
47+
const access_mask_t all_existing_optional_access;
48+
deny_masks_t deny_masks;
4549
};
4650

4751
#ifdef CONFIG_AUDIT

security/landlock/domain.c

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
* Copyright © 2024-2025 Microsoft Corporation
88
*/
99

10+
#include <kunit/test.h>
11+
#include <linux/bitops.h>
12+
#include <linux/bits.h>
1013
#include <linux/cred.h>
1114
#include <linux/file.h>
1215
#include <linux/mm.h>
@@ -15,6 +18,8 @@
1518
#include <linux/sched.h>
1619
#include <linux/uidgid.h>
1720

21+
#include "access.h"
22+
#include "common.h"
1823
#include "domain.h"
1924
#include "id.h"
2025

@@ -126,4 +131,132 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
126131
return 0;
127132
}
128133

134+
static deny_masks_t
135+
get_layer_deny_mask(const access_mask_t all_existing_optional_access,
136+
const unsigned long access_bit, const size_t layer)
137+
{
138+
unsigned long access_weight;
139+
140+
/* This may require change with new object types. */
141+
WARN_ON_ONCE(all_existing_optional_access !=
142+
_LANDLOCK_ACCESS_FS_OPTIONAL);
143+
144+
if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS))
145+
return 0;
146+
147+
access_weight = hweight_long(all_existing_optional_access &
148+
GENMASK(access_bit, 0));
149+
if (WARN_ON_ONCE(access_weight < 1))
150+
return 0;
151+
152+
return layer
153+
<< ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1));
154+
}
155+
156+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
157+
158+
static void test_get_layer_deny_mask(struct kunit *const test)
159+
{
160+
const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE);
161+
const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV);
162+
163+
KUNIT_EXPECT_EQ(test, 0,
164+
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
165+
truncate, 0));
166+
KUNIT_EXPECT_EQ(test, 0x3,
167+
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
168+
truncate, 3));
169+
170+
KUNIT_EXPECT_EQ(test, 0,
171+
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
172+
ioctl_dev, 0));
173+
KUNIT_EXPECT_EQ(test, 0xf0,
174+
get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL,
175+
ioctl_dev, 15));
176+
}
177+
178+
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
179+
180+
deny_masks_t
181+
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
182+
const access_mask_t optional_access,
183+
const layer_mask_t (*const layer_masks)[],
184+
const size_t layer_masks_size)
185+
{
186+
const unsigned long access_opt = optional_access;
187+
unsigned long access_bit;
188+
deny_masks_t deny_masks = 0;
189+
190+
/* This may require change with new object types. */
191+
WARN_ON_ONCE(access_opt !=
192+
(optional_access & all_existing_optional_access));
193+
194+
if (WARN_ON_ONCE(!layer_masks))
195+
return 0;
196+
197+
if (WARN_ON_ONCE(!access_opt))
198+
return 0;
199+
200+
for_each_set_bit(access_bit, &access_opt, layer_masks_size) {
201+
const layer_mask_t mask = (*layer_masks)[access_bit];
202+
203+
if (!mask)
204+
continue;
205+
206+
/* __fls(1) == 0 */
207+
deny_masks |= get_layer_deny_mask(all_existing_optional_access,
208+
access_bit, __fls(mask));
209+
}
210+
return deny_masks;
211+
}
212+
213+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
214+
215+
static void test_landlock_get_deny_masks(struct kunit *const test)
216+
{
217+
const layer_mask_t layers1[BITS_PER_TYPE(access_mask_t)] = {
218+
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) |
219+
BIT_ULL(9),
220+
[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1),
221+
[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = BIT_ULL(2) |
222+
BIT_ULL(0),
223+
};
224+
225+
KUNIT_EXPECT_EQ(test, 0x1,
226+
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
227+
LANDLOCK_ACCESS_FS_TRUNCATE,
228+
&layers1, ARRAY_SIZE(layers1)));
229+
KUNIT_EXPECT_EQ(test, 0x20,
230+
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
231+
LANDLOCK_ACCESS_FS_IOCTL_DEV,
232+
&layers1, ARRAY_SIZE(layers1)));
233+
KUNIT_EXPECT_EQ(
234+
test, 0x21,
235+
landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL,
236+
LANDLOCK_ACCESS_FS_TRUNCATE |
237+
LANDLOCK_ACCESS_FS_IOCTL_DEV,
238+
&layers1, ARRAY_SIZE(layers1)));
239+
}
240+
241+
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
242+
243+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
244+
245+
static struct kunit_case test_cases[] = {
246+
/* clang-format off */
247+
KUNIT_CASE(test_get_layer_deny_mask),
248+
KUNIT_CASE(test_landlock_get_deny_masks),
249+
{}
250+
/* clang-format on */
251+
};
252+
253+
static struct kunit_suite test_suite = {
254+
.name = "landlock_domain",
255+
.test_cases = test_cases,
256+
};
257+
258+
kunit_test_suite(test_suite);
259+
260+
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
261+
129262
#endif /* CONFIG_AUDIT */

security/landlock/domain.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <linux/sched.h>
1919
#include <linux/slab.h>
2020

21+
#include "access.h"
2122
#include "audit.h"
2223

2324
enum landlock_log_status {
@@ -107,6 +108,12 @@ struct landlock_hierarchy {
107108

108109
#ifdef CONFIG_AUDIT
109110

111+
deny_masks_t
112+
landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
113+
const access_mask_t optional_access,
114+
const layer_mask_t (*const layer_masks)[],
115+
size_t layer_masks_size);
116+
110117
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
111118

112119
static inline void

0 commit comments

Comments
 (0)