diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-06-19 12:20:25 -0700 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-06-19 12:20:25 -0700 |
| commit | 5e2e14749c3d969e263a879db104db6e9f0eb484 (patch) | |
| tree | 065f3f60b48f249fca91931fa261b9cf93172c50 /tools | |
| parent | e2c0595b56e9526e67ddd228fc35fa9ff20724ec (diff) | |
| parent | 1c236e7fe740a009ad8dd40a5ee0602ec402fffe (diff) | |
| download | lwn-5e2e14749c3d969e263a879db104db6e9f0eb484.tar.gz lwn-5e2e14749c3d969e263a879db104db6e9f0eb484.zip | |
Merge tag 'landlock-7.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux
Pull landlock updates from Mickaël Salaün:
"This adds new Landlock access rights to control UDP bind and
connect/send operations, and a new "quiet" feature to mute specific
specific audit logs (and other future observability events).
A few commits also fix Landlock issues"
* tag 'landlock-7.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: (24 commits)
selftests/landlock: Add tests for invalid use of quiet flag
selftests/landlock: Add tests for quiet flag with scope
selftests/landlock: Add tests for quiet flag with net rules
selftests/landlock: Add tests for quiet flag with fs rules
selftests/landlock: Replace hard-coded 16 with a constant
samples/landlock: Add quiet flag support to sandboxer
landlock: Suppress logging when quiet flag is present
landlock: Add API support and docs for the quiet flags
landlock: Add a place for flags to layer rules
landlock: Add documentation for UDP support
samples/landlock: Add sandboxer UDP access control
selftests/landlock: Add tests for UDP send
selftests/landlock: Add tests for UDP bind/connect
landlock: Add UDP send+connect access control
landlock: Add UDP bind() access control
landlock: Fix unmarked concurrent access to socket family
selftests/landlock: Explicitly disable audit in teardowns
selftests/landlock: Test SCOPE_SIGNAL on the SIGIO/fowner pgid path
landlock: Fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path
landlock: Demonstrate best-effort allowed_access filtering
...
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/testing/selftests/landlock/audit.h | 140 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/audit_test.c | 33 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/base_test.c | 122 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/common.h | 2 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/fs_test.c | 2445 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/net_test.c | 1358 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/ptrace_test.c | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/scoped_abstract_unix_test.c | 78 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/scoped_signal_test.c | 182 |
9 files changed, 4188 insertions, 173 deletions
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h index 834005b2b0f0..f45fdef35681 100644 --- a/tools/testing/selftests/landlock/audit.h +++ b/tools/testing/selftests/landlock/audit.h @@ -45,17 +45,25 @@ struct audit_message { }; }; -static const struct timeval audit_tv_dom_drop = { +static const struct timeval audit_tv_default = { /* - * Because domain deallocation is tied to asynchronous credential - * freeing, receiving such event may take some time. In practice, - * on a small VM, it should not exceed 100k usec, but let's wait up - * to 1 second to be safe. + * Default socket timeout for audit_match_record() callers that expect a + * record to arrive. Asynchronous kauditd delivery can exceed 1 usec + * under heavy debug configs (KASAN, lockdep), where kauditd_thread + * scheduling between audit_log_end() and netlink_unicast() takes longer + * than the previous 1 usec timeout. 1 second is a generous ceiling: on + * the happy path, kauditd delivers within dozens of usec. */ .tv_sec = 1, }; -static const struct timeval audit_tv_default = { +static const struct timeval audit_tv_fast = { + /* + * Fast timeout for paths that expect no record (audit_init() drain, + * audit_count_records(), probes). Causes audit_recv() to return + * -EAGAIN once the socket buffer is empty, naturally terminating the + * read loop. + */ .tv_usec = 1, }; @@ -334,8 +342,13 @@ static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid, * Matches a domain deallocation record. When expected_domain_id is non-zero, * the pattern includes the specific domain ID so that stale deallocation * records from a previous test (with a different domain ID) are skipped by - * audit_match_record(), and the socket timeout is temporarily increased to - * audit_tv_dom_drop to wait for the asynchronous kworker deallocation. + * audit_match_record(), waiting for the asynchronous kworker deallocation with + * the default patient timeout. + * + * When expected_domain_id is zero, the caller is probing for any dealloc record + * that may or may not arrive. Temporarily lowers the socket timeout to + * audit_tv_fast for this probe so it returns promptly when no record is + * pending; restores audit_tv_default after. */ static int __maybe_unused matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, @@ -361,16 +374,21 @@ matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, if (log_match_len >= sizeof(log_match)) return -E2BIG; - if (expected_domain_id) - setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, - &audit_tv_dom_drop, sizeof(audit_tv_dom_drop)); + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_fast, sizeof(audit_tv_fast))) + return -errno; + } err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, domain_id); - if (expected_domain_id) - setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, - sizeof(audit_tv_default)); + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, sizeof(audit_tv_default)) && + !err) + err = -errno; + } return err; } @@ -381,30 +399,46 @@ struct audit_records { }; /* - * WARNING: Do not assert records.domain == 0 without a preceding - * audit_match_record() call. Domain deallocation records are emitted - * asynchronously from kworker threads and can arrive after the drain in - * audit_init(), corrupting the domain count. A preceding audit_match_record() - * call consumes stale records while scanning, making the assertion safe in - * practice because stale deallocation records arrive before the expected access - * records. + * Counts remaining audit records by type, skipping domain deallocation records. + * Deallocation records are emitted asynchronously from kworker threads after a + * previous test's child has exited, so they can arrive after the drain in + * audit_init() and after the preceding audit_match_record() call. Allocation + * records are emitted synchronously during landlock_log_denial() in the current + * test's syscall context, so only those are counted in records->domain. + * + * Temporarily lowers SO_RCVTIMEO to audit_tv_fast for the read loop: this is a + * "no record expected" path that should terminate on the first -EAGAIN. The + * default patient timeout is restored on exit for subsequent + * audit_match_record() callers. */ static int audit_count_records(int audit_fd, struct audit_records *records) { + static const char dealloc_pattern[] = REGEX_LANDLOCK_PREFIX + " status=deallocated "; struct audit_message msg; - int err; + regex_t dealloc_re; + int ret, err = 0; + + ret = regcomp(&dealloc_re, dealloc_pattern, 0); + if (ret) + return -ENOMEM; records->access = 0; records->domain = 0; + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast))) { + err = -errno; + goto out; + } + do { memset(&msg, 0, sizeof(msg)); err = audit_recv(audit_fd, &msg); if (err) { if (err == -EAGAIN) - return 0; - else - return err; + err = 0; + break; } switch (msg.header.nlmsg_type) { @@ -412,12 +446,24 @@ static int audit_count_records(int audit_fd, struct audit_records *records) records->access++; break; case AUDIT_LANDLOCK_DOMAIN: - records->domain++; + ret = regexec(&dealloc_re, msg.data, 0, NULL, 0); + if (ret == REG_NOMATCH) { + records->domain++; + } else if (ret != 0) { + err = -EIO; + goto out; + } break; } } while (true); - return 0; +out: + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)) && + !err) + err = -errno; + regfree(&dealloc_re); + return err; } static int audit_init(void) @@ -436,9 +482,9 @@ static int audit_init(void) if (err) goto err_close; - /* Sets a timeout for negative tests. */ - err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, - sizeof(audit_tv_default)); + /* Uses the fast timeout to drain stale records below. */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast)); if (err) { err = -errno; goto err_close; @@ -454,6 +500,19 @@ static int audit_init(void) while (audit_recv(fd, NULL) == 0) ; + /* + * Restores the default timeout for audit_match_record() callers that + * expect a record to arrive. Paths that expect no record restore the + * fast timeout locally (audit_count_records(), the expected_domain_id + * == 0 probe in matches_log_domain_deallocated()). + */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)); + if (err) { + err = -errno; + goto err_close; + } + return fd; err_close: @@ -494,10 +553,9 @@ static int audit_init_filter_exe(struct audit_filter *filter, const char *path) static int audit_cleanup(int audit_fd, struct audit_filter *filter) { struct audit_filter new_filter; + int err = 0; if (audit_fd < 0 || !filter) { - int err; - /* * Simulates audit_init_with_exe_filter() when called from * FIXTURE_TEARDOWN_PARENT(). @@ -508,23 +566,19 @@ static int audit_cleanup(int audit_fd, struct audit_filter *filter) filter = &new_filter; err = audit_init_filter_exe(filter, NULL); - if (err) { - close(audit_fd); - return err; - } + if (err) + goto err_close; } /* Filters might not be in place. */ audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE); audit_filter_drop(audit_fd, AUDIT_DEL_RULE); - /* - * Because audit_cleanup() might not be called by the test auditd - * process, it might not be possible to explicitly set it. Anyway, - * AUDIT_STATUS_ENABLED will implicitly be set to 0 when the auditd - * process will exit. - */ - return close(audit_fd); + err = audit_set_status(audit_fd, AUDIT_STATUS_ENABLED, 0); + +err_close: + close(audit_fd); + return err; } static int audit_init_with_exe_filter(struct audit_filter *filter) diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c index 93ae5bd0dcce..72b5612375dd 100644 --- a/tools/testing/selftests/landlock/audit_test.c +++ b/tools/testing/selftests/landlock/audit_test.c @@ -76,7 +76,7 @@ TEST_F(audit, layers) .scoped = LANDLOCK_SCOPE_SIGNAL, }; int status, ruleset_fd, i; - __u64(*domain_stack)[16]; + __u64(*domain_stack)[LANDLOCK_MAX_NUM_LAYERS]; __u64 prev_dom = 3; pid_t child; @@ -607,30 +607,42 @@ FIXTURE(audit_flags) FIXTURE_VARIANT(audit_flags) { const int restrict_flags; + const __u64 quiet_scoped; }; /* clang-format off */ FIXTURE_VARIANT_ADD(audit_flags, default) { /* clang-format on */ .restrict_flags = 0, + .quiet_scoped = 0, }; /* clang-format off */ FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) { /* clang-format on */ .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + .quiet_scoped = 0, }; /* clang-format off */ FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) { /* clang-format on */ .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, + .quiet_scoped = 0, }; /* clang-format off */ FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) { /* clang-format on */ .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, signal_quieted) { + /* clang-format on */ + .restrict_flags = 0, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, }; FIXTURE_SETUP(audit_flags) @@ -674,12 +686,16 @@ TEST_F(audit_flags, signal) pid_t child; struct audit_records records; __u64 deallocated_dom = 2; + bool expect_audit = !(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) && + !(variant->quiet_scoped & LANDLOCK_SCOPE_SIGNAL); child = fork(); ASSERT_LE(0, child); if (child == 0) { const struct landlock_ruleset_attr ruleset_attr = { .scoped = LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = variant->quiet_scoped, }; int ruleset_fd; @@ -696,8 +712,7 @@ TEST_F(audit_flags, signal) EXPECT_EQ(-1, kill(getppid(), 0)); EXPECT_EQ(EPERM, errno); - if (variant->restrict_flags & - LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + if (!expect_audit) { EXPECT_EQ(-EAGAIN, matches_log_signal( _metadata, self->audit_fd, getppid(), self->domain_id)); @@ -724,12 +739,12 @@ TEST_F(audit_flags, signal) /* Makes sure there is no superfluous logged records. */ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); - if (variant->restrict_flags & - LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + if (!expect_audit) { EXPECT_EQ(0, records.access); } else { EXPECT_EQ(1, records.access); } + EXPECT_EQ(0, records.domain); /* Updates filter rules to match the drop record. */ set_cap(_metadata, CAP_AUDIT_CONTROL); @@ -748,8 +763,7 @@ TEST_F(audit_flags, signal) WEXITSTATUS(status) != EXIT_SUCCESS) _metadata->exit_code = KSFT_FAIL; - if (variant->restrict_flags & - LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + if (!expect_audit) { /* * No deallocation record: denials=0 never matches a real * record. @@ -849,10 +863,8 @@ FIXTURE_SETUP(audit_exec) FIXTURE_TEARDOWN(audit_exec) { set_cap(_metadata, CAP_AUDIT_CONTROL); - EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, - AUDIT_DEL_RULE)); + EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter)); clear_cap(_metadata, CAP_AUDIT_CONTROL); - EXPECT_EQ(0, close(self->audit_fd)); } TEST_F(audit_exec, signal_and_open) @@ -917,6 +929,7 @@ TEST_F(audit_exec, signal_and_open) /* Tests that there was no denial until now. */ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); /* * Wait for the child to do a first denied action by layer1 and diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 30d37234086c..cbd3c1669951 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -76,8 +76,8 @@ TEST(abi_version) const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, }; - ASSERT_EQ(9, landlock_create_ruleset(NULL, 0, - LANDLOCK_CREATE_RULESET_VERSION)); + ASSERT_EQ(10, landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION)); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, LANDLOCK_CREATE_RULESET_VERSION)); @@ -201,7 +201,7 @@ TEST(add_rule_checks_ordering) ASSERT_LE(0, ruleset_fd); /* Checks invalid flags. */ - ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 1)); + ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100)); ASSERT_EQ(EINVAL, errno); /* Checks invalid ruleset FD. */ @@ -526,4 +526,120 @@ TEST(cred_transfer) EXPECT_EQ(EACCES, errno); } +TEST(useless_quiet_rule_fs) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = 0, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, + }; + int ruleset_fd, root_fd; + + drop_caps(_metadata); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + root_fd = open("/", O_PATH | O_CLOEXEC); + ASSERT_LE(0, root_fd); + path_beneath_attr.parent_fd = root_fd; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, + LANDLOCK_ADD_RULE_QUIET)); + ASSERT_EQ(EINVAL, errno); + + /* Check that the rule had not been added. */ + ASSERT_EQ(0, close(root_fd)); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + ASSERT_EQ(EACCES, errno); +} + +TEST(useless_quiet_rule_net) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = 0, + }; + struct landlock_net_port_attr net_port_attr = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = 1024, + }; + int ruleset_fd; + + drop_caps(_metadata); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(-1, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &net_port_attr, LANDLOCK_ADD_RULE_QUIET)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST(invalid_quiet_bits_1) +{ + const struct landlock_ruleset_attr ruleset_attr_fs = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + const struct landlock_ruleset_attr ruleset_attr_net = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + const struct landlock_ruleset_attr ruleset_attr_scoped = { + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, + }; + + /* Quiet bit set but not part of the handled mask. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs, + sizeof(ruleset_attr_fs), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net, + sizeof(ruleset_attr_net), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped, + sizeof(ruleset_attr_scoped), 0)); + ASSERT_EQ(EINVAL, errno); +} + +TEST(invalid_quiet_bits_2) +{ + const struct landlock_ruleset_attr ruleset_attr_fs = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = 1ULL << 63, + }; + const struct landlock_ruleset_attr ruleset_attr_net = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = 1ULL << 63, + }; + const struct landlock_ruleset_attr ruleset_attr_scoped = { + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = 1ULL << 63, + }; + + /* Quiet bit outside of the valid access range. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs, + sizeof(ruleset_attr_fs), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net, + sizeof(ruleset_attr_net), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped, + sizeof(ruleset_attr_scoped), 0)); + ASSERT_EQ(EINVAL, errno); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index 90551650299c..7206d5105d66 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -25,6 +25,8 @@ /* TEST_F_FORK() should not be used for new tests. */ #define TEST_F_FORK(fixture_name, test_name) TEST_F(fixture_name, test_name) +#define LANDLOCK_MAX_NUM_LAYERS 16 + static const char bin_sandbox_and_launch[] = "./sandbox-and-launch"; static const char bin_wait_pipe[] = "./wait-pipe"; static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox"; diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index cdb47fc1fc0a..86e08aa6e0a7 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -720,7 +720,7 @@ TEST_F_FORK(layout1, rule_with_unhandled_access) static void add_path_beneath(struct __test_metadata *const _metadata, const int ruleset_fd, const __u64 allowed_access, - const char *const path) + const char *const path, __u32 flags) { struct landlock_path_beneath_attr path_beneath = { .allowed_access = allowed_access, @@ -733,7 +733,7 @@ static void add_path_beneath(struct __test_metadata *const _metadata, strerror(errno)); } ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, - &path_beneath, 0)) + &path_beneath, flags)) { TH_LOG("Failed to update the ruleset with \"%s\": %s", path, strerror(errno)); @@ -780,7 +780,7 @@ static int create_ruleset(struct __test_metadata *const _metadata, continue; add_path_beneath(_metadata, ruleset_fd, rules[i].access, - rules[i].path); + rules[i].path, 0); } return ruleset_fd; } @@ -1310,7 +1310,7 @@ TEST_F_FORK(layout1, inherit_subset) * ANDed with the previous ones. */ add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, - dir_s1d2); + dir_s1d2, 0); /* * According to ruleset_fd, dir_s1d2 should now have the * LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE @@ -1342,7 +1342,7 @@ TEST_F_FORK(layout1, inherit_subset) * Try to get more privileges by adding new access rights to the parent * directory: dir_s1d1. */ - add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1); + add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0); enforce_ruleset(_metadata, ruleset_fd); /* Same tests and results as above. */ @@ -1365,7 +1365,7 @@ TEST_F_FORK(layout1, inherit_subset) * that there was no rule tied to it before. */ add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, - dir_s1d3); + dir_s1d3, 0); enforce_ruleset(_metadata, ruleset_fd); ASSERT_EQ(0, close(ruleset_fd)); @@ -1417,7 +1417,7 @@ TEST_F_FORK(layout1, inherit_superset) add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR, - dir_s1d2); + dir_s1d2, 0); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); @@ -1441,7 +1441,7 @@ TEST_F_FORK(layout0, max_layers) }; const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); - for (i = 0; i < 16; i++) + for (i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++) enforce_ruleset(_metadata, ruleset_fd); for (i = 0; i < 2; i++) { @@ -3970,7 +3970,7 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd, unsigned int cmd) { char buf[128]; /* sufficiently large */ - int res, stdinbak_fd; + int res, stdinbak_fd, err; /* * Depending on the IOCTL command, parts of the zeroed-out buffer might @@ -3985,13 +3985,14 @@ static int ioctl_error(struct __test_metadata *const _metadata, int fd, /* Invokes the IOCTL with a zeroed-out buffer. */ bzero(&buf, sizeof(buf)); res = ioctl(fd, cmd, &buf); + err = errno; /* Restores the old FD 0 and closes the backup FD. */ ASSERT_EQ(0, dup2(stdinbak_fd, 0)); ASSERT_EQ(0, close(stdinbak_fd)); if (res < 0) - return errno; + return err; return 0; } @@ -4789,6 +4790,7 @@ FIXTURE(layout1_bind) {}; static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; +static const char bind_file2_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f2"; /* Move targets for disconnected path tests. */ static const char dir_s4d1[] = TMP_DIR "/s4d1"; @@ -7764,4 +7766,2427 @@ TEST_F(audit_layout1, mount) EXPECT_EQ(1, records.domain); } +static bool debug_quiet_tests; + +FIXTURE(audit_quiet_layout1) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_quiet_layout1) +{ + prepare_layout(_metadata); + create_layout1(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + if (getenv("DEBUG_QUIET_TESTS")) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT(audit_quiet_layout1) +{ + remove_layout1(_metadata); + cleanup_layout(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(-1, NULL)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +struct a_rule { + const char *path; + __u64 access; + bool quiet; +}; + +struct a_layer { + __u64 handled_access_fs; + __u64 quiet_access_fs; + struct a_rule rules[6]; + __u64 restrict_flags; +}; + +struct a_target { + /* File/dir to try open. */ + const char *target; + /* Open mode (one of O_RDONLY, O_WRONLY, or O_RDWR). */ + int open_mode; + /* Should open succeed? */ + bool expect_open_success; + /* If open fails, whether to expect an audit log for read. */ + bool audit_read_blocked; + /* If open fails, whether to expect an audit log for write. */ + bool audit_write_blocked; + /* If ftruncate() is expected to be allowed. */ + bool expect_truncate_success; + /* If ftruncate fails, whether to expect an audit log. */ + bool audit_truncate; + /* + * If ioctl() is expected to be allowed (ioctl not attempted if neither + * this nor expect_ioctl_denied is set). + */ + bool expect_ioctl_allowed; + /* If ioctl() is expected to be denied. */ + bool expect_ioctl_denied; + /* If ioctl fails, whether to expect an audit log. */ + bool audit_ioctl; +}; + +#define AUDIT_QUIET_MAX_TARGETS 10 + +FIXTURE_VARIANT(audit_quiet_layout1) +{ + struct a_layer layers[3]; + struct a_target targets[AUDIT_QUIET_MAX_TARGETS]; +}; + +#define FS_R LANDLOCK_ACCESS_FS_READ_FILE +#define FS_W LANDLOCK_ACCESS_FS_WRITE_FILE +#define FS_TRUNC LANDLOCK_ACCESS_FS_TRUNCATE +#define FS_IOCTL LANDLOCK_ACCESS_FS_IOCTL_DEV + +static int sprint_access_bits(char *buf, size_t buflen, __u64 access) +{ + size_t offset = 0; + + if (buflen < strlen("rwti make_reg remove_file refer") + 1) + abort(); + + buf[0] = '\0'; + if (access & FS_R) + offset += snprintf(buf + offset, buflen - offset, "r"); + if (access & FS_W) + offset += snprintf(buf + offset, buflen - offset, "w"); + if (access & FS_TRUNC) + offset += snprintf(buf + offset, buflen - offset, "t"); + if (access & FS_IOCTL) + offset += snprintf(buf + offset, buflen - offset, "i"); + if (access & LANDLOCK_ACCESS_FS_MAKE_REG) + offset += snprintf(buf + offset, buflen - offset, ",make_reg"); + if (access & LANDLOCK_ACCESS_FS_REMOVE_FILE) + offset += + snprintf(buf + offset, buflen - offset, ",remove_file"); + if (access & LANDLOCK_ACCESS_FS_REFER) + offset += snprintf(buf + offset, buflen - offset, ",refer"); + + if (buf[0] == ',') { + offset--; + memmove(buf, buf + 1, offset); + buf[offset] = '\0'; + } + + return offset; +} + +static int apply_a_layer(struct __test_metadata *const _metadata, + const struct a_layer *l) +{ + struct landlock_ruleset_attr rs_attr = { + .handled_access_fs = l->handled_access_fs, + .quiet_access_fs = l->quiet_access_fs, + }; + int rs_fd; + int i; + const struct a_rule *r; + char handled_access_s[33], quiet_access_s[33], rule_access_s[33]; + + if (!l->handled_access_fs) + return 0; + + rs_fd = landlock_create_ruleset(&rs_attr, sizeof(rs_attr), 0); + ASSERT_LE(0, rs_fd); + + for (i = 0; i < ARRAY_SIZE(l->rules); i++) { + r = &l->rules[i]; + if (!r->path) + continue; + + add_path_beneath(_metadata, rs_fd, r->access, r->path, + r->quiet ? LANDLOCK_ADD_RULE_QUIET : 0); + } + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(rs_fd, l->restrict_flags)) + { + TH_LOG("Failed to enforce ruleset: %s", strerror(errno)); + } + ASSERT_EQ(0, close(rs_fd)); + + if (debug_quiet_tests) { + sprint_access_bits(handled_access_s, sizeof(handled_access_s), + l->handled_access_fs); + sprint_access_bits(quiet_access_s, sizeof(quiet_access_s), + l->quiet_access_fs); + TH_LOG("applied layer: handled=%s quiet=%s restrict_flags=0x%llx", + handled_access_s, quiet_access_s, + (unsigned long long)l->restrict_flags); + for (i = 0; i < ARRAY_SIZE(l->rules); i++) { + r = &l->rules[i]; + if (!r->path) + continue; + + sprint_access_bits(rule_access_s, sizeof(rule_access_s), + r->access); + TH_LOG(" rule[%d]: path=%s access=%s quiet=%d", i, + r->path, rule_access_s, r->quiet); + } + } + return 0; +} + +void audit_quiet_layout1_test_body(struct __test_metadata *const _metadata, + FIXTURE_DATA(audit_quiet_layout1) * self, + const struct a_target *targets) +{ + struct audit_records records = {}; + int i; + const struct a_target *target; + int fd = -1; + int open_mode; + int ret; + bool expect_audit; + const char *blocker; + + for (i = 0; i < AUDIT_QUIET_MAX_TARGETS; i++) { + target = &targets[i]; + if (!target->target) + continue; + + open_mode = target->open_mode & (O_RDONLY | O_WRONLY | O_RDWR); + + EXPECT_TRUE(open_mode == O_RDONLY || open_mode == O_WRONLY || + open_mode == O_RDWR); + + if (target->expect_open_success) { + EXPECT_FALSE(target->audit_read_blocked); + EXPECT_FALSE(target->audit_write_blocked); + } + if (target->expect_truncate_success) + EXPECT_TRUE(target->expect_open_success && + !target->audit_truncate); + + if (debug_quiet_tests) + TH_LOG("Try open \"%s\" with %s%s", target->target, + open_mode != O_WRONLY ? "r" : "", + open_mode != O_RDONLY ? "w" : ""); + + fd = openat(AT_FDCWD, target->target, open_mode | O_CLOEXEC); + if (target->expect_open_success) { + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open \"%s\": %s", + target->target, strerror(errno)); + }; + } else { + ASSERT_EQ(-1, fd); + ASSERT_EQ(EACCES, errno); + } + + expect_audit = true; + + if (target->audit_read_blocked && target->audit_write_blocked) + blocker = "fs\\.write_file,fs\\.read_file"; + else if (target->audit_read_blocked) + blocker = "fs\\.read_file"; + else if (target->audit_write_blocked) + blocker = "fs\\.write_file"; + else + expect_audit = false; + + if (expect_audit) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + blocker, target->target)); + + /* Check that we see no (other) logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); + + if (target->expect_open_success && fd >= 0) { + if (debug_quiet_tests) + TH_LOG("Try ftruncate \"%s\"", target->target); + + ret = ftruncate(fd, 0); + if (target->expect_truncate_success) { + ASSERT_EQ(0, ret); + } else { + ASSERT_EQ(-1, ret); + if (open_mode != O_RDONLY) + ASSERT_EQ(EACCES, errno); + } + + if (target->audit_truncate) + ASSERT_EQ(0, matches_log_fs(_metadata, + self->audit_fd, + "fs\\.truncate", + target->target)); + + if (target->expect_ioctl_allowed || + target->expect_ioctl_denied) { + if (debug_quiet_tests) + TH_LOG("Try ioctl FIONREAD on \"%s\"", + target->target); + + ret = ioctl_error(_metadata, fd, FIONREAD); + if (target->expect_ioctl_allowed) { + ASSERT_NE(EACCES, ret); + } else { + ASSERT_EQ(EACCES, ret); + } + } + + if (target->audit_ioctl) + ASSERT_EQ(0, matches_log_fs_extra( + _metadata, self->audit_fd, + "fs\\.ioctl_dev", + target->target, + " ioctlcmd=0x541b\\+")); + + /* Check that we see no other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, + &records)); + ASSERT_EQ(0, records.access); + ASSERT_EQ(0, close(fd)); + } + } +} + +TEST_F(audit_quiet_layout1, base) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_simple) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* + * Quiet flag only takes effect if all blocked access bits are + * quieted, otherwise audit log emitted as normal (with all + * blockers) + */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_read) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_W, + .rules = { + { .path = dir_s1d1, .access = FS_R, .quiet = true }, + /* Quiet flags inherit down and are not overridden */ + { .path = file1_s1d1, .access = FS_R, .quiet = false }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read ok */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .expect_open_success = true, + }, + /* Write quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + /* Read allowed, write quieted so no audit */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d2, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_WRONLY, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Some access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_write) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + }, + }, + }, + .targets = { + /* Read quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Truncate not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Write allowed, read quieted so no audit */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_write_quiet_trunc) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_W, .quiet = false }, + }, + }, + }, + .targets = { + /* Read not allowed and not quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Truncate quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + /* Not covered by quiet (truncate) */ + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet (read/write) */ + { + .target = file1_s3d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_rw_quiet_trunc) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_R | FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .audit_truncate = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_all) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d1, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + { .path = dir_s3d1, .access = FS_W, .quiet = false }, + { .path = "/dev/zero", .access = FS_R, .quiet = false }, + { .path = "/dev/null", .access = FS_R, .quiet = true }, + }, + }, + }, + .targets = { + /* No logs */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Truncate quieted - no log */ + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + /* Truncate not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Ioctl quieted */ + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + /* Ioctl not quieted */ + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_across_mountpoint) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s3d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s3d3, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s3d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_all_quiet) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_allowed = true, + }, + }, +}; + +/* + * With LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, it doesn't matter what the + * quiet flags below the layer say. + */ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, subdomains_off) { + .layers = { + { + .handled_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, + .rules = { + { .path = "/", .access = FS_R, .quiet = false }, + } + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +/* + * With LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, it doesn't matter what the + * quiet flags on the layer say. + */ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, same_exec_off) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_1) { + /* Here, rules that deny access are always quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = true, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_2) { + /* Here, rules that deny access are never quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = false + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = false + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = false + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_3) { + /* This time only the second layer quiets things. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_quiet_access) { + /* Here, rules that deny access are always quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = true, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_1) { + /* Quiet from layer 1 */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = FS_R, + .quiet = true, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = false, + }, + /* Nothing for file2_s1d1 */ + { + .path = file1_s1d2, + .access = FS_W, + .quiet = false, + }, + /* Nothing for file2_s1d2 */ + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Missing both, youngest layer denies write, not quiet */ + { + .target = file2_s1d1, + .open_mode = O_RDWR, + .audit_write_blocked = true, + }, + /* Missing read, denied and quieted by layer 1 */ + { + .target = file1_s1d2, + .open_mode = O_RDWR, + }, + /* Missing write, denied and not quieted by layer 2 */ + { + .target = file2_s1d2, + .open_mode = O_RDWR, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_2) { + /* Quiet from layer 2 */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = false, + }, + /* Nothing for file2_s1d1 and file1_s1d2 */ + { + .path = file2_s1d2, + .access = FS_R, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = 0, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Missing both, youngest layer denies write, quiet */ + { + .target = file2_s1d1, + .open_mode = O_RDWR, + }, + /* Missing read, denied and not quieted by layer 1 */ + { + .target = file1_s1d2, + .open_mode = O_RDWR, + .audit_read_blocked = true, + }, + /* Missing write, denied and quieted by layer 2 */ + { + .target = file2_s1d2, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_3) { + /* Quiet from both layers */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = FS_R, + .quiet = true, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = 0, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + { + .target = file2_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s1d2, + .open_mode = O_RDWR, + }, + { + .target = file2_s1d2, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, without_quiet_then_with_quiet) { + .layers = { + { + .handled_access_fs = FS_R | FS_W, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = false }, + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Write ok */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Write ok, read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + }, +}; + +/* + * The following TEST_F extend the above test cases to test more layers, with + * the inserted layers having varying configurations. + */ + +/* Extra allow all layers, quiet or not, does not change any behaviour. */ +TEST_F(audit_quiet_layout1, allow_all_layer) +{ + struct a_layer allow_all_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = 0, + .rules = { + { + .path = "/", + .access = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet = false, + }, + }, + }; + int i; + + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); + + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); + + /* + * SELF_LOG flags or quiet bits from inner allowing layers should not + * affect behaviour. + */ + allow_all_layer.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL; + allow_all_layer.rules[0].quiet = true; + /* + * Note: this only works because we're not checking counts of domain + * alloc/dealloc logs + */ + allow_all_layer.restrict_flags = + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF; + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +/* + * Add useless outer layers until we reach the layer limit. Should not change + * anything. + */ +TEST_F(audit_quiet_layout1, many_outer_layers) +{ + struct a_layer useless_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC, + .rules = { + { .path = "/", .access = FS_R | FS_W | FS_TRUNC, .quiet = true }, + }, + }; + int i; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) { + if (variant->layers[i].handled_access_fs == 0) + break; + } + + for (; i < LANDLOCK_MAX_NUM_LAYERS; i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &useless_layer)); + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +/* An inner layer that denies and quiets everything should result in no logs. */ +TEST_F(audit_quiet_layout1, deny_all_quiet_layer) +{ + struct a_layer deny_all_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { .path = "/", .access = 0, .quiet = true }, + }, + }; + int i; + FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {}; + + /* Any open should fail with no logs. */ + for (i = 0; i < ARRAY_SIZE(variant->targets); i++) { + const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target){ + .target = target->target, + .open_mode = target->open_mode, + /* We denied everything, open should always fail. */ + .expect_open_success = false, + }; + } + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant_2.targets); +} + +/* + * An inner layer that denies everything without quiet should produce logs for + * all access. + */ +TEST_F(audit_quiet_layout1, deny_all_layer) +{ + struct a_layer deny_all_layer = { + .handled_access_fs = FS_R | FS_W, + .quiet_access_fs = FS_R | FS_W, + }; + int i; + FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {}; + bool test_has_subdomains_off = false; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) { + if (variant->layers[i].restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF) { + test_has_subdomains_off = true; + break; + } + } + + for (i = 0; i < ARRAY_SIZE(variant->targets); i++) { + const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target){ + .target = target->target, + .open_mode = target->open_mode, + + /* We denied everything, open should always fail. */ + .expect_open_success = false, + /* Audit should always happen as long as open request contains read. */ + .audit_read_blocked = !test_has_subdomains_off && + target->open_mode != O_WRONLY, + /* Audit should always happen as long as open request contains write. */ + .audit_write_blocked = !test_has_subdomains_off && + target->open_mode != O_RDONLY, + }; + } + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant_2.targets); +} + +/* Uses layout1_bind hierarchy */ +FIXTURE(audit_quiet_rename) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_quiet_rename) +{ + prepare_layout(_metadata); + create_layout1(_metadata); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(dir_s1d2, dir_s2d2, NULL, MS_BIND, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + if (getenv("DEBUG_QUIET_TESTS")) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT(audit_quiet_rename) +{ + remove_layout1(_metadata); + cleanup_layout(_metadata); + + /* umount(dir_s2d2)) is handled by namespace lifetime. */ + + remove_path(file1_s4d1); + remove_path(file2_s4d1); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(-1, NULL)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +static void simple_quiet_rename(struct __test_metadata *const _metadata, + FIXTURE_DATA(audit_quiet_rename) *const self, + __u64 handled_access, __u64 quiet_access, + bool source_allow, bool dest_allow, + bool source_quiet, bool dest_quiet, + const char *source_blockers, + const char *dest_blockers) +{ + /* We will move file1_s1d1 to file1_s2d1 */ + struct a_layer layer = { + .handled_access_fs = handled_access, + .quiet_access_fs = quiet_access, + .rules = { + { + .path = dir_s1d1, + .access = source_allow ? handled_access : 0, + .quiet = source_quiet, + }, + { + .path = dir_s2d1, + .access = dest_allow ? handled_access : 0, + .quiet = dest_quiet, + }, + }, + }; + struct audit_records records = {}; + int ret, err; + + /* Skip landlock_add_rule for useless rules. */ + if (!source_allow && !source_quiet) + layer.rules[0].path = NULL; + if (!dest_allow && !dest_quiet) + layer.rules[1].path = NULL; + + EXPECT_EQ(0, unlink(file1_s2d1)); + EXPECT_EQ(0, apply_a_layer(_metadata, &layer)); + + if (debug_quiet_tests) + TH_LOG("Try renameat \"%s\" to \"%s\"", file1_s1d1, file1_s2d1); + ret = renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1); + err = errno; + if (ret != 0 && debug_quiet_tests) { + TH_LOG("renameat error: %s", err == EXDEV ? "EXDEV" : + err == EACCES ? "EACCES" : + strerror(err)); + } + if (source_allow && dest_allow) { + ASSERT_EQ(0, ret); + } else { + ASSERT_EQ(-1, ret); + if (handled_access & (LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE)) { + ASSERT_EQ(EACCES, err); + } else { + ASSERT_EQ(EXDEV, err); + } + + if (source_blockers) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + source_blockers, dir_s1d1)); + if (dest_blockers) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + dest_blockers, dir_s2d1)); + } + /* + * No other logs. records.domain not checked per reasoning in + * audit_quiet_layout1_test_body. + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, rename_ok) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, true, true, false, + false, NULL, NULL); +} + +TEST_F(audit_quiet_rename, no_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, + false, false, "fs\\.remove_file,fs\\.refer", + "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, true, + true, NULL, NULL); +} + +TEST_F(audit_quiet_rename, source_no_quiet_dest_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, + false, true, "fs\\.remove_file,fs\\.refer", NULL); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_no_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, true, + false, NULL, "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, only_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, LANDLOCK_ACCESS_FS_REFER, + false, false, true, true, + "fs\\.remove_file,fs\\.refer", + "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, source_allow_dest_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, true, false, false, + true, NULL, NULL); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_allow) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, true, true, + false, NULL, NULL); +} + +TEST_F(audit_quiet_rename, handle_all_deny_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + /* No logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_not_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = 0, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_refer_quiet_source_not_quiet_dest) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_same_dir) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + /* We didn't unlink destination file */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg,fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored_same_dir) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, + matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg", dir_s1d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet1) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2. Refer is quieted but we are + * also missing remove_file on source. + */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet2) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = LANDLOCK_ACCESS_FS_REFER, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REFER, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2, but refer is quieted (and that + * layer does not handle any other accesses). + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet3) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2, in which everything is quieted. + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, + first_layer_quiet_deny_all_second_layer_not_quiet_deny_all) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = {}, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s2d1)); + /* No other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, + first_layer_quiet_deny_all_second_layer_dest_not_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* Source is quieted but destination is not. */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s2d1)); + /* No other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, rename_xchg) +{ + struct a_layer layer = { + .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG, + .rules = { { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet = false, + } }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD, + bind_file2_s1d3)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_ignored) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD, + bind_file2_s1d3)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg", + bind_dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount_disconnected) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + int bind_s1d3_fd; + + EXPECT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY); + ASSERT_GE(bind_s1d3_fd, 0); + + /* Make s1d3 disconnected. */ + create_directory(_metadata, dir_s4d1); + ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_disconnected) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s4d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + int bind_s1d3_fd; + + EXPECT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY); + ASSERT_GE(bind_s1d3_fd, 0); + + /* Make s1d3 disconnected. */ + create_directory(_metadata, dir_s4d1); + ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c index 4c528154ea92..2ed1f76b7a8b 100644 --- a/tools/testing/selftests/landlock/net_test.c +++ b/tools/testing/selftests/landlock/net_test.c @@ -35,6 +35,7 @@ enum sandbox_type { NO_SANDBOX, /* This may be used to test rules that allow *and* deny accesses. */ TCP_SANDBOX, + UDP_SANDBOX, }; static int set_service(struct service_fixture *const srv, @@ -93,23 +94,53 @@ static bool prot_is_tcp(const struct protocol_variant *const prot) (prot->protocol == IPPROTO_TCP || prot->protocol == IPPROTO_IP); } +static bool prot_is_udp(const struct protocol_variant *const prot) +{ + return (prot->domain == AF_INET || prot->domain == AF_INET6) && + prot->type == SOCK_DGRAM && + (prot->protocol == IPPROTO_UDP || prot->protocol == IPPROTO_IP); +} + static bool is_restricted(const struct protocol_variant *const prot, const enum sandbox_type sandbox) { if (sandbox == TCP_SANDBOX) return prot_is_tcp(prot); + else if (sandbox == UDP_SANDBOX) + return prot_is_udp(prot); return false; } static int socket_variant(const struct service_fixture *const srv) { + /* Arbitrary value just to not block other tests indefinitely. */ + const struct timeval timeout = { + .tv_sec = 0, + .tv_usec = 100000, + }; + int sockfd; int ret; - ret = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC, - srv->protocol.protocol); - if (ret < 0) + sockfd = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC, + srv->protocol.protocol); + if (sockfd < 0) return -errno; - return ret; + + ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, + sizeof(timeout)); + if (ret != 0) { + ret = -errno; + close(sockfd); + return ret; + } + ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, + sizeof(timeout)); + if (ret != 0) { + ret = -errno; + close(sockfd); + return ret; + } + return sockfd; } #ifndef SIN6_LEN_RFC2133 @@ -258,9 +289,163 @@ static int connect_variant(const int sock_fd, return connect_variant_addrlen(sock_fd, srv, get_addrlen(srv, false)); } +static int sendto_variant_addrlen(const int sock_fd, + const struct service_fixture *const srv, + const socklen_t addrlen, void *buf, + size_t len, size_t flags) +{ + const struct sockaddr *dst = NULL; + ssize_t ret; + + /* + * We never want our processes to be killed by SIGPIPE: we check return + * codes and errno, so that we have actual error messages. + */ + flags |= MSG_NOSIGNAL; + + if (srv != NULL) { + switch (srv->protocol.domain) { + case AF_UNSPEC: + case AF_INET: + dst = (const struct sockaddr *)&srv->ipv4_addr; + break; + + case AF_INET6: + dst = (const struct sockaddr *)&srv->ipv6_addr; + break; + + case AF_UNIX: + dst = (const struct sockaddr *)&srv->unix_addr; + break; + + default: + errno = EAFNOSUPPORT; + return -errno; + } + } + + ret = sendto(sock_fd, buf, len, flags, dst, addrlen); + if (ret < 0) + return -errno; + + /* errno is not set in cases of partial writes. */ + if (ret != len) + return -EINTR; + + return 0; +} + +static int sendto_variant(const int sock_fd, + const struct service_fixture *const srv, void *buf, + size_t len, size_t flags) +{ + socklen_t addrlen = 0; + + if (srv != NULL) + addrlen = get_addrlen(srv, false); + + return sendto_variant_addrlen(sock_fd, srv, addrlen, buf, len, flags); +} + +static int test_sendmsg(struct __test_metadata *const _metadata, + const struct protocol_variant *prot, int client_fd, + int server_fd, const struct service_fixture *srv, + bool bind_denied, bool send_denied) +{ + int ret; + socklen_t opt_len; + int sock_type; + int addr_family; + struct sockaddr_storage peer_addr = { 0 }; + bool has_remote_port; + bool needs_autobind; + char read_buf[1] = { 0 }; + + /* + * Prepare the test by inspecting the socket type and whether it has a + * local/remote address set (all of which determine the expected + * outcomes). + */ + opt_len = sizeof(sock_type); + ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_TYPE, &sock_type, + &opt_len)); + opt_len = sizeof(addr_family); + ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_DOMAIN, &addr_family, + &opt_len)); + opt_len = sizeof(peer_addr); + has_remote_port = (getpeername(client_fd, (struct sockaddr *)&peer_addr, + &opt_len) == 0); + needs_autobind = (addr_family == AF_INET || addr_family == AF_INET6) && + get_binded_port(client_fd, prot) == 0; + + /* First, check error code with truncated explicit address. */ + if (srv != NULL) { + ret = sendto_variant_addrlen( + client_fd, srv, get_addrlen(srv, true) - 1, "A", 1, 0); + if (sock_type == SOCK_STREAM && !has_remote_port) { + EXPECT_EQ(-EPIPE, ret) + { + return -1; + } + } else if (bind_denied && needs_autobind) { + EXPECT_EQ(-EACCES, ret) + { + return -1; + } + } else { + EXPECT_EQ(-EINVAL, ret) + { + return -1; + } + } + } + + /* With or without explicit destination address (srv can be NULL). */ + ret = sendto_variant(client_fd, srv, "B", 1, 0); + if (sock_type == SOCK_STREAM && !has_remote_port) { + EXPECT_EQ(-EPIPE, ret) + { + return -1; + } + } else if ((send_denied && srv != NULL) || + (bind_denied && needs_autobind)) { + ASSERT_EQ(-EACCES, ret) + { + return -1; + } + } else if (srv == NULL && !has_remote_port) { + if (addr_family == AF_UNIX) { + ASSERT_EQ(-ENOTCONN, ret) + { + return -1; + } + } else if (sock_type == SOCK_STREAM) { + ASSERT_EQ(-EPIPE, ret) + { + return -1; + } + } else { + ASSERT_EQ(-EDESTADDRREQ, ret) + { + return -1; + } + } + } else { + ASSERT_EQ(0, ret); + ASSERT_EQ(1, recv(server_fd, read_buf, 1, 0)); + ASSERT_EQ(read_buf[0], 'B') + { + return -1; + } + } + + return 0; +} + FIXTURE(protocol) { - struct service_fixture srv0, srv1, srv2, unspec_any0, unspec_srv0; + struct service_fixture srv0, srv1, srv2; + struct service_fixture unspec_any0, unspec_srv0, unspec_srv1; }; FIXTURE_VARIANT(protocol) @@ -271,10 +456,9 @@ FIXTURE_VARIANT(protocol) FIXTURE_SETUP(protocol) { - const struct protocol_variant prot_unspec = { - .domain = AF_UNSPEC, - .type = SOCK_STREAM, - }; + struct protocol_variant prot_unspec = variant->prot; + + prot_unspec.domain = AF_UNSPEC; disable_caps(_metadata); @@ -283,6 +467,7 @@ FIXTURE_SETUP(protocol) ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2)); ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0)); + ASSERT_EQ(0, set_service(&self->unspec_srv1, prot_unspec, 1)); ASSERT_EQ(0, set_service(&self->unspec_any0, prot_unspec, 0)); self->unspec_any0.ipv4_addr.sin_addr.s_addr = htonl(INADDR_ANY); @@ -510,6 +695,92 @@ FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_unix_datagram) { }, }; +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp1) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + .protocol = IPPROTO_UDP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp2) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp1) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + .protocol = IPPROTO_UDP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp2) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_stream) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_datagram) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_DGRAM, + }, +}; + static void test_bind_and_connect(struct __test_metadata *const _metadata, const struct service_fixture *const srv, const bool deny_bind, const bool deny_connect) @@ -602,7 +873,7 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata, ret = connect_variant(connect_fd, srv); if (deny_connect) { EXPECT_EQ(-EACCES, ret); - } else if (deny_bind) { + } else if (deny_bind && srv->protocol.type == SOCK_STREAM) { /* No listening server. */ EXPECT_EQ(-ECONNREFUSED, ret); } else { @@ -641,18 +912,25 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata, TEST_F(protocol, bind) { - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_net = bind_access | conn_access, }; - const struct landlock_net_port_attr tcp_bind_connect_p0 = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr bind_connect_p0 = { + .allowed_access = bind_access | conn_access, .port = self->srv0.port, }; - const struct landlock_net_port_attr tcp_connect_p1 = { - .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr connect_p1 = { + .allowed_access = conn_access, .port = self->srv1.port, }; int ruleset_fd; @@ -664,12 +942,26 @@ TEST_F(protocol, bind) /* Allows connect and bind for the first port. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_connect_p0, 0)); + &bind_connect_p0, 0)); /* Allows connect and denies bind for the second port. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_connect_p1, 0)); + &connect_p1, 0)); + + /* + * For UDP sockets, allows binding to ephemeral ports (required + * to connect or send a first datagram) + */ + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_net_port_attr bind_ephemeral = { + .allowed_access = bind_access, + .port = 0, + }; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &bind_ephemeral, 0)); + } enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); @@ -691,18 +983,25 @@ TEST_F(protocol, bind) TEST_F(protocol, connect) { - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_net = bind_access | conn_access, }; - const struct landlock_net_port_attr tcp_bind_connect_p0 = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr bind_connect_p0 = { + .allowed_access = bind_access | conn_access, .port = self->srv0.port, }; - const struct landlock_net_port_attr tcp_bind_p1 = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + const struct landlock_net_port_attr bind_p1 = { + .allowed_access = bind_access, .port = self->srv1.port, }; int ruleset_fd; @@ -714,12 +1013,26 @@ TEST_F(protocol, connect) /* Allows connect and bind for the first port. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_connect_p0, 0)); + &bind_connect_p0, 0)); /* Allows bind and denies connect for the second port. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_p1, 0)); + &bind_p1, 0)); + + /* + * For UDP sockets, allows binding to ephemeral ports (required + * to connect or send a first datagram) + */ + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_net_port_attr bind_ephemeral = { + .allowed_access = bind_access, + .port = 0, + }; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &bind_ephemeral, 0)); + } enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); @@ -737,16 +1050,20 @@ TEST_F(protocol, connect) TEST_F(protocol, bind_unspec) { + const __u64 bind_access = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .handled_access_net = bind_access, }; - const struct landlock_net_port_attr tcp_bind = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + const struct landlock_net_port_attr rule_bind = { + .allowed_access = bind_access, .port = self->srv0.port, }; int bind_fd, ret; - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { const int ruleset_fd = landlock_create_ruleset( &ruleset_attr, sizeof(ruleset_attr), 0); ASSERT_LE(0, ruleset_fd); @@ -754,7 +1071,7 @@ TEST_F(protocol, bind_unspec) /* Allows bind. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind, 0)); + &rule_bind, 0)); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); } @@ -782,7 +1099,8 @@ TEST_F(protocol, bind_unspec) } EXPECT_EQ(0, close(bind_fd)); - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { const int ruleset_fd = landlock_create_ruleset( &ruleset_attr, sizeof(ruleset_attr), 0); ASSERT_LE(0, ruleset_fd); @@ -828,11 +1146,21 @@ TEST_F(protocol, bind_unspec) TEST_F(protocol, connect_unspec) { - const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP, + const __u64 connect_right = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const struct landlock_ruleset_attr ruleset_conn = { + .handled_access_net = connect_right, }; - const struct landlock_net_port_attr tcp_connect = { - .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_ruleset_attr ruleset_conn_bind = { + .handled_access_net = connect_right | bind_right, + }; + const struct landlock_net_port_attr rule_connect = { + .allowed_access = connect_right, .port = self->srv0.port, }; int bind_fd, client_fd, status; @@ -865,15 +1193,16 @@ TEST_F(protocol, connect_unspec) EXPECT_EQ(0, ret); } - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { const int ruleset_fd = landlock_create_ruleset( - &ruleset_attr, sizeof(ruleset_attr), 0); + &ruleset_conn, sizeof(ruleset_conn), 0); ASSERT_LE(0, ruleset_fd); /* Allows connect. */ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_connect, 0)); + &rule_connect, 0)); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); } @@ -896,12 +1225,14 @@ TEST_F(protocol, connect_unspec) EXPECT_EQ(0, ret); } - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { const int ruleset_fd = landlock_create_ruleset( - &ruleset_attr, sizeof(ruleset_attr), 0); + &ruleset_conn_bind, sizeof(ruleset_conn_bind), + 0); ASSERT_LE(0, ruleset_fd); - /* Denies connect. */ + /* Denies connect and bind. */ enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); } @@ -950,6 +1281,441 @@ TEST_F(protocol, connect_unspec) EXPECT_EQ(0, close(bind_fd)); } +TEST_F(protocol, sendmsg_stream) +{ + int srv0_fd, tmp_fd, client_fd, res; + char read_buf[1] = { 0 }; + + /* + * Simple test for stream sockets: just deny all connect()/ + * send(explicit addr)/bind(), and make sure we don't interfere with any + * operation. + */ + if (variant->prot.type != SOCK_STREAM) + return; + + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + ASSERT_EQ(0, listen(srv0_fd, backlog)); + + /* Send on a non-connected socket. */ + res = sendto_variant(client_fd, NULL, "A", 1, 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-ENOTCONN, res); + } else { + EXPECT_EQ(-EPIPE, res); + } + + /* Send to a truncated (invalid) address on a non-connected socket. */ + res = sendto_variant_addrlen(client_fd, &self->srv0, + get_addrlen(&self->srv0, true) - 1, "B", 1, + 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EOPNOTSUPP, res); + } else { + EXPECT_EQ(-EPIPE, res); + } + + /* Connect. */ + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + tmp_fd = accept(srv0_fd, NULL, 0); + ASSERT_LE(0, tmp_fd); + EXPECT_EQ(0, close(srv0_fd)); + srv0_fd = tmp_fd; + + /* Send without an explicit address. */ + EXPECT_EQ(0, sendto_variant(client_fd, NULL, "C", 1, 0)); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'C'); + + /* Send to a truncated (invalid) address. */ + res = sendto_variant_addrlen(client_fd, &self->srv0, + get_addrlen(&self->srv0, true) - 1, "D", 1, + 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EISCONN, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'D'); + } + + /* Send to a valid but different address. */ + res = sendto_variant(client_fd, &self->srv1, "E", 1, 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EISCONN, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'E'); + } + + EXPECT_EQ(0, close(client_fd)); +} + +TEST_F(protocol, sendmsg_dgram) +{ + const bool restricted = is_restricted(&variant->prot, variant->sandbox); + int srv0_fd, srv1_fd, client_fd, child, status, res; + + if (variant->prot.type != SOCK_DGRAM) + return; + + /* Prepare server on port #0 to be allowed. */ + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + + /* And another server on port #1 to be denied. */ + ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1)); + ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1)); + + /* + * Check that sockets connected before restrictions are not impacted in + * any way. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + if (variant->sandbox == UDP_SANDBOX) { + /* Deny all connect()/send(explicit addr)/bind(). */ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + EXPECT_EQ(0, + test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, NULL, restricted, restricted)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + restricted)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv1_fd, &self->srv1, restricted, + restricted)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Restrict connect/send, but not bind(). Then try sending with no + * destination (and no remote peer set), an allowed destination, then a + * denied destination. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const struct landlock_net_port_attr send_p0 = { + .allowed_access = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv0.port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &send_p0, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + -1, NULL, false, false)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, false, false)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv1_fd, &self->srv1, false, + restricted)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Rest of this test is just for autobind enforcement, which only exists + * in IP sockets. + */ + if (variant->prot.domain != AF_INET && variant->prot.domain != AF_INET6) + return; + + /* Restrict bind() to explicit calls with an arbitrary (non-0) port. */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const uint16_t allowed_src_port = 42424; + struct service_fixture allowed_src; + + allowed_src = self->srv0; + set_port(&allowed_src, allowed_src_port); + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .port = allowed_src_port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + + /* Check that implicit bind(0) in sendmsg() is denied. */ + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + false)); + + /* Same thing for autobind in connect(). */ + res = connect_variant(client_fd, &self->srv0); + if (restricted) { + EXPECT_EQ(-EACCES, res); + } else { + EXPECT_EQ(0, res); + } + EXPECT_EQ(0, close(client_fd)); + + /* Make sendmsg() work by explicitly binding to the only allowed port. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, bind_variant(client_fd, &allowed_src)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + false)); + EXPECT_EQ(0, close(client_fd)); + + /* Make connect() work by explicitly binding to the only allowed port. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, bind_variant(client_fd, &allowed_src)); + EXPECT_EQ(0, connect_variant(client_fd, &self->srv0)); + EXPECT_EQ(0, close(client_fd)); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Check that %LANDLOCK_ACCESS_NET_BIND_UDP on port 0 allows implicit + * autobinds. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .port = 0, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, false, false)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + +TEST_F(protocol, sendmsg_unspec) +{ + const bool restricted = is_restricted(&variant->prot, variant->sandbox); + int client_fd, srv0_fd, srv1_fd, res; + char read_buf[1] = { 0 }; + + /* + * We already test for the absence of influence on sendmsg for other + * socket types and other address families, there's no point in adapting + * this test for stream sockets too. + */ + if (variant->prot.type != SOCK_DGRAM) + return; + + /* Prepare client of the right family. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + + /* Prepare server on port #0 to be allowed. */ + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + + /* And another server on port #1 to be denied. */ + ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1)); + ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1)); + + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv0.port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* Explicit AF_UNSPEC address but truncated. */ + EXPECT_EQ(-EINVAL, sendto_variant_addrlen( + client_fd, &self->unspec_srv0, + get_addrlen(&self->unspec_srv0, true) - 1, + "A", 1, 0)); + + /* + * Explicit AF_UNSPEC address, should be treated as AF_INET by IPv4 + * sockets (and thus map to srv0, allowed), but be denied by IPv6 + * sockets. + */ + res = sendto_variant(client_fd, &self->unspec_srv0, "B", 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* Always denied on IPv6 socket. */ + EXPECT_EQ(-EACCES, res); + } else { + /* IPv6 sockets treat AF_UNSPEC as a NULL address. */ + EXPECT_EQ(-EDESTADDRREQ, res); + } + } else if (variant->prot.domain == AF_INET) { + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv0_fd, read_buf, 1)) + { + TH_LOG("read() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'B'); + } else { + /* Unix sockets don't accept AF_UNSPEC. */ + EXPECT_EQ(-EINVAL, res); + } + + /* + * Explicit AF_UNSPEC address, should be treated as AF_INET on IPv4 + * sockets (and thus map to srv1, denied), and be denied on IPv6 sockets + * as always. + */ + res = sendto_variant(client_fd, &self->unspec_srv1, "C", 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* Always denied on IPv6 socket. */ + EXPECT_EQ(-EACCES, res); + } else { + /* IPv6 sockets treat AF_UNSPEC as a NULL address. */ + EXPECT_EQ(-EDESTADDRREQ, res); + } + } else if (variant->prot.domain == AF_INET) { + if (restricted) { + /* Sending to srv1 is not allowed, only srv0. */ + EXPECT_EQ(-EACCES, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv1_fd, read_buf, 1)) + { + TH_LOG("read() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'C'); + } + } else { + /* Unix sockets don't accept AF_UNSPEC. */ + EXPECT_EQ(-EINVAL, res); + } + + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + + /* Minimal explicit AF_UNSPEC address (just the sa_family_t field) */ + res = sendto_variant_addrlen(client_fd, &self->unspec_srv0, + get_addrlen(&self->unspec_srv0, true), "D", + 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* AF_UNSPEC is always denied in IPv6. */ + EXPECT_EQ(-EACCES, res); + } else { + /* + * IPv6 sockets treat AF_UNSPEC as a NULL address, + * falling back to the connected address. + */ + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv0_fd, read_buf, 1)); + EXPECT_EQ(read_buf[0], 'D'); + } + } else { + /* + * IPv4 socket will expect a struct sockaddr_in, our address is + * considered truncated. And Unix sockets don't accept + * AF_UNSPEC at all. + */ + EXPECT_EQ(-EINVAL, res); + } +} + FIXTURE(ipv4) { struct service_fixture srv0, srv1; @@ -976,6 +1742,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_tcp) { }; /* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .type = SOCK_STREAM, +}; + +/* clang-format off */ FIXTURE_VARIANT_ADD(ipv4, no_sandbox_with_udp) { /* clang-format on */ .sandbox = NO_SANDBOX, @@ -989,6 +1762,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_udp) { .type = SOCK_DGRAM, }; +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_udp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .type = SOCK_DGRAM, +}; + FIXTURE_SETUP(ipv4) { const struct protocol_variant prot = { @@ -1012,14 +1792,19 @@ TEST_F(ipv4, from_unix_to_inet) { int unix_stream_fd, unix_dgram_fd; - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_net = access_rights, }; const struct landlock_net_port_attr tcp_bind_connect_p0 = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .allowed_access = access_rights, .port = self->srv0.port, }; int ruleset_fd; @@ -1326,11 +2111,13 @@ FIXTURE_TEARDOWN(mini) /* clang-format off */ -#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_TCP +#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP #define ACCESS_ALL ( \ LANDLOCK_ACCESS_NET_BIND_TCP | \ - LANDLOCK_ACCESS_NET_CONNECT_TCP) + LANDLOCK_ACCESS_NET_CONNECT_TCP | \ + LANDLOCK_ACCESS_NET_BIND_UDP | \ + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) /* clang-format on */ @@ -1678,6 +2465,7 @@ TEST_F(ipv4_tcp, with_fs) FIXTURE(port_specific) { struct service_fixture srv0; + struct service_fixture cli1; }; FIXTURE_VARIANT(port_specific) @@ -1697,7 +2485,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv4) { }; /* clang-format off */ -FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) { +FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv4) { /* clang-format on */ .sandbox = TCP_SANDBOX, .prot = { @@ -1707,6 +2495,16 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) { }; /* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv4) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) { /* clang-format on */ .sandbox = NO_SANDBOX, @@ -1717,7 +2515,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) { }; /* clang-format off */ -FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) { +FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv6) { /* clang-format on */ .sandbox = TCP_SANDBOX, .prot = { @@ -1726,11 +2524,22 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) { }, }; +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv6) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + FIXTURE_SETUP(port_specific) { disable_caps(_metadata); ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + ASSERT_EQ(0, set_service(&self->cli1, variant->prot, 1)); setup_loopback(_metadata); }; @@ -1745,14 +2554,19 @@ TEST_F(port_specific, bind_connect_zero) uint16_t port; /* Adds a rule layer with bind and connect actions. */ - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP + .handled_access_net = access_rights, }; - const struct landlock_net_port_attr tcp_bind_connect_zero = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr bind_connect_zero = { + .allowed_access = access_rights, .port = 0, }; int ruleset_fd; @@ -1764,7 +2578,7 @@ TEST_F(port_specific, bind_connect_zero) /* Checks zero port value on bind and connect actions. */ EXPECT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_connect_zero, 0)); + &bind_connect_zero, 0)); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); @@ -1785,11 +2599,16 @@ TEST_F(port_specific, bind_connect_zero) ret = bind_variant(bind_fd, &self->srv0); EXPECT_EQ(0, ret); - EXPECT_EQ(0, listen(bind_fd, backlog)); + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); /* Connects on port 0. */ ret = connect_variant(connect_fd, &self->srv0); - EXPECT_EQ(-ECONNREFUSED, ret); + if (variant->prot.type == SOCK_STREAM) { + EXPECT_EQ(-ECONNREFUSED, ret); + } else { + EXPECT_EQ(0, ret); + } /* Sets binded port for both protocol families. */ port = get_binded_port(bind_fd, &variant->prot); @@ -1813,23 +2632,35 @@ TEST_F(port_specific, bind_connect_1023) int bind_fd, connect_fd, ret; /* Adds a rule layer with bind and connect actions. */ - if (variant->sandbox == TCP_SANDBOX) { + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + (LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP) : + (LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP + .handled_access_net = access_rights, }; /* A rule with port value less than 1024. */ - const struct landlock_net_port_attr tcp_bind_connect_low_range = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr bind_connect_low_range = { + .allowed_access = access_rights, .port = 1023, }; /* A rule with 1024 port. */ - const struct landlock_net_port_attr tcp_bind_connect = { - .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + const struct landlock_net_port_attr bind_connect = { + .allowed_access = access_rights, .port = 1024, }; + /* A rule with cli1's port, to use as source port. */ + const struct landlock_net_port_attr srcport = { + .allowed_access = bind_right, + .port = self->cli1.port, + }; int ruleset_fd; ruleset_fd = landlock_create_ruleset(&ruleset_attr, @@ -1838,10 +2669,15 @@ TEST_F(port_specific, bind_connect_1023) ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_connect_low_range, 0)); + &bind_connect_low_range, 0)); ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, - &tcp_bind_connect, 0)); + &bind_connect, 0)); + if (variant->sandbox == UDP_SANDBOX) { + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &srcport, 0)); + } enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); @@ -1850,9 +2686,6 @@ TEST_F(port_specific, bind_connect_1023) bind_fd = socket_variant(&self->srv0); ASSERT_LE(0, bind_fd); - connect_fd = socket_variant(&self->srv0); - ASSERT_LE(0, connect_fd); - /* Sets address port to 1023 for both protocol families. */ set_port(&self->srv0, 1023); /* Binds on port 1023. */ @@ -1865,8 +2698,19 @@ TEST_F(port_specific, bind_connect_1023) ret = bind_variant(bind_fd, &self->srv0); clear_cap(_metadata, CAP_NET_BIND_SERVICE); EXPECT_EQ(0, ret); - EXPECT_EQ(0, listen(bind_fd, backlog)); + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + connect_fd = socket_variant(&self->srv0); + ASSERT_LE(0, connect_fd); + if (variant->prot.type == SOCK_DGRAM) { + /* + * We are about to connect(), but bind() is restricted, so for + * UDP sockets we need to use cli1's port as source port (the + * only one we are allowed to use). + */ + EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1)); + } /* Connects on the binded port 1023. */ ret = connect_variant(connect_fd, &self->srv0); EXPECT_EQ(0, ret); @@ -1885,7 +2729,10 @@ TEST_F(port_specific, bind_connect_1023) /* Binds on port 1024. */ ret = bind_variant(bind_fd, &self->srv0); EXPECT_EQ(0, ret); - EXPECT_EQ(0, listen(bind_fd, backlog)); + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + if (variant->prot.type == SOCK_DGRAM) + EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1)); /* Connects on the binded port 1024. */ ret = connect_variant(connect_fd, &self->srv0); @@ -1895,23 +2742,41 @@ TEST_F(port_specific, bind_connect_1023) EXPECT_EQ(0, close(bind_fd)); } -static int matches_log_tcp(const int audit_fd, const char *const blockers, - const char *const dir_addr, const char *const addr, - const char *const dir_port) +/** + * matches_auditlog - Check audit log for a network access denial + * + * @audit_fd: Audit file descriptor. + * @blockers: A regex-escaped blocker string, e.g., "net\.bind_tcp". + * @dir_addr: Either "saddr" or "daddr", ignored if addr is NULL. + * @addr: A regex-escaped IP address string, or NULL. + * @dir_port: Either "src" or "dest", ignored if addr is NULL. + * @port: A port number, ignored if addr is NULL. + */ +static int matches_auditlog(const int audit_fd, const char *const blockers, + const char *const dir_addr, const char *const addr, + const char *const dir_port, const __u16 port) { - static const char log_template[] = REGEX_LANDLOCK_PREFIX - " blockers=%s %s=%s %s=1024$"; + static const char log_with_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX + " blockers=%s %s=%s %s=%u$"; + static const char log_without_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX + " blockers=%s"; /* * Max strlen(blockers): 16 * Max strlen(dir_addr): 5 * Max strlen(addr): 12 * Max strlen(dir_port): 4 + * Max strlen(%u port): 5 */ - char log_match[sizeof(log_template) + 37]; + char log_match[sizeof(log_with_addrport_tmpl) + 42]; int log_match_len; - log_match_len = snprintf(log_match, sizeof(log_match), log_template, - blockers, dir_addr, addr, dir_port); + if (addr == NULL) + log_match_len = snprintf(log_match, sizeof(log_match), + log_without_addrport_tmpl, blockers); + else + log_match_len = snprintf(log_match, sizeof(log_match), + log_with_addrport_tmpl, blockers, + dir_addr, addr, dir_port, port); if (log_match_len > sizeof(log_match)) return -E2BIG; @@ -1922,6 +2787,10 @@ static int matches_log_tcp(const int audit_fd, const char *const blockers, FIXTURE(audit) { struct service_fixture srv0; + struct service_fixture srv1; + /* srv2 has a rule with no access but quiet bit set. */ + struct service_fixture srv2; + struct service_fixture unspec_srv0; struct audit_filter audit_filter; int audit_fd; }; @@ -1933,7 +2802,7 @@ FIXTURE_VARIANT(audit) }; /* clang-format off */ -FIXTURE_VARIANT_ADD(audit, ipv4) { +FIXTURE_VARIANT_ADD(audit, ipv4_tcp) { /* clang-format on */ .addr = "127\\.0\\.0\\.1", .prot = { @@ -1943,7 +2812,17 @@ FIXTURE_VARIANT_ADD(audit, ipv4) { }; /* clang-format off */ -FIXTURE_VARIANT_ADD(audit, ipv6) { +FIXTURE_VARIANT_ADD(audit, ipv4_udp) { + /* clang-format on */ + .addr = "127\\.0\\.0\\.1", + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv6_tcp) { /* clang-format on */ .addr = "::1", .prot = { @@ -1952,9 +2831,27 @@ FIXTURE_VARIANT_ADD(audit, ipv6) { }, }; +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv6_udp) { + /* clang-format on */ + .addr = "::1", + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + FIXTURE_SETUP(audit) { + struct protocol_variant prot_unspec = variant->prot; + + prot_unspec.domain = AF_UNSPEC; + ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1)); + ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2)); + ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0)); + setup_loopback(_metadata); set_cap(_metadata, CAP_AUDIT_CONTROL); @@ -1972,9 +2869,22 @@ FIXTURE_TEARDOWN(audit) TEST_F(audit, bind) { + const char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.bind_tcp" : + "net\\.bind_udp"); + const __u64 access_rights = + (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_net = access_rights, + .quiet_access_net = access_rights, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, }; struct audit_records records; int ruleset_fd, sock_fd; @@ -1982,27 +2892,58 @@ TEST_F(audit, bind) ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); sock_fd = socket_variant(&self->srv0); ASSERT_LE(0, sock_fd); EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0)); - EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr", - variant->addr, "src")); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "saddr", + variant->addr, "src", self->srv0.port)); EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); EXPECT_EQ(0, records.access); EXPECT_EQ(1, records.domain); EXPECT_EQ(0, close(sock_fd)); + + /* Bind to srv2 (with quiet rule): no new audit logs. */ + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv2)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(0, close(sock_fd)); } TEST_F(audit, connect) { + const char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const __u64 bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const __u64 access_rights = bind_right | conn_right; const struct landlock_ruleset_attr ruleset_attr = { - .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | - LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_net = access_rights, + .quiet_access_net = access_rights, + }; + const struct landlock_net_port_attr rule_connect_p1 = { + .allowed_access = conn_right, + .port = self->srv1.port, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, }; struct audit_records records; int ruleset_fd, sock_fd; @@ -2010,14 +2951,179 @@ TEST_F(audit, connect) ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule_connect_p1, 0)); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); enforce_ruleset(_metadata, ruleset_fd); EXPECT_EQ(0, close(ruleset_fd)); sock_fd = socket_variant(&self->srv0); ASSERT_LE(0, sock_fd); EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv0)); - EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.connect_tcp", - "daddr", variant->addr, "dest")); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr", + variant->addr, "dest", self->srv0.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + if (variant->prot.type == SOCK_DGRAM) { + /* Check that autobind generates a denied bind event. */ + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1)); + + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", + NULL, NULL, NULL, 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + } + + EXPECT_EQ(0, close(sock_fd)); + + /* Connect to srv2 (with quiet rule): no new audit logs. */ + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + +/* Quieting bind access has no effect on connect. */ +TEST_F(audit, connect_quiet_bind) +{ + const char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const int bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const int conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const int access_rights = bind_right | conn_right; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + .quiet_access_net = bind_right, + }; + const struct landlock_ruleset_attr ruleset_attr_2 = { + .handled_access_net = access_rights, + .quiet_access_net = conn_right, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, + }; + struct audit_records records; + int ruleset_fd, sock_fd; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr", + variant->addr, "dest", self->srv2.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(sock_fd)); + + /* New layer that also denies connect but has the correct quiet bit. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr_2, + sizeof(ruleset_attr_2), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + + /* Quieted - no logs expected. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(sock_fd)); +} + +static int matches_log_connect_bound(int audit_fd, const char *const blockers, + const char *const addr, __u16 lport, + __u16 dport) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=%s laddr=%s lport=%u daddr=%s dest=%u$"; + /* Slack for the blockers, two addresses and two port numbers. */ + char log_match[sizeof(log_template) + 60]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template, + blockers, addr, lport, addr, dport); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +/* + * After a bind() to an allowed port, a denied connect must report laddr/lport + * from the bound socket (made available through audit_net.sk) in addition to + * the connect sockaddr's daddr/dest. + */ +TEST_F(audit, connect_bound) +{ + const __u64 bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const char *const audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = bind_right | conn_right, + }; + const struct landlock_net_port_attr rule_bind = { + .allowed_access = bind_right, + .port = self->srv0.port, + }; + struct service_fixture srv_remote; + struct audit_records records; + int ruleset_fd, sock_fd; + + /* Uses a second port as the denied connect target. */ + ASSERT_EQ(0, set_service(&srv_remote, variant->prot, 1)); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule_bind, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv0); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(0, bind_variant(sock_fd, &self->srv0)); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &srv_remote)); + EXPECT_EQ(0, matches_log_connect_bound(self->audit_fd, audit_evt, + variant->addr, self->srv0.port, + srv_remote.port)); EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); EXPECT_EQ(0, records.access); @@ -2026,4 +3132,60 @@ TEST_F(audit, connect) EXPECT_EQ(0, close(sock_fd)); } +TEST_F(audit, sendmsg) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP | + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv1.port, + }; + struct audit_records records; + int ruleset_fd; + int sock_fd; + + /* Sendmsg on stream sockets is never denied. */ + if (variant->prot.type != SOCK_DGRAM) + return; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv0); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv0, "A", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp", + "daddr", variant->addr, "dest", + self->srv0.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + /* Check that autobind generates a denied bind event. */ + EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv1, "A", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", NULL, + NULL, NULL, 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(-EACCES, + sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp", + "daddr", NULL, "dest", 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c index 1b6c8b53bf33..4f64c90583cd 100644 --- a/tools/testing/selftests/landlock/ptrace_test.c +++ b/tools/testing/selftests/landlock/ptrace_test.c @@ -342,6 +342,7 @@ TEST_F(audit, trace) /* Makes sure there is no superfluous logged records. */ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); yama_ptrace_scope = get_yama_ptrace_scope(); ASSERT_LE(0, yama_ptrace_scope); diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c index c47491d2d1c1..40fc82fbf01d 100644 --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -293,6 +293,45 @@ FIXTURE_TEARDOWN_PARENT(scoped_audit) EXPECT_EQ(0, audit_cleanup(-1, NULL)); } +FIXTURE_VARIANT(scoped_audit) +{ + const __u64 scoped; + const __u64 quiet_scoped; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, no_quiet) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket_2) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | + LANDLOCK_SCOPE_SIGNAL, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_unrelated) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, +}; + /* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */ #define ABSTRACT_SOCKET_PATH_PREFIX \ "0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D" @@ -308,10 +347,18 @@ TEST_F(scoped_audit, connect_to_child) char buf; int dgram_client; struct audit_records records; + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = variant->scoped, + .quiet_scoped = variant->quiet_scoped, + }; + bool should_audit = + !(variant->quiet_scoped & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); /* Makes sure there is no superfluous logged records. */ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); @@ -344,7 +391,14 @@ TEST_F(scoped_audit, connect_to_child) EXPECT_EQ(0, close(pipe_child[1])); EXPECT_EQ(0, close(pipe_parent[0])); - create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd) + { + TH_LOG("Failed to create a ruleset: %s", strerror(errno)); + } + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); /* Signals that the parent is in a domain, if any. */ ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); @@ -359,14 +413,20 @@ TEST_F(scoped_audit, connect_to_child) EXPECT_EQ(-1, err_dgram); EXPECT_EQ(EPERM, errno); - EXPECT_EQ( - 0, - audit_match_record( - self->audit_fd, AUDIT_LANDLOCK_ACCESS, - REGEX_LANDLOCK_PREFIX - " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX - "[0-9A-F]\\+$", - NULL)); + if (should_audit) { + EXPECT_EQ( + 0, + audit_match_record( + self->audit_fd, AUDIT_LANDLOCK_ACCESS, + REGEX_LANDLOCK_PREFIX + " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX + "[0-9A-F]\\+$", + NULL)); + } + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); EXPECT_EQ(0, close(dgram_client)); diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c index d8bf33417619..f24f2c28f62e 100644 --- a/tools/testing/selftests/landlock/scoped_signal_test.c +++ b/tools/testing/selftests/landlock/scoped_signal_test.c @@ -559,4 +559,186 @@ TEST_F(fown, sigurg_socket) _metadata->exit_code = KSFT_FAIL; } +/* + * Checks that LANDLOCK_SCOPE_SIGNAL is enforced on the asynchronous SIGIO + * delivery path (fcntl(F_SETOWN)) when the file owner is a process group. + * + * A sandboxed process sitting at the head of its process group's PID hlist (the + * default position right after fork()) used to escape the fcntl(F_SETOWN, + * -pgrp) domain recording: pid_task(pgrp, PIDTYPE_PGID) resolved to the process + * itself, so the same-thread-group exemption skipped recording its Landlock + * domain. At SIGIO time that domain was then unset and the signal fanned out + * to every group member, including non-sandboxed processes outside the domain. + */ +TEST(sigio_to_pgid_members) +{ + int trigger[2], sync_child[2]; + char buf; + pid_t child; + int status, i; + + drop_caps(_metadata); + + /* + * Isolates the test in its own process group so the SIGIO fan-out stays + * bounded to this parent and the child forked below. + */ + ASSERT_EQ(0, setpgid(0, 0)); + + /* The non-sandboxed parent is the protected (out-of-domain) target. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + /* + * The child inherits the parent's new process group and, just + * attached with hlist_add_head_rcu(), is now the head of the + * pgid hlist: this is the case that used to skip the recording. + */ + EXPECT_EQ(0, close(sync_child[0])); + + /* In-domain positive control: the child must be signaled. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + /* Owns the SIGIO source for the whole process group. */ + ASSERT_EQ(0, fcntl(trigger[0], F_SETSIG, SIGURG)); + ASSERT_EQ(0, fcntl(trigger[0], F_SETOWN, -getpgrp())); + ASSERT_EQ(0, fcntl(trigger[0], F_SETFL, O_ASYNC)); + + /* Fans SIGURG out to every member of the process group. */ + ASSERT_EQ(1, write(trigger[1], ".", 1)); + + /* + * The sandboxed child is in its own domain and must always be + * signaled: this proves the SIGIO actually fired. + */ + for (i = 0; i < 1000 && !signal_received; i++) + usleep(1000); + EXPECT_EQ(1, signal_received); + + ASSERT_EQ(1, write(sync_child[1], ".", 1)); + EXPECT_EQ(0, close(sync_child[1])); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(sync_child[1])); + EXPECT_EQ(0, close(trigger[0])); + EXPECT_EQ(0, close(trigger[1])); + + /* Waits for the child to generate the SIGIO. */ + ASSERT_EQ(1, read(sync_child[0], &buf, 1)); + EXPECT_EQ(0, close(sync_child[0])); + + /* Lets a delivered-but-pending signal run our handler, if any. */ + for (i = 0; i < 100 && !signal_received; i++) + usleep(1000); + + /* + * SCOPE_SIGNAL must block the fan-out to this non-sandboxed parent, + * which is outside the child's Landlock domain. Before the fix the + * parent was signaled here. + */ + EXPECT_EQ(0, signal_received); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +static void *thread_setown_scoped(void *arg) +{ + const int fd = *(int *)arg; + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + + /* Sandboxes only this non-leader thread (no thread syncing). */ + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) + return (void *)THREAD_ERROR; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || + landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + return (void *)THREAD_ERROR; + } + close(ruleset_fd); + + /* Makes this process group own the SIGIO source. */ + if (fcntl(fd, F_SETSIG, SIGURG) || fcntl(fd, F_SETOWN, -getpgrp()) || + fcntl(fd, F_SETFL, O_ASYNC)) + return (void *)THREAD_ERROR; + + return (void *)THREAD_SUCCESS; +} + +/* + * Checks that the SIGIO fan-out is still delivered to the file owner's own + * process when fcntl(F_SETOWN, -pgrp) was issued from a sandboxed non-leader + * thread. + * + * The Landlock domain is recorded for a process-group owner (so out-of-domain + * members stay blocked, see sigio_to_pgid_members), but the kernel signals a + * process group through its members' thread-group leaders. Here the leader is + * not sandboxed and thus has a different domain than the registering thread, so + * the registration-time check cannot tell that it belongs to the owner's own + * process. hook_file_send_sigiotask() must recognize it through the recorded + * thread group and allow the delivery, matching the same-process guarantee of + * commit 18eb75f3af40. Without that exemption the leader is wrongly denied and + * never signaled. + */ +TEST(sigio_to_pgid_self) +{ + int trigger[2]; + pthread_t thread; + enum thread_return ret = THREAD_INVALID; + int i; + + drop_caps(_metadata); + + /* Bounds the SIGIO fan-out to this process. */ + ASSERT_EQ(0, setpgid(0, 0)); + + /* The non-sandboxed thread-group leader is the SIGIO target. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); + + /* + * Registers the process-group fowner from a sibling thread that + * sandboxes only itself, so its domain differs from the leader's. + */ + ASSERT_EQ(0, pthread_create(&thread, NULL, thread_setown_scoped, + &trigger[0])); + ASSERT_EQ(0, pthread_join(thread, (void **)&ret)); + ASSERT_EQ(THREAD_SUCCESS, ret); + + /* Fans SIGURG out to the process group. */ + ASSERT_EQ(1, write(trigger[1], ".", 1)); + + for (i = 0; i < 1000 && !signal_received; i++) + usleep(1000); + + /* + * Same-process delivery must always be allowed, even though the owner + * was registered from a sandboxed sibling thread. + */ + EXPECT_EQ(1, signal_received); + + EXPECT_EQ(0, close(trigger[0])); + EXPECT_EQ(0, close(trigger[1])); +} + TEST_HARNESS_MAIN |
