Skip to content

Commit 960ed6c

Browse files
committed
selftests/landlock: Test audit with restrict flags
Add audit_exec tests to filter Landlock denials according to cross-execution or muted subdomains. Add a wait-pipe-sandbox.c test program to sandbox itself and send a (denied) signals to its parent. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Link: https://lore.kernel.org/r/20250320190717.2287696-24-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent 6a500b2 commit 960ed6c

File tree

5 files changed

+357
-1
lines changed

5 files changed

+357
-1
lines changed

tools/testing/selftests/landlock/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/sandbox-and-launch
33
/true
44
/wait-pipe
5+
/wait-pipe-sandbox

tools/testing/selftests/landlock/Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ src_test := $(wildcard *_test.c)
1010

1111
TEST_GEN_PROGS := $(src_test:.c=)
1212

13-
TEST_GEN_PROGS_EXTENDED := true sandbox-and-launch wait-pipe
13+
TEST_GEN_PROGS_EXTENDED := \
14+
true \
15+
sandbox-and-launch \
16+
wait-pipe \
17+
wait-pipe-sandbox
1418

1519
# Short targets:
1620
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread

tools/testing/selftests/landlock/audit_test.c

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
#define _GNU_SOURCE
99
#include <errno.h>
10+
#include <limits.h>
1011
#include <linux/landlock.h>
12+
#include <stdlib.h>
1113
#include <sys/mount.h>
1214
#include <sys/prctl.h>
1315
#include <sys/types.h>
@@ -329,4 +331,221 @@ TEST_F(audit_flags, signal)
329331
}
330332
}
331333

