mirror of
https://github.com/torvalds/linux.git
synced 2026-09-11 20:13:02 +02:00
Landlock fix for v7.3-rc3
-----BEGIN PGP SIGNATURE----- iIYEABYKAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCaqF0GxAcbWljQGRpZ2lr b2QubmV0AAoJEOXj0OiMgvbSs+YBALj3Ttl+T8cnEmxExfOYnPt6eL+oIsZFo6HU zSXUqyiNAQDxtpucp/JgwBNbuk0XA+BfLSVWuw94jdqbPpCrjUW0BA== =egGv -----END PGP SIGNATURE----- Merge tag 'landlock-7.3-rc3' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux 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
This commit is contained in:
commit
50d05c7c76
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -183,6 +242,107 @@ TEST_F(trace_fs, add_rule_fs)
|
|||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user