summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-09-09 11:00:35 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-09-09 11:00:35 -0700
commit50d05c7c76c96b90462f24debacca971d2e86713 (patch)
tree25602b0e57ea0a59ae40f5f55a5e73bd99b29646
parent5e1287972b649aab54a894addeaf1fdd6bc23e6b (diff)
parentd41d0021a6ea3e9fcd14126a00fead47f981c46e (diff)
Merge tag 'landlock-7.3-rc3' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linuxHEADmaster
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.h70
-rw-r--r--security/landlock/.kunitconfig2
-rw-r--r--security/landlock/domain.c3
-rw-r--r--security/landlock/fs.c18
-rw-r--r--security/landlock/ruleset.c2
-rw-r--r--security/landlock/trace.c182
-rw-r--r--tools/testing/selftests/landlock/scoped_abstract_unix_test.c76
-rw-r--r--tools/testing/selftests/landlock/trace_fs_test.c160
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).
*/