Landlock updates for v6.14-rc1

-----BEGIN PGP SIGNATURE-----
 
 iIYEABYKAC4WIQSVyBthFV4iTW/VU1/l49DojIL20gUCZ5EMhBAcbWljQGRpZ2lr
 b2QubmV0AAoJEOXj0OiMgvbSMv0BAMOG2TFwq+UhbtxtL6pM7qzxfdWg6GR/t4t8
 MFasAcCaAQDtTnW0HymHge8k7JFgWHHp0JBu7V7dhFrdJoS+718aDA==
 =1Hfr
 -----END PGP SIGNATURE-----

Merge tag 'landlock-6.14-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux

Pull landlock updates from Mickaël Salaün:
 "This mostly factors out some Landlock code and prepares for upcoming
  audit support.

  Because files with invalid modes might be visible after filesystem
  corruption, Landlock now handles those weird files too.

  A few sample and test issues are also fixed"

* tag 'landlock-6.14-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux:
  selftests/landlock: Add layout1.umount_sandboxer tests
  selftests/landlock: Add wrappers.h
  selftests/landlock: Fix error message
  landlock: Optimize file path walks and prepare for audit support
  selftests/landlock: Add test to check partial access in a mount tree
  landlock: Align partial refer access checks with final ones
  landlock: Simplify initially denied access rights
  landlock: Move access types
  landlock: Factor out check_access_path()
  selftests/landlock: Fix build with non-default pthread linking
  landlock: Use scoped guards for ruleset in landlock_add_rule()
  landlock: Use scoped guards for ruleset
  landlock: Constify get_mode_access()
  landlock: Handle weird files
  samples/landlock: Fix possible NULL dereference in parse_path()
  selftests/landlock: Remove unused macros in ptrace_test.c
This commit is contained in:
Linus Torvalds 2025-01-22 20:20:55 -08:00
commit de5817bbfb
14 changed files with 489 additions and 195 deletions

View File

@ -91,6 +91,9 @@ static int parse_path(char *env_path, const char ***const path_list)
}
}
*path_list = malloc(num_paths * sizeof(**path_list));
if (!*path_list)
return -1;
for (i = 0; i < num_paths; i++)
(*path_list)[i] = strsep(&env_path, ENV_DELIMITER);
@ -127,6 +130,10 @@ static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd,
env_path_name = strdup(env_path_name);
unsetenv(env_var);
num_paths = parse_path(env_path_name, &path_list);
if (num_paths < 0) {
fprintf(stderr, "Failed to allocate memory\n");
goto out_free_name;
}
if (num_paths == 1 && path_list[0][0] == '\0') {
/*
* Allows to not use all possible restrictions (e.g. use

View File

@ -0,0 +1,77 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Landlock LSM - Access types and helpers
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2024-2025 Microsoft Corporation
*/
#ifndef _SECURITY_LANDLOCK_ACCESS_H
#define _SECURITY_LANDLOCK_ACCESS_H
#include <linux/bitops.h>
#include <linux/build_bug.h>
#include <linux/kernel.h>
#include <uapi/linux/landlock.h>
#include "limits.h"
/*
* All access rights that are denied by default whether they are handled or not
* by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
* entries when we need to get the absolute handled access masks, see
* landlock_upgrade_handled_access_masks().
*/
/* clang-format off */
#define _LANDLOCK_ACCESS_FS_INITIALLY_DENIED ( \
LANDLOCK_ACCESS_FS_REFER)
/* clang-format on */
typedef u16 access_mask_t;
/* Makes sure all filesystem access rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
/* Makes sure all network access rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET);
/* Makes sure all scoped rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_SCOPE);
/* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */
static_assert(sizeof(unsigned long) >= sizeof(access_mask_t));
/* Ruleset access masks. */
struct access_masks {
access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
access_mask_t scope : LANDLOCK_NUM_SCOPE;
};
union access_masks_all {
struct access_masks masks;
u32 all;
};
/* Makes sure all fields are covered. */
static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
sizeof(typeof_member(union access_masks_all, all)));
typedef u16 layer_mask_t;
/* Makes sure all layers can be checked. */
static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
/* Upgrades with all initially denied by default access rights. */
static inline struct access_masks
landlock_upgrade_handled_access_masks(struct access_masks access_masks)
{
/*
* All access rights that are denied by default whether they are
* explicitly handled or not.
*/
if (access_masks.fs)
access_masks.fs |= _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
return access_masks;
}
#endif /* _SECURITY_LANDLOCK_ACCESS_H */

