Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,6 @@
/*_test
/fs_bench
/sandbox-and-launch
/true
/wait-pipe
/wait-pipe-sandbox
@@ -0,0 +1,28 @@
# SPDX-License-Identifier: GPL-2.0
#
# First run: make -C ../../../.. headers_install
CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
LOCAL_HDRS += $(wildcard *.h)
src_test := $(wildcard *_test.c)
TEST_GEN_PROGS := $(src_test:.c=)
TEST_GEN_PROGS += fs_bench
TEST_GEN_PROGS_EXTENDED := \
true \
sandbox-and-launch \
wait-pipe \
wait-pipe-sandbox
# Short targets:
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread
$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static
include ../lib.mk
# Targets with $(OUTPUT)/ prefix:
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread
$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static
@@ -0,0 +1,605 @@
/* 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);
}
/* 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(&regex, 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(&regex, 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(&regex);
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;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,645 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Common user space base
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/landlock.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include "common.h"
#ifndef O_PATH
#define O_PATH 010000000
#endif
TEST(inconsistent_attr)
{
const long page_size = sysconf(_SC_PAGESIZE);
char *const buf = malloc(page_size + 1);
struct landlock_ruleset_attr *const ruleset_attr = (void *)buf;
ASSERT_NE(NULL, buf);
/* Checks copy_from_user(). */
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 0, 0));
/* The size if less than sizeof(struct landlock_attr_enforce). */
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 1, 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 7, 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, 1, 0));
/* The size if less than sizeof(struct landlock_attr_enforce). */
ASSERT_EQ(EFAULT, errno);
ASSERT_EQ(-1, landlock_create_ruleset(
NULL, sizeof(struct landlock_ruleset_attr), 0));
ASSERT_EQ(EFAULT, errno);
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0));
ASSERT_EQ(E2BIG, errno);
/* Checks minimal valid attribute size. */
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 8, 0));
ASSERT_EQ(ENOMSG, errno);
ASSERT_EQ(-1, landlock_create_ruleset(
ruleset_attr,
sizeof(struct landlock_ruleset_attr), 0));
ASSERT_EQ(ENOMSG, errno);
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0));
ASSERT_EQ(ENOMSG, errno);
/* Checks non-zero value. */
buf[page_size - 2] = '.';
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0));
ASSERT_EQ(E2BIG, errno);
ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0));
ASSERT_EQ(E2BIG, errno);
free(buf);
}
TEST(abi_version)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1,
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION |
1 << 31));
ASSERT_EQ(EINVAL, errno);
}
/*
* Old source trees might not have the set of Kselftest fixes related to kernel
* UAPI headers.
*/
#ifndef LANDLOCK_CREATE_RULESET_ERRATA
#define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1)
#endif
TEST(errata)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
int errata;
errata = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_ERRATA);
/* The errata bitmask will not be backported to tests. */
ASSERT_LE(0, errata);
TH_LOG("errata: 0x%x", errata);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1,
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr),
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(
NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION |
LANDLOCK_CREATE_RULESET_ERRATA));
ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_ERRATA |
1 << 31));
ASSERT_EQ(EINVAL, errno);
}
/* Tests ordering of syscall argument checks. */
TEST(create_ruleset_checks_ordering)
{
const int last_flag = LANDLOCK_CREATE_RULESET_ERRATA;
const int invalid_flag = last_flag << 1;
int ruleset_fd;
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
/* Checks priority for invalid flags. */
ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0, invalid_flag));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, invalid_flag));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr),
invalid_flag));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1,
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr),
invalid_flag));
ASSERT_EQ(EINVAL, errno);
/* Checks too big ruleset_attr size. */
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, -1, 0));
ASSERT_EQ(E2BIG, errno);
/* Checks too small ruleset_attr size. */
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 1, 0));
ASSERT_EQ(EINVAL, errno);
/* Checks valid call. */
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
}
/* Tests ordering of syscall argument checks. */
TEST(add_rule_checks_ordering)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = -1,
};
const int ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
/* Checks invalid flags. */
ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100));
ASSERT_EQ(EINVAL, errno);
/* Checks invalid ruleset FD. */
ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 0));
ASSERT_EQ(EBADF, errno);
/* Checks invalid rule type. */
ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, 0, NULL, 0));
ASSERT_EQ(EINVAL, errno);
/* Checks invalid rule attr. */
ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
NULL, 0));
ASSERT_EQ(EFAULT, errno);
/* Checks invalid path_beneath.parent_fd. */
ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr, 0));
ASSERT_EQ(EBADF, errno);
/* Checks valid call. */
path_beneath_attr.parent_fd =
open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, path_beneath_attr.parent_fd);
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr, 0));
ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
ASSERT_EQ(0, close(ruleset_fd));
}
/* Tests ordering of syscall argument and permission checks. */
TEST(restrict_self_checks_ordering)
{
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
struct landlock_path_beneath_attr path_beneath_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = -1,
};
const int ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
path_beneath_attr.parent_fd =
open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, path_beneath_attr.parent_fd);
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr, 0));
ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
/* Checks unprivileged enforcement without no_new_privs. */
drop_caps(_metadata);
ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
/* Checks invalid flags. */
ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
ASSERT_EQ(EINVAL, errno);
/* Checks invalid ruleset FD. */
ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
ASSERT_EQ(EBADF, errno);
/* Checks valid call. */
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
ASSERT_EQ(0, close(ruleset_fd));
}
TEST(restrict_self_fd)
{
int fd;
fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, fd);
EXPECT_EQ(-1, landlock_restrict_self(fd, 0));
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_fd_logging_flags)
{
int fd;
fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, fd);
/*
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF accepts -1 but not any file
* descriptor.
*/
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADFD, errno);
}
TEST(restrict_self_logging_flags)
{
const __u32 last_flag = LANDLOCK_RESTRICT_SELF_TSYNC;
/* Tests invalid flag combinations. */
EXPECT_EQ(-1, landlock_restrict_self(-1, last_flag << 1));
EXPECT_EQ(EINVAL, errno);
EXPECT_EQ(-1, landlock_restrict_self(-1, -1));
EXPECT_EQ(EINVAL, errno);
/* Tests valid flag combinations. */
EXPECT_EQ(-1, landlock_restrict_self(-1, 0));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1,
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1,
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(-1,
landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
/* Tests with an invalid ruleset_fd. */
EXPECT_EQ(-1, landlock_restrict_self(
-2, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADF, errno);
EXPECT_EQ(0, landlock_restrict_self(
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
}
TEST(ruleset_fd_io)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
int ruleset_fd;
char buf;
drop_caps(_metadata);
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(-1, write(ruleset_fd, ".", 1));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, read(ruleset_fd, &buf, 1));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(0, close(ruleset_fd));
}
/* Tests enforcement of a ruleset FD transferred through a UNIX socket. */
TEST(ruleset_fd_transfer)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
struct landlock_path_beneath_attr path_beneath_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd_tx, dir_fd;
int socket_fds[2];
pid_t child;
int status;
drop_caps(_metadata);
/* Creates a test ruleset with a simple rule. */
ruleset_fd_tx =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd_tx);
path_beneath_attr.parent_fd =
open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, path_beneath_attr.parent_fd);
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd_tx, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr, 0));
ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
/* Sends the ruleset FD over a socketpair and then close it. */
ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0,
socket_fds));
ASSERT_EQ(0, send_fd(socket_fds[0], ruleset_fd_tx));
ASSERT_EQ(0, close(socket_fds[0]));
ASSERT_EQ(0, close(ruleset_fd_tx));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
const int ruleset_fd_rx = recv_fd(socket_fds[1]);
ASSERT_LE(0, ruleset_fd_rx);
ASSERT_EQ(0, close(socket_fds[1]));
/* Enforces the received ruleset on the child. */
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd_rx, 0));
ASSERT_EQ(0, close(ruleset_fd_rx));
/* Checks that the ruleset enforcement. */
ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
ASSERT_EQ(EACCES, errno);
dir_fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, dir_fd);
ASSERT_EQ(0, close(dir_fd));
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(0, close(socket_fds[1]));
/* Checks that the parent is unrestricted. */
dir_fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, dir_fd);
ASSERT_EQ(0, close(dir_fd));
dir_fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
ASSERT_LE(0, dir_fd);
ASSERT_EQ(0, close(dir_fd));
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(1, WIFEXITED(status));
ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
}
TEST(cred_transfer)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd, dir_fd;
pid_t child;
int status;
drop_caps(_metadata);
dir_fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
EXPECT_LE(0, dir_fd);
EXPECT_EQ(0, close(dir_fd));
/* Denies opening directories. */
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
EXPECT_EQ(0, close(ruleset_fd));
/* Checks ruleset enforcement. */
EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
EXPECT_EQ(EACCES, errno);
/* Needed for KEYCTL_SESSION_TO_PARENT permission checks */
EXPECT_NE(-1, syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL, 0,
0, 0))
{
TH_LOG("Failed to join session keyring: %s", strerror(errno));
}
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
/* Checks ruleset enforcement. */
EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
EXPECT_EQ(EACCES, errno);
/*
* KEYCTL_SESSION_TO_PARENT is a no-op unless we have a
* different session keyring in the child, so make that happen.
*/
EXPECT_NE(-1, syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING,
NULL, 0, 0, 0));
/*
* KEYCTL_SESSION_TO_PARENT installs credentials on the parent
* that never go through the cred_prepare hook, this path uses
* cred_transfer instead.
*/
EXPECT_EQ(0, syscall(__NR_keyctl, KEYCTL_SESSION_TO_PARENT, 0,
0, 0, 0));
/* Re-checks ruleset enforcement. */
EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
EXPECT_EQ(EACCES, errno);
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(child, waitpid(child, &status, 0));
EXPECT_EQ(1, WIFEXITED(status));
EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
/* Re-checks ruleset enforcement. */
EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
EXPECT_EQ(EACCES, errno);
}
TEST(useless_quiet_rule_fs)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
.quiet_access_fs = 0,
};
struct landlock_path_beneath_attr path_beneath_attr = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd, root_fd;
drop_caps(_metadata);
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
root_fd = open("/", O_PATH | O_CLOEXEC);
ASSERT_LE(0, root_fd);
path_beneath_attr.parent_fd = root_fd;
ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath_attr,
LANDLOCK_ADD_RULE_QUIET));
ASSERT_EQ(EINVAL, errno);
/* Check that the rule had not been added. */
ASSERT_EQ(0, close(root_fd));
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
ASSERT_EQ(EACCES, errno);
}
TEST(useless_quiet_rule_net)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
.quiet_access_net = 0,
};
struct landlock_net_port_attr net_port_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.port = 1024,
};
int ruleset_fd;
drop_caps(_metadata);
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
ASSERT_EQ(-1,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
&net_port_attr, LANDLOCK_ADD_RULE_QUIET));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(0, close(ruleset_fd));
}
TEST(invalid_quiet_bits_1)
{
const struct landlock_ruleset_attr ruleset_attr_fs = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
.quiet_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
};
const struct landlock_ruleset_attr ruleset_attr_net = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
.quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
};
const struct landlock_ruleset_attr ruleset_attr_scoped = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
.quiet_scoped = LANDLOCK_SCOPE_SIGNAL,
};
/* Quiet bit set but not part of the handled mask. */
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs,
sizeof(ruleset_attr_fs), 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net,
sizeof(ruleset_attr_net), 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped,
sizeof(ruleset_attr_scoped), 0));
ASSERT_EQ(EINVAL, errno);
}
TEST(invalid_quiet_bits_2)
{
const struct landlock_ruleset_attr ruleset_attr_fs = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
.quiet_access_fs = 1ULL << 63,
};
const struct landlock_ruleset_attr ruleset_attr_net = {
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
.quiet_access_net = 1ULL << 63,
};
const struct landlock_ruleset_attr ruleset_attr_scoped = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
.quiet_scoped = 1ULL << 63,
};
/* Quiet bit outside of the valid access range. */
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs,
sizeof(ruleset_attr_fs), 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net,
sizeof(ruleset_attr_net), 0));
ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped,
sizeof(ruleset_attr_scoped), 0));
ASSERT_EQ(EINVAL, errno);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,255 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock test helpers
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2021 Microsoft Corporation
*/
#include <arpa/inet.h>
#include <errno.h>
#include <linux/securebits.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include "kselftest_harness.h"
#include "wrappers.h"
#define TMP_DIR "tmp"
/* TEST_F_FORK() should not be used for new tests. */
#define TEST_F_FORK(fixture_name, test_name) TEST_F(fixture_name, test_name)
#define LANDLOCK_MAX_NUM_LAYERS 16
static const char bin_sandbox_and_launch[] = "./sandbox-and-launch";
static const char bin_wait_pipe[] = "./wait-pipe";
static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox";
static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)
{
cap_t cap_p;
/* Only these three capabilities are useful for the tests. */
const cap_value_t caps[] = {
/* clang-format off */
CAP_AUDIT_CONTROL,
CAP_DAC_OVERRIDE,
CAP_MKNOD,
CAP_NET_ADMIN,
CAP_NET_BIND_SERVICE,
CAP_SETUID,
CAP_SYS_ADMIN,
CAP_SYS_CHROOT,
/* clang-format on */
};
const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED;
if ((cap_get_secbits() & noroot) != noroot)
EXPECT_EQ(0, cap_set_secbits(noroot));
cap_p = cap_get_proc();
EXPECT_NE(NULL, cap_p);
EXPECT_NE(-1, cap_clear(cap_p));
if (!drop_all) {
EXPECT_NE(-1, cap_set_flag(cap_p, CAP_PERMITTED,
ARRAY_SIZE(caps), caps, CAP_SET));
}
/* Automatically resets ambient capabilities. */
EXPECT_NE(-1, cap_set_proc(cap_p))
{
TH_LOG("Failed to set capabilities: %s", strerror(errno));
}
EXPECT_NE(-1, cap_free(cap_p));
/* Quickly checks that ambient capabilities are cleared. */
EXPECT_NE(-1, cap_get_ambient(caps[0]));
}
/* We cannot put such helpers in a library because of kselftest_harness.h . */
static void __maybe_unused disable_caps(struct __test_metadata *const _metadata)
{
_init_caps(_metadata, false);
}
static void __maybe_unused drop_caps(struct __test_metadata *const _metadata)
{
_init_caps(_metadata, true);
}
static void _change_cap(struct __test_metadata *const _metadata,
const cap_flag_t flag, const cap_value_t cap,
const cap_flag_value_t value)
{
cap_t cap_p;
cap_p = cap_get_proc();
EXPECT_NE(NULL, cap_p);
EXPECT_NE(-1, cap_set_flag(cap_p, flag, 1, &cap, value));
EXPECT_NE(-1, cap_set_proc(cap_p))
{
TH_LOG("Failed to set capability %d: %s", cap, strerror(errno));
}
EXPECT_NE(-1, cap_free(cap_p));
}
static void __maybe_unused set_cap(struct __test_metadata *const _metadata,
const cap_value_t cap)
{
_change_cap(_metadata, CAP_EFFECTIVE, cap, CAP_SET);
}
static void __maybe_unused clear_cap(struct __test_metadata *const _metadata,
const cap_value_t cap)
{
_change_cap(_metadata, CAP_EFFECTIVE, cap, CAP_CLEAR);
}
static void __maybe_unused
set_ambient_cap(struct __test_metadata *const _metadata, const cap_value_t cap)
{
_change_cap(_metadata, CAP_INHERITABLE, cap, CAP_SET);
EXPECT_NE(-1, cap_set_ambient(cap, CAP_SET))
{
TH_LOG("Failed to set ambient capability %d: %s", cap,
strerror(errno));
}
}
static void __maybe_unused clear_ambient_cap(
struct __test_metadata *const _metadata, const cap_value_t cap)
{
EXPECT_EQ(1, cap_get_ambient(cap));
_change_cap(_metadata, CAP_INHERITABLE, cap, CAP_CLEAR);
EXPECT_EQ(0, cap_get_ambient(cap));
}
/* Receives an FD from a UNIX socket. Returns the received FD, or -errno. */
static int __maybe_unused recv_fd(int usock)
{
int fd_rx;
union {
/* Aligned ancillary data buffer. */
char buf[CMSG_SPACE(sizeof(fd_rx))];
struct cmsghdr _align;
} cmsg_rx = {};
char data = '\0';
struct iovec io = {
.iov_base = &data,
.iov_len = sizeof(data),
};
struct msghdr msg = {
.msg_iov = &io,
.msg_iovlen = 1,
.msg_control = &cmsg_rx.buf,
.msg_controllen = sizeof(cmsg_rx.buf),
};
struct cmsghdr *cmsg;
int res;
res = recvmsg(usock, &msg, MSG_CMSG_CLOEXEC);
if (res < 0)
return -errno;
cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg->cmsg_len != CMSG_LEN(sizeof(fd_rx)))
return -EIO;
memcpy(&fd_rx, CMSG_DATA(cmsg), sizeof(fd_rx));
return fd_rx;
}
/* Sends an FD on a UNIX socket. Returns 0 on success or -errno. */
static int __maybe_unused send_fd(int usock, int fd_tx)
{
union {
/* Aligned ancillary data buffer. */
char buf[CMSG_SPACE(sizeof(fd_tx))];
struct cmsghdr _align;
} cmsg_tx = {};
char data_tx = '.';
struct iovec io = {
.iov_base = &data_tx,
.iov_len = sizeof(data_tx),
};
struct msghdr msg = {
.msg_iov = &io,
.msg_iovlen = 1,
.msg_control = &cmsg_tx.buf,
.msg_controllen = sizeof(cmsg_tx.buf),
};
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(fd_tx));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
memcpy(CMSG_DATA(cmsg), &fd_tx, sizeof(fd_tx));
if (sendmsg(usock, &msg, 0) < 0)
return -errno;
return 0;
}
static void __maybe_unused
enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd)
{
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0))
{
TH_LOG("Failed to enforce ruleset: %s", strerror(errno));
}
}
static void __maybe_unused
drop_access_rights(struct __test_metadata *const _metadata,
const struct landlock_ruleset_attr *const ruleset_attr)
{
int ruleset_fd;
ruleset_fd =
landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0);
EXPECT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
struct protocol_variant {
int domain;
int type;
int protocol;
};
struct service_fixture {
struct protocol_variant protocol;
/* port is also stored in ipv4_addr.sin_port or ipv6_addr.sin6_port */
unsigned short port;
union {
struct sockaddr_in ipv4_addr;
struct sockaddr_in6 ipv6_addr;
struct {
struct sockaddr_un unix_addr;
socklen_t unix_addr_len;
};
struct sockaddr_storage _largest;
};
};
static void __maybe_unused set_unix_address(struct service_fixture *const srv,
const unsigned short index)
{
srv->unix_addr.sun_family = AF_UNIX;
sprintf(srv->unix_addr.sun_path,
"_selftests-landlock-abstract-unix-tid%d-index%d", sys_gettid(),
index);
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}
@@ -0,0 +1,19 @@
CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_KEYS=y
CONFIG_MPTCP=y
CONFIG_MPTCP_IPV6=y
CONFIG_NET=y
CONFIG_NET_NS=y
CONFIG_OVERLAY_FS=y
CONFIG_PROC_FS=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y
CONFIG_SHMEM=y
CONFIG_SYSFS=y
CONFIG_TMPFS=y
CONFIG_TMPFS_XATTR=y
@@ -0,0 +1 @@
CONFIG_HOSTFS=y
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock filesystem benchmark
*
* This program benchmarks the time required for file access checks. We use a
* large number (-d flag) of nested directories where each directory inode has
* an associated Landlock rule, and we repeatedly (-n flag) exercise a file
* access for which Landlock has to walk the path all the way up to the root.
*
* With an increasing number of nested subdirectories, Landlock's portion of the
* overall system call time increases, which makes the effects of Landlock
* refactorings more measurable.
*
* This benchmark does *not* measure the building of the Landlock ruleset. The
* time required to add all these rules is not large enough to be easily
* measurable. A separate benchmark tool would be better to test that, and that
* tool could then also use a simpler file system layout.
*
* Copyright © 2026 Google LLC
*/
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <linux/prctl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/times.h>
#include <time.h>
#include <unistd.h>
#include "wrappers.h"
static void usage(const char *const argv0)
{
printf("Usage:\n");
printf(" %s [OPTIONS]\n", argv0);
printf("\n");
printf(" Benchmark expensive Landlock checks for D nested dirs\n");
printf("\n");
printf("Options:\n");
printf(" -h help\n");
printf(" -L disable Landlock (as a baseline)\n");
printf(" -d D set directory depth to D\n");
printf(" -n N set number of benchmark iterations to N\n");
}
/*
* Build a deep directory, enforce Landlock and return the FD to the
* deepest dir. On any failure, exit the process with an error.
*/
static int build_directory(size_t depth, const bool use_landlock)
{
const char *path = "d"; /* directory name */
int abi, ruleset_fd, curr, prev;
if (use_landlock) {
abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 7)
err(1, "Landlock ABI too low: got %d, wanted 7+", abi);
}
ruleset_fd = -1;
if (use_landlock) {
struct landlock_ruleset_attr attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG,
};
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0U);
if (ruleset_fd < 0)
err(1, "landlock_create_ruleset");
}
curr = open(".", O_PATH);
if (curr < 0)
err(1, "open(.)");
while (depth--) {
if (use_landlock) {
struct landlock_path_beneath_attr attr = {
.allowed_access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
.parent_fd = curr,
};
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &attr,
0) < 0)
err(1, "landlock_add_rule");
}
if (mkdirat(curr, path, 0700) < 0)
err(1, "mkdirat(%s)", path);
prev = curr;
curr = openat(curr, path, O_PATH);
if (curr < 0)
err(1, "openat(%s)", path);
close(prev);
}
if (use_landlock) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
err(1, "prctl");
if (landlock_restrict_self(ruleset_fd, 0) < 0)
err(1, "landlock_restrict_self");
}
close(ruleset_fd);
return curr;
}
static void remove_recursively(const size_t depth)
{
const char *path = "d"; /* directory name */
int fd = openat(AT_FDCWD, ".", O_PATH);
if (fd < 0)
err(1, "openat(.)");
for (size_t i = 0; i < depth - 1; i++) {
int oldfd = fd;
fd = openat(fd, path, O_PATH);
if (fd < 0)
err(1, "openat(%s)", path);
close(oldfd);
}
for (size_t i = 0; i < depth; i++) {
if (unlinkat(fd, path, AT_REMOVEDIR) < 0)
err(1, "unlinkat(%s)", path);
int newfd = openat(fd, "..", O_PATH);
close(fd);
fd = newfd;
}
close(fd);
}
int main(int argc, char *argv[])
{
bool use_landlock = true;
size_t num_iterations = 100000;
size_t num_subdirs = 10000;
int c, curr, fd;
struct tms start_time, end_time;
setbuf(stdout, NULL);
while ((c = getopt(argc, argv, "hLd:n:")) != -1) {
switch (c) {
case 'h':
usage(argv[0]);
return EXIT_SUCCESS;
case 'L':
use_landlock = false;
break;
case 'd':
num_subdirs = atoi(optarg);
break;
case 'n':
num_iterations = atoi(optarg);
break;
default:
usage(argv[0]);
return EXIT_FAILURE;
}
}
printf("*** Benchmark ***\n");
printf("%zu dirs, %zu iterations, %s Landlock\n", num_subdirs,
num_iterations, use_landlock ? "with" : "without");
if (times(&start_time) == -1)
err(1, "times");
curr = build_directory(num_subdirs, use_landlock);
for (int i = 0; i < num_iterations; i++) {
fd = openat(curr, "file.txt", O_CREAT | O_TRUNC | O_WRONLY,
0600);
if (use_landlock) {
if (fd == 0)
errx(1, "openat succeeded, expected EACCES");
if (errno != EACCES)
err(1, "openat expected EACCES, but got");
}
if (fd != -1)
close(fd);
}
if (times(&end_time) == -1)
err(1, "times");
printf("*** Benchmark concluded ***\n");
printf("System: %ld clocks\n",
end_time.tms_stime - start_time.tms_stime);
printf("User : %ld clocks\n",
end_time.tms_utime - start_time.tms_utime);
printf("Clocks per second: %ld\n", CLOCKS_PER_SEC);
close(curr);
remove_recursively(num_subdirs);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,433 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Ptrace
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "audit.h"
#include "common.h"
/* Copied from security/yama/yama_lsm.c */
#define YAMA_SCOPE_DISABLED 0
#define YAMA_SCOPE_RELATIONAL 1
static void create_domain(struct __test_metadata *const _metadata)
{
int ruleset_fd;
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK,
};
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
EXPECT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
EXPECT_EQ(0, close(ruleset_fd));
}
static int test_ptrace_read(const pid_t pid)
{
static const char path_template[] = "/proc/%d/environ";
char procenv_path[sizeof(path_template) + 10];
int procenv_path_size, fd;
procenv_path_size = snprintf(procenv_path, sizeof(procenv_path),
path_template, pid);
if (procenv_path_size >= sizeof(procenv_path))
return E2BIG;
fd = open(procenv_path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return errno;
/*
* Mixing error codes from close(2) and open(2) should not lead to any
* (access type) confusion for this test.
*/
if (close(fd) != 0)
return errno;
return 0;
}
static int get_yama_ptrace_scope(void)
{
int ret;
char buf[2] = {};
const int fd = open("/proc/sys/kernel/yama/ptrace_scope", O_RDONLY);
if (fd < 0)
return 0;
if (read(fd, buf, 1) < 0) {
close(fd);
return -1;
}
ret = atoi(buf);
close(fd);
return ret;
}
/* clang-format off */
FIXTURE(scoped_domains) {};
/* clang-format on */
/*
* Test multiple tracing combinations between a parent process P1 and a child
* process P2.
*
* Yama's scoped ptrace is presumed disabled. If enabled, this optional
* restriction is enforced in addition to any Landlock check, which means that
* all P2 requests to trace P1 would be denied.
*/
#include "scoped_base_variants.h"
FIXTURE_SETUP(scoped_domains)
{
}
FIXTURE_TEARDOWN(scoped_domains)
{
}
/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(scoped_domains, trace)
{
pid_t child, parent;
int status, err_proc_read;
int pipe_child[2], pipe_parent[2];
int yama_ptrace_scope;
char buf_parent;
long ret;
bool can_read_child, can_trace_child, can_read_parent, can_trace_parent;
yama_ptrace_scope = get_yama_ptrace_scope();
ASSERT_LE(0, yama_ptrace_scope);
if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
yama_ptrace_scope);
/*
* can_read_child is true if a parent process can read its child
* process, which is only the case when the parent process is not
* isolated from the child with a dedicated Landlock domain.
*/
can_read_child = !variant->domain_parent;
/*
* can_trace_child is true if a parent process can trace its child
* process. This depends on two conditions:
* - The parent process is not isolated from the child with a dedicated
* Landlock domain.
* - Yama allows tracing children (up to YAMA_SCOPE_RELATIONAL).
*/
can_trace_child = can_read_child &&
yama_ptrace_scope <= YAMA_SCOPE_RELATIONAL;
/*
* can_read_parent is true if a child process can read its parent
* process, which is only the case when the child process is not
* isolated from the parent with a dedicated Landlock domain.
*/
can_read_parent = !variant->domain_child;
/*
* can_trace_parent is true if a child process can trace its parent
* process. This depends on two conditions:
* - The child process is not isolated from the parent with a dedicated
* Landlock domain.
* - Yama is disabled (YAMA_SCOPE_DISABLED).
*/
can_trace_parent = can_read_parent &&
yama_ptrace_scope <= YAMA_SCOPE_DISABLED;
/*
* Removes all effective and permitted capabilities to not interfere
* with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
*/
drop_caps(_metadata);
parent = getpid();
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
if (variant->domain_both) {
create_domain(_metadata);
if (!__test_passed(_metadata))
/* Aborts before forking. */
return;
}
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
ASSERT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(0, close(pipe_child[0]));
if (variant->domain_child)
create_domain(_metadata);
/* Waits for the parent to be in a domain, if any. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
/* Tests PTRACE_MODE_READ on the parent. */
err_proc_read = test_ptrace_read(parent);
if (can_read_parent) {
EXPECT_EQ(0, err_proc_read);
} else {
EXPECT_EQ(EACCES, err_proc_read);
}
/* Tests PTRACE_ATTACH on the parent. */
ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
if (can_trace_parent) {
EXPECT_EQ(0, ret);
} else {
EXPECT_EQ(-1, ret);
EXPECT_EQ(EPERM, errno);
}
if (ret == 0) {
ASSERT_EQ(parent, waitpid(parent, &status, 0));
ASSERT_EQ(1, WIFSTOPPED(status));
ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0));
}
/* Tests child PTRACE_TRACEME. */
ret = ptrace(PTRACE_TRACEME);
if (can_trace_child) {
EXPECT_EQ(0, ret);
} else {
EXPECT_EQ(-1, ret);
EXPECT_EQ(EPERM, errno);
}
/*
* Signals that the PTRACE_ATTACH test is done and the
* PTRACE_TRACEME test is ongoing.
*/
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
if (can_trace_child) {
ASSERT_EQ(0, raise(SIGSTOP));
}
/* Waits for the parent PTRACE_ATTACH test. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(0, close(pipe_child[1]));
ASSERT_EQ(0, close(pipe_parent[0]));
if (variant->domain_parent)
create_domain(_metadata);
/* Signals that the parent is in a domain, if any. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/*
* Waits for the child to test PTRACE_ATTACH on the parent and start
* testing PTRACE_TRACEME.
*/
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests child PTRACE_TRACEME. */
if (can_trace_child) {
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(1, WIFSTOPPED(status));
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
} else {
/* The child should not be traced by the parent. */
EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
EXPECT_EQ(ESRCH, errno);
}
/* Tests PTRACE_MODE_READ on the child. */
err_proc_read = test_ptrace_read(child);
if (can_read_child) {
EXPECT_EQ(0, err_proc_read);
} else {
EXPECT_EQ(EACCES, err_proc_read);
}
/* Tests PTRACE_ATTACH on the child. */
ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
if (can_trace_child) {
EXPECT_EQ(0, ret);
} else {
EXPECT_EQ(-1, ret);
EXPECT_EQ(EPERM, errno);
}
if (ret == 0) {
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(1, WIFSTOPPED(status));
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
}
/* Signals that the parent PTRACE_ATTACH test is done. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
static int matches_log_ptrace(struct __test_metadata *const _metadata,
int audit_fd, const pid_t opid)
{
static const char log_template[] = REGEX_LANDLOCK_PREFIX
" blockers=ptrace opid=%d ocomm=\"ptrace_test\"$";
char log_match[sizeof(log_template) + 10];
int log_match_len;
log_match_len =
snprintf(log_match, sizeof(log_match), log_template, opid);
if (log_match_len > sizeof(log_match))
return -E2BIG;
return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
NULL);
}
FIXTURE(audit)
{
struct audit_filter audit_filter;
int audit_fd;
};
FIXTURE_SETUP(audit)
{
disable_caps(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
EXPECT_LE(0, self->audit_fd);
clear_cap(_metadata, CAP_AUDIT_CONTROL);
}
FIXTURE_TEARDOWN_PARENT(audit)
{
EXPECT_EQ(0, audit_cleanup(-1, NULL));
}
/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
TEST_F(audit, trace)
{
pid_t child;
int status;
int pipe_child[2], pipe_parent[2];
int yama_ptrace_scope;
char buf_parent;
struct audit_records records;
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
yama_ptrace_scope = get_yama_ptrace_scope();
ASSERT_LE(0, yama_ptrace_scope);
if (yama_ptrace_scope > YAMA_SCOPE_DISABLED)
TH_LOG("Incomplete tests due to Yama restrictions (scope %d)",
yama_ptrace_scope);
/*
* Removes all effective and permitted capabilities to not interfere
* with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS.
*/
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
ASSERT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(0, close(pipe_child[0]));
/* Waits for the parent to be in a domain, if any. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
/* Tests child PTRACE_TRACEME. */
EXPECT_EQ(-1, ptrace(PTRACE_TRACEME));
EXPECT_EQ(EPERM, errno);
/* We should see the child process. */
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd,
getpid()));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
/* Checks for a domain creation. */
EXPECT_EQ(1, records.domain);
/*
* Signals that the PTRACE_ATTACH test is done and the
* PTRACE_TRACEME test is ongoing.
*/
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
/* Waits for the parent PTRACE_ATTACH test. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
_exit(_metadata->exit_code);
return;
}
ASSERT_EQ(0, close(pipe_child[1]));
ASSERT_EQ(0, close(pipe_parent[0]));
create_domain(_metadata);
/* Signals that the parent is in a domain. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/*
* Waits for the child to test PTRACE_ATTACH on the parent and start
* testing PTRACE_TRACEME.
*/
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* The child should not be traced by the parent. */
EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
EXPECT_EQ(ESRCH, errno);
/* Tests PTRACE_ATTACH on the child. */
EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0));
EXPECT_EQ(EPERM, errno);
EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child));
/* Signals that the parent PTRACE_ATTACH test is done. */
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
/* Makes sure there is no superfluous logged records. */
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(0, records.domain);
}
TEST_HARNESS_MAIN
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Sandbox itself and execute another program (in a different mount point).
*
* Used by layout1.umount_sandboxer from fs_test.c
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>
#include "wrappers.h"
int main(int argc, char *argv[])
{
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int pipe_child, pipe_parent, ruleset_fd;
char buf;
/*
* The first argument must be the file descriptor number of a pipe.
* The second argument must be the program to execute.
*/
if (argc != 4) {
fprintf(stderr, "Wrong number of arguments (not three)\n");
return 1;
}
pipe_child = atoi(argv[2]);
pipe_parent = atoi(argv[3]);
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror("Failed to create ruleset");
return 1;
}
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to call prctl()");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to restrict self");
return 1;
}
if (close(ruleset_fd)) {
perror("Failed to close ruleset");
return 1;
}
/* Signals that we are sandboxed. */
errno = 0;
if (write(pipe_child, ".", 1) != 1) {
perror("Failed to write to the second argument");
return 1;
}
/* Waits for the parent to try to umount. */
if (read(pipe_parent, &buf, 1) != 1) {
perror("Failed to write to the third argument");
return 1;
}
/* Shifts arguments. */
argv[0] = argv[1];
argv[1] = argv[2];
argv[2] = argv[3];
argv[3] = NULL;
execve(argv[0], argv, NULL);
perror("Failed to execute the provided binary");
return 1;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,161 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock scoped_domains test variant definition.
*
* This file defines a fixture variant "scoped_domains" that has all
* permutations of parent/child process being in separate or shared
* Landlock domain, or not being in a Landlock domain at all.
*
* Scoped access tests can include this file to avoid repeating these
* combinations.
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
/* clang-format on */
FIXTURE_VARIANT(scoped_domains)
{
bool domain_both;
bool domain_parent;
bool domain_child;
};
/*
* No domain
*
* P1-. P1 -> P2 : allow
* \ P2 -> P1 : allow
* 'P2
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, without_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = false,
.domain_child = false,
};
/*
* Child domain
*
* P1--. P1 -> P2 : allow
* \ P2 -> P1 : deny
* .'-----.
* | P2 |
* '------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, child_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = false,
.domain_child = true,
};
/*
* Parent domain
* .------.
* | P1 --. P1 -> P2 : deny
* '------' \ P2 -> P1 : allow
* '
* P2
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, parent_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = true,
.domain_child = false,
};
/*
* Parent + child domain (siblings)
* .------.
* | P1 ---. P1 -> P2 : deny
* '------' \ P2 -> P1 : deny
* .---'--.
* | P2 |
* '------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, sibling_domain) {
/* clang-format on */
.domain_both = false,
.domain_parent = true,
.domain_child = true,
};
/*
* Same domain (inherited)
* .-------------.
* | P1----. | P1 -> P2 : allow
* | \ | P2 -> P1 : allow
* | ' |
* | P2 |
* '-------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, inherited_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = false,
.domain_child = false,
};
/*
* Inherited + child domain
* .-----------------.
* | P1----. | P1 -> P2 : allow
* | \ | P2 -> P1 : deny
* | .-'----. |
* | | P2 | |
* | '------' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, nested_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = false,
.domain_child = true,
};
/*
* Inherited + parent domain
* .-----------------.
* |.------. | P1 -> P2 : deny
* || P1 ----. | P2 -> P1 : allow
* |'------' \ |
* | ' |
* | P2 |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, nested_and_parent_domain) {
/* clang-format on */
.domain_both = true,
.domain_parent = true,
.domain_child = false,
};
/*
* Inherited + parent and child domain (siblings)
* .-----------------.
* | .------. | P1 -> P2 : deny
* | | P1 . | P2 -> P1 : deny
* | '------'\ |
* | \ |
* | .--'---. |
* | | P2 | |
* | '------' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_domains, forked_domains) {
/* clang-format on */
.domain_both = true,
.domain_parent = true,
.domain_child = true,
};
@@ -0,0 +1,28 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock scope test helpers
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <sys/types.h>
static void create_scoped_domain(struct __test_metadata *const _metadata,
const __u16 scope)
{
int ruleset_fd;
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = scope,
};
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd)
{
TH_LOG("Failed to create a ruleset: %s", strerror(errno));
}
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -0,0 +1,152 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Landlock variants for three processes with various domains.
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
enum sandbox_type {
NO_SANDBOX,
SCOPE_SANDBOX,
/* Any other type of sandboxing domain */
OTHER_SANDBOX,
};
/* clang-format on */
FIXTURE_VARIANT(scoped_vs_unscoped)
{
const int domain_all;
const int domain_parent;
const int domain_children;
const int domain_child;
const int domain_grand_child;
};
/*
* .-----------------.
* | ####### | P3 -> P2 : allow
* | P1----# P2 # | P3 -> P1 : deny
* | # | # |
* | # P3 # |
* | ####### |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_scoped) {
.domain_all = OTHER_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = SCOPE_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###################
* # ####### # P3 -> P2 : allow
* # P1----# P2 # # P3 -> P1 : deny
* # # | # #
* # # P3 # #
* # ####### #
* ###################
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, all_scoped) {
.domain_all = SCOPE_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = SCOPE_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* .-----------------.
* | .-----. | P3 -> P2 : allow
* | P1----| P2 | | P3 -> P1 : allow
* | | | |
* | | P3 | |
* | '-----' |
* '-----------------'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_other_domain) {
.domain_all = OTHER_SANDBOX,
.domain_parent = NO_SANDBOX,
.domain_children = OTHER_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* .----. ###### P3 -> P2 : allow
* | P1 |----# P2 # P3 -> P1 : allow
* '----' ######
* |
* P3
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_one_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = OTHER_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = SCOPE_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### .-----. P3 -> P2 : allow
* # P1 #----| P2 | P3 -> P1 : allow
* ###### '-----'
* |
* P3
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_grand_parent_scoped) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = OTHER_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### ###### P3 -> P2 : allow
* # P1 #----# P2 # P3 -> P1 : allow
* ###### ######
* |
* .----.
* | P3 |
* '----'
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_parents_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = SCOPE_SANDBOX,
.domain_grand_child = NO_SANDBOX,
/* clang-format on */
};
/*
* ###### P3 -> P2 : deny
* # P1 #----P2 P3 -> P1 : deny
* ###### |
* |
* ######
* # P3 #
* ######
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_with_self_and_grandparent_domain) {
.domain_all = NO_SANDBOX,
.domain_parent = SCOPE_SANDBOX,
.domain_children = NO_SANDBOX,
.domain_child = NO_SANDBOX,
.domain_grand_child = SCOPE_SANDBOX,
/* clang-format on */
};
@@ -0,0 +1,765 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Signal Scoping
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "common.h"
#include "scoped_common.h"
/* This variable is used for handling several signals. */
static volatile sig_atomic_t is_signaled;
/* clang-format off */
FIXTURE(scoping_signals) {};
/* clang-format on */
FIXTURE_VARIANT(scoping_signals)
{
int sig;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigtrap) {
/* clang-format on */
.sig = SIGTRAP,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigurg) {
/* clang-format on */
.sig = SIGURG,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sighup) {
/* clang-format on */
.sig = SIGHUP,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(scoping_signals, sigtstp) {
/* clang-format on */
.sig = SIGTSTP,
};
FIXTURE_SETUP(scoping_signals)
{
drop_caps(_metadata);
is_signaled = 0;
}
FIXTURE_TEARDOWN(scoping_signals)
{
}
static void scope_signal_handler(int sig, siginfo_t *info, void *ucontext)
{
if (sig == SIGTRAP || sig == SIGURG || sig == SIGHUP || sig == SIGTSTP)
is_signaled = 1;
}
/*
* In this test, a child process sends a signal to parent before and
* after getting scoped.
*/
TEST_F(scoping_signals, send_sig_to_parent)
{
int pipe_parent[2];
int status;
pid_t child;
pid_t parent = getpid();
struct sigaction action = {
.sa_sigaction = scope_signal_handler,
.sa_flags = SA_SIGINFO,
};
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_LE(0, sigaction(variant->sig, &action, NULL));
/* The process should not have already been signaled. */
EXPECT_EQ(0, is_signaled);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
int err;
EXPECT_EQ(0, close(pipe_parent[1]));
/*
* The child process can send signal to parent when
* domain is not scoped.
*/
err = kill(parent, variant->sig);
ASSERT_EQ(0, err);
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* The child process cannot send signal to the parent
* anymore.
*/
err = kill(parent, variant->sig);
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
/*
* No matter of the domain, a process should be able to
* send a signal to itself.
*/
ASSERT_EQ(0, is_signaled);
ASSERT_EQ(0, raise(variant->sig));
ASSERT_EQ(1, is_signaled);
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
/* Waits for a first signal to be received, without race condition. */
while (!is_signaled && !usleep(1))
;
ASSERT_EQ(1, is_signaled);
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
is_signaled = 0;
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
EXPECT_EQ(0, is_signaled);
}
/* clang-format off */
FIXTURE(scoped_domains) {};
/* clang-format on */
#include "scoped_base_variants.h"
FIXTURE_SETUP(scoped_domains)
{
drop_caps(_metadata);
}
FIXTURE_TEARDOWN(scoped_domains)
{
}
/*
* This test ensures that a scoped process cannot send signal out of
* scoped domain.
*/
TEST_F(scoped_domains, check_access_signal)
{
pid_t child;
pid_t parent = getpid();
int status;
bool can_signal_child, can_signal_parent;
int pipe_parent[2], pipe_child[2];
char buf_parent;
int err;
can_signal_parent = !variant->domain_child;
can_signal_child = !variant->domain_parent;
if (variant->domain_both)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char buf_child;
EXPECT_EQ(0, close(pipe_child[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
if (variant->domain_child)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
/* Waits for the parent to send signals. */
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
err = kill(parent, 0);
if (can_signal_parent) {
ASSERT_EQ(0, err);
} else {
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
}
/*
* No matter of the domain, a process should be able to
* send a signal to itself.
*/
ASSERT_EQ(0, raise(0));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
if (variant->domain_parent)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
err = kill(child, 0);
if (can_signal_child) {
ASSERT_EQ(0, err);
} else {
ASSERT_EQ(-1, err);
ASSERT_EQ(EPERM, errno);
}
ASSERT_EQ(0, raise(0));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
/* clang-format off */
#define THREAD_INVALID ((void *)0)
#define THREAD_SUCCESS ((void *)1)
#define THREAD_ERROR ((void *)2)
#define THREAD_TEST_FAILED ((void *)3)
/* clang-format on */
static void *thread_sync(void *arg)
{
const int pipe_read = *(int *)arg;
char buf;
if (read(pipe_read, &buf, 1) != 1)
return THREAD_ERROR;
return THREAD_SUCCESS;
}
TEST(signal_scoping_thread_before)
{
pthread_t no_sandbox_thread;
void *ret = THREAD_INVALID;
int thread_pipe[2];
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC));
ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_sync,
&thread_pipe[0]));
/* Enforces restriction after creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
EXPECT_EQ(0, pthread_kill(no_sandbox_thread, 0));
EXPECT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_join(no_sandbox_thread, &ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
EXPECT_EQ(0, close(thread_pipe[0]));
EXPECT_EQ(0, close(thread_pipe[1]));
}
TEST(signal_scoping_thread_after)
{
pthread_t scoped_thread;
void *ret = THREAD_INVALID;
int thread_pipe[2];
drop_caps(_metadata);
ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC));
/* Enforces restriction before creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_sync,
&thread_pipe[0]));
EXPECT_EQ(0, pthread_kill(scoped_thread, 0));
EXPECT_EQ(1, write(thread_pipe[1], ".", 1));
EXPECT_EQ(0, pthread_join(scoped_thread, &ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
EXPECT_EQ(0, close(thread_pipe[0]));
EXPECT_EQ(0, close(thread_pipe[1]));
}
struct thread_setuid_args {
int pipe_read, new_uid;
};
void *thread_setuid(void *ptr)
{
const struct thread_setuid_args *arg = ptr;
char buf;
if (read(arg->pipe_read, &buf, 1) != 1)
return THREAD_ERROR;
/* libc's setuid() should update all thread's credentials. */
if (getuid() != arg->new_uid)
return THREAD_TEST_FAILED;
return THREAD_SUCCESS;
}
TEST(signal_scoping_thread_setuid)
{
struct thread_setuid_args arg;
pthread_t no_sandbox_thread;
void *ret = THREAD_INVALID;
int pipe_parent[2];
int prev_uid;
disable_caps(_metadata);
/* This test does not need to be run as root. */
prev_uid = getuid();
arg.new_uid = prev_uid + 1;
EXPECT_LT(0, arg.new_uid);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
arg.pipe_read = pipe_parent[0];
/* Capabilities must be set before creating a new thread. */
set_cap(_metadata, CAP_SETUID);
ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_setuid,
&arg));
/* Enforces restriction after creating the thread. */
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
EXPECT_NE(arg.new_uid, getuid());
EXPECT_EQ(0, setuid(arg.new_uid));
EXPECT_EQ(arg.new_uid, getuid());
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, pthread_join(no_sandbox_thread, &ret));
EXPECT_EQ(THREAD_SUCCESS, ret);
clear_cap(_metadata, CAP_SETUID);
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
}
const short backlog = 10;
static volatile sig_atomic_t signal_received;
static void handle_sigurg(int sig)
{
if (sig == SIGURG)
signal_received = 1;
else
signal_received = -1;
}
static int setup_signal_handler(int signal)
{
struct sigaction sa = {
.sa_handler = handle_sigurg,
};
if (sigemptyset(&sa.sa_mask))
return -1;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
return sigaction(SIGURG, &sa, NULL);
}
/*
* MSG_OOB might be disabled in the kernel via the CONFIG_AF_UNIX_OOB
* switch, so this function can be used for probing for its availability.
*/
static bool has_af_unix_oob(void)
{
bool available = false;
int sp[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0) {
available = (send(sp[0], ".", 1, MSG_OOB) == 1);
close(sp[0]);
close(sp[1]);
}
return available;
}
/* clang-format off */
FIXTURE(fown) {};
/* clang-format on */
enum fown_sandbox {
SANDBOX_NONE,
SANDBOX_BEFORE_FORK,
SANDBOX_BEFORE_SETOWN,
SANDBOX_AFTER_SETOWN,
};
FIXTURE_VARIANT(fown)
{
const enum fown_sandbox sandbox_setown;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, no_sandbox) {
/* clang-format on */
.sandbox_setown = SANDBOX_NONE,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_before_fork) {
/* clang-format on */
.sandbox_setown = SANDBOX_BEFORE_FORK,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_before_setown) {
/* clang-format on */
.sandbox_setown = SANDBOX_BEFORE_SETOWN,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(fown, sandbox_after_setown) {
/* clang-format on */
.sandbox_setown = SANDBOX_AFTER_SETOWN,
};
FIXTURE_SETUP(fown)
{
drop_caps(_metadata);
}
FIXTURE_TEARDOWN(fown)
{
}
/*
* Sending an out of bound message will trigger the SIGURG signal
* through file_send_sigiotask.
*/
TEST_F(fown, sigurg_socket)
{
int server_socket, recv_socket;
struct service_fixture server_address;
char buffer_parent;
int status;
int pipe_parent[2], pipe_child[2];
pid_t child;
if (!has_af_unix_oob())
SKIP(return, "CONFIG_AF_UNIX_OOB / MSG_OOB not available");
memset(&server_address, 0, sizeof(server_address));
set_unix_address(&server_address, 0);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
if (variant->sandbox_setown == SANDBOX_BEFORE_FORK)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_socket;
char buffer_child;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(0, setup_signal_handler(SIGURG));
client_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, client_socket);
/* Waits for the parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr,
server_address.unix_addr_len));
/*
* Waits for the parent to accept the connection, sandbox
* itself, and call fcntl(2).
*/
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
/* May signal itself. */
ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB));
EXPECT_EQ(0, close(client_socket));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
/* Waits for the message to be received. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
EXPECT_EQ(0, close(pipe_parent[0]));
if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) {
ASSERT_EQ(0, signal_received);
} else {
/*
* A signal is only received if fcntl(F_SETOWN) was
* called before any sandboxing or if the signal
* receiver is in the same domain.
*/
ASSERT_EQ(1, signal_received);
}
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
server_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, server_socket);
ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr,
server_address.unix_addr_len));
ASSERT_EQ(0, listen(server_socket, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
recv_socket = accept(server_socket, NULL, NULL);
ASSERT_LE(0, recv_socket);
if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* Sets the child to receive SIGURG for MSG_OOB. This uncommon use is
* a valid attack scenario which also simplifies this test.
*/
ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child));
if (variant->sandbox_setown == SANDBOX_AFTER_SETOWN)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for the child to send MSG_OOB. */
ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB));
EXPECT_EQ(0, close(recv_socket));
EXPECT_EQ(0, close(server_socket));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
EXPECT_EQ(0, close(pipe_parent[1]));
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
/*
* Checks that LANDLOCK_SCOPE_SIGNAL is enforced on the asynchronous SIGIO
* delivery path (fcntl(F_SETOWN)) when the file owner is a process group.
*
* A sandboxed process sitting at the head of its process group's PID hlist (the
* default position right after fork()) used to escape the fcntl(F_SETOWN,
* -pgrp) domain recording: pid_task(pgrp, PIDTYPE_PGID) resolved to the process
* itself, so the same-thread-group exemption skipped recording its Landlock
* domain. At SIGIO time that domain was then unset and the signal fanned out
* to every group member, including non-sandboxed processes outside the domain.
*/
TEST(sigio_to_pgid_members)
{
int trigger[2], sync_child[2];
char buf;
pid_t child;
int status, i;
drop_caps(_metadata);
/*
* Isolates the test in its own process group so the SIGIO fan-out stays
* bounded to this parent and the child forked below.
*/
ASSERT_EQ(0, setpgid(0, 0));
/* The non-sandboxed parent is the protected (out-of-domain) target. */
ASSERT_EQ(0, setup_signal_handler(SIGURG));
signal_received = 0;
ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC));
ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
/*
* The child inherits the parent's new process group and, just
* attached with hlist_add_head_rcu(), is now the head of the
* pgid hlist: this is the case that used to skip the recording.
*/
EXPECT_EQ(0, close(sync_child[0]));
/* In-domain positive control: the child must be signaled. */
ASSERT_EQ(0, setup_signal_handler(SIGURG));
signal_received = 0;
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/* Owns the SIGIO source for the whole process group. */
ASSERT_EQ(0, fcntl(trigger[0], F_SETSIG, SIGURG));
ASSERT_EQ(0, fcntl(trigger[0], F_SETOWN, -getpgrp()));
ASSERT_EQ(0, fcntl(trigger[0], F_SETFL, O_ASYNC));
/* Fans SIGURG out to every member of the process group. */
ASSERT_EQ(1, write(trigger[1], ".", 1));
/*
* The sandboxed child is in its own domain and must always be
* signaled: this proves the SIGIO actually fired.
*/
for (i = 0; i < 1000 && !signal_received; i++)
usleep(1000);
EXPECT_EQ(1, signal_received);
ASSERT_EQ(1, write(sync_child[1], ".", 1));
EXPECT_EQ(0, close(sync_child[1]));
_exit(_metadata->exit_code);
return;
}
EXPECT_EQ(0, close(sync_child[1]));
EXPECT_EQ(0, close(trigger[0]));
EXPECT_EQ(0, close(trigger[1]));
/* Waits for the child to generate the SIGIO. */
ASSERT_EQ(1, read(sync_child[0], &buf, 1));
EXPECT_EQ(0, close(sync_child[0]));
/* Lets a delivered-but-pending signal run our handler, if any. */
for (i = 0; i < 100 && !signal_received; i++)
usleep(1000);
/*
* SCOPE_SIGNAL must block the fan-out to this non-sandboxed parent,
* which is outside the child's Landlock domain. Before the fix the
* parent was signaled here.
*/
EXPECT_EQ(0, signal_received);
ASSERT_EQ(child, waitpid(child, &status, 0));
if (WIFSIGNALED(status) || !WIFEXITED(status) ||
WEXITSTATUS(status) != EXIT_SUCCESS)
_metadata->exit_code = KSFT_FAIL;
}
static void *thread_setown_scoped(void *arg)
{
const int fd = *(int *)arg;
int ruleset_fd;
const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
/* Sandboxes only this non-leader thread (no thread syncing). */
ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
return THREAD_ERROR;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
landlock_restrict_self(ruleset_fd, 0)) {
close(ruleset_fd);
return THREAD_ERROR;
}
close(ruleset_fd);
/* Makes this process group own the SIGIO source. */
if (fcntl(fd, F_SETSIG, SIGURG) || fcntl(fd, F_SETOWN, -getpgrp()) ||
fcntl(fd, F_SETFL, O_ASYNC))
return THREAD_ERROR;
return THREAD_SUCCESS;
}
/*
* Checks that the SIGIO fan-out is still delivered to the file owner's own
* process when fcntl(F_SETOWN, -pgrp) was issued from a sandboxed non-leader
* thread.
*
* The Landlock domain is recorded for a process-group owner (so out-of-domain
* members stay blocked, see sigio_to_pgid_members), but the kernel signals a
* process group through its members' thread-group leaders. Here the leader is
* not sandboxed and thus has a different domain than the registering thread, so
* the registration-time check cannot tell that it belongs to the owner's own
* process. hook_file_send_sigiotask() must recognize it through the recorded
* thread group and allow the delivery, matching the same-process guarantee of
* commit 18eb75f3af40. Without that exemption the leader is wrongly denied and
* never signaled.
*/
TEST(sigio_to_pgid_self)
{
int trigger[2];
pthread_t thread;
void *ret = THREAD_INVALID;
int i;
drop_caps(_metadata);
/* Bounds the SIGIO fan-out to this process. */
ASSERT_EQ(0, setpgid(0, 0));
/* The non-sandboxed thread-group leader is the SIGIO target. */
ASSERT_EQ(0, setup_signal_handler(SIGURG));
signal_received = 0;
ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC));
/*
* Registers the process-group fowner from a sibling thread that
* sandboxes only itself, so its domain differs from the leader's.
*/
ASSERT_EQ(0, pthread_create(&thread, NULL, thread_setown_scoped,
&trigger[0]));
ASSERT_EQ(0, pthread_join(thread, &ret));
ASSERT_EQ(THREAD_SUCCESS, ret);
/* Fans SIGURG out to the process group. */
ASSERT_EQ(1, write(trigger[1], ".", 1));
for (i = 0; i < 1000 && !signal_received; i++)
usleep(1000);
/*
* Same-process delivery must always be allowed, even though the owner
* was registered from a sandboxed sibling thread.
*/
EXPECT_EQ(1, signal_received);
EXPECT_EQ(0, close(trigger[0]));
EXPECT_EQ(0, close(trigger[1]));
}
TEST_HARNESS_MAIN
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Common scope restriction
*
* Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com>
*/
#define _GNU_SOURCE
#include <errno.h>
#include <linux/landlock.h>
#include <sys/prctl.h>
#include "common.h"
#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL
TEST(ruleset_with_unknown_scope)
{
__u64 scoped_mask;
for (scoped_mask = 1ULL << 63; scoped_mask != ACCESS_LAST;
scoped_mask >>= 1) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = scoped_mask,
};
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0));
ASSERT_EQ(EINVAL, errno);
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: GPL-2.0
int main(void)
{
return 0;
}
@@ -0,0 +1,327 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Landlock tests - Enforcing the same restrictions across multiple threads
*
* Copyright © 2025 Günther Noack <gnoack3000@gmail.com>
*/
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include "common.h"
/* create_ruleset - Create a simple ruleset FD common to all tests */
static int create_ruleset(struct __test_metadata *const _metadata)
{
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = (LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_TRUNCATE),
};
const int ruleset_fd =
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd)
{
TH_LOG("landlock_create_ruleset: %s", strerror(errno));
}
return ruleset_fd;
}
TEST(single_threaded_success)
{
const int ruleset_fd = create_ruleset(_metadata);
disable_caps(_metadata);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, landlock_restrict_self(ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC));
EXPECT_EQ(0, close(ruleset_fd));
}
static void store_no_new_privs(void *data)
{
bool *nnp = data;
if (!nnp)
return;
*nnp = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0);
}
static void *idle(void *data)
{
pthread_cleanup_push(store_no_new_privs, data);
while (true)
sleep(1);
pthread_cleanup_pop(1);
}
TEST(multi_threaded_success)
{
pthread_t t1, t2;
bool no_new_privs1, no_new_privs2;
const int ruleset_fd = create_ruleset(_metadata);
disable_caps(_metadata);
ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC));
ASSERT_EQ(0, pthread_cancel(t1));
ASSERT_EQ(0, pthread_cancel(t2));
ASSERT_EQ(0, pthread_join(t1, NULL));
ASSERT_EQ(0, pthread_join(t2, NULL));
/* The no_new_privs flag was implicitly enabled on all threads. */
EXPECT_TRUE(no_new_privs1);
EXPECT_TRUE(no_new_privs2);
EXPECT_EQ(0, close(ruleset_fd));
}
TEST(multi_threaded_success_despite_diverging_domains)
{
pthread_t t1, t2;
const int ruleset_fd = create_ruleset(_metadata);
disable_caps(_metadata);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, pthread_create(&t1, NULL, idle, NULL));
ASSERT_EQ(0, pthread_create(&t2, NULL, idle, NULL));
/*
* The main thread enforces a ruleset,
* thereby bringing the threads' Landlock domains out of sync.
*/
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
/* Still, TSYNC succeeds, bringing the threads in sync again. */
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC));
ASSERT_EQ(0, pthread_cancel(t1));
ASSERT_EQ(0, pthread_cancel(t2));
ASSERT_EQ(0, pthread_join(t1, NULL));
ASSERT_EQ(0, pthread_join(t2, NULL));
EXPECT_EQ(0, close(ruleset_fd));
}
struct thread_restrict_data {
pthread_t t;
int ruleset_fd;
int result;
};
static void *thread_restrict(void *data)
{
struct thread_restrict_data *d = data;
d->result = landlock_restrict_self(d->ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC);
return NULL;
}
TEST(competing_enablement)
{
const int ruleset_fd = create_ruleset(_metadata);
struct thread_restrict_data d[] = {
{ .ruleset_fd = ruleset_fd },
{ .ruleset_fd = ruleset_fd },
};
disable_caps(_metadata);
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ASSERT_EQ(0, pthread_create(&d[0].t, NULL, thread_restrict, &d[0]));
ASSERT_EQ(0, pthread_create(&d[1].t, NULL, thread_restrict, &d[1]));
/* Wait for threads to finish. */
ASSERT_EQ(0, pthread_join(d[0].t, NULL));
ASSERT_EQ(0, pthread_join(d[1].t, NULL));
/* Expect that both succeeded. */
EXPECT_EQ(0, d[0].result);
EXPECT_EQ(0, d[1].result);
EXPECT_EQ(0, close(ruleset_fd));
}
static void signal_nop_handler(int sig)
{
}
struct signaler_data {
pthread_t target;
volatile bool stop;
};
static void *signaler_thread(void *data)
{
struct signaler_data *sd = data;
while (!sd->stop)
pthread_kill(sd->target, SIGUSR1);
return NULL;
}
/*
* Number of idle sibling threads. This must be large enough that even on
* machines with many cores, the sibling threads cannot all complete their
* credential preparation in a single parallel wave, otherwise the signaler
* thread has no window to interrupt wait_for_completion_interruptible().
* 200 threads on a 64-core machine yields ~3 serialized waves, giving the
* tight signal loop enough time to land an interruption.
*/
#define NUM_IDLE_THREADS 200
/*
* Exercises the tsync interruption and cancellation paths in tsync.c.
*
* When a signal interrupts the calling thread while it waits for sibling
* threads to finish their credential preparation
* (wait_for_completion_interruptible in landlock_restrict_sibling_threads),
* the kernel sets ERESTARTNOINTR, cancels queued task works that have not
* started yet (cancel_tsync_works), then waits for the remaining works to
* finish. On the error return, syscalls.c aborts the prepared credentials.
* The kernel automatically restarts the syscall, so userspace sees success.
*/
TEST(tsync_interrupt)
{
size_t i;
pthread_t threads[NUM_IDLE_THREADS];
pthread_t signaler;
struct signaler_data sd;
struct sigaction sa = {};
const int ruleset_fd = create_ruleset(_metadata);
disable_caps(_metadata);
/* Install a no-op SIGUSR1 handler so the signal does not kill us. */
sa.sa_handler = signal_nop_handler;
sigemptyset(&sa.sa_mask);
ASSERT_EQ(0, sigaction(SIGUSR1, &sa, NULL));
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
for (i = 0; i < NUM_IDLE_THREADS; i++)
ASSERT_EQ(0, pthread_create(&threads[i], NULL, idle, NULL));
/*
* Start a signaler thread that continuously sends SIGUSR1 to the
* calling thread. This maximizes the chance of interrupting
* wait_for_completion_interruptible() in the kernel's tsync path.
*/
sd.target = pthread_self();
sd.stop = false;
ASSERT_EQ(0, pthread_create(&signaler, NULL, signaler_thread, &sd));
/*
* The syscall may be interrupted and transparently restarted by the
* kernel (ERESTARTNOINTR). From userspace, it should always succeed.
*/
EXPECT_EQ(0, landlock_restrict_self(ruleset_fd,
LANDLOCK_RESTRICT_SELF_TSYNC));
sd.stop = true;
ASSERT_EQ(0, pthread_join(signaler, NULL));
for (i = 0; i < NUM_IDLE_THREADS; i++) {
ASSERT_EQ(0, pthread_cancel(threads[i]));
ASSERT_EQ(0, pthread_join(threads[i], NULL));
}
EXPECT_EQ(0, close(ruleset_fd));
}
/* clang-format off */
FIXTURE(tsync_without_ruleset) {};
/* clang-format on */
FIXTURE_VARIANT(tsync_without_ruleset)
{
const __u32 flags;
const int expected_errno;
};
/* clang-format off */
FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
/* clang-format on */
.flags = LANDLOCK_RESTRICT_SELF_TSYNC,
.expected_errno = EBADF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
/* clang-format on */
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_TSYNC,
.expected_errno = EBADF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
/* clang-format on */
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_TSYNC,
.expected_errno = EBADF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
/* clang-format on */
.flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_TSYNC,
.expected_errno = EBADF,
};
/* clang-format off */
FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
/* clang-format on */
.flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
LANDLOCK_RESTRICT_SELF_TSYNC,
.expected_errno = 0,
};
FIXTURE_SETUP(tsync_without_ruleset)
{
disable_caps(_metadata);
}
FIXTURE_TEARDOWN(tsync_without_ruleset)
{
}
TEST_F(tsync_without_ruleset, check)
{
int ret;
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
ret = landlock_restrict_self(-1, variant->flags);
if (variant->expected_errno) {
EXPECT_EQ(-1, ret);
EXPECT_EQ(variant->expected_errno, errno);
} else {
EXPECT_EQ(0, ret);
}
}
TEST_HARNESS_MAIN
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Write in a pipe, wait, sandbox itself, test sandboxing, and wait again.
*
* Used by audit_exec.flags from audit_test.c
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/landlock.h>
#include <linux/prctl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>
#include "wrappers.h"
static int sync_with(int pipe_child, int pipe_parent)
{
char buf;
/* Signals that we are waiting. */
if (write(pipe_child, ".", 1) != 1) {
perror("Failed to write to first argument");
return 1;
}
/* Waits for the parent do its test. */
if (read(pipe_parent, &buf, 1) != 1) {
perror("Failed to write to the second argument");
return 1;
}
return 0;
}
int main(int argc, char *argv[])
{
const struct landlock_ruleset_attr layer2 = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
};
const struct landlock_ruleset_attr layer3 = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int err, pipe_child, pipe_parent, ruleset_fd;
/* The first argument must be the file descriptor number of a pipe. */
if (argc != 3) {
fprintf(stderr, "Wrong number of arguments (not two)\n");
return 1;
}
pipe_child = atoi(argv[1]);
pipe_parent = atoi(argv[2]);
/* PR_SET_NO_NEW_PRIVS already set by parent. */
/* First step to test parent's layer1. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
/* Tries to send a signal, denied by layer1. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
/* Second step to test parent's layer1 and our layer2. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
ruleset_fd = landlock_create_ruleset(&layer2, sizeof(layer2), 0);
if (ruleset_fd < 0) {
perror("Failed to create the layer2 ruleset");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to restrict self");
return 1;
}
close(ruleset_fd);
/* Tries to send a signal, denied by layer1. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
/* Tries to open ., denied by layer2. */
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
fprintf(stderr, "Successfully opened /");
return 1;
}
/* Third step to test our layer2 and layer3. */
err = sync_with(pipe_child, pipe_parent);
if (err)
return err;
ruleset_fd = landlock_create_ruleset(&layer3, sizeof(layer3), 0);
if (ruleset_fd < 0) {
perror("Failed to create the layer3 ruleset");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("Failed to restrict self");
return 1;
}
close(ruleset_fd);
/* Tries to open ., denied by layer2. */
if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) {
fprintf(stderr, "Successfully opened /");
return 1;
}
/* Tries to send a signal, denied by layer3. */
if (!kill(getppid(), 0)) {
fprintf(stderr, "Successfully sent a signal to the parent");
return 1;
}
return 0;
}
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Write in a pipe and wait.
*
* Used by layout1.umount_sandboxer from fs_test.c
*
* Copyright © 2024-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int pipe_child, pipe_parent;
char buf;
/* The first argument must be the file descriptor number of a pipe. */
if (argc != 3) {
fprintf(stderr, "Wrong number of arguments (not two)\n");
return 1;
}
pipe_child = atoi(argv[1]);
pipe_parent = atoi(argv[2]);
/* Signals that we are waiting. */
if (write(pipe_child, ".", 1) != 1) {
perror("Failed to write to first argument");
return 1;
}
/* Waits for the parent do its test. */
if (read(pipe_parent, &buf, 1) != 1) {
perror("Failed to write to the second argument");
return 1;
}
return 0;
}
@@ -0,0 +1,47 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Syscall wrappers
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
* Copyright © 2021-2025 Microsoft Corporation
*/
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef landlock_create_ruleset
static inline int
landlock_create_ruleset(const struct landlock_ruleset_attr *const attr,
const size_t size, const __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
#endif
#ifndef landlock_add_rule
static inline int landlock_add_rule(const int ruleset_fd,
const enum landlock_rule_type rule_type,
const void *const rule_attr,
const __u32 flags)
{
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr,
flags);
}
#endif
#ifndef landlock_restrict_self
static inline int landlock_restrict_self(const int ruleset_fd,
const __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
#endif
static inline pid_t sys_gettid(void)
{
return syscall(__NR_gettid);
}