Skip to content

Commit

Permalink
Merge tag 'landlock-6.14-rc1' of git://git.kernel.org/pub/scm/linux/k…
Browse files Browse the repository at this point in the history
…ernel/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
  • Loading branch information
Linus Torvalds committed Jan 23, 2025
2 parents 37b33c6 + 2a794ee commit de5817b
Show file tree
Hide file tree
Showing 14 changed files with 489 additions and 195 deletions.
7 changes: 7 additions & 0 deletions samples/landlock/sandboxer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions security/landlock/access.h
Original file line number Diff line number Diff line change
@@ -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 */
114 changes: 59 additions & 55 deletions security/landlock/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -847,31 +852,39 @@ 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.
*/
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)
Expand All @@ -895,8 +908,10 @@ static bool is_access_to_paths_allowed(
* 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);
Expand All @@ -908,39 +923,29 @@ static bool is_access_to_paths_allowed(
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:
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions security/landlock/fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <linux/init.h>
#include <linux/rcupdate.h>

#include "access.h"
#include "ruleset.h"
#include "setup.h"

Expand Down
Loading

0 comments on commit de5817b

Please sign in to comment.