View File

@ -36,6 +36,7 @@
#include <uapi/linux/fiemap.h>
#include <uapi/linux/landlock.h>
#include "access.h"
#include "common.h"
#include "cred.h"
#include "fs.h"
@ -388,14 +389,6 @@ static bool is_nouser_or_private(const struct dentry *dentry)
unlikely(IS_PRIVATE(d_backing_inode(dentry))));
}
static access_mask_t
get_handled_fs_accesses(const struct landlock_ruleset *const domain)
{
/* Handles all initially denied by default access rights. */
return landlock_union_access_masks(domain).fs |
LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
}
static const struct access_masks any_fs = {
.fs = ~0,
};
@ -572,6 +565,12 @@ static void test_no_more_access(struct kunit *const test)
#undef NMA_TRUE
#undef NMA_FALSE
static bool is_layer_masks_allowed(
layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS])
{
return !memchr_inv(layer_masks, 0, sizeof(*layer_masks));
}
/*
* Removes @layer_masks accesses that are not requested.
*
@ -589,7 +588,8 @@ scope_to_request(const access_mask_t access_request,
for_each_clear_bit(access_bit, &access_req, ARRAY_SIZE(*layer_masks))
(*layer_masks)[access_bit] = 0;
return !memchr_inv(layer_masks, 0, sizeof(*layer_masks));
return is_layer_masks_allowed(layer_masks);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
@ -778,16 +778,21 @@ static bool is_access_to_paths_allowed(
if (WARN_ON_ONCE(domain->num_layers < 1 || !layer_masks_parent1))
return false;
allowed_parent1 = is_layer_masks_allowed(layer_masks_parent1);
if (unlikely(layer_masks_parent2)) {
if (WARN_ON_ONCE(!dentry_child1))
return false;
allowed_parent2 = is_layer_masks_allowed(layer_masks_parent2);
/*
* For a double request, first check for potential privilege
* escalation by looking at domain handled accesses (which are
* a superset of the meaningful requested accesses).
*/
access_masked_parent1 = access_masked_parent2 =
get_handled_fs_accesses(domain);
landlock_union_access_masks(domain).fs;
is_dom_check = true;
} else {
if (WARN_ON_ONCE(dentry_child1 || dentry_child2))
@ -847,15 +852,6 @@ static bool is_access_to_paths_allowed(
child1_is_directory, layer_masks_parent2,
layer_masks_child2,
child2_is_directory))) {
allowed_parent1 = scope_to_request(
access_request_parent1, layer_masks_parent1);
allowed_parent2 = scope_to_request(
access_request_parent2, layer_masks_parent2);
/* Stops when all accesses are granted. */
if (allowed_parent1 && allowed_parent2)
break;
/*
* Now, downgrades the remaining checks from domain
* handled accesses to requested accesses.
@ -863,15 +859,32 @@ static bool is_access_to_paths_allowed(
is_dom_check = false;
access_masked_parent1 = access_request_parent1;
access_masked_parent2 = access_request_parent2;
allowed_parent1 =
allowed_parent1 ||
scope_to_request(access_masked_parent1,
layer_masks_parent1);
allowed_parent2 =
allowed_parent2 ||
scope_to_request(access_masked_parent2,
layer_masks_parent2);
/* Stops when all accesses are granted. */
if (allowed_parent1 && allowed_parent2)
break;
}
rule = find_rule(domain, walker_path.dentry);
allowed_parent1 = landlock_unmask_layers(
rule, access_masked_parent1, layer_masks_parent1,
ARRAY_SIZE(*layer_masks_parent1));
allowed_parent2 = landlock_unmask_layers(
rule, access_masked_parent2, layer_masks_parent2,
ARRAY_SIZE(*layer_masks_parent2));
allowed_parent1 = allowed_parent1 ||
landlock_unmask_layers(
rule, access_masked_parent1,
layer_masks_parent1,
ARRAY_SIZE(*layer_masks_parent1));
allowed_parent2 = allowed_parent2 ||
landlock_unmask_layers(
rule, access_masked_parent2,
layer_masks_parent2,
ARRAY_SIZE(*layer_masks_parent2));
/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2)
@ -895,8 +908,10 @@ jump_up:
* access to internal filesystems (e.g. nsfs, which is
* reachable through /proc/<pid>/ns/<namespace>).
*/
allowed_parent1 = allowed_parent2 =
!!(walker_path.mnt->mnt_flags & MNT_INTERNAL);
if (walker_path.mnt->mnt_flags & MNT_INTERNAL) {
allowed_parent1 = true;
allowed_parent2 = true;
}
break;
}
parent_dentry = dget_parent(walker_path.dentry);
@ -908,39 +923,29 @@ jump_up:
return allowed_parent1 && allowed_parent2;
}
static int check_access_path(const struct landlock_ruleset *const domain,
const struct path *const path,
access_mask_t access_request)
{
layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
access_request = landlock_init_layer_masks(
domain, access_request, &layer_masks, LANDLOCK_KEY_INODE);
if (is_access_to_paths_allowed(domain, path, access_request,
&layer_masks, NULL, 0, NULL, NULL))
return 0;
return -EACCES;
}
static int current_check_access_path(const struct path *const path,
const access_mask_t access_request)
access_mask_t access_request)
{
const struct landlock_ruleset *const dom = get_current_fs_domain();
layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
if (!dom)
return 0;
return check_access_path(dom, path, access_request);
access_request = landlock_init_layer_masks(
dom, access_request, &layer_masks, LANDLOCK_KEY_INODE);
if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks,
NULL, 0, NULL, NULL))
return 0;
return -EACCES;
}
static access_mask_t get_mode_access(const umode_t mode)
static __attribute_const__ access_mask_t get_mode_access(const umode_t mode)
{
switch (mode & S_IFMT) {
case S_IFLNK:
return LANDLOCK_ACCESS_FS_MAKE_SYM;
case 0:
/* A zero mode translates to S_IFREG. */
case S_IFREG:
return LANDLOCK_ACCESS_FS_MAKE_REG;
case S_IFDIR:
return LANDLOCK_ACCESS_FS_MAKE_DIR;
case S_IFCHR:
@ -951,9 +956,12 @@ static access_mask_t get_mode_access(const umode_t mode)
return LANDLOCK_ACCESS_FS_MAKE_FIFO;
case S_IFSOCK:
return LANDLOCK_ACCESS_FS_MAKE_SOCK;
case S_IFREG:
case 0:
/* A zero mode translates to S_IFREG. */
default:
WARN_ON_ONCE(1);
return 0;
/* Treats weird files as regular files. */
return LANDLOCK_ACCESS_FS_MAKE_REG;
}
}
@ -1414,11 +1422,7 @@ static int hook_path_mknod(const struct path *const dir,
struct dentry *const dentry, const umode_t mode,
const unsigned int dev)
{
const struct landlock_ruleset *const dom = get_current_fs_domain();
if (!dom)
return 0;
return check_access_path(dom, dir, get_mode_access(mode));
return current_check_access_path(dir, get_mode_access(mode));
}
static int hook_path_symlink(const struct path *const dir,

View File

@ -13,6 +13,7 @@
#include <linux/init.h>
#include <linux/rcupdate.h>
#include "access.h"
#include "ruleset.h"
#include "setup.h"

View File

@ -8,11 +8,13 @@
#include <linux/bits.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
#include <linux/compiler_types.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/lockdep.h>
#include <linux/mutex.h>
#include <linux/overflow.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
@ -20,6 +22,7 @@
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
@ -384,7 +387,8 @@ static int merge_ruleset(struct landlock_ruleset *const dst,
err = -EINVAL;
goto out_unlock;
}
dst->access_masks[dst->num_layers - 1] = src->access_masks[0];
dst->access_masks[dst->num_layers - 1] =
landlock_upgrade_handled_access_masks(src->access_masks[0]);
/* Merges the @src inode tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
@ -537,7 +541,7 @@ struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset)
{
struct landlock_ruleset *new_dom;
struct landlock_ruleset *new_dom __free(landlock_put_ruleset) = NULL;
u32 num_layers;
int err;
@ -557,29 +561,25 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
new_dom = create_ruleset(num_layers);
if (IS_ERR(new_dom))
return new_dom;
new_dom->hierarchy =
kzalloc(sizeof(*new_dom->hierarchy), GFP_KERNEL_ACCOUNT);
if (!new_dom->hierarchy) {
err = -ENOMEM;
goto out_put_dom;
}
if (!new_dom->hierarchy)
return ERR_PTR(-ENOMEM);
refcount_set(&new_dom->hierarchy->usage, 1);
/* ...as a child of @parent... */
err = inherit_ruleset(parent, new_dom);
if (err)
goto out_put_dom;
return ERR_PTR(err);
/* ...and including @ruleset. */
err = merge_ruleset(new_dom, ruleset);
if (err)
goto out_put_dom;
return ERR_PTR(err);
return new_dom;
out_put_dom:
landlock_put_ruleset(new_dom);
return ERR_PTR(err);
return no_free_ptr(new_dom);
}
/*

View File

@ -9,58 +9,17 @@
#ifndef _SECURITY_LANDLOCK_RULESET_H
#define _SECURITY_LANDLOCK_RULESET_H
#include <linux/bitops.h>
#include <linux/build_bug.h>
#include <linux/kernel.h>
#include <linux/cleanup.h>
#include <linux/err.h>
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
#include "access.h"
#include "limits.h"
#include "object.h"
/*
* All access rights that are denied by default whether they are handled or not
* by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
* entries when we need to get the absolute handled access masks.
*/
/* clang-format off */
#define LANDLOCK_ACCESS_FS_INITIALLY_DENIED ( \
LANDLOCK_ACCESS_FS_REFER)
/* clang-format on */
typedef u16 access_mask_t;
/* Makes sure all filesystem access rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS);
/* Makes sure all network access rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET);
/* Makes sure all scoped rights can be stored. */
static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_SCOPE);
/* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */
static_assert(sizeof(unsigned long) >= sizeof(access_mask_t));
/* Ruleset access masks. */
struct access_masks {
access_mask_t fs : LANDLOCK_NUM_ACCESS_FS;
access_mask_t net : LANDLOCK_NUM_ACCESS_NET;
access_mask_t scope : LANDLOCK_NUM_SCOPE;
};
union access_masks_all {
struct access_masks masks;
u32 all;
};
/* Makes sure all fields are covered. */
static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
sizeof(typeof_member(union access_masks_all, all)));
typedef u16 layer_mask_t;
/* Makes sure all layers can be checked. */
static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
/**
* struct landlock_layer - Access rights for a given layer
*/
@ -252,6 +211,9 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access);
@ -366,7 +328,7 @@ landlock_get_fs_access_mask(const struct landlock_ruleset *const ruleset,
{
/* Handles all initially denied by default access rights. */
return ruleset->access_masks[layer_level].fs |
LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
_LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
}
static inline access_mask_t

View File

@ -10,6 +10,7 @@
#include <linux/anon_inodes.h>
#include <linux/build_bug.h>
#include <linux/capability.h>
#include <linux/cleanup.h>
#include <linux/compiler_types.h>
#include <linux/dcache.h>
#include <linux/err.h>
@ -398,8 +399,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
const enum landlock_rule_type, rule_type,
const void __user *const, rule_attr, const __u32, flags)
{
struct landlock_ruleset *ruleset;
int err;
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
if (!is_initialized())
return -EOPNOTSUPP;
@ -415,17 +415,12 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
switch (rule_type) {
case LANDLOCK_RULE_PATH_BENEATH:
err = add_rule_path_beneath(ruleset, rule_attr);
break;
return add_rule_path_beneath(ruleset, rule_attr);
case LANDLOCK_RULE_NET_PORT:
err = add_rule_net_port(ruleset, rule_attr);
break;
return add_rule_net_port(ruleset, rule_attr);
default:
err = -EINVAL;
break;
return -EINVAL;
}
landlock_put_ruleset(ruleset);
return err;
}
/* Enforcement */
@ -456,10 +451,10 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
struct landlock_ruleset *new_dom, *ruleset;
struct landlock_ruleset *new_dom,
*ruleset __free(landlock_put_ruleset) = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
int err;
if (!is_initialized())
return -EOPNOTSUPP;
@ -483,10 +478,9 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
/* Prepares new credentials. */
new_cred = prepare_creds();
if (!new_cred) {
err = -ENOMEM;
goto out_put_ruleset;
}
if (!new_cred)
return -ENOMEM;
new_llcred = landlock_cred(new_cred);
/*
@ -495,21 +489,12 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
*/
new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
err = PTR_ERR(new_dom);
goto out_put_creds;
abort_creds(new_cred);
return PTR_ERR(new_dom);
}
/* Replaces the old (prepared) domain. */
landlock_put_ruleset(new_llcred->domain);
new_llcred->domain = new_dom;
landlock_put_ruleset(ruleset);
return commit_creds(new_cred);
out_put_creds:
abort_creds(new_cred);
out_put_ruleset:
landlock_put_ruleset(ruleset);
return err;
}

