mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/chenhuacai/linux-loongson
synced 2025-08-28 00:19:36 +00:00

The audit_init_filter_exe() helper incorrectly checks the readlink(2)
error because an unsigned integer is used to store the result. Use a
signed integer for this check.
Reported-by: Dan Carpenter <dan.carpenter@linaro.org>
Closes: https://lore.kernel.org/r/aDbFwyZ_fM-IO7sC@stanley.mountain
Fixes: 6a500b2297
("selftests/landlock: Add tests for audit flags and domain IDs")
Reviewed-by: Günther Noack <gnoack@google.com>
Link: https://lore.kernel.org/r/20250528144426.1709063-1-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
481 lines
10 KiB
C
481 lines
10 KiB
C
/* 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>
|
|
|
|
#ifndef ARRAY_SIZE
|
|
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
|
|
#endif
|
|
|
|
#ifndef __maybe_unused
|
|
#define __maybe_unused __attribute__((__unused__))
|
|
#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;
|
|
}
|