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