aboutsummaryrefslogtreecommitdiff
path: root/security
diff options
context:
space:
mode:
authorMickaël Salaün <mic@digikod.net>2026-09-07 17:43:59 +0200
committerMickaël Salaün <mic@digikod.net>2026-09-08 11:49:39 +0200
commitd41d0021a6ea3e9fcd14126a00fead47f981c46e (patch)
treea3847fa653f616a63287a5ba1135948ae7591308 /security
parent3125751cd1de76a01b18daef399d6b88d159bd17 (diff)
landlock: Test trace path output boundaries
Use focused KUnit tests to exercise the renderer's internal boundary and composition contracts with synthetic scratch states, including both sibling-helper evaluation orders. Check the exact output and reservation boundaries, including a four-byte octal escape accepted at exact capacity and rejected one byte short. Also verify an unchanged cursor on failure, that bracketed process names and embedded NUL bytes remain data, and that input ellipsis bytes are escaped rather than mistaken for the raw truncation marker. The composition test requires generic trace output helpers. Enable CONFIG_FTRACE and CONFIG_SCHED_TRACER because the latter selects the otherwise-hidden CONFIG_TRACING support required by trace_print_flags_seq(). Use kselftests to exercise the complete tracefs path for both affected filesystem events. A valid path containing 2640 spaces exceeds the scratch output budget. Require its escaped prefix to end in the raw UTF-8 ellipsis while access_rights and blockers remain intact. This division keeps the exact safety contract compiler-independent while proving that real tracepoints preserve their surrounding symbolic fields. The end-to-end assertions fail after a full fix revert with both GCC and Clang, while the composition KUnit test fails if the scratch reserve is removed. Cc: Günther Noack <gnoack@google.com> Link: https://patch.msgid.link/20260907154401.124362-2-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
Diffstat (limited to 'security')
-rw-r--r--security/landlock/.kunitconfig2
-rw-r--r--security/landlock/trace.c182
2 files changed, 184 insertions, 0 deletions
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/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 */