| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Landlock - Tracepoint helpers |
| * |
| * Copyright © 2025 Microsoft Corporation |
| * Copyright © 2026 Cloudflare, Inc. |
| */ |
| |
| #include <linux/cleanup.h> |
| #include <linux/dcache.h> |
| #include <linux/err.h> |
| #include <linux/fs.h> |
| #include <linux/lsm_audit.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 set of denied access rights. |
| * @same_exec: Whether the current task 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()) { |
| 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, |
| missing, path, pathname); |
| } |
| break; |
| case LANDLOCK_REQUEST_NET_ACCESS: |
| if (trace_landlock_deny_access_net_enabled()) |
| trace_landlock_deny_access_net( |
| youngest_denied, same_exec, logged, missing, |
| request->audit.u.net->sk, |
| ntohs(request->audit.u.net->sport), |
| ntohs(request->audit.u.net->dport)); |
| break; |
| case LANDLOCK_REQUEST_PTRACE: |
| if (trace_landlock_deny_ptrace_enabled()) |
| trace_landlock_deny_ptrace(youngest_denied, same_exec, |
| logged, |
| request->other_domain_id, |
| request->audit.u.tsk); |
| break; |
| case LANDLOCK_REQUEST_SCOPE_SIGNAL: |
| if (trace_landlock_deny_scope_signal_enabled()) |
| trace_landlock_deny_scope_signal( |
| youngest_denied, same_exec, logged, |
| request->other_domain_id, request->audit.u.tsk); |
| 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; |
| } |
| } |