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:
Linus Torvalds 2026-09-09 11:00:35 -07:00
commit 50d05c7c76
8 changed files with 466 additions and 47 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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 */

View File

@ -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). */

View File

@ -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).