mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs.git
synced 2026-10-28 21:02:43 +01:00
The signal-scope denial callback identifies its target but not the
effective signal. This loses permission-probe signal zero and makes the
file-owner hook's zero sentinel ambiguous.
Append an int signal argument to the typed-BPF callback. Preserve sig,
including zero, in hook_task_kill(). In hook_file_send_sigiotask(),
translate signum zero to SIGIO at the producer, where its meaning is
known.
Carry the effective signal and target domain ID in a private,
stack-backed context consumed synchronously. This requires no allocation
or task reference in the interrupt-capable file-owner path. Gate this
context and the remaining scope-only domain IDs with CONFIG_TRACEPOINTS.
Keep the tracefs record and audit output unchanged.
Cc: Günther Noack <[email protected]>
Cc: Steven Rostedt <[email protected]>
Fixes: bb91730f16 ("landlock: Add tracepoints for ptrace and scope denials")
Link: https://patch.msgid.link/[email protected]
Signed-off-by: Mickaël Salaün <[email protected]>
411 lines
14 KiB
C
411 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Landlock - Tracepoint helpers
|
|
*
|
|
* Copyright © 2025 Microsoft Corporation
|
|
* Copyright © 2026 Cloudflare, Inc.
|
|
*/
|
|
|
|
#include <kunit/test.h>
|
|
#include <linux/cleanup.h>
|
|
#include <linux/dcache.h>
|
|
#include <linux/err.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/lsm_audit.h>
|
|
#include <linux/socket.h>
|
|
#include <net/sock.h>
|
|
|
|
#include "access.h"
|
|
#include "domain.h"
|
|
#include "fs.h"
|
|
#include "log.h"
|
|
#include "ruleset.h"
|
|
#include "trace.h"
|
|
|
|
/*
|
|
* Generates the tracepoint definitions in this translation unit. The trace
|
|
* event header dereferences the traced objects in TP_fast_assign, so the full
|
|
* struct definitions (e.g. ruleset.h, domain.h) must be included before it.
|
|
*/
|
|
#define CREATE_TRACE_POINTS
|
|
#include <trace/events/landlock.h>
|
|
|
|
/**
|
|
* landlock_trace_free_domain - Emit a tracepoint on domain deallocation
|
|
*
|
|
* @hierarchy: The domain's hierarchy being deallocated.
|
|
*
|
|
* Fires only for a hierarchy whose creation event was emitted, i.e. one that
|
|
* left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the
|
|
* create/free pair balanced: a hierarchy that never became observable is freed
|
|
* silently, while a domain that landlock_restrict_self() created and a
|
|
* thread-sync failure then aborted still fires free_domain, because its
|
|
* creation event already fired.
|
|
*
|
|
* Called from landlock_log_free_domain().
|
|
*/
|
|
void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
|
|
{
|
|
/*
|
|
* The log_status read is a correctness guard (keep the create/free pair
|
|
* balanced), not a cost guard, so this cold path needs no
|
|
* trace_..._enabled() check: the tracepoint is a static-branch no-op
|
|
* when disabled. The denial path guards trace_..._enabled() instead
|
|
* because it does expensive __getname()/path work before emitting.
|
|
*/
|
|
if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
|
|
trace_landlock_free_domain(hierarchy);
|
|
}
|
|
|
|
/**
|
|
* landlock_trace_denial - Emit a tracepoint for a denied access request
|
|
*
|
|
* @request: Detail of the user space request.
|
|
* @youngest_denied: The youngest hierarchy node that denied the access.
|
|
* @missing: The final missing access subset, when applicable.
|
|
* @same_exec: Whether the policy subject is the same executable that called
|
|
* landlock_restrict_self() for the denying domain, as computed
|
|
* by landlock_log_denial().
|
|
* @logged: Whether the domain's policy selects this denial for logging, as
|
|
* computed by landlock_log_denial().
|
|
*
|
|
* Emits the tracepoint matching @request->type when its event is enabled.
|
|
* Unlike audit, fires regardless of @logged; the value is recorded in the event
|
|
* so consumers can filter on it.
|
|
*
|
|
* Called from landlock_log_denial().
|
|
*/
|
|
void landlock_trace_denial(
|
|
const struct landlock_request *const request,
|
|
const struct landlock_hierarchy *const youngest_denied,
|
|
const access_mask_t missing, const bool same_exec, const bool logged)
|
|
{
|
|
switch (request->type) {
|
|
case LANDLOCK_REQUEST_FS_ACCESS:
|
|
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
|
|
if (trace_landlock_deny_access_fs_enabled()) {
|
|
const struct landlock_blockers blockers = {
|
|
.access = missing,
|
|
.type = request->type,
|
|
};
|
|
char *buf __free(__putname) = __getname();
|
|
struct path dentry_path;
|
|
const char *pathname;
|
|
const struct path *path = NULL;
|
|
|
|
/*
|
|
* Selects the path from the audit data type, as
|
|
* dump_common_audit_data() does. A FS_ACCESS denial
|
|
* carries a file (hook_file_truncate) or an ioctl op
|
|
* (hook_file_ioctl) rather than a path;
|
|
* FS_CHANGE_TOPOLOGY carries a path or a bare dentry.
|
|
* Reading the wrong union member would dereference
|
|
* garbage, so every reachable type is handled here.
|
|
*/
|
|
switch (request->audit.type) {
|
|
case LSM_AUDIT_DATA_FILE:
|
|
path = &request->audit.u.file->f_path;
|
|
break;
|
|
case LSM_AUDIT_DATA_IOCTL_OP:
|
|
path = &request->audit.u.op->path;
|
|
break;
|
|
case LSM_AUDIT_DATA_DENTRY:
|
|
/*
|
|
* Build a path on the stack with the real
|
|
* dentry so TP_fast_assign can extract dev and
|
|
* ino; the mnt field is unused there.
|
|
*/
|
|
dentry_path = (struct path){
|
|
.dentry = request->audit.u.dentry,
|
|
};
|
|
path = &dentry_path;
|
|
break;
|
|
case LSM_AUDIT_DATA_PATH:
|
|
path = &request->audit.u.path;
|
|
break;
|
|
default:
|
|
WARN_ONCE(1,
|
|
"Unhandled Landlock FS audit type %d",
|
|
request->audit.type);
|
|
break;
|
|
}
|
|
|
|
if (!path)
|
|
break;
|
|
|
|
if (!buf) {
|
|
pathname = "<no_mem>";
|
|
} else if (request->audit.type ==
|
|
LSM_AUDIT_DATA_DENTRY) {
|
|
/* No vfsmount: render the dentry path alone. */
|
|
pathname = dentry_path_raw(
|
|
request->audit.u.dentry, buf, PATH_MAX);
|
|
if (IS_ERR(pathname))
|
|
pathname =
|
|
PTR_ERR(pathname) ==
|
|
-ENAMETOOLONG ?
|
|
"<too_long>" :
|
|
"<unreachable>";
|
|
} else {
|
|
pathname = resolve_path_for_trace(path, buf);
|
|
}
|
|
|
|
trace_landlock_deny_access_fs(youngest_denied,
|
|
same_exec, logged,
|
|
&blockers, path,
|
|
pathname);
|
|
}
|
|
break;
|
|
case LANDLOCK_REQUEST_NET_ACCESS:
|
|
if (trace_landlock_deny_access_net_enabled()) {
|
|
const struct landlock_net_trace *const trace_net =
|
|
request->trace_net;
|
|
const struct landlock_blockers blockers = {
|
|
.access = missing,
|
|
.type = request->type,
|
|
};
|
|
struct sockaddr_storage address = {};
|
|
|
|
if (WARN_ON_ONCE(!trace_net || !trace_net->address))
|
|
return;
|
|
|
|
if (WARN_ON_ONCE(
|
|
trace_net->addrlen <
|
|
(int)offsetofend(struct sockaddr,
|
|
sa_family) ||
|
|
trace_net->addrlen > (int)sizeof(address)))
|
|
return;
|
|
|
|
memcpy(&address, trace_net->address,
|
|
trace_net->addrlen);
|
|
trace_landlock_deny_access_net(
|
|
youngest_denied, same_exec, logged, &blockers,
|
|
request->audit.u.net->sk,
|
|
trace_net->socket_family, &address,
|
|
trace_net->addrlen);
|
|
}
|
|
break;
|
|
case LANDLOCK_REQUEST_PTRACE:
|
|
if (trace_landlock_deny_ptrace_enabled()) {
|
|
const struct landlock_ptrace_trace *const trace_ptrace =
|
|
request->trace_ptrace;
|
|
|
|
if (WARN_ON_ONCE(!trace_ptrace ||
|
|
!trace_ptrace->tracer))
|
|
return;
|
|
|
|
trace_landlock_deny_ptrace(
|
|
youngest_denied, same_exec, logged,
|
|
trace_ptrace->tracee_domain_id,
|
|
request->audit.u.tsk, trace_ptrace->tracer);
|
|
}
|
|
break;
|
|
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
|
|
if (trace_landlock_deny_scope_signal_enabled()) {
|
|
const struct landlock_signal_trace *const trace_signal =
|
|
request->trace_signal;
|
|
|
|
if (WARN_ON_ONCE(!trace_signal))
|
|
return;
|
|
|
|
trace_landlock_deny_scope_signal(
|
|
youngest_denied, same_exec, logged,
|
|
trace_signal->target_domain_id,
|
|
request->audit.u.tsk, trace_signal->signal);
|
|
}
|
|
break;
|
|
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
|
|
if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
|
|
trace_landlock_deny_scope_abstract_unix_socket(
|
|
youngest_denied, same_exec, logged,
|
|
request->other_domain_id,
|
|
request->audit.u.net->sk);
|
|
break;
|
|
default:
|
|
WARN_ONCE(1, "Unhandled Landlock request type %d",
|
|
request->type);
|
|
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 */
|