selftests/landlock: Add user space tests
Test all Landlock system calls, ptrace hooks semantic and filesystem access-control with multiple layouts. Test coverage for security/landlock/ is 93.6% of lines. The code not covered only deals with internal kernel errors (e.g. memory allocation) and race conditions. Cc: James Morris <jmorris@namei.org> Cc: Jann Horn <jannh@google.com> Cc: Serge E. Hallyn <serge@hallyn.com> Cc: Shuah Khan <shuah@kernel.org> Signed-off-by: Mickaël Salaün <mic@linux.microsoft.com> Reviewed-by: Vincent Dagonneau <vincent.dagonneau@ssi.gouv.fr> Reviewed-by: Kees Cook <keescook@chromium.org> Link: https://lore.kernel.org/r/20210422154123.13086-11-mic@digikod.net Signed-off-by: James Morris <jamorris@linux.microsoft.com>
This commit is contained in:
parent
265885daf3
commit
e1199815b4
@ -10005,6 +10005,7 @@ W: https://landlock.io
|
||||
T: git https://github.com/landlock-lsm/linux.git
|
||||
F: include/uapi/linux/landlock.h
|
||||
F: security/landlock/
|
||||
F: tools/testing/selftests/landlock/
|
||||
K: landlock
|
||||
K: LANDLOCK
|
||||
|
||||
|
@ -25,6 +25,7 @@ TARGETS += ir
|
||||
TARGETS += kcmp
|
||||
TARGETS += kexec
|
||||
TARGETS += kvm
|
||||
TARGETS += landlock
|
||||
TARGETS += lib
|
||||
TARGETS += livepatch
|
||||
TARGETS += lkdtm
|
||||
|
2
tools/testing/selftests/landlock/.gitignore
vendored
Normal file
2
tools/testing/selftests/landlock/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*_test
|
||||
/true
|
24
tools/testing/selftests/landlock/Makefile
Normal file
24
tools/testing/selftests/landlock/Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
CFLAGS += -Wall -O2
|
||||
|
||||
src_test := $(wildcard *_test.c)
|
||||
|
||||
TEST_GEN_PROGS := $(src_test:.c=)
|
||||
|
||||
TEST_GEN_PROGS_EXTENDED := true
|
||||
|
||||
KSFT_KHDR_INSTALL := 1
|
||||
OVERRIDE_TARGETS := 1
|
||||
include ../lib.mk
|
||||
|
||||
khdr_dir = $(top_srcdir)/usr/include
|
||||
|
||||
$(khdr_dir)/linux/landlock.h: khdr
|
||||
@:
|
||||
|
||||
$(OUTPUT)/true: true.c
|
||||
$(LINK.c) $< $(LDLIBS) -o $@ -static
|
||||
|
||||
$(OUTPUT)/%_test: %_test.c $(khdr_dir)/linux/landlock.h ../kselftest_harness.h common.h
|
||||
$(LINK.c) $< $(LDLIBS) -o $@ -lcap -I$(khdr_dir)
|
219
tools/testing/selftests/landlock/base_test.c
Normal file
219
tools/testing/selftests/landlock/base_test.c
Normal file
@ -0,0 +1,219 @@
|
||||
// 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/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(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);
|
||||
|
||||
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(empty_path_beneath_attr) {
|
||||
const struct landlock_ruleset_attr ruleset_attr = {
|
||||
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
|
||||
};
|
||||
const int ruleset_fd = landlock_create_ruleset(&ruleset_attr,
|
||||
sizeof(ruleset_attr), 0);
|
||||
|
||||
ASSERT_LE(0, ruleset_fd);
|
||||
|
||||
/* Similar to struct landlock_path_beneath_attr.parent_fd = 0 */
|
||||
ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
NULL, 0));
|
||||
ASSERT_EQ(EFAULT, errno);
|
||||
ASSERT_EQ(0, close(ruleset_fd));
|
||||
}
|
||||
|
||||
TEST(inval_fd_enforce) {
|
||||
ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
|
||||
|
||||
ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
|
||||
ASSERT_EQ(EBADF, errno);
|
||||
}
|
||||
|
||||
TEST(unpriv_enforce_without_no_new_privs) {
|
||||
int err;
|
||||
|
||||
drop_caps(_metadata);
|
||||
err = landlock_restrict_self(-1, 0);
|
||||
ASSERT_EQ(EPERM, errno);
|
||||
ASSERT_EQ(err, -1);
|
||||
}
|
||||
|
||||
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;
|
||||
union {
|
||||
/* Aligned ancillary data buffer. */
|
||||
char buf[CMSG_SPACE(sizeof(ruleset_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;
|
||||
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));
|
||||
|
||||
cmsg = CMSG_FIRSTHDR(&msg);
|
||||
ASSERT_NE(NULL, cmsg);
|
||||
cmsg->cmsg_len = CMSG_LEN(sizeof(ruleset_fd_tx));
|
||||
cmsg->cmsg_level = SOL_SOCKET;
|
||||
cmsg->cmsg_type = SCM_RIGHTS;
|
||||
memcpy(CMSG_DATA(cmsg), &ruleset_fd_tx, sizeof(ruleset_fd_tx));
|
||||
|
||||
/* 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(sizeof(data_tx), sendmsg(socket_fds[0], &msg, 0));
|
||||
ASSERT_EQ(0, close(socket_fds[0]));
|
||||
ASSERT_EQ(0, close(ruleset_fd_tx));
|
||||
|
||||
child = fork();
|
||||
ASSERT_LE(0, child);
|
||||
if (child == 0) {
|
||||
int ruleset_fd_rx;
|
||||
|
||||
*(char *)msg.msg_iov->iov_base = '\0';
|
||||
ASSERT_EQ(sizeof(data_tx), recvmsg(socket_fds[1], &msg, MSG_CMSG_CLOEXEC));
|
||||
ASSERT_EQ('.', *(char *)msg.msg_iov->iov_base);
|
||||
ASSERT_EQ(0, close(socket_fds[1]));
|
||||
cmsg = CMSG_FIRSTHDR(&msg);
|
||||
ASSERT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(ruleset_fd_tx)));
|
||||
memcpy(&ruleset_fd_rx, CMSG_DATA(cmsg), sizeof(ruleset_fd_tx));
|
||||
|
||||
/* 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->passed ? EXIT_SUCCESS : EXIT_FAILURE);
|
||||
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_HARNESS_MAIN
|
183
tools/testing/selftests/landlock/common.h
Normal file
183
tools/testing/selftests/landlock/common.h
Normal file
@ -0,0 +1,183 @@
|
||||
/* 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 <errno.h>
|
||||
#include <linux/landlock.h>
|
||||
#include <sys/capability.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../kselftest_harness.h"
|
||||
|
||||
#ifndef ARRAY_SIZE
|
||||
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* TEST_F_FORK() is useful when a test drop privileges but the corresponding
|
||||
* FIXTURE_TEARDOWN() requires them (e.g. to remove files from a directory
|
||||
* where write actions are denied). For convenience, FIXTURE_TEARDOWN() is
|
||||
* also called when the test failed, but not when FIXTURE_SETUP() failed. For
|
||||
* this to be possible, we must not call abort() but instead exit smoothly
|
||||
* (hence the step print).
|
||||
*/
|
||||
#define TEST_F_FORK(fixture_name, test_name) \
|
||||
static void fixture_name##_##test_name##_child( \
|
||||
struct __test_metadata *_metadata, \
|
||||
FIXTURE_DATA(fixture_name) *self, \
|
||||
const FIXTURE_VARIANT(fixture_name) *variant); \
|
||||
TEST_F(fixture_name, test_name) \
|
||||
{ \
|
||||
int status; \
|
||||
const pid_t child = fork(); \
|
||||
if (child < 0) \
|
||||
abort(); \
|
||||
if (child == 0) { \
|
||||
_metadata->no_print = 1; \
|
||||
fixture_name##_##test_name##_child(_metadata, self, variant); \
|
||||
if (_metadata->skip) \
|
||||
_exit(255); \
|
||||
if (_metadata->passed) \
|
||||
_exit(0); \
|
||||
_exit(_metadata->step); \
|
||||
} \
|
||||
if (child != waitpid(child, &status, 0)) \
|
||||
abort(); \
|
||||
if (WIFSIGNALED(status) || !WIFEXITED(status)) { \
|
||||
_metadata->passed = 0; \
|
||||
_metadata->step = 1; \
|
||||
return; \
|
||||
} \
|
||||
switch (WEXITSTATUS(status)) { \
|
||||
case 0: \
|
||||
_metadata->passed = 1; \
|
||||
break; \
|
||||
case 255: \
|
||||
_metadata->passed = 1; \
|
||||
_metadata->skip = 1; \
|
||||
break; \
|
||||
default: \
|
||||
_metadata->passed = 0; \
|
||||
_metadata->step = WEXITSTATUS(status); \
|
||||
break; \
|
||||
} \
|
||||
} \
|
||||
static void fixture_name##_##test_name##_child( \
|
||||
struct __test_metadata __attribute__((unused)) *_metadata, \
|
||||
FIXTURE_DATA(fixture_name) __attribute__((unused)) *self, \
|
||||
const FIXTURE_VARIANT(fixture_name) \
|
||||
__attribute__((unused)) *variant)
|
||||
|
||||
#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 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[] = {
|
||||
CAP_DAC_OVERRIDE,
|
||||
CAP_MKNOD,
|
||||
CAP_SYS_ADMIN,
|
||||
CAP_SYS_CHROOT,
|
||||
};
|
||||
|
||||
cap_p = cap_get_proc();
|
||||
EXPECT_NE(NULL, cap_p) {
|
||||
TH_LOG("Failed to cap_get_proc: %s", strerror(errno));
|
||||
}
|
||||
EXPECT_NE(-1, cap_clear(cap_p)) {
|
||||
TH_LOG("Failed to cap_clear: %s", strerror(errno));
|
||||
}
|
||||
if (!drop_all) {
|
||||
EXPECT_NE(-1, cap_set_flag(cap_p, CAP_PERMITTED,
|
||||
ARRAY_SIZE(caps), caps, CAP_SET)) {
|
||||
TH_LOG("Failed to cap_set_flag: %s", strerror(errno));
|
||||
}
|
||||
}
|
||||
EXPECT_NE(-1, cap_set_proc(cap_p)) {
|
||||
TH_LOG("Failed to cap_set_proc: %s", strerror(errno));
|
||||
}
|
||||
EXPECT_NE(-1, cap_free(cap_p)) {
|
||||
TH_LOG("Failed to cap_free: %s", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
/* We cannot put such helpers in a library because of kselftest_harness.h . */
|
||||
__attribute__((__unused__))
|
||||
static void disable_caps(struct __test_metadata *const _metadata)
|
||||
{
|
||||
_init_caps(_metadata, false);
|
||||
}
|
||||
|
||||
__attribute__((__unused__))
|
||||
static void drop_caps(struct __test_metadata *const _metadata)
|
||||
{
|
||||
_init_caps(_metadata, true);
|
||||
}
|
||||
|
||||
static void _effective_cap(struct __test_metadata *const _metadata,
|
||||
const cap_value_t caps, const cap_flag_value_t value)
|
||||
{
|
||||
cap_t cap_p;
|
||||
|
||||
cap_p = cap_get_proc();
|
||||
EXPECT_NE(NULL, cap_p) {
|
||||
TH_LOG("Failed to cap_get_proc: %s", strerror(errno));
|
||||
}
|
||||
EXPECT_NE(-1, cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &caps, value)) {
|
||||
TH_LOG("Failed to cap_set_flag: %s", strerror(errno));
|
||||
}
|
||||
EXPECT_NE(-1, cap_set_proc(cap_p)) {
|
||||
TH_LOG("Failed to cap_set_proc: %s", strerror(errno));
|
||||
}
|
||||
EXPECT_NE(-1, cap_free(cap_p)) {
|
||||
TH_LOG("Failed to cap_free: %s", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((__unused__))
|
||||
static void set_cap(struct __test_metadata *const _metadata,
|
||||
const cap_value_t caps)
|
||||
{
|
||||
_effective_cap(_metadata, caps, CAP_SET);
|
||||
}
|
||||
|
||||
__attribute__((__unused__))
|
||||
static void clear_cap(struct __test_metadata *const _metadata,
|
||||
const cap_value_t caps)
|
||||
{
|
||||
_effective_cap(_metadata, caps, CAP_CLEAR);
|
||||
}
|
7
tools/testing/selftests/landlock/config
Normal file
7
tools/testing/selftests/landlock/config
Normal file
@ -0,0 +1,7 @@
|
||||
CONFIG_OVERLAY_FS=y
|
||||
CONFIG_SECURITY_LANDLOCK=y
|
||||
CONFIG_SECURITY_PATH=y
|
||||
CONFIG_SECURITY=y
|
||||
CONFIG_SHMEM=y
|
||||
CONFIG_TMPFS_XATTR=y
|
||||
CONFIG_TMPFS=y
|
2791
tools/testing/selftests/landlock/fs_test.c
Normal file
2791
tools/testing/selftests/landlock/fs_test.c
Normal file
File diff suppressed because it is too large
Load Diff
337
tools/testing/selftests/landlock/ptrace_test.c
Normal file
337
tools/testing/selftests/landlock/ptrace_test.c
Normal file
@ -0,0 +1,337 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Landlock tests - Ptrace
|
||||
*
|
||||
* 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/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 "common.h"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
FIXTURE(hierarchy) { };
|
||||
|
||||
FIXTURE_VARIANT(hierarchy) {
|
||||
const bool domain_both;
|
||||
const bool domain_parent;
|
||||
const bool domain_child;
|
||||
};
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* No domain
|
||||
*
|
||||
* P1-. P1 -> P2 : allow
|
||||
* \ P2 -> P1 : allow
|
||||
* 'P2
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) {
|
||||
.domain_both = false,
|
||||
.domain_parent = false,
|
||||
.domain_child = false,
|
||||
};
|
||||
|
||||
/*
|
||||
* Child domain
|
||||
*
|
||||
* P1--. P1 -> P2 : allow
|
||||
* \ P2 -> P1 : deny
|
||||
* .'-----.
|
||||
* | P2 |
|
||||
* '------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) {
|
||||
.domain_both = false,
|
||||
.domain_parent = false,
|
||||
.domain_child = true,
|
||||
};
|
||||
|
||||
/*
|
||||
* Parent domain
|
||||
* .------.
|
||||
* | P1 --. P1 -> P2 : deny
|
||||
* '------' \ P2 -> P1 : allow
|
||||
* '
|
||||
* P2
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) {
|
||||
.domain_both = false,
|
||||
.domain_parent = true,
|
||||
.domain_child = false,
|
||||
};
|
||||
|
||||
/*
|
||||
* Parent + child domain (siblings)
|
||||
* .------.
|
||||
* | P1 ---. P1 -> P2 : deny
|
||||
* '------' \ P2 -> P1 : deny
|
||||
* .---'--.
|
||||
* | P2 |
|
||||
* '------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) {
|
||||
.domain_both = false,
|
||||
.domain_parent = true,
|
||||
.domain_child = true,
|
||||
};
|
||||
|
||||
/*
|
||||
* Same domain (inherited)
|
||||
* .-------------.
|
||||
* | P1----. | P1 -> P2 : allow
|
||||
* | \ | P2 -> P1 : allow
|
||||
* | ' |
|
||||
* | P2 |
|
||||
* '-------------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) {
|
||||
.domain_both = true,
|
||||
.domain_parent = false,
|
||||
.domain_child = false,
|
||||
};
|
||||
|
||||
/*
|
||||
* Inherited + child domain
|
||||
* .-----------------.
|
||||
* | P1----. | P1 -> P2 : allow
|
||||
* | \ | P2 -> P1 : deny
|
||||
* | .-'----. |
|
||||
* | | P2 | |
|
||||
* | '------' |
|
||||
* '-----------------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) {
|
||||
.domain_both = true,
|
||||
.domain_parent = false,
|
||||
.domain_child = true,
|
||||
};
|
||||
|
||||
/*
|
||||
* Inherited + parent domain
|
||||
* .-----------------.
|
||||
* |.------. | P1 -> P2 : deny
|
||||
* || P1 ----. | P2 -> P1 : allow
|
||||
* |'------' \ |
|
||||
* | ' |
|
||||
* | P2 |
|
||||
* '-----------------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) {
|
||||
.domain_both = true,
|
||||
.domain_parent = true,
|
||||
.domain_child = false,
|
||||
};
|
||||
|
||||
/*
|
||||
* Inherited + parent and child domain (siblings)
|
||||
* .-----------------.
|
||||
* | .------. | P1 -> P2 : deny
|
||||
* | | P1 . | P2 -> P1 : deny
|
||||
* | '------'\ |
|
||||
* | \ |
|
||||
* | .--'---. |
|
||||
* | | P2 | |
|
||||
* | '------' |
|
||||
* '-----------------'
|
||||
*/
|
||||
FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) {
|
||||
.domain_both = true,
|
||||
.domain_parent = true,
|
||||
.domain_child = true,
|
||||
};
|
||||
|
||||
FIXTURE_SETUP(hierarchy)
|
||||
{ }
|
||||
|
||||
FIXTURE_TEARDOWN(hierarchy)
|
||||
{ }
|
||||
|
||||
/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */
|
||||
TEST_F(hierarchy, trace)
|
||||
{
|
||||
pid_t child, parent;
|
||||
int status, err_proc_read;
|
||||
int pipe_child[2], pipe_parent[2];
|
||||
char buf_parent;
|
||||
long ret;
|
||||
|
||||
/*
|
||||
* 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 (!_metadata->passed)
|
||||
/* 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_ATTACH and PTRACE_MODE_READ on the parent. */
|
||||
err_proc_read = test_ptrace_read(parent);
|
||||
ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
|
||||
if (variant->domain_child) {
|
||||
EXPECT_EQ(-1, ret);
|
||||
EXPECT_EQ(EPERM, errno);
|
||||
EXPECT_EQ(EACCES, err_proc_read);
|
||||
} else {
|
||||
EXPECT_EQ(0, ret);
|
||||
EXPECT_EQ(0, err_proc_read);
|
||||
}
|
||||
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 (variant->domain_parent) {
|
||||
EXPECT_EQ(-1, ret);
|
||||
EXPECT_EQ(EPERM, errno);
|
||||
} else {
|
||||
EXPECT_EQ(0, ret);
|
||||
}
|
||||
|
||||
/*
|
||||
* Signals that the PTRACE_ATTACH test is done and the
|
||||
* PTRACE_TRACEME test is ongoing.
|
||||
*/
|
||||
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
|
||||
|
||||
if (!variant->domain_parent) {
|
||||
ASSERT_EQ(0, raise(SIGSTOP));
|
||||
}
|
||||
|
||||
/* Waits for the parent PTRACE_ATTACH test. */
|
||||
ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
|
||||
_exit(_metadata->passed ? EXIT_SUCCESS : EXIT_FAILURE);
|
||||
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 (!variant->domain_parent) {
|
||||
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_ATTACH and PTRACE_MODE_READ on the child. */
|
||||
err_proc_read = test_ptrace_read(child);
|
||||
ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
|
||||
if (variant->domain_parent) {
|
||||
EXPECT_EQ(-1, ret);
|
||||
EXPECT_EQ(EPERM, errno);
|
||||
EXPECT_EQ(EACCES, err_proc_read);
|
||||
} else {
|
||||
EXPECT_EQ(0, ret);
|
||||
EXPECT_EQ(0, err_proc_read);
|
||||
}
|
||||
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->passed = 0;
|
||||
}
|
||||
|
||||
TEST_HARNESS_MAIN
|
5
tools/testing/selftests/landlock/true.c
Normal file
5
tools/testing/selftests/landlock/true.c
Normal file
@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
int main(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user