summaryrefslogtreecommitdiff
path: root/security/landlock
diff options
context:
space:
mode:
Diffstat (limited to 'security/landlock')
-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
5 files changed, 199 insertions, 8 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/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 */