selftests/landlock: Add trace event test infrastructure and tests

Add tracefs test infrastructure in trace.h: helpers for mounting
tracefs, enabling/disabling events, reading the trace buffer, counting
regex matches, and extracting field values, plus per-event regex
patterns.  The patterns are anchored with ^ and $, verify every
TP_printk field, and use no unescaped dot characters; TRACE_PREFIX
matches the ftrace line format with either the expected task name
(truncated to TASK_COMM_LEN - 1) or "<...>" for an evicted comm cache
entry.

Add trace_test.c with the trace fixture (setup enables all available
events with a PID filter, teardown disables and clears) and the
lifecycle, API, denial-field, and log-flag tests.  Extend the existing
true helper to open its working directory before exiting, triggering a
read_dir denial inside a sandbox, so the exec-based tests can verify
same_exec and the logged decision across an exec.  Move regex_escape()
from audit.h to common.h for shared use by the audit and trace tests.

Enable CONFIG_ENABLE_DEFAULT_TRACERS alongside CONFIG_FTRACE in the
selftest config: CONFIG_FTRACE alone only enables the tracer menu
without activating any tracer, while CONFIG_ENABLE_DEFAULT_TRACERS
selects TRACING (and thus TRACEPOINTS and event tracing) without
depending on architecture-specific syscall tracepoints.  When
CONFIG_FTRACE is disabled it cannot be set, so TRACEPOINTS is correctly
disabled too.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20260811094338.288094-15-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
Mickaël Salaün 2026-08-11 11:43:28 +02:00
parent bb91730f16
commit 2651712a15
No known key found for this signature in database
GPG Key ID: E5E3D0E88C82F6D2
6 changed files with 1794 additions and 35 deletions

View File

@ -214,41 +214,6 @@ static int audit_set_status(int fd, __u32 key, __u32 val)
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

View File

@ -253,3 +253,50 @@ static void __maybe_unused set_unix_address(struct service_fixture *const srv,
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}
/**
* regex_escape - Escape BRE metacharacters in a string
*
* @src: Source string to escape.
* @dst: Destination buffer for the escaped string.
* @dst_size: Size of the destination buffer.
*
* Escapes characters that have special meaning in POSIX Basic Regular
* Expressions: $ * . [ \ ] ^
*
* Returns a pointer to the NUL terminator in @dst (cursor-style API for
* chaining), or (char *)-ENOMEM if the buffer is too small.
*/
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;
}

View File

@ -2,6 +2,8 @@ CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_ENABLE_DEFAULT_TRACERS=y
CONFIG_FTRACE=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_KEYS=y

View File

