diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-09-09 11:00:35 -0700 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-09-09 11:00:35 -0700 |
| commit | 50d05c7c76c96b90462f24debacca971d2e86713 (patch) | |
| tree | 25602b0e57ea0a59ae40f5f55a5e73bd99b29646 | |
| parent | 5e1287972b649aab54a894addeaf1fdd6bc23e6b (diff) | |
| parent | d41d0021a6ea3e9fcd14126a00fead47f981c46e (diff) | |
Pull Landlock fixes from Mickaël Salaün:
"This fixes a use-after-free and a lockdep assert NULL dereferencing,
and properly truncates too-long strings printed by a Landlock
tracepoint. Most of the changes are brought by new tests"
* tag 'landlock-7.3-rc3' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux:
landlock: Test trace path output boundaries
landlock: Bound escaped trace path output
landlock: Clean up ruleset validation checks
selftests/landlock: Test abstract socket trace name limits
landlock: Fix use-after-free of the source's parent directory
| -rw-r--r-- | include/trace/events/landlock.h | 70 | ||||
| -rw-r--r-- | security/landlock/.kunitconfig | 2 | ||||
| -rw-r--r-- | security/landlock/domain.c | 3 | ||||
| -rw-r--r-- | security/landlock/fs.c | 18 | ||||
| -rw-r--r-- | security/landlock/ruleset.c | 2 | ||||
| -rw-r--r-- | security/landlock/trace.c | 182 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/scoped_abstract_unix_test.c | 76 | ||||
| -rw-r--r-- | tools/testing/selftests/landlock/trace_fs_test.c | 160 |
8 files changed, 466 insertions, 47 deletions
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index f82588f6f90e..d05253afaf59 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -28,6 +28,16 @@ struct task_struct; #ifdef CREATE_TRACE_POINTS +/* About 6 KiB, leaving about 2 KiB for sibling helpers and fixed fields. */ +#define TRACE_UNTRUSTED_STR_OUTPUT_SIZE \ + (TRACE_SEQ_BUFFER_SIZE - TRACE_SEQ_BUFFER_SIZE / 4) + +/* + * A raw UTF-8 ellipsis (…) marks truncation and cannot collide with escaped + * input: ESCAPE_NAP renders every non-ASCII input byte in octal. + */ +#define TRACE_TRUNCATION_MARKER "\xe2\x80\xa6" + /* * Escapes @len bytes of an untrusted string into the trace sequence @p so it * cannot inject field separators or control characters into the ftrace text @@ -37,33 +47,59 @@ struct task_struct; * NUL-terminated or carries embedded NUL bytes (an abstract socket name) is * escaped in full instead of being truncated at the first NUL. * - * Return: a pointer into @p's buffer, or NULL if @src is NULL or the buffer is - * exhausted (normal when the trace buffer is full). + * Strings that exceed the output limit retain the largest complete escaped + * prefix followed by the truncation marker. + * + * Return: a pointer into @p's buffer, or NULL if @src is NULL or the fixed + * output reservation is unavailable. */ static inline const char * __trace_print_untrusted_str(struct trace_seq *p, const char *src, size_t len) { + const unsigned int escape_flags = ESCAPE_SPACE | ESCAPE_SPECIAL | + ESCAPE_NAP | ESCAPE_APPEND | + ESCAPE_OCTAL; + const size_t marker_len = sizeof(TRACE_TRUNCATION_MARKER) - 1; + size_t buf_size, prefix_len, prefix_size; int escaped_size; char *buf; - size_t buf_size = seq_buf_get_buf(&p->seq, &buf); - const char *ret = trace_seq_buffer_ptr(p); + const char *ret; - /* Buffer exhaustion is normal when the trace buffer is full. */ - if (!src || buf_size == 0) + buf_size = seq_buf_get_buf(&p->seq, &buf); + if (!src || buf_size < TRACE_UNTRUSTED_STR_OUTPUT_SIZE) return NULL; - escaped_size = - string_escape_mem(src, len, buf, buf_size, - ESCAPE_SPACE | ESCAPE_SPECIAL | ESCAPE_NAP | - ESCAPE_APPEND | ESCAPE_OCTAL, - " ='\"\\"); - if (unlikely(escaped_size >= buf_size)) { - /* We need some room for the final '\0'. */ - seq_buf_set_overflow(&p->seq); - p->full = 1; - return NULL; + ret = trace_seq_buffer_ptr(p); + escaped_size = string_escape_mem(src, len, buf, + TRACE_UNTRUSTED_STR_OUTPUT_SIZE, + escape_flags, " ='\"\\"); + if (likely(escaped_size < TRACE_UNTRUSTED_STR_OUTPUT_SIZE)) { + seq_buf_commit(&p->seq, escaped_size); + trace_seq_putc(p, 0); + return ret; } - seq_buf_commit(&p->seq, escaped_size); + + prefix_len = 0; + prefix_size = 0; + while (prefix_len < len) { + const char *const src_char = src + prefix_len; + int char_size; + + char_size = string_escape_mem(src_char, 1, NULL, 0, + escape_flags, " ='\"\\"); + if (char_size > TRACE_UNTRUSTED_STR_OUTPUT_SIZE - marker_len - + 1 - prefix_size) + break; + prefix_size += char_size; + prefix_len++; + } + + escaped_size = string_escape_mem(src, prefix_len, buf, prefix_size, + escape_flags, " ='\"\\"); + if (WARN_ON_ONCE(escaped_size != prefix_size)) + return NULL; + memcpy(buf + prefix_size, TRACE_TRUNCATION_MARKER, marker_len); + seq_buf_commit(&p->seq, prefix_size + marker_len); trace_seq_putc(p, 0); return ret; } diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig index f9423f01ac5b..fe36228d37ea 100644 --- a/security/landlock/.kunitconfig +++ b/security/landlock/.kunitconfig @@ -1,6 +1,8 @@ CONFIG_AUDIT=y +CONFIG_FTRACE=y CONFIG_KUNIT=y CONFIG_NET=y +CONFIG_SCHED_TRACER=y CONFIG_SECURITY=y CONFIG_SECURITY_LANDLOCK=y CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 93c7104fd6b2..4031b581be07 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -439,10 +439,11 @@ landlock_merge_ruleset(struct landlock_domain *const parent, int err; might_sleep(); - lockdep_assert_held(&ruleset->lock); if (WARN_ON_ONCE(!ruleset)) return ERR_PTR(-EINVAL); + lockdep_assert_held(&ruleset->lock); + if (parent) { if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS) return ERR_PTR(-E2BIG); diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 30aa6ce13590..330a1871bf94 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1298,11 +1298,12 @@ static int current_check_refer_path(struct dentry *const old_dentry, /* * old_dentry may be the root of the common mount point and * !IS_ROOT(old_dentry) at the same time (e.g. with open_tree() and - * OPEN_TREE_CLONE). We do not need to call dget(old_parent) because - * we keep a reference to old_dentry. + * OPEN_TREE_CLONE). Pin the dentry used as old_parent in either case. + * Otherwise, dget_parent() safely fetches and pins the current parent + * against a concurrent rename(2). */ - old_parent = (old_dentry == mnt_dir.dentry) ? old_dentry : - old_dentry->d_parent; + old_parent = (old_dentry == mnt_dir.dentry) ? dget(old_dentry) : + dget_parent(old_dentry); /* new_dir->dentry is equal to new_dentry->d_parent */ allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry, @@ -1311,8 +1312,10 @@ static int current_check_refer_path(struct dentry *const old_dentry, allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry, new_dir->dentry, &layer_masks_parent2); - if (allow_parent1 && allow_parent2) + if (allow_parent1 && allow_parent2) { + dput(old_parent); return 0; + } /* * To be able to compare source and destination domain access rights, @@ -1324,8 +1327,10 @@ static int current_check_refer_path(struct dentry *const old_dentry, subject->domain, &mnt_dir, access_request_parent1, &layer_masks_parent1, &request1, old_dentry, access_request_parent2, &layer_masks_parent2, &request2, - exchange ? new_dentry : NULL)) + exchange ? new_dentry : NULL)) { + dput(old_parent); return 0; + } if (request1.access) { request1.audit.u.path.dentry = old_parent; @@ -1335,6 +1340,7 @@ static int current_check_refer_path(struct dentry *const old_dentry, request2.audit.u.path.dentry = new_dir->dentry; landlock_log_denial(subject, &request2); } + dput(old_parent); /* * This prioritizes EACCES over EXDEV for all actions, including diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index 0d07707523cd..a5d135d085cb 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -58,7 +58,7 @@ landlock_create_ruleset(const access_mask_t fs_access_mask, new_ruleset->id = landlock_get_id_range(1); #endif /* CONFIG_TRACEPOINTS */ - /* Should already be checked in landlock_create_ruleset(). */ + /* The caller must only pass supported access rights and scopes. */ if (fs_access_mask) { const access_mask_t mask = fs_access_mask & LANDLOCK_MASK_ACCESS_FS; diff --git a/security/landlock/trace.c b/security/landlock/trace.c index 2ea7aac8d75d..8c21e5de6f0d 100644 --- a/security/landlock/trace.c +++ b/security/landlock/trace.c @@ -6,6 +6,7 @@ * Copyright © 2026 Cloudflare, Inc. */ +#include <kunit/test.h> #include <linux/cleanup.h> #include <linux/dcache.h> #include <linux/err.h> @@ -183,3 +184,184 @@ void landlock_trace_denial( break; } } + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_trace_seq_init(struct trace_seq *const seq, const size_t size) +{ + memset(seq, 0, sizeof(*seq)); + seq_buf_init(&seq->seq, seq->buffer, size); +} + +static void test_untrusted_str_data(struct kunit *const test) +{ + const char binary[] = { 'a', '\0', '<' }; + static const char ellipsis[] = "\xe2\x80\xa6"; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, "<too_long>", 10); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "<too_long>"); + + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, binary, sizeof(binary)); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "a\\000<"); + + /* Input ellipsis bytes are escaped and cannot mimic the raw marker. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, ellipsis, + sizeof(ellipsis) - 1); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "\\342\\200\\246"); +} + +static void test_untrusted_str_boundaries(struct kunit *const test) +{ + static const char escaped_space[] = "\\040"; + const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE; + const size_t marker_len = sizeof(TRACE_TRUNCATION_MARKER) - 1; + const size_t escape_len = sizeof(escaped_space) - 1; + const size_t exact_prefix_len = + output_size - marker_len - 1 - escape_len; + const size_t short_prefix_len = exact_prefix_len + 1; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const input = kunit_kmalloc(test, output_size + 1, GFP_KERNEL); + char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, input); + KUNIT_ASSERT_NOT_NULL(test, expected); + + /* The escaped string and its trailing NUL exactly fit the limit. */ + memset(input, 'a', output_size - 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, output_size - 1); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_EQ(test, seq->seq.len, output_size); + KUNIT_EXPECT_EQ(test, memcmp(output, input, output_size - 1), 0); + + /* Stop before a four-byte escape when only three bytes remain. */ + memset(input, 'a', short_prefix_len); + input[short_prefix_len] = ' '; + memset(input + short_prefix_len + 1, 'b', 5); + memset(expected, 'a', short_prefix_len); + memcpy(expected + short_prefix_len, TRACE_TRUNCATION_MARKER, + marker_len + 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, short_prefix_len + 6); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, expected); + + /* Include a four-byte escape that exactly fills the prefix capacity. */ + memset(input, 'a', exact_prefix_len); + input[exact_prefix_len] = ' '; + memset(input + exact_prefix_len + 1, 'b', marker_len + 1); + memset(expected, 'a', exact_prefix_len); + memcpy(expected + exact_prefix_len, escaped_space, escape_len); + memcpy(expected + exact_prefix_len + escape_len, + TRACE_TRUNCATION_MARKER, marker_len + 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, + exact_prefix_len + marker_len + 2); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, expected); + + /* Literal backslashes remain escaped in complete output. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, "/\\000", 5); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "/\\\\000"); +} + +static void test_untrusted_str_cursor(struct kunit *const test) +{ + const size_t padding_len = + TRACE_SEQ_BUFFER_SIZE - TRACE_UNTRUSTED_STR_OUTPUT_SIZE + 1; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const padding = kunit_kzalloc(test, padding_len, GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, padding); + + /* Accept available space exactly equal to the fixed reservation. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + trace_seq_putmem(seq, padding, padding_len - 1); + output = __trace_print_untrusted_str(seq, "/a", 2); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "/a"); + KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len - 1 + sizeof("/a")); + + /* Reject one byte less without changing the scratch cursor. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + trace_seq_putmem(seq, padding, padding_len); + output = __trace_print_untrusted_str(seq, "/a", 2); + KUNIT_EXPECT_NULL(test, output); + KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len); +} + +static void test_untrusted_str_composition(struct kunit *const test) +{ + static const struct trace_print_flags flags[] = { + { .mask = 1, .name = "read" }, + }; + const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE; + const size_t prefix_len = output_size - sizeof(TRACE_TRUNCATION_MARKER); + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL); + char *const path = kunit_kmalloc(test, output_size, GFP_KERNEL); + const char *flags_output, *path_output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, expected); + KUNIT_ASSERT_NOT_NULL(test, path); + memset(path, 'a', output_size); + memset(expected, 'a', prefix_len); + memcpy(expected + prefix_len, TRACE_TRUNCATION_MARKER, + sizeof(TRACE_TRUNCATION_MARKER)); + + /* Exercise both legal TP_printk() sibling evaluation orders. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + path_output = __trace_print_untrusted_str(seq, path, output_size); + flags_output = + trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags)); + KUNIT_ASSERT_NOT_NULL(test, path_output); + KUNIT_EXPECT_STREQ(test, path_output, expected); + KUNIT_EXPECT_STREQ(test, flags_output, "read"); + + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + flags_output = + trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags)); + path_output = __trace_print_untrusted_str(seq, path, output_size); + KUNIT_ASSERT_NOT_NULL(test, path_output); + KUNIT_EXPECT_STREQ(test, path_output, expected); + KUNIT_EXPECT_STREQ(test, flags_output, "read"); +} + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_untrusted_str_data), + KUNIT_CASE(test_untrusted_str_boundaries), + KUNIT_CASE(test_untrusted_str_cursor), + KUNIT_CASE(test_untrusted_str_composition), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_trace", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c index 6dbe863ea571..5dc0debacb2a 100644 --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -1222,7 +1222,7 @@ FIXTURE_SETUP(trace_unix) int ret; set_cap(_metadata, CAP_SYS_ADMIN); - ASSERT_EQ(0, unshare(CLONE_NEWNS)); + ASSERT_EQ(0, unshare(CLONE_NEWNS | CLONE_NEWNET)); ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); ret = tracefs_fixture_setup(); @@ -1252,6 +1252,11 @@ FIXTURE_TEARDOWN(trace_unix) clear_cap(_metadata, CAP_SYS_ADMIN); } +static const char + trace_unix_max_name[sizeof(((struct sockaddr_un *)0)->sun_path)] = { + [0 ... sizeof(trace_unix_max_name) - 2] = 'x', + }; + /* clang-format off */ FIXTURE_VARIANT(trace_unix) { /* clang-format on */ @@ -1259,6 +1264,8 @@ FIXTURE_VARIANT(trace_unix) { bool sandbox; bool sandbox_target; /* Peer owned by a domain: peer_domain != 0. */ int expect_denied; + const char *name; /* NULL generates a PID-based binary name. */ + size_t name_len; }; /* clang-format off */ @@ -1281,6 +1288,26 @@ FIXTURE_VARIANT_ADD(trace_unix, stream_allowed) { .sandbox_target = false, .expect_denied = 0, }; +/* Stream: lower abstract-name length boundary. */ +FIXTURE_VARIANT_ADD(trace_unix, stream_denied_empty_name) { + .sock_type = SOCK_STREAM, + .sandbox = true, + .sandbox_target = false, + .expect_denied = 1, + .name = "", + .name_len = 0, +}; + +/* Stream: upper abstract-name length boundary. */ +FIXTURE_VARIANT_ADD(trace_unix, stream_denied_max_name) { + .sock_type = SOCK_STREAM, + .sandbox = true, + .sandbox_target = false, + .expect_denied = 1, + .name = trace_unix_max_name, + .name_len = sizeof(trace_unix_max_name) - 1, +}; + /* Datagram: sandboxed client sendto() an unsandboxed peer (peer_domain=0). */ FIXTURE_VARIANT_ADD(trace_unix, dgram_denied) { .sock_type = SOCK_DGRAM, .sandbox = true, @@ -1304,12 +1331,11 @@ FIXTURE_VARIANT_ADD(trace_unix, dgram_allowed) { /* * A sandboxed thread reaching an abstract unix socket peer through connect(2) * (stream) or sendto(2) (datagram) is denied and emits - * landlock_deny_scope_abstract_unix_socket. The abstract name is crafted with - * a space and an embedded NUL followed by an "END" marker to check the - * tracepoint escaping and its length handling (a raw space would break the - * sun_path field regex; strlen() would truncate at the NUL and drop "END"). - * peer_pid is only meaningful for a stream peer (a datagram peer has no - * SO_PEERCRED), so it is asserted only there. + * landlock_deny_scope_abstract_unix_socket. The default abstract name has a + * space and an embedded NUL followed by an "END" marker to check escaping and + * binary length handling. Additional stream variants cover the minimum and + * maximum abstract-name lengths. peer_pid is only meaningful for a stream peer + * (a datagram peer has no SO_PEERCRED), so it is asserted only there. */ TEST_F(trace_unix, deny_scope_unix) { @@ -1336,12 +1362,19 @@ TEST_F(trace_unix, deny_scope_unix) ASSERT_LE(0, server_fd); addr.sun_path[0] = '\0'; - name_len = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, - "landlock_trace_test_%d ", getpid()); - addr.sun_path[1 + name_len] = '\0'; - memcpy(addr.sun_path + 1 + name_len + 1, "END", 3); - addr_len = - offsetof(struct sockaddr_un, sun_path) + 1 + name_len + 1 + 3; + if (variant->name) { + ASSERT_LE(variant->name_len, sizeof(addr.sun_path) - 1); + memcpy(addr.sun_path + 1, variant->name, variant->name_len); + name_len = variant->name_len; + } else { + name_len = snprintf(addr.sun_path + 1, + sizeof(addr.sun_path) - 1, + "landlock_trace_test_%d ", getpid()); + addr.sun_path[1 + name_len] = '\0'; + memcpy(addr.sun_path + 1 + name_len + 1, "END", 3); + name_len += 1 + 3; + } + addr_len = offsetof(struct sockaddr_un, sun_path) + 1 + name_len; ASSERT_EQ(0, bind(server_fd, (struct sockaddr *)&addr, addr_len)); if (variant->sock_type == SOCK_STREAM) @@ -1430,19 +1463,18 @@ TEST_F(trace_unix, deny_scope_unix) count, buf); } - /* - * sun_path is escaped: a raw space would break this field's [^ ]*$ - * regex, so a successful extract proves the space was escaped, and its - * full length is honored: the "END" marker after the embedded NUL must - * survive (strlen() would truncate it at the NUL). - */ ASSERT_EQ(0, tracefs_extract_field( buf, REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK), "sun_path", field, sizeof(field))); - EXPECT_NE(NULL, strstr(field, "END")) - { - TH_LOG("sun_path truncated or unescaped: %s", field); + if (variant->name) { + EXPECT_STREQ(variant->name, field); + } else { + /* An embedded NUL must not truncate the following marker. */ + EXPECT_NE(NULL, strstr(field, "END")) + { + TH_LOG("sun_path truncated or unescaped: %s", field); + } } /* peer_pid is the parent's PID for a stream peer (0 for datagram). */ diff --git a/tools/testing/selftests/landlock/trace_fs_test.c b/tools/testing/selftests/landlock/trace_fs_test.c index 5220f6a4bee1..4543a25c1f55 100644 --- a/tools/testing/selftests/landlock/trace_fs_test.c +++ b/tools/testing/selftests/landlock/trace_fs_test.c @@ -6,8 +6,10 @@ */ #define _GNU_SOURCE +#include <assert.h> #include <errno.h> #include <fcntl.h> +#include <limits.h> #include <linux/landlock.h> #include <sched.h> #include <stdio.h> @@ -23,6 +25,63 @@ #define TRACE_TASK "trace_fs_test" +/* Mirrors TRACE_SEQ_SIZE, conservatively larger than the usable buffer. */ +#define TRACE_SEQUENCE_SIZE 8192 +#define OCTAL_ESCAPE_LEN 4 +#define LONG_PATH_COMPONENT_COUNT 11 +#define LONG_PATH_COMPONENT_LEN 240 +#define LONG_PATH_LEN \ + (LONG_PATH_COMPONENT_COUNT * (LONG_PATH_COMPONENT_LEN + 1) + \ + sizeof("/tmp")) +#define LONG_ESCAPED_PATH_LEN \ + (LONG_PATH_COMPONENT_COUNT * LONG_PATH_COMPONENT_LEN * OCTAL_ESCAPE_LEN) + +static_assert(LONG_ESCAPED_PATH_LEN > TRACE_SEQUENCE_SIZE, + "escaped path must exceed the trace sequence"); +static_assert(LONG_PATH_LEN < PATH_MAX, "path must fit in PATH_MAX"); + +static void create_long_path(struct __test_metadata *const _metadata, + char *path) +{ + size_t path_len; + + strcpy(path, "/tmp"); + path_len = strlen(path); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount("tmpfs", "/tmp", "tmpfs", 0, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + for (int i = 0; i < LONG_PATH_COMPONENT_COUNT; i++) { + path[path_len++] = '/'; + memset(path + path_len, ' ', LONG_PATH_COMPONENT_LEN); + path_len += LONG_PATH_COMPONENT_LEN; + path[path_len] = '\0'; + ASSERT_EQ(0, mkdir(path, 0700)); + } +} + +static void expect_truncated_path(struct __test_metadata *const _metadata, + const char *const trace, + const char *const event_regex) +{ + static const char marker[] = "\xe2\x80\xa6"; + char *path; + size_t path_len; + + path = malloc(TRACE_SEQUENCE_SIZE); + ASSERT_NE(NULL, path); + ASSERT_EQ(0, tracefs_extract_field(trace, event_regex, "path", path, + TRACE_SEQUENCE_SIZE)); + EXPECT_EQ(path, strstr(path, "/tmp/")); + EXPECT_NE(NULL, strstr(path, "\\040")); + + path_len = strlen(path); + ASSERT_LE(sizeof(marker) - 1, path_len); + EXPECT_STREQ(marker, path + path_len - (sizeof(marker) - 1)); + free(path); +} + /* * Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value * ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged @@ -184,6 +243,107 @@ TEST_F(trace_fs, add_rule_fs) } /* + * Verifies that a path whose escaping exceeds the trace scratch sequence does + * not corrupt a sibling symbolic field. + */ +TEST_F(trace_fs, add_rule_fs_escaped_path_overflow) +{ + static const char access_prefix[] = "execute|write_file|read_file|"; + static const char access_suffix[] = "|ioctl_dev|resolve_unix"; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, + }; + char path[PATH_MAX]; + char *buf, field_buf[256]; + size_t field_len; + int ruleset_fd, count; + + create_long_path(_metadata, path); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + path_beneath.parent_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf); + } + + /* + * The marker catches a full revert with any compiler. The symbolic + * field also catches scratch-sequence poisoning when the compiler + * evaluates the overflowing path first, as GCC currently does. + */ + ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK), + "access_rights", field_buf, + sizeof(field_buf))); + EXPECT_EQ(0, + strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1)); + EXPECT_EQ(NULL, strstr(field_buf, "|refer|")); + field_len = strlen(field_buf); + ASSERT_LE(sizeof(access_suffix) - 1, field_len); + EXPECT_STREQ(access_suffix, + field_buf + field_len - (sizeof(access_suffix) - 1)); + expect_truncated_path(_metadata, buf, REGEX_ADD_RULE_FS(TRACE_TASK)); + + free(buf); +} + +/* + * Verifies that an overflowing denied path does not corrupt its sibling + * symbolic blockers field. + */ +TEST_F(trace_fs, deny_access_fs_escaped_path_overflow) +{ + char path[PATH_MAX]; + char *buf, field_buf[64]; + int count, err; + + create_long_path(_metadata, path); + ASSERT_EQ(0, tracefs_clear_buf()); + + sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_READ_DIR, path); + + buf = tracefs_read_buf(); + ASSERT_NE(NULL, buf); + + count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + EXPECT_EQ(1, count) + { + TH_LOG("Expected 1 deny_access_fs event, got %d\n%s", count, + buf); + } + + /* + * The marker catches a full revert with any compiler. The symbolic + * field also catches scratch-sequence poisoning when the compiler + * evaluates the overflowing path first, as GCC currently does. + */ + err = tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), + "blockers", field_buf, sizeof(field_buf)); + ASSERT_EQ(0, err); + EXPECT_STREQ("read_dir", field_buf); + expect_truncated_path(_metadata, buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)); + + free(buf); +} + +/* * Verifies that an allowed access emits check_rule events (rule matched during * pathwalk) but does NOT emit deny_access events (no denial). */ |
