| 1 | |
| 2 | |
| 3 | * Landlock - Tracepoint helpers
|
| 4 | *
|
| 5 | * Copyright © 2025 Microsoft Corporation
|
| 6 | * Copyright © 2026 Cloudflare, Inc.
|
| 7 |
|
| 8 |
|
| 9 | #include <kunit/test.h>
|
| 10 | #include <linux/cleanup.h>
|
| 11 | #include <linux/dcache.h>
|
| 12 | #include <linux/err.h>
|
| 13 | #include <linux/fs.h>
|
| 14 | #include <linux/lsm_audit.h>
|
| 15 | #include <linux/socket.h>
|
| 16 | #include <net/sock.h>
|
| 17 |
|
| 18 | #include "access.h"
|
| 19 | #include "domain.h"
|
| 20 | #include "fs.h"
|
| 21 | #include "log.h"
|
| 22 | #include "ruleset.h"
|
| 23 | #include "trace.h"
|
| 24 |
|
| 25 | |
| 26 | * Generates the tracepoint definitions in this translation unit. The trace
|
| 27 | * event header dereferences the traced objects in TP_fast_assign, so the full
|
| 28 | * struct definitions (e.g. ruleset.h, domain.h) must be included before it.
|
| 29 |
|
| 30 | #define CREATE_TRACE_POINTS
|
| 31 | #include <trace/events/landlock.h>
|
| 32 |
|
| 33 | |
| 34 | * landlock_trace_free_domain - Emit a tracepoint on domain deallocation
|
| 35 | *
|
| 36 | * @hierarchy: The domain's hierarchy being deallocated.
|
| 37 | *
|
| 38 | * Fires only for a hierarchy whose creation event was emitted, i.e. one that
|
| 39 | * left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the
|
| 40 | * create/free pair balanced: a hierarchy that never became observable is freed
|
| 41 | * silently, while a domain that landlock_restrict_self() created and a
|
| 42 | * thread-sync failure then aborted still fires free_domain, because its
|
| 43 | * creation event already fired.
|
| 44 | *
|
| 45 | * Called from landlock_log_free_domain().
|
| 46 |
|
| 47 | void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
|
| 48 | {
|
| 49 | |
| 50 | * The log_status read is a correctness guard (keep the create/free pair
|
| 51 | * balanced), not a cost guard, so this cold path needs no
|
| 52 | * trace_..._enabled() check: the tracepoint is a static-branch no-op
|
| 53 | * when disabled. The denial path guards trace_..._enabled() instead
|
| 54 | * because it does expensive __getname()/path work before emitting.
|
| 55 |
|
| 56 | if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
|
| 57 | trace_landlock_free_domain(hierarchy);
|
| 58 | }
|
| 59 |
|
| 60 | |
| 61 | * landlock_trace_denial - Emit a tracepoint for a denied access request
|
| 62 | *
|
| 63 | * @request: Detail of the user space request.
|
| 64 | * @youngest_denied: The youngest hierarchy node that denied the access.
|
| 65 | * @missing: The final missing access subset, when applicable.
|
| 66 | * @same_exec: Whether the policy subject is the same executable that called
|
| 67 | * landlock_restrict_self() for the denying domain, as computed
|
| 68 | * by landlock_log_denial().
|
| 69 | * @logged: Whether the domain's policy selects this denial for logging, as
|
| 70 | * computed by landlock_log_denial().
|
| 71 | *
|
| 72 | * Emits the tracepoint matching @request->type when its event is enabled.
|
| 73 | * Unlike audit, fires regardless of @logged; the value is recorded in the event
|
| 74 | * so consumers can filter on it.
|
| 75 | *
|
| 76 | * Called from landlock_log_denial().
|
| 77 |
|
| 78 | void landlock_trace_denial(
|
| 79 | const struct landlock_request *const request,
|
| 80 | const struct landlock_hierarchy *const youngest_denied,
|
| 81 | const access_mask_t missing, const bool same_exec, const bool logged)
|
| 82 | {
|
| 83 | switch (request->type) {
|
| 84 | case LANDLOCK_REQUEST_FS_ACCESS:
|
| 85 | case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
|
| 86 | if (trace_landlock_deny_access_fs_enabled()) {
|
| 87 | const struct landlock_blockers blockers = {
|
| 88 | .access = missing,
|
| 89 | .type = request->type,
|
| 90 | };
|
| 91 | char *buf __free(__putname) = __getname();
|
| 92 | struct path dentry_path;
|
| 93 | const char *pathname;
|
| 94 | const struct path *path = NULL;
|
| 95 |
|
| 96 | |
| 97 | * Selects the path from the audit data type, as
|
| 98 | * dump_common_audit_data() does. A FS_ACCESS denial
|
| 99 | * carries a file (hook_file_truncate) or an ioctl op
|
| 100 | * (hook_file_ioctl) rather than a path;
|
| 101 | * FS_CHANGE_TOPOLOGY carries a path or a bare dentry.
|
| 102 | * Reading the wrong union member would dereference
|
| 103 | * garbage, so every reachable type is handled here.
|
| 104 |
|
| 105 | switch (request->audit.type) {
|
| 106 | case LSM_AUDIT_DATA_FILE:
|
| 107 | path = &request->audit.u.file->f_path;
|
| 108 | break;
|
| 109 | case LSM_AUDIT_DATA_IOCTL_OP:
|
| 110 | path = &request->audit.u.op->path;
|
| 111 | break;
|
| 112 | case LSM_AUDIT_DATA_DENTRY:
|
| 113 | |
| 114 | * Build a path on the stack with the real
|
| 115 | * dentry so TP_fast_assign can extract dev and
|
| 116 | * ino; the mnt field is unused there.
|
| 117 |
|
| 118 | dentry_path = (struct path){
|
| 119 | .dentry = request->audit.u.dentry,
|
| 120 | };
|
| 121 | path = &dentry_path;
|
| 122 | break;
|
| 123 | case LSM_AUDIT_DATA_PATH:
|
| 124 | path = &request->audit.u.path;
|
| 125 | break;
|
| 126 | default:
|
| 127 | WARN_ONCE(1,
|
| 128 | "Unhandled Landlock FS audit type %d",
|
| 129 | request->audit.type);
|
| 130 | break;
|
| 131 | }
|
| 132 |
|
| 133 | if (!path)
|
| 134 | break;
|
| 135 |
|
| 136 | if (!buf) {
|
| 137 | pathname = "<no_mem>";
|
| 138 | } else if (request->audit.type ==
|
| 139 | LSM_AUDIT_DATA_DENTRY) {
|
| 140 |
|
| 141 | pathname = dentry_path_raw(
|
| 142 | request->audit.u.dentry, buf, PATH_MAX);
|
| 143 | if (IS_ERR(pathname))
|
| 144 | pathname =
|
| 145 | PTR_ERR(pathname) ==
|
| 146 | -ENAMETOOLONG ?
|
| 147 | "<too_long>" :
|
| 148 | "<unreachable>";
|
| 149 | } else {
|
| 150 | pathname = resolve_path_for_trace(path, buf);
|
| 151 | }
|
| 152 |
|
| 153 | trace_landlock_deny_access_fs(youngest_denied,
|
| 154 | same_exec, logged,
|
| 155 | &blockers, path,
|
| 156 | pathname);
|
| 157 | }
|
| 158 | break;
|
| 159 | case LANDLOCK_REQUEST_NET_ACCESS:
|
| 160 | if (trace_landlock_deny_access_net_enabled()) {
|
| 161 | const struct landlock_net_trace *const trace_net =
|
| 162 | request->trace_net;
|
| 163 | const struct landlock_blockers blockers = {
|
| 164 | .access = missing,
|
| 165 | .type = request->type,
|
| 166 | };
|
| 167 | struct sockaddr_storage address = {};
|
| 168 |
|
| 169 | if (WARN_ON_ONCE(!trace_net || !trace_net->address))
|
| 170 | return;
|
| 171 |
|
| 172 | if (WARN_ON_ONCE(
|
| 173 | trace_net->addrlen <
|
| 174 | (int)offsetofend(struct sockaddr,
|
| 175 | sa_family) ||
|
| 176 | trace_net->addrlen > (int)sizeof(address)))
|
| 177 | return;
|
| 178 |
|
| 179 | memcpy(&address, trace_net->address,
|
| 180 | trace_net->addrlen);
|
| 181 | trace_landlock_deny_access_net(
|
| 182 | youngest_denied, same_exec, logged, &blockers,
|
| 183 | request->audit.u.net->sk,
|
| 184 | trace_net->socket_family, &address,
|
| 185 | trace_net->addrlen);
|
| 186 | }
|
| 187 | break;
|
| 188 | case LANDLOCK_REQUEST_PTRACE:
|
| 189 | if (trace_landlock_deny_ptrace_enabled()) {
|
| 190 | const struct landlock_ptrace_trace *const trace_ptrace =
|
| 191 | request->trace_ptrace;
|
| 192 |
|
| 193 | if (WARN_ON_ONCE(!trace_ptrace ||
|
| 194 | !trace_ptrace->tracer))
|
| 195 | return;
|
| 196 |
|
| 197 | trace_landlock_deny_ptrace(
|
| 198 | youngest_denied, same_exec, logged,
|
| 199 | trace_ptrace->tracee_domain_id,
|
| 200 | request->audit.u.tsk, trace_ptrace->tracer);
|
| 201 | }
|
| 202 | break;
|
| 203 | case LANDLOCK_REQUEST_SCOPE_SIGNAL:
|
| 204 | if (trace_landlock_deny_scope_signal_enabled()) {
|
| 205 | const struct landlock_signal_trace *const trace_signal =
|
| 206 | request->trace_signal;
|
| 207 |
|
| 208 | if (WARN_ON_ONCE(!trace_signal))
|
| 209 | return;
|
| 210 |
|
| 211 | trace_landlock_deny_scope_signal(
|
| 212 | youngest_denied, same_exec, logged,
|
| 213 | trace_signal->target_domain_id,
|
| 214 | request->audit.u.tsk, trace_signal->signal);
|
| 215 | }
|
| 216 | break;
|
| 217 | case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
|
| 218 | if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
|
| 219 | trace_landlock_deny_scope_abstract_unix_socket(
|
| 220 | youngest_denied, same_exec, logged,
|
| 221 | request->other_domain_id,
|
| 222 | request->audit.u.net->sk);
|
| 223 | break;
|
| 224 | default:
|
| 225 | WARN_ONCE(1, "Unhandled Landlock request type %d",
|
| 226 | request->type);
|
| 227 | break;
|
| 228 | }
|
| 229 | }
|
| 230 |
|
| 231 | #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
|
| 232 |
|
| 233 | static void test_trace_seq_init(struct trace_seq *const seq, const size_t size)
|
| 234 | {
|
| 235 | memset(seq, 0, sizeof(*seq));
|
| 236 | seq_buf_init(&seq->seq, seq->buffer, size);
|
| 237 | }
|
| 238 |
|
| 239 | static void test_untrusted_str_data(struct kunit *const test)
|
| 240 | {
|
| 241 | const char binary[] = { 'a', '\0', '<' };
|
| 242 | static const char ellipsis[] = "\xe2\x80\xa6";
|
| 243 | struct trace_seq *const seq =
|
| 244 | kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL);
|
| 245 | const char *output;
|
| 246 |
|
| 247 | KUNIT_ASSERT_NOT_NULL(test, seq);
|
| 248 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 249 | output = __trace_print_untrusted_str(seq, "<too_long>", 10);
|
| 250 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 251 | KUNIT_EXPECT_STREQ(test, output, "<too_long>");
|
| 252 |
|
| 253 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 254 | output = __trace_print_untrusted_str(seq, binary, sizeof(binary));
|
| 255 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 256 | KUNIT_EXPECT_STREQ(test, output, "a\\000<");
|
| 257 |
|
| 258 |
|
| 259 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 260 | output = __trace_print_untrusted_str(seq, ellipsis,
|
| 261 | sizeof(ellipsis) - 1);
|
| 262 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 263 | KUNIT_EXPECT_STREQ(test, output, "\\342\\200\\246");
|
| 264 | }
|
| 265 |
|
| 266 | static void test_untrusted_str_boundaries(struct kunit *const test)
|
| 267 | {
|
| 268 | static const char escaped_space[] = "\\040";
|
| 269 | const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE;
|
| 270 | const size_t marker_len = sizeof(TRACE_TRUNCATION_MARKER) - 1;
|
| 271 | const size_t escape_len = sizeof(escaped_space) - 1;
|
| 272 | const size_t exact_prefix_len =
|
| 273 | output_size - marker_len - 1 - escape_len;
|
| 274 | const size_t short_prefix_len = exact_prefix_len + 1;
|
| 275 | struct trace_seq *const seq =
|
| 276 | kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL);
|
| 277 | char *const input = kunit_kmalloc(test, output_size + 1, GFP_KERNEL);
|
| 278 | char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL);
|
| 279 | const char *output;
|
| 280 |
|
| 281 | KUNIT_ASSERT_NOT_NULL(test, seq);
|
| 282 | KUNIT_ASSERT_NOT_NULL(test, input);
|
| 283 | KUNIT_ASSERT_NOT_NULL(test, expected);
|
| 284 |
|
| 285 |
|
| 286 | memset(input, 'a', output_size - 1);
|
| 287 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 288 | output = __trace_print_untrusted_str(seq, input, output_size - 1);
|
| 289 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 290 | KUNIT_EXPECT_EQ(test, seq->seq.len, output_size);
|
| 291 | KUNIT_EXPECT_EQ(test, memcmp(output, input, output_size - 1), 0);
|
| 292 |
|
| 293 |
|
| 294 | memset(input, 'a', short_prefix_len);
|
| 295 | input[short_prefix_len] = ' ';
|
| 296 | memset(input + short_prefix_len + 1, 'b', 5);
|
| 297 | memset(expected, 'a', short_prefix_len);
|
| 298 | memcpy(expected + short_prefix_len, TRACE_TRUNCATION_MARKER,
|
| 299 | marker_len + 1);
|
| 300 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 301 | output = __trace_print_untrusted_str(seq, input, short_prefix_len + 6);
|
| 302 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 303 | KUNIT_EXPECT_STREQ(test, output, expected);
|
| 304 |
|
| 305 |
|
| 306 | memset(input, 'a', exact_prefix_len);
|
| 307 | input[exact_prefix_len] = ' ';
|
| 308 | memset(input + exact_prefix_len + 1, 'b', marker_len + 1);
|
| 309 | memset(expected, 'a', exact_prefix_len);
|
| 310 | memcpy(expected + exact_prefix_len, escaped_space, escape_len);
|
| 311 | memcpy(expected + exact_prefix_len + escape_len,
|
| 312 | TRACE_TRUNCATION_MARKER, marker_len + 1);
|
| 313 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 314 | output = __trace_print_untrusted_str(seq, input,
|
| 315 | exact_prefix_len + marker_len + 2);
|
| 316 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 317 | KUNIT_EXPECT_STREQ(test, output, expected);
|
| 318 |
|
| 319 |
|
| 320 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 321 | output = __trace_print_untrusted_str(seq, "/\\000", 5);
|
| 322 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 323 | KUNIT_EXPECT_STREQ(test, output, "/\\\\000");
|
| 324 | }
|
| 325 |
|
| 326 | static void test_untrusted_str_cursor(struct kunit *const test)
|
| 327 | {
|
| 328 | const size_t padding_len =
|
| 329 | TRACE_SEQ_BUFFER_SIZE - TRACE_UNTRUSTED_STR_OUTPUT_SIZE + 1;
|
| 330 | struct trace_seq *const seq =
|
| 331 | kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL);
|
| 332 | char *const padding = kunit_kzalloc(test, padding_len, GFP_KERNEL);
|
| 333 | const char *output;
|
| 334 |
|
| 335 | KUNIT_ASSERT_NOT_NULL(test, seq);
|
| 336 | KUNIT_ASSERT_NOT_NULL(test, padding);
|
| 337 |
|
| 338 |
|
| 339 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 340 | trace_seq_putmem(seq, padding, padding_len - 1);
|
| 341 | output = __trace_print_untrusted_str(seq, "/a", 2);
|
| 342 | KUNIT_ASSERT_NOT_NULL(test, output);
|
| 343 | KUNIT_EXPECT_STREQ(test, output, "/a");
|
| 344 | KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len - 1 + sizeof("/a"));
|
| 345 |
|
| 346 |
|
| 347 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 348 | trace_seq_putmem(seq, padding, padding_len);
|
| 349 | output = __trace_print_untrusted_str(seq, "/a", 2);
|
| 350 | KUNIT_EXPECT_NULL(test, output);
|
| 351 | KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len);
|
| 352 | }
|
| 353 |
|
| 354 | static void test_untrusted_str_composition(struct kunit *const test)
|
| 355 | {
|
| 356 | static const struct trace_print_flags flags[] = {
|
| 357 | { .mask = 1, .name = "read" },
|
| 358 | };
|
| 359 | const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE;
|
| 360 | const size_t prefix_len = output_size - sizeof(TRACE_TRUNCATION_MARKER);
|
| 361 | struct trace_seq *const seq =
|
| 362 | kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL);
|
| 363 | char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL);
|
| 364 | char *const path = kunit_kmalloc(test, output_size, GFP_KERNEL);
|
| 365 | const char *flags_output, *path_output;
|
| 366 |
|
| 367 | KUNIT_ASSERT_NOT_NULL(test, seq);
|
| 368 | KUNIT_ASSERT_NOT_NULL(test, expected);
|
| 369 | KUNIT_ASSERT_NOT_NULL(test, path);
|
| 370 | memset(path, 'a', output_size);
|
| 371 | memset(expected, 'a', prefix_len);
|
| 372 | memcpy(expected + prefix_len, TRACE_TRUNCATION_MARKER,
|
| 373 | sizeof(TRACE_TRUNCATION_MARKER));
|
| 374 |
|
| 375 |
|
| 376 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 377 | path_output = __trace_print_untrusted_str(seq, path, output_size);
|
| 378 | flags_output =
|
| 379 | trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags));
|
| 380 | KUNIT_ASSERT_NOT_NULL(test, path_output);
|
| 381 | KUNIT_EXPECT_STREQ(test, path_output, expected);
|
| 382 | KUNIT_EXPECT_STREQ(test, flags_output, "read");
|
| 383 |
|
| 384 | test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE);
|
| 385 | flags_output =
|
| 386 | trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags));
|
| 387 | path_output = __trace_print_untrusted_str(seq, path, output_size);
|
| 388 | KUNIT_ASSERT_NOT_NULL(test, path_output);
|
| 389 | KUNIT_EXPECT_STREQ(test, path_output, expected);
|
| 390 | KUNIT_EXPECT_STREQ(test, flags_output, "read");
|
| 391 | }
|
| 392 |
|
| 393 | static struct kunit_case test_cases[] = {
|
| 394 |
|
| 395 | KUNIT_CASE(test_untrusted_str_data),
|
| 396 | KUNIT_CASE(test_untrusted_str_boundaries),
|
| 397 | KUNIT_CASE(test_untrusted_str_cursor),
|
| 398 | KUNIT_CASE(test_untrusted_str_composition),
|
| 399 | {}
|
| 400 |
|
| 401 | };
|
| 402 |
|
| 403 | static struct kunit_suite test_suite = {
|
| 404 | .name = "landlock_trace",
|
| 405 | .test_cases = test_cases,
|
| 406 | };
|
| 407 |
|
| 408 | kunit_test_suite(test_suite);
|
| 409 |
|
| 410 | #endif
|