@ -0,0 +1,634 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock trace test helpers
*
* Copyright © 2026 Cloudflare, Inc.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include "kselftest_harness.h"
#define TRACEFS_ROOT "/sys/kernel/tracing"
#define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
#define TRACEFS_CREATE_RULESET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
#define TRACEFS_CREATE_DOMAIN_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
#define TRACEFS_ADD_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
#define TRACEFS_ADD_RULE_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
#define TRACEFS_CHECK_RULE_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
#define TRACEFS_CHECK_RULE_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
#define TRACEFS_DENY_ACCESS_FS_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
#define TRACEFS_DENY_ACCESS_NET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
#define TRACEFS_DENY_PTRACE_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
#define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
TRACEFS_LANDLOCK_DIR \
"/landlock_deny_scope_abstract_unix_socket/enable"
#define TRACEFS_FREE_DOMAIN_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
#define TRACEFS_FREE_RULESET_ENABLE \
TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
#define TRACEFS_TRACE TRACEFS_ROOT "/trace"
#define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
#define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
#define TRACE_BUFFER_SIZE (64 * 1024)
/*
* Trace line prefix: matches the ftrace "trace" file format. Format: "
* <task>-<pid> [<cpu>] <flags> <timestamp>: "
*
* The task parameter must be a string literal truncated to 15 chars
* (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The
* pattern accepts either the expected task name or "<...>" because the ftrace
* comm cache may evict short-lived processes (e.g., forked children that exit
* before the trace buffer is read).
*
* No unescaped '.' in any REGEX macro; literal dots use '\\.'.
*/
#define TRACE_PREFIX(task) \
"^ *\\(<\\.\\.\\.>" \
"\\|" task "\\)" \
"-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
/*
* Task name for events emitted by kworker threads (e.g., free_domain fires from
* a work queue, not from the test process).
*/
#define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
#define REGEX_ADD_RULE_FS(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_fs: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"access_rights=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"path=[^ ]\\+$"
#define REGEX_ADD_RULE_NET(task) \
TRACE_PREFIX(task) \
"landlock_add_rule_net: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"access_rights=[a-z_|]* " \
"port=[0-9]\\+$"
#define REGEX_CREATE_RULESET(task) \
TRACE_PREFIX(task) \
"landlock_create_ruleset: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
"handled_fs=[a-z_|]* " \
"handled_net=[a-z_|]* " \
"scoped=[a-z_|]*$"
#define REGEX_CREATE_DOMAIN(task) \
TRACE_PREFIX(task) \
"landlock_create_domain: " \
"domain=[0-9a-f]\\+ " \
"parent=[0-9a-f]\\+ " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
#define REGEX_CHECK_RULE_FS(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_fs: " \
"domain=[0-9a-f]\\+ " \
"access_request=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"grants={[a-z_|,]*}$"
#define REGEX_CHECK_RULE_NET(task) \
TRACE_PREFIX(task) \
"landlock_check_rule_net: " \
"domain=[0-9a-f]\\+ " \
"access_request=[a-z_|]* " \
"port=[0-9]\\+ " \
"grants={[a-z_|,]*}$"
#define REGEX_DENY_ACCESS_FS(task) \
TRACE_PREFIX(task) \
"landlock_deny_access_fs: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"blockers=[a-z_|]* " \
"dev=[0-9]\\+:[0-9]\\+ " \
"ino=[0-9]\\+ " \
"path=[^ ]*$"
#define REGEX_DENY_ACCESS_NET(task) \
TRACE_PREFIX(task) \
"landlock_deny_access_net: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"blockers=[a-z_|]* " \
"sport=[0-9]\\+ " \
"dport=[0-9]\\+$"
#define REGEX_DENY_PTRACE(task) \
TRACE_PREFIX(task) \
"landlock_deny_ptrace: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"tracee_pid=[0-9]\\+ " \
"tracee_comm=[^ ]*$"
#define REGEX_DENY_SCOPE_SIGNAL(task) \
TRACE_PREFIX(task) \
"landlock_deny_scope_signal: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"target_pid=[0-9]\\+ " \
"target_comm=[^ ]*$"
#define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \
TRACE_PREFIX(task) \
"landlock_deny_scope_abstract_unix_socket: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"peer_pid=[0-9]\\+ " \
"sun_path=[^ ]*$"
#define REGEX_FREE_DOMAIN(task) \
TRACE_PREFIX(task) \
"landlock_free_domain: " \
"domain=[0-9a-f]\\+ " \
"denials=[0-9]\\+$"
#define REGEX_FREE_RULESET(task) \
TRACE_PREFIX(task) \
"landlock_free_ruleset: " \
"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
static int __maybe_unused tracefs_write(const char *path, const char *value)
{
int fd;
ssize_t ret;
size_t len = strlen(value);
fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
if (fd < 0)
return -errno;
ret = write(fd, value, len);
close(fd);
if (ret < 0)
return -errno;
if ((size_t)ret != len)
return -EIO;
return 0;
}
static int __maybe_unused tracefs_write_int(const char *path, int value)
{
char buf[32];
snprintf(buf, sizeof(buf), "%d", value);
return tracefs_write(path, buf);
}
static int __maybe_unused tracefs_setup(void)
{
struct stat st;
/* Mount tracefs if not already mounted. */
if (stat(TRACEFS_ROOT, &st) != 0) {
int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
if (ret)
return -errno;
}
/* Verify landlock events are available. */
if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
return -ENOENT;
return 0;
}
/*
* Set up PID-based event filtering so only events from the current process and
* its children are recorded. This is analogous to audit's AUDIT_EXE filter: it
* prevents events from unrelated processes from polluting the trace buffer.
*/
static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
{
int ret;
/* Enable event-fork so children inherit the PID filter. */
ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
if (ret)
return ret;
return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
}
/* Clear the PID filter to stop filtering by PID. */
static int __maybe_unused tracefs_clear_pid_filter(void)
{
return tracefs_write(TRACEFS_SET_EVENT_PID, "");
}
static int __maybe_unused tracefs_enable_event(const char *enable_path,
bool enable)
{
return tracefs_write(enable_path, enable ? "1" : "0");
}
static int __maybe_unused tracefs_clear(void)
{
return tracefs_write(TRACEFS_TRACE, "");
}
/*
* Reads the trace buffer content into a newly allocated buffer. The caller is
* responsible for freeing the returned buffer. Returns NULL on error.
*/
static char __maybe_unused *tracefs_read_trace(void)
{
char *buf;
int fd;
ssize_t total = 0, ret;
buf = malloc(TRACE_BUFFER_SIZE);
if (!buf)
return NULL;
fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
free(buf);
return NULL;
}
while (total < TRACE_BUFFER_SIZE - 1) {
ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
}
close(fd);
buf[total] = '\0';
return buf;
}
/* Counts the number of lines in @buf matching the basic regex @pattern. */
static int __maybe_unused tracefs_count_matches(const char *buf,
const char *pattern)
{
regex_t regex;
int count = 0;
const char *line, *end;
if (regcomp(&regex, pattern, 0) != 0)
return -EINVAL;
line = buf;
while (*line) {
end = strchr(line, '\n');
if (!end)
end = line + strlen(line);
/* Create a temporary NUL-terminated line. */
size_t len = end - line;
char *tmp = malloc(len + 1);
if (tmp) {
memcpy(tmp, line, len);
tmp[len] = '\0';
if (regexec(&regex, tmp, 0, NULL, 0) == 0)
count++;
free(tmp);
}
if (*end == '\n')
line = end + 1;
else
break;
}
regfree(&regex);
return count;
}
/*
* Extracts the value of a named field from a trace line in @buf. Searches for
* the first line matching @line_pattern, then extracts the value after
* "@field_name=" into @out. Stops at space or newline.
*
* Returns 0 on success, -ENOENT if no match.
*/
static int __maybe_unused tracefs_extract_field(const char *buf,
const char *line_pattern,
const char *field_name,
char *out, size_t out_size)
{
regex_t regex;
const char *line, *end;
if (regcomp(&regex, line_pattern, 0) != 0)
return -EINVAL;
line = buf;
while (*line) {
end = strchr(line, '\n');
if (!end)
end = line + strlen(line);
size_t len = end - line;
char *tmp = malloc(len + 1);
if (tmp) {
const char *field, *val_start;
size_t field_len, val_len;
memcpy(tmp, line, len);
tmp[len] = '\0';
if (regexec(&regex, tmp, 0, NULL, 0) != 0) {
free(tmp);
goto next;
}
/*
* Find "field_name=" in the line, ensuring a word
* boundary before the field name to avoid substring
* matches (e.g., "port" in "sport").
*/
field_len = strlen(field_name);
field = tmp;
while ((field = strstr(field, field_name))) {
if (field[field_len] == '=' &&
(field == tmp || field[-1] == ' '))
break;
field++;
}
if (!field) {
free(tmp);
regfree(&regex);
return -ENOENT;
}
val_start = field + field_len + 1;
val_len = 0;
while (val_start[val_len] &&
val_start[val_len] != ' ' &&
val_start[val_len] != '\n')
val_len++;
if (val_len >= out_size)
val_len = out_size - 1;
memcpy(out, val_start, val_len);
out[val_len] = '\0';
free(tmp);
regfree(&regex);
return 0;
}
next:
if (*end == '\n')
line = end + 1;
else
break;
}
regfree(&regex);
return -ENOENT;
}
/*
* Common fixture setup for trace tests. Mounts tracefs if needed and sets a
* PID filter. The caller must create a mount namespace first
* (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
* mount; the trace buffer, per-event enable flags, and PID filter are global
* kernel state, scoped to the test by the PID filter.
*
* Returns 0 on success, -errno on failure (caller should SKIP).
*/
static int __maybe_unused tracefs_fixture_setup(void)
{
int ret;
ret = tracefs_setup();
if (ret)
return ret;
return tracefs_set_pid_filter(getpid());
}
static void __maybe_unused tracefs_fixture_teardown(void)
{
tracefs_clear_pid_filter();
}
/*
* Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
* drops the capability. Returns the value from @func, or -EPERM if the
* capability manipulation fails.
*/
static int __maybe_unused tracefs_priv_call(int (*func)(void))
{
const cap_value_t admin = CAP_SYS_ADMIN;
cap_t cap_p;
int ret;
cap_p = cap_get_proc();
if (!cap_p)
return -EPERM;
if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
cap_set_proc(cap_p)) {
cap_free(cap_p);
return -EPERM;
}
ret = func();
cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
cap_set_proc(cap_p);
cap_free(cap_p);
return ret;
}
/* Read the trace buffer with elevated privileges. Returns NULL on failure. */
static char __maybe_unused *tracefs_read_buf(void)
{
/* Cannot use tracefs_priv_call() because the return type is char *. */
cap_t cap_p;
char *buf;
const cap_value_t admin = CAP_SYS_ADMIN;
cap_p = cap_get_proc();
if (!cap_p)
return NULL;
if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
cap_set_proc(cap_p)) {
cap_free(cap_p);
return NULL;
}
buf = tracefs_read_trace();
cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
cap_set_proc(cap_p);
cap_free(cap_p);
return buf;
}
/* Clear the trace buffer with elevated privileges. Returns 0 on success. */
static int __maybe_unused tracefs_clear_buf(void)
{
return tracefs_priv_call(tracefs_clear);
}
/*
* Forks a child that creates a Landlock sandbox and performs an FS access. The
* parent waits for the child, then reads the trace buffer.
*
* Requires common.h and wrappers.h to be included before trace.h.
*/
static void __maybe_unused sandbox_child_fs_access(
struct __test_metadata *const _metadata, const char *rule_path,
__u64 handled_access, __u64 allowed_access, const char *access_path)
{
pid_t pid;
int status;
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = handled_access,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = allowed_access,
};
int ruleset_fd, fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
close(ruleset_fd);
_exit(1);
}
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0)) {
close(path_beneath.parent_fd);
close(ruleset_fd);
_exit(1);
}
close(path_beneath.parent_fd);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
_exit(1);
}
close(ruleset_fd);
fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
close(fd);
_exit(0);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
}
/*
* Forks a child that creates a Landlock sandbox allowing execute+read_dir for
* /usr and execute-only for ".", then execs ./true. The true binary opens "."
* on startup, triggering a read_dir denial with same_exec=0. The parent waits
* for the child to exit.
*/
static void __maybe_unused sandbox_child_exec_true(
struct __test_metadata *const _metadata, __u32 restrict_flags)
{
pid_t pid;
int status;
pid = fork();
ASSERT_LE(0, pid);
if (pid == 0) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0)
_exit(1);
path_beneath.parent_fd =
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd >= 0) {
landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0);
close(path_beneath.parent_fd);
}
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
path_beneath.parent_fd =
open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (path_beneath.parent_fd >= 0) {
landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0);
close(path_beneath.parent_fd);
}
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if (landlock_restrict_self(ruleset_fd, restrict_flags))
_exit(1);
close(ruleset_fd);
execl("./true", "./true", NULL);
_exit(1);
}
ASSERT_EQ(pid, waitpid(pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Minimal helper for Landlock selftests. Opens its own working directory
* before exiting, which may trigger access denials depending on the sandbox
* configuration.
*/
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
close(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
return 0;
}