Landlock updates for v6.3-rc1
-----BEGIN PGP SIGNATURE----- iIYEABYIAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCY/O9MBAcbWljQGRpZ2lr b2QubmV0AAoJEOXj0OiMgvbSFbgA/RPjQO0J/todz9qJMhbx4QhZizKK8F8hM9Yl rwhOZWmjAP4wTxOsnrjdR9UusYAr818j01D7ncpp9bM4e2ZNj1wEDw== =Xe3q -----END PGP SIGNATURE----- Merge tag 'landlock-6.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux Pull landlock updates from Mickaël Salaün: "This improves documentation, and makes some tests more flexible to be able to run on systems without overlayfs or with Yama restrictions" * tag 'landlock-6.3-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux: MAINTAINERS: Update Landlock repository selftests/landlock: Test ptrace as much as possible with Yama selftests/landlock: Skip overlayfs tests when not supported landlock: Explain file descriptor access rights
This commit is contained in:
commit
291a73a8e6
|
@ -7,7 +7,7 @@ Landlock LSM: kernel documentation
|
|||
==================================
|
||||
|
||||
:Author: Mickaël Salaün
|
||||
:Date: September 2022
|
||||
:Date: December 2022
|
||||
|
||||
Landlock's goal is to create scoped access-control (i.e. sandboxing). To
|
||||
harden a whole system, this feature should be available to any process,
|
||||
|
@ -41,12 +41,16 @@ Guiding principles for safe access controls
|
|||
processes.
|
||||
* Computation related to Landlock operations (e.g. enforcing a ruleset) shall
|
||||
only impact the processes requesting them.
|
||||
* Resources (e.g. file descriptors) directly obtained from the kernel by a
|
||||
sandboxed process shall retain their scoped accesses (at the time of resource
|
||||
acquisition) whatever process use them.
|
||||
Cf. `File descriptor access rights`_.
|
||||
|
||||
Design choices
|
||||
==============
|
||||
|
||||
Filesystem access rights
|
||||
------------------------
|
||||
Inode access rights
|
||||
-------------------
|
||||
|
||||
All access rights are tied to an inode and what can be accessed through it.
|
||||
Reading the content of a directory does not imply to be allowed to read the
|
||||
|
@ -57,6 +61,30 @@ directory, not the unlinked inode. This is the reason why
|
|||
``LANDLOCK_ACCESS_FS_REMOVE_FILE`` or ``LANDLOCK_ACCESS_FS_REFER`` are not
|
||||
allowed to be tied to files but only to directories.
|
||||
|
||||
File descriptor access rights
|
||||
-----------------------------
|
||||
|
||||
Access rights are checked and tied to file descriptors at open time. The
|
||||
underlying principle is that equivalent sequences of operations should lead to
|
||||
the same results, when they are executed under the same Landlock domain.
|
||||
|
||||
Taking the ``LANDLOCK_ACCESS_FS_TRUNCATE`` right as an example, it may be
|
||||
allowed to open a file for writing without being allowed to
|
||||
:manpage:`ftruncate` the resulting file descriptor if the related file
|
||||
hierarchy doesn't grant such access right. The following sequences of
|
||||
operations have the same semantic and should then have the same result:
|
||||
|
||||
* ``truncate(path);``
|
||||
* ``int fd = open(path, O_WRONLY); ftruncate(fd); close(fd);``
|
||||
|
||||
Similarly to file access modes (e.g. ``O_RDWR``), Landlock access rights
|
||||
attached to file descriptors are retained even if they are passed between
|
||||
processes (e.g. through a Unix domain socket). Such access rights will then be
|
||||
enforced even if the receiving process is not sandboxed by Landlock. Indeed,
|
||||
this is required to keep a consistent access control over the whole system, and
|
||||
this avoids unattended bypasses through file descriptor passing (i.e. confused
|
||||
deputy attack).
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
|
|
|
@ -11592,7 +11592,7 @@ M: Mickaël Salaün <mic@digikod.net>
|
|||
L: linux-security-module@vger.kernel.org
|
||||
S: Supported
|
||||
W: https://landlock.io
|
||||
T: git https://github.com/landlock-lsm/linux.git
|
||||
T: git https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git
|
||||
F: Documentation/security/landlock.rst
|
||||
F: Documentation/userspace-api/landlock.rst
|
||||
F: include/uapi/linux/landlock.h
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <fcntl.h>
|
||||
#include <linux/landlock.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/capability.h>
|
||||
#include <sys/mount.h>
|
||||
|
@ -89,6 +90,40 @@ static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
|
|||
* └── s3d3
|
||||
*/
|
||||
|
||||
static bool fgrep(FILE *const inf, const char *const str)
|
||||
{
|
||||
char line[32];
|
||||
const int slen = strlen(str);
|
||||
|
||||
while (!feof(inf)) {
|
||||
if (!fgets(line, sizeof(line), inf))
|
||||
break;
|
||||
if (strncmp(line, str, slen))
|
||||
continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool supports_overlayfs(void)
|
||||
{
|
||||
bool res;
|
||||
FILE *const inf = fopen("/proc/filesystems", "r");
|
||||
|
||||
/*
|
||||
* Consider that the filesystem is supported if we cannot get the
|
||||
* supported ones.
|
||||
*/
|
||||
if (!inf)
|
||||
return true;
|
||||
|
||||
res = fgrep(inf, "nodev\toverlay\n");
|
||||
fclose(inf);
|
||||
return res;
|
||||
}
|
||||
|
||||
static void mkdir_parents(struct __test_metadata *const _metadata,
|
||||
const char *const path)
|
||||
{
|
||||
|
@ -4001,6 +4036,9 @@ FIXTURE(layout2_overlay) {};
|
|||
|
||||
FIXTURE_SETUP(layout2_overlay)
|
||||
{
|
||||
if (!supports_overlayfs())
|
||||
SKIP(return, "overlayfs is not supported");
|
||||
|
||||
prepare_layout(_metadata);
|
||||
|
||||
create_directory(_metadata, LOWER_BASE);
|
||||
|
@ -4037,6 +4075,9 @@ FIXTURE_SETUP(layout2_overlay)
|
|||
|
||||
FIXTURE_TEARDOWN(layout2_overlay)
|
||||
{
|
||||
if (!supports_overlayfs())
|
||||
SKIP(return, "overlayfs is not supported");
|
||||
|
||||
EXPECT_EQ(0, remove_path(lower_do1_fl3));
|
||||
EXPECT_EQ(0, remove_path(lower_dl1_fl2));
|
||||
EXPECT_EQ(0, remove_path(lower_fl1));
|
||||
|
@ -4068,6 +4109,9 @@ FIXTURE_TEARDOWN(layout2_overlay)
|
|||
|
||||
TEST_F_FORK(layout2_overlay, no_restriction)
|
||||
{
|
||||
if (!supports_overlayfs())
|
||||
SKIP(return, "overlayfs is not supported");
|
||||
|
||||
ASSERT_EQ(0, test_open(lower_fl1, O_RDONLY));
|
||||
ASSERT_EQ(0, test_open(lower_dl1, O_RDONLY));
|
||||
ASSERT_EQ(0, test_open(lower_dl1_fl2, O_RDONLY));
|
||||
|
@ -4231,6 +4275,9 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
|
|||
size_t i;
|
||||
const char *path_entry;
|
||||
|
||||
if (!supports_overlayfs())
|
||||
SKIP(return, "overlayfs is not supported");
|
||||
|
||||
/* Sets rules on base directories (i.e. outside overlay scope). */
|
||||
ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_base);
|
||||
ASSERT_LE(0, ruleset_fd);
|
||||
|
|
|
@ -19,6 +19,12 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
/* Copied from security/yama/yama_lsm.c */
|
||||
#define YAMA_SCOPE_DISABLED 0
|
||||
#define YAMA_SCOPE_RELATIONAL 1
|
||||
#define YAMA_SCOPE_CAPABILITY 2
|
||||
#define YAMA_SCOPE_NO_ATTACH 3
|
||||
|
||||
static void create_domain(struct __test_metadata *const _metadata)
|
||||
{
|
||||
int ruleset_fd;
|
||||
|
@ -60,6 +66,25 @@ static int test_ptrace_read(const pid_t pid)
|
|||
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(hierarchy) {};
|
||||
/* clang-format on */
|
||||
|
@ -232,8 +257,51 @@ TEST_F(hierarchy, 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
|
||||
|
@ -264,16 +332,21 @@ TEST_F(hierarchy, trace)
|
|||
/* 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. */
|
||||
/* 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 (variant->domain_child) {
|
||||
if (can_trace_parent) {
|
||||
EXPECT_EQ(0, ret);
|
||||
} else {
|
||||
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));
|
||||
|
@ -283,11 +356,11 @@ TEST_F(hierarchy, trace)
|
|||
|
||||
/* Tests child PTRACE_TRACEME. */
|
||||
ret = ptrace(PTRACE_TRACEME);
|
||||
if (variant->domain_parent) {
|
||||
if (can_trace_child) {
|
||||
EXPECT_EQ(0, ret);
|
||||
} else {
|
||||
EXPECT_EQ(-1, ret);
|
||||
EXPECT_EQ(EPERM, errno);
|
||||
} else {
|
||||
EXPECT_EQ(0, ret);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -296,7 +369,7 @@ TEST_F(hierarchy, trace)
|
|||
*/
|
||||
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
|
||||
|
||||
if (!variant->domain_parent) {
|
||||
if (can_trace_child) {
|
||||
ASSERT_EQ(0, raise(SIGSTOP));
|
||||
}
|
||||
|
||||
|
@ -321,7 +394,7 @@ TEST_F(hierarchy, trace)
|
|||
ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));
|
||||
|
||||
/* Tests child PTRACE_TRACEME. */
|
||||
if (!variant->domain_parent) {
|
||||
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));
|
||||
|
@ -331,17 +404,23 @@ TEST_F(hierarchy, trace)
|
|||
EXPECT_EQ(ESRCH, errno);
|
||||
}
|
||||
|
||||
/* Tests PTRACE_ATTACH and PTRACE_MODE_READ on the child. */
|
||||
/* 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 (variant->domain_parent) {
|
||||
if (can_trace_child) {
|
||||
EXPECT_EQ(0, ret);
|
||||
} else {
|
||||
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));
|
||||
|
|
Loading…
Reference in New Issue