View File

@ -10,14 +10,14 @@ src_test := $(wildcard *_test.c)
TEST_GEN_PROGS := $(src_test:.c=)
TEST_GEN_PROGS_EXTENDED := true
TEST_GEN_PROGS_EXTENDED := true sandbox-and-launch wait-pipe
# Short targets:
$(TEST_GEN_PROGS): LDLIBS += -lcap
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread
$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static
include ../lib.mk
# Targets with $(OUTPUT)/ prefix:
$(TEST_GEN_PROGS): LDLIBS += -lcap
$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread
$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static

View File

@ -9,17 +9,15 @@
#include <arpa/inet.h>
#include <errno.h>
#include <linux/landlock.h>
#include <linux/securebits.h>
#include <sys/capability.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../kselftest_harness.h"
#include "wrappers.h"
#define TMP_DIR "tmp"
@ -30,33 +28,8 @@
/* TEST_F_FORK() should not be used for new tests. */
#define TEST_F_FORK(fixture_name, test_name) TEST_F(fixture_name, test_name)
#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 const char bin_sandbox_and_launch[] = "./sandbox-and-launch";
static const char bin_wait_pipe[] = "./wait-pipe";
static void _init_caps(struct __test_metadata *const _metadata, bool drop_all)
{
@ -250,11 +223,6 @@ struct service_fixture {
};
};
static pid_t __maybe_unused sys_gettid(void)
{
return syscall(__NR_gettid);
}
static void __maybe_unused set_unix_address(struct service_fixture *const srv,
const unsigned short index)
{

View File

@ -59,7 +59,7 @@ int open_tree(int dfd, const char *filename, unsigned int flags)
#define RENAME_EXCHANGE (1 << 1)
#endif
#define BINARY_PATH "./true"
static const char bin_true[] = "./true";
/* Paths (sibling number and depth) */
static const char dir_s1d1[] = TMP_DIR "/s1d1";
@ -85,6 +85,9 @@ static const char file1_s3d1[] = TMP_DIR "/s3d1/f1";
/* dir_s3d2 is a mount point. */
static const char dir_s3d2[] = TMP_DIR "/s3d1/s3d2";
static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
static const char file1_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3/f1";
static const char dir_s3d4[] = TMP_DIR "/s3d1/s3d2/s3d4";
static const char file1_s3d4[] = TMP_DIR "/s3d1/s3d2/s3d4/f1";
/*
* layout1 hierarchy:
@ -108,8 +111,11 @@ static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3";
*    f2
* s3d1
*    f1
* s3d2
* s3d3
* s3d2 [mount point]
*    s3d3
*    f1
*    s3d4
*    f1
*/
static bool fgrep(FILE *const inf, const char *const str)
@ -358,7 +364,8 @@ static void create_layout1(struct __test_metadata *const _metadata)
ASSERT_EQ(0, mount_opt(&mnt_tmp, dir_s3d2));
clear_cap(_metadata, CAP_SYS_ADMIN);
ASSERT_EQ(0, mkdir(dir_s3d3, 0700));
create_file(_metadata, file1_s3d3);
create_file(_metadata, file1_s3d4);
}
static void remove_layout1(struct __test_metadata *const _metadata)
@ -378,7 +385,8 @@ static void remove_layout1(struct __test_metadata *const _metadata)
EXPECT_EQ(0, remove_path(dir_s2d2));
EXPECT_EQ(0, remove_path(file1_s3d1));
EXPECT_EQ(0, remove_path(dir_s3d3));
EXPECT_EQ(0, remove_path(file1_s3d3));
EXPECT_EQ(0, remove_path(file1_s3d4));
set_cap(_metadata, CAP_SYS_ADMIN);
umount(dir_s3d2);
clear_cap(_metadata, CAP_SYS_ADMIN);
@ -1957,8 +1965,8 @@ TEST_F_FORK(layout1, relative_chroot_chdir)
test_relative_path(_metadata, REL_CHROOT_CHDIR);
}
static void copy_binary(struct __test_metadata *const _metadata,
const char *const dst_path)
static void copy_file(struct __test_metadata *const _metadata,
const char *const src_path, const char *const dst_path)
{
int dst_fd, src_fd;
struct stat statbuf;
@ -1968,11 +1976,10 @@ static void copy_binary(struct __test_metadata *const _metadata,
{
TH_LOG("Failed to open \"%s\": %s", dst_path, strerror(errno));
}
src_fd = open(BINARY_PATH, O_RDONLY | O_CLOEXEC);
src_fd = open(src_path, O_RDONLY | O_CLOEXEC);
ASSERT_LE(0, src_fd)
{
TH_LOG("Failed to open \"" BINARY_PATH "\": %s",
strerror(errno));
TH_LOG("Failed to open \"%s\": %s", src_path, strerror(errno));
}
ASSERT_EQ(0, fstat(src_fd, &statbuf));
ASSERT_EQ(statbuf.st_size,
@ -2003,8 +2010,7 @@ static void test_execute(struct __test_metadata *const _metadata, const int err,
ASSERT_EQ(1, WIFEXITED(status));
ASSERT_EQ(err ? 2 : 0, WEXITSTATUS(status))
{
TH_LOG("Unexpected return code for \"%s\": %s", path,
strerror(errno));
TH_LOG("Unexpected return code for \"%s\"", path);
};
}
@ -2021,9 +2027,9 @@ TEST_F_FORK(layout1, execute)
create_ruleset(_metadata, rules[0].access, rules);
ASSERT_LE(0, ruleset_fd);
copy_binary(_metadata, file1_s1d1);
copy_binary(_metadata, file1_s1d2);
copy_binary(_metadata, file1_s1d3);
copy_file(_metadata, bin_true, file1_s1d1);
copy_file(_metadata, bin_true, file1_s1d2);
copy_file(_metadata, bin_true, file1_s1d3);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
@ -2041,6 +2047,83 @@ TEST_F_FORK(layout1, execute)
test_execute(_metadata, 0, file1_s1d3);
}
TEST_F_FORK(layout1, umount_sandboxer)
{
int pipe_child[2], pipe_parent[2];
char buf_parent;
pid_t child;
int status;
copy_file(_metadata, bin_sandbox_and_launch, file1_s3d3);
ASSERT_EQ(0, pipe2(pipe_child, 0));
ASSERT_EQ(0, pipe2(pipe_parent, 0));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char pipe_child_str[12], pipe_parent_str[12];
char *const argv[] = { (char *)file1_s3d3,
(char *)bin_wait_pipe, pipe_child_str,
pipe_parent_str, NULL };
/* Passes the pipe FDs to the executed binary and its child. */
EXPECT_EQ(0, close(pipe_child[0]));
EXPECT_EQ(0, close(pipe_parent[1]));
snprintf(pipe_child_str, sizeof(pipe_child_str), "%d",
pipe_child[1]);
snprintf(pipe_parent_str, sizeof(pipe_parent_str), "%d",
pipe_parent[0]);
/*
* We need bin_sandbox_and_launch (copied inside the mount as
* file1_s3d3) to execute bin_wait_pipe (outside the mount) to
* make sure the mount point will not be EBUSY because of
* file1_s3d3 being in use. This avoids a potential race
* condition between the following read() and umount() calls.
*/
ASSERT_EQ(0, execve(argv[0], argv, NULL))
{
TH_LOG("Failed to execute \"%s\": %s", argv[0],
strerror(errno));
};
_exit(1);
return;
}
EXPECT_EQ(0, close(pipe_child[1]));
EXPECT_EQ(0, close(pipe_parent[0]));
/* Waits for the child to sandbox itself. */
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests that the sandboxer is tied to its mount point. */
set_cap(_metadata, CAP_SYS_ADMIN);
EXPECT_EQ(-1, umount(dir_s3d2));
EXPECT_EQ(EBUSY, errno);
clear_cap(_metadata, CAP_SYS_ADMIN);
/* Signals the child to launch a grandchild. */
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for the grandchild. */
EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1));
/* Tests that the domain's sandboxer is not tied to its mount point. */
set_cap(_metadata, CAP_SYS_ADMIN);
EXPECT_EQ(0, umount(dir_s3d2))
{
TH_LOG("Failed to umount \"%s\": %s", dir_s3d2,
strerror(errno));
};
clear_cap(_metadata, CAP_SYS_ADMIN);
/* Signals the grandchild to terminate. */
EXPECT_EQ(1, write(pipe_parent[1], ".", 1));
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_EQ(1, WIFEXITED(status));
ASSERT_EQ(0, WEXITSTATUS(status));
}
TEST_F_FORK(layout1, link)
{
const struct rule layer1[] = {
@ -2444,6 +2527,44 @@ TEST_F_FORK(layout1, refer_mount_root_deny)
EXPECT_EQ(0, close(root_fd));
}
TEST_F_FORK(layout1, refer_part_mount_tree_is_allowed)
{
const struct rule layer1[] = {
{
/* Parent mount point. */
.path = dir_s3d1,
.access = LANDLOCK_ACCESS_FS_REFER |
LANDLOCK_ACCESS_FS_MAKE_REG,
},
{
/*
* Removing the source file is allowed because its
* access rights are already a superset of the
* destination.
*/
.path = dir_s3d4,
.access = LANDLOCK_ACCESS_FS_REFER |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE,
},
{},
};
int ruleset_fd;
ASSERT_EQ(0, unlink(file1_s3d3));
ruleset_fd = create_ruleset(_metadata,
LANDLOCK_ACCESS_FS_REFER |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE,
layer1);
ASSERT_LE(0, ruleset_fd);
enforce_ruleset(_metadata, ruleset_fd);
ASSERT_EQ(0, close(ruleset_fd));
ASSERT_EQ(0, rename(file1_s3d4, file1_s3d3));
}
TEST_F_FORK(layout1, reparent_link)
{
const struct rule layer1[] = {

View File

@ -22,8 +22,6 @@
/* 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)
{

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}