/* SPDX-License-Identifier: GPL-2.0 */ /* * Landlock audit helpers * * Copyright © 2024-2025 Microsoft Corporation */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kselftest.h" #ifndef ARRAY_SIZE #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) #endif #define REGEX_LANDLOCK_PREFIX "^audit([0-9.:]\\+): domain=\\([0-9a-f]\\+\\)" struct audit_filter { __u32 record_type; size_t exe_len; char exe[PATH_MAX]; }; struct audit_message { struct nlmsghdr header; union { struct audit_status status; struct audit_features features; struct audit_rule_data rule; struct nlmsgerr err; char data[PATH_MAX + 200]; }; }; static const struct timeval audit_tv_default = { /* * Default socket timeout for audit_match_record() callers that expect a * record to arrive. Asynchronous kauditd delivery can exceed 1 usec * under heavy debug configs (KASAN, lockdep), where kauditd_thread * scheduling between audit_log_end() and netlink_unicast() takes longer * than the previous 1 usec timeout. 1 second is a generous ceiling: on * the happy path, kauditd delivers within dozens of usec. */ .tv_sec = 1, }; static const struct timeval audit_tv_fast = { /* * Fast timeout for paths that expect no record (audit_init() drain, * audit_count_records(), probes). Causes audit_recv() to return * -EAGAIN once the socket buffer is empty, naturally terminating the * read loop. */ .tv_usec = 1, }; static int audit_send(const int fd, const struct audit_message *const msg) { struct sockaddr_nl addr = { .nl_family = AF_NETLINK, }; int ret; do { ret = sendto(fd, msg, msg->header.nlmsg_len, 0, (struct sockaddr *)&addr, sizeof(addr)); } while (ret < 0 && errno == EINTR); if (ret < 0) return -errno; if (ret != msg->header.nlmsg_len) return -E2BIG; return 0; } static int audit_recv(const int fd, struct audit_message *msg) { struct sockaddr_nl addr; socklen_t addrlen = sizeof(addr); struct audit_message msg_tmp; int err; if (!msg) msg = &msg_tmp; do { err = recvfrom(fd, msg, sizeof(*msg), 0, (struct sockaddr *)&addr, &addrlen); } while (err < 0 && errno == EINTR); if (err < 0) return -errno; if (addrlen != sizeof(addr) || addr.nl_pid != 0) return -EINVAL; /* Checks Netlink error or end of messages. */ if (msg->header.nlmsg_type == NLMSG_ERROR) return msg->err.error; return 0; } static int audit_request(const int fd, const struct audit_message *const request, struct audit_message *reply) { struct audit_message msg_tmp; bool first_reply = true; int err; err = audit_send(fd, request); if (err) return err; if (!reply) reply = &msg_tmp; do { if (first_reply) first_reply = false; else reply = &msg_tmp; err = audit_recv(fd, reply); if (err) return err; } while (reply->header.nlmsg_type != NLMSG_ERROR && reply->err.msg.nlmsg_type != request->header.nlmsg_type); return reply->err.error; } static int audit_filter_exe(const int audit_fd, const struct audit_filter *const filter, const __u16 type) { struct audit_message msg = { .header = { .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)) + NLMSG_ALIGN(filter->exe_len), .nlmsg_type = type, .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, }, .rule = { .flags = AUDIT_FILTER_EXCLUDE, .action = AUDIT_NEVER, .field_count = 1, .fields[0] = filter->record_type, .fieldflags[0] = AUDIT_NOT_EQUAL, .values[0] = filter->exe_len, .buflen = filter->exe_len, } }; if (filter->record_type != AUDIT_EXE) return -EINVAL; memcpy(msg.rule.buf, filter->exe, filter->exe_len); return audit_request(audit_fd, &msg, NULL); } static int audit_filter_drop(const int audit_fd, const __u16 type) { struct audit_message msg = { .header = { .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)), .nlmsg_type = type, .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, }, .rule = { .flags = AUDIT_FILTER_EXCLUDE, .action = AUDIT_NEVER, .field_count = 1, .fields[0] = AUDIT_MSGTYPE, .fieldflags[0] = AUDIT_NOT_EQUAL, .values[0] = AUDIT_LANDLOCK_DOMAIN, } }; return audit_request(audit_fd, &msg, NULL); } static int audit_set_status(int fd, __u32 key, __u32 val) { const struct audit_message msg = { .header = { .nlmsg_len = NLMSG_SPACE(sizeof(msg.status)), .nlmsg_type = AUDIT_SET, .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, }, .status = { .mask = key, .enabled = key == AUDIT_STATUS_ENABLED ? val : 0, .pid = key == AUDIT_STATUS_PID ? val : 0, } }; return audit_request(fd, &msg, NULL); } /* Returns a pointer to the last filled character of @dst, which is `\0`. */ static __maybe_unused char *regex_escape(const char *const src, char *dst, size_t dst_size) { char *d = dst; for (const char *s = src; *s; s++) { switch (*s) { case '$': case '*': case '.': case '[': case '\\': case ']': case '^': if (d >= dst + dst_size - 2) return (char *)-ENOMEM; *d++ = '\\'; *d++ = *s; break; default: if (d >= dst + dst_size - 1) return (char *)-ENOMEM; *d++ = *s; } } if (d >= dst + dst_size - 1) return (char *)-ENOMEM; *d = '\0'; return d; } /* * @domain_id: The domain ID extracted from the audit message (if the first part * of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is * not found. */ static int audit_match_record(int audit_fd, const __u16 type, const char *const pattern, __u64 *domain_id) { struct audit_message msg, last_mismatch = {}; int ret, err = 0; int num_type_match = 0; regmatch_t matches[2]; regex_t regex; ret = regcomp(®ex, pattern, 0); if (ret) return -EINVAL; /* * Reads records until one matches both the expected type and the * pattern. Type-matching records with non-matching content are * silently consumed, which handles stale domain deallocation records * from a previous test emitted asynchronously by kworker threads. */ while (true) { memset(&msg, 0, sizeof(msg)); err = audit_recv(audit_fd, &msg); if (err) { if (num_type_match) { printf("DATA: %s\n", last_mismatch.data); printf("ERROR: %d record(s) matched type %u" " but not pattern: %s\n", num_type_match, type, pattern); } goto out; } if (type && msg.header.nlmsg_type != type) continue; ret = regexec(®ex, msg.data, ARRAY_SIZE(matches), matches, 0); if (!ret) break; num_type_match++; last_mismatch = msg; } if (domain_id) { *domain_id = 0; if (matches[1].rm_so != -1) { int match_len = matches[1].rm_eo - matches[1].rm_so; /* The maximal characters of a 2^64 hexadecimal number is 17. */ char dom_id[18]; if (match_len > 0 && match_len < sizeof(dom_id)) { memcpy(dom_id, msg.data + matches[1].rm_so, match_len); dom_id[match_len] = '\0'; if (domain_id) *domain_id = strtoull(dom_id, NULL, 16); } } } out: regfree(®ex); return err; } static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid, __u64 *domain_id) { static const char log_template[] = REGEX_LANDLOCK_PREFIX " status=allocated mode=enforcing pid=%d uid=[0-9]\\+" " exe=\"[^\"]\\+\" comm=\".*_test\"$"; char log_match[sizeof(log_template) + 10]; int log_match_len; log_match_len = snprintf(log_match, sizeof(log_match), log_template, pid); if (log_match_len >= sizeof(log_match)) return -E2BIG; return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, domain_id); } /* * Matches a domain deallocation record. When expected_domain_id is non-zero, * the pattern includes the specific domain ID so that stale deallocation * records from a previous test (with a different domain ID) are skipped by * audit_match_record(), waiting for the asynchronous kworker deallocation with * the default patient timeout. * * When expected_domain_id is zero, the caller is probing for any dealloc record * that may or may not arrive. Temporarily lowers the socket timeout to * audit_tv_fast for this probe so it returns promptly when no record is * pending; restores audit_tv_default after. */ static int __maybe_unused matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, __u64 expected_domain_id, __u64 *domain_id) { static const char log_template[] = REGEX_LANDLOCK_PREFIX " status=deallocated denials=%u$"; static const char log_template_with_id[] = "^audit([0-9.:]\\+): domain=\\(%llx\\)" " status=deallocated denials=%u$"; char log_match[sizeof(log_template_with_id) + 32]; int log_match_len, err; if (expected_domain_id) log_match_len = snprintf(log_match, sizeof(log_match), log_template_with_id, (unsigned long long)expected_domain_id, num_denials); else log_match_len = snprintf(log_match, sizeof(log_match), log_template, num_denials); if (log_match_len >= sizeof(log_match)) return -E2BIG; if (!expected_domain_id) { if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, sizeof(audit_tv_fast))) return -errno; } err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, domain_id); if (!expected_domain_id) { if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, sizeof(audit_tv_default)) && !err) err = -errno; } return err; } struct audit_records { size_t access; size_t domain; }; /* * Counts remaining audit records by type, skipping domain deallocation records. * Deallocation records are emitted asynchronously from kworker threads after a * previous test's child has exited, so they can arrive after the drain in * audit_init() and after the preceding audit_match_record() call. Allocation * records are emitted synchronously during landlock_log_denial() in the current * test's syscall context, so only those are counted in records->domain. * * Temporarily lowers SO_RCVTIMEO to audit_tv_fast for the read loop: this is a * "no record expected" path that should terminate on the first -EAGAIN. The * default patient timeout is restored on exit for subsequent * audit_match_record() callers. */ static int audit_count_records(int audit_fd, struct audit_records *records) { static const char dealloc_pattern[] = REGEX_LANDLOCK_PREFIX " status=deallocated "; struct audit_message msg; regex_t dealloc_re; int ret, err = 0; ret = regcomp(&dealloc_re, dealloc_pattern, 0); if (ret) return -ENOMEM; records->access = 0; records->domain = 0; if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, sizeof(audit_tv_fast))) { err = -errno; goto out; } do { memset(&msg, 0, sizeof(msg)); err = audit_recv(audit_fd, &msg); if (err) { if (err == -EAGAIN) err = 0; break; } switch (msg.header.nlmsg_type) { case AUDIT_LANDLOCK_ACCESS: records->access++; break; case AUDIT_LANDLOCK_DOMAIN: ret = regexec(&dealloc_re, msg.data, 0, NULL, 0); if (ret == REG_NOMATCH) { records->domain++; } else if (ret != 0) { err = -EIO; goto out; } break; } } while (true); out: if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, sizeof(audit_tv_default)) && !err) err = -errno; regfree(&dealloc_re); return err; } static int audit_init(void) { int fd, err; fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT); if (fd < 0) return -errno; err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1); if (err) goto err_close; err = audit_set_status(fd, AUDIT_STATUS_PID, getpid()); if (err) goto err_close; /* Uses the fast timeout to drain stale records below. */ err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, sizeof(audit_tv_fast)); if (err) { err = -errno; goto err_close; } /* * Drains stale audit records that accumulated in the kernel backlog * while no audit daemon socket was open. This happens when non-audit * Landlock tests generate records while audit_enabled is non-zero (e.g. * from boot configuration), or when domain deallocation records arrive * asynchronously after a previous test's socket was closed. */ while (audit_recv(fd, NULL) == 0) ; /* * Restores the default timeout for audit_match_record() callers that * expect a record to arrive. Paths that expect no record restore the * fast timeout locally (audit_count_records(), the expected_domain_id * == 0 probe in matches_log_domain_deallocated()). */ err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, sizeof(audit_tv_default)); if (err) { err = -errno; goto err_close; } return fd; err_close: close(fd); return err; } static int audit_init_filter_exe(struct audit_filter *filter, const char *path) { char *absolute_path = NULL; /* It is assume that there is not already filtering rules. */ filter->record_type = AUDIT_EXE; if (!path) { int ret = readlink("/proc/self/exe", filter->exe, sizeof(filter->exe) - 1); if (ret < 0) return -errno; filter->exe_len = ret; return 0; } absolute_path = realpath(path, NULL); if (!absolute_path) return -errno; /* No need for the terminating NULL byte. */ filter->exe_len = strlen(absolute_path); if (filter->exe_len > sizeof(filter->exe)) return -E2BIG; memcpy(filter->exe, absolute_path, filter->exe_len); free(absolute_path); return 0; } static int audit_cleanup(int audit_fd, struct audit_filter *filter) { struct audit_filter new_filter; int err = 0; if (audit_fd < 0 || !filter) { /* * Simulates audit_init_with_exe_filter() when called from * FIXTURE_TEARDOWN_PARENT(). */ audit_fd = audit_init(); if (audit_fd < 0) return audit_fd; filter = &new_filter; err = audit_init_filter_exe(filter, NULL); if (err) goto err_close; } /* Filters might not be in place. */ audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE); audit_filter_drop(audit_fd, AUDIT_DEL_RULE); err = audit_set_status(audit_fd, AUDIT_STATUS_ENABLED, 0); err_close: close(audit_fd); return err; } static int audit_init_with_exe_filter(struct audit_filter *filter) { int fd, err; fd = audit_init(); if (fd < 0) return fd; err = audit_init_filter_exe(filter, NULL); if (err) goto err_close; err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE); if (err) goto err_close; return fd; err_close: close(fd); return err; }