334+
static int matches_log_fs_read_root(int audit_fd)
335+
{
336+
return audit_match_record(
337+
audit_fd, AUDIT_LANDLOCK_ACCESS,
338+
REGEX_LANDLOCK_PREFIX
339+
" blockers=fs\\.read_dir path=\"/\" dev=\"[^\"]\\+\" ino=[0-9]\\+$",
340+
NULL);
341+
}
342+
343+
FIXTURE(audit_exec)
344+
{
345+
struct audit_filter audit_filter;
346+
int audit_fd;
347+
};
348+
349+
FIXTURE_VARIANT(audit_exec)
350+
{
351+
const int restrict_flags;
352+
};
353+
354+
/* clang-format off */
355+
FIXTURE_VARIANT_ADD(audit_exec, default) {
356+
/* clang-format on */
357+
.restrict_flags = 0,
358+
};
359+
360+
/* clang-format off */
361+
FIXTURE_VARIANT_ADD(audit_exec, same_exec_off) {
362+
/* clang-format on */
363+
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF,
364+
};
365+
366+
/* clang-format off */
367+
FIXTURE_VARIANT_ADD(audit_exec, subdomains_off) {
368+
/* clang-format on */
369+
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
370+
};
371+
372+
/* clang-format off */
373+
FIXTURE_VARIANT_ADD(audit_exec, cross_exec_on) {
374+
/* clang-format on */
375+
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
376+
};
377+
378+
/* clang-format off */
379+
FIXTURE_VARIANT_ADD(audit_exec, subdomains_off_and_cross_exec_on) {
380+
/* clang-format on */
381+
.restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
382+
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON,
383+
};
384+
385+
FIXTURE_SETUP(audit_exec)
386+
{
387+
disable_caps(_metadata);
388+
set_cap(_metadata, CAP_AUDIT_CONTROL);
389+
390+
self->audit_fd = audit_init();
391+
EXPECT_LE(0, self->audit_fd)
392+
{
393+
const char *error_msg;
394+
395+
/* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */
396+
if (self->audit_fd == -EEXIST)
397+
error_msg = "socket already in use (e.g. auditd)";
398+
else
399+
error_msg = strerror(-self->audit_fd);
400+
TH_LOG("Failed to initialize audit: %s", error_msg);
401+
}
402+
403+
/* Applies test filter for the bin_wait_pipe_sandbox program. */
404+
EXPECT_EQ(0, audit_init_filter_exe(&self->audit_filter,
405+
bin_wait_pipe_sandbox));
406+
EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter,
407+
AUDIT_ADD_RULE));
408+
409+
clear_cap(_metadata, CAP_AUDIT_CONTROL);
410+
}
411+
412+
FIXTURE_TEARDOWN(audit_exec)
413+
{
414+
set_cap(_metadata, CAP_AUDIT_CONTROL);
415+
EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter,
416+
AUDIT_DEL_RULE));
417+
clear_cap(_metadata, CAP_AUDIT_CONTROL);
418+
EXPECT_EQ(0, close(self->audit_fd));
419+
}
420+
421+
TEST_F(audit_exec, signal_and_open)
422+
{
423+
struct audit_records records;
424+
int pipe_child[2], pipe_parent[2];
425+
char buf_parent;
426+
pid_t child;
427+
int status;
428+
429+
ASSERT_EQ(0, pipe2(pipe_child, 0));
430+
ASSERT_EQ(0, pipe2(pipe_parent, 0));
431+
432+
child = fork();
433+
ASSERT_LE(0, child);
434+
if (child == 0) {
435+
const struct landlock_ruleset_attr layer1 = {
436+
.scoped = LANDLOCK_SCOPE_SIGNAL,
437+
};
438+
char pipe_child_str[12], pipe_parent_str[12];
439+
char *const argv[] = { (char *)bin_wait_pipe_sandbox,
440+
pipe_child_str, pipe_parent_str, NULL };
441+
int ruleset_fd;
442+
443+
/* Passes the pipe FDs to the executed binary. */
444+
EXPECT_EQ(0, close(pipe_child[0]));
445+
EXPECT_EQ(0, close(pipe_parent[1]));
446+
snprintf(pipe_child_str, sizeof(pipe_child_str), "%d",
447+
pipe_child[1]);
448+
snprintf(pipe_parent_str, sizeof(pipe_parent_str), "%d",
449+
pipe_parent[0]);
450+
451+
ruleset_fd =
452+
landlock_create_ruleset(&layer1, sizeof(layer1), 0);
453+
if (ruleset_fd < 0) {
454+
perror("Failed to create a ruleset");
455+
_exit(1);
456+
}
457+
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
458+
if (landlock_restrict_self(ruleset_fd,
459+
variant->restrict_flags)) {
460+
perror("Failed to restrict self");
461+
_exit(1);
462+
}
463+
close(ruleset_fd);
464+
465+
ASSERT_EQ(0, execve(argv[0], argv, NULL))
466+
{
467+
TH_LOG("Failed to execute \"%s\": %s", argv[0],
468+
strerror(errno));
469+
};
470+
_exit(1);
471+
return;
472+
}
473+
474+
EXPECT_EQ(0, close(pipe_child[1]));
475+
EXPECT_EQ(0, close(pipe_parent[0]));
476+
477+
/* Waits for the child. */
478+
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
479+
480+
/* Tests that there was no denial until now. */
481+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
482+
EXPECT_EQ(0, records.access);
483+
EXPECT_EQ(0, records.domain);
484+
485+
/*
486+
* Wait for the child to do a first denied action by layer1 and
487+
* sandbox itself with layer2.
488+
*/
489+
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
490+
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
491+
492+
/* Tests that the audit record only matches the child. */
493+
if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) {
494+
/* Matches the current domain. */
495+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
496+
getpid(), NULL));
497+
}
498+
499+
/* Checks that we didn't miss anything. */
500+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
501+
EXPECT_EQ(0, records.access);
502+
503+
/*
504+
* Wait for the child to do a second denied action by layer1 and
505+
* layer2, and sandbox itself with layer3.
506+
*/
507+
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
508+
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
509+
510+
/* Tests that the audit record only matches the child. */
511+
if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) {
512+
/* Matches the current domain. */
513+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
514+
getpid(), NULL));
515+
}
516+
517+
if (!(variant->restrict_flags &
518+
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
519+
/* Matches the child domain. */
520+
EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd));
521+
}
522+
523+
/* Checks that we didn't miss anything. */
524+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
525+
EXPECT_EQ(0, records.access);
526+
527+
/* Waits for the child to terminate. */
528+
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
529+
ASSERT_EQ(child, waitpid(child, &status, 0));
530+
ASSERT_EQ(1, WIFEXITED(status));
531+
ASSERT_EQ(0, WEXITSTATUS(status));
532+
533+
/* Tests that the audit record only matches the child. */
534+
if (!(variant->restrict_flags &
535+
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
536+
/*
537+
* Matches the child domains, which tests that the
538+
* llcred->domain_exec bitmask is correctly updated with a new
539+
* domain.
540+
*/
541+
EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd));
542+
EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
543+
getpid(), NULL));
544+
}
545+
546+
/* Checks that we didn't miss anything. */
547+
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
548+
EXPECT_EQ(0, records.access);
549+
}
550+
332551
TEST_HARNESS_MAIN

