| /* SPDX-License-Identifier: GPL-2.0 */ | |
| /* | |
| * Landlock audit helpers | |
| * | |
| * Copyright © 2024-2025 Microsoft Corporation | |
| */ | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <linux/audit.h> | |
| #include <linux/limits.h> | |
| #include <linux/netlink.h> | |
| #include <regex.h> | |
| #include <stdbool.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/socket.h> | |
| #include <sys/time.h> | |
| #include <unistd.h> | |
| #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); | |
| } | |
| /* | |
| * @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; | |
| } |