| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- /* 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_dom_drop = {
- /*
- * Because domain deallocation is tied to asynchronous credential
- * freeing, receiving such event may take some time. In practice,
- * on a small VM, it should not exceed 100k usec, but let's wait up
- * to 1 second to be safe.
- */
- .tv_sec = 1,
- };
- static const struct timeval audit_tv_default = {
- .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;
- int ret, err = 0;
- bool matches_record = !type;
- regmatch_t matches[2];
- regex_t regex;
- ret = regcomp(®ex, pattern, 0);
- if (ret)
- return -EINVAL;
- do {
- memset(&msg, 0, sizeof(msg));
- err = audit_recv(audit_fd, &msg);
- if (err)
- goto out;
- if (msg.header.nlmsg_type == type)
- matches_record = true;
- } while (!matches_record);
- ret = regexec(®ex, msg.data, ARRAY_SIZE(matches), matches, 0);
- if (ret) {
- printf("DATA: %s\n", msg.data);
- printf("ERROR: no match for pattern: %s\n", pattern);
- err = -ENOENT;
- }
- 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);
- }
- static int __maybe_unused matches_log_domain_deallocated(
- int audit_fd, unsigned int num_denials, __u64 *domain_id)
- {
- static const char log_template[] = REGEX_LANDLOCK_PREFIX
- " status=deallocated denials=%u$";
- char log_match[sizeof(log_template) + 10];
- int log_match_len;
- log_match_len = snprintf(log_match, sizeof(log_match), log_template,
- num_denials);
- if (log_match_len > sizeof(log_match))
- return -E2BIG;
- return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match,
- domain_id);
- }
- struct audit_records {
- size_t access;
- size_t domain;
- };
- static int audit_count_records(int audit_fd, struct audit_records *records)
- {
- struct audit_message msg;
- int err;
- records->access = 0;
- records->domain = 0;
- do {
- memset(&msg, 0, sizeof(msg));
- err = audit_recv(audit_fd, &msg);
- if (err) {
- if (err == -EAGAIN)
- return 0;
- else
- return err;
- }
- switch (msg.header.nlmsg_type) {
- case AUDIT_LANDLOCK_ACCESS:
- records->access++;
- break;
- case AUDIT_LANDLOCK_DOMAIN:
- records->domain++;
- break;
- }
- } while (true);
- return 0;
- }
- 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)
- return err;
- err = audit_set_status(fd, AUDIT_STATUS_PID, getpid());
- if (err)
- return err;
- /* Sets a timeout for negative tests. */
- err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default,
- sizeof(audit_tv_default));
- if (err)
- return -errno;
- return fd;
- }
- 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;
- if (audit_fd < 0 || !filter) {
- int err;
- /*
- * 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)
- return err;
- }
- /* Filters might not be in place. */
- audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE);
- audit_filter_drop(audit_fd, AUDIT_DEL_RULE);
- /*
- * Because audit_cleanup() might not be called by the test auditd
- * process, it might not be possible to explicitly set it. Anyway,
- * AUDIT_STATUS_ENABLED will implicitly be set to 0 when the auditd
- * process will exit.
- */
- return close(audit_fd);
- }
- 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)
- return err;
- err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE);
- if (err)
- return err;
- return fd;
- }
|