tools/testing/selftests/landlock/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
static const char bin_sandbox_and_launch[] = "./sandbox-and-launch";
3333
static const char bin_wait_pipe[] = "./wait-pipe";
34+
static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox";
3435

3536
static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)
3637
{
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
/*
3+
* Write in a pipe, wait, sandbox itself, test sandboxing, and wait again.
4+
*
5+
* Used by audit_exec.flags from audit_test.c
6+
*
7+
* Copyright © 2024-2025 Microsoft Corporation
8+
*/
9+
10+
#define _GNU_SOURCE
11+
#include <fcntl.h>
12+
#include <linux/landlock.h>
13+
#include <linux/prctl.h>
14+
#include <signal.h>
15+
#include <stdio.h>
16+
#include <stdlib.h>
17+
#include <sys/prctl.h>
18+
#include <unistd.h>
19+
20+
#include "wrappers.h"
21+
22+
static int sync_with(int pipe_child, int pipe_parent)
23+
{
24+
char buf;
25+
26+
/* Signals that we are waiting. */
27+
if (write(pipe_child, ".", 1) != 1) {
28+
perror("Failed to write to first argument");
29+
return 1;
30+
}
31+
32+
/* Waits for the parent do its test. */
33+
if (read(pipe_parent, &buf, 1) != 1) {
34+
perror("Failed to write to the second argument");
35+
return 1;
36+
}
37+
38+
return 0;
39+
}
40+
41+
int main(int argc, char *argv[])
42+
{
43+
const struct landlock_ruleset_attr layer2 = {
44+
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
45+
};
46+
const struct landlock_ruleset_attr layer3 = {
47+
.scoped = LANDLOCK_SCOPE_SIGNAL,
48+
};
49+
int err, pipe_child, pipe_parent, ruleset_fd;
50+
51+
/* The first argument must be the file descriptor number of a pipe. */
52+
if (argc != 3) {
53+
fprintf(stderr, "Wrong number of arguments (not two)\n");
54+
return 1;
55+
}
56+
57+
pipe_child = atoi(argv[1]);
58+
pipe_parent = atoi(argv[2]);
59+
/* PR_SET_NO_NEW_PRIVS already set by parent. */
60+
61+
/* First step to test parent's layer1. */
62+
err = sync_with(pipe_child, pipe_parent);
63+
if (err)
64+
return err;
65+
66+
/* Tries to send a signal, denied by layer1. */
67+
if (!kill(getppid(), 0)) {
68+
fprintf(stderr, "Successfully sent a signal to the parent");
69+
return 1;
70+
}
71+
72+
/* Second step to test parent's layer1 and our layer2. */
73+
err = sync_with(pipe_child, pipe_parent);
74+
if (err)
75+
return err;
76+
77+
ruleset_fd = landlock_create_ruleset(&layer2, sizeof(layer2), 0);
78+
if (ruleset_fd < 0) {
79+
perror("Failed to create the layer2 ruleset");
80+
return 1;
81+
}
82+
83+
if (landlock_restrict_self(ruleset_fd, 0)) {
84+
perror("Failed to restrict self");
85+
return 1;
86+
}
87+
close(ruleset_fd);
88+
89+
/* Tries to send a signal, denied by layer1. */
90+
if (!kill(getppid(), 0)) {
91+
fprintf(stderr, "Successfully sent a signal to the parent");
92+
return 1;
93+
}
94+
95+
/* Tries to open ., denied by layer2. */
96+
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
97+
fprintf(stderr, "Successfully opened /");
98+
return 1;
99+
}
100+
101+
/* Third step to test our layer2 and layer3. */
102+
err = sync_with(pipe_child, pipe_parent);
103+
if (err)
104+
return err;
105+
106+
ruleset_fd = landlock_create_ruleset(&layer3, sizeof(layer3), 0);
107+
if (ruleset_fd < 0) {
108+
perror("Failed to create the layer3 ruleset");
109+
return 1;
110+
}
111+
112+
if (landlock_restrict_self(ruleset_fd, 0)) {
113+
perror("Failed to restrict self");
114+
return 1;
115+
}
116+
close(ruleset_fd);
117+
118+
/* Tries to open ., denied by layer2. */
119+
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
120+
fprintf(stderr, "Successfully opened /");
121+
return 1;
122+
}
123+
124+
/* Tries to send a signal, denied by layer3. */
125+
if (!kill(getppid(), 0)) {
126+
fprintf(stderr, "Successfully sent a signal to the parent");
127+
return 1;
128+
}
129+
130+
return 0;
131+
}

0 commit comments

Comments
 (0)