* [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks
@ 2026-11-10 20:24 Mickaël Salaün
2026-11-10 20:24 ` [PATCH 1/6] landlock: Factor out capability denial checks Mickaël Salaün
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw)
To: Günther Noack
Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris,
Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel,
linux-security-module
Hi,
This is series 1 of 3:
* 1. fix Landlock mediation of commoncap's capability checks;
2. fix PR_SET_SECUREBITS mediation and add focused Landlock tests;
3. fix Landlock mediation of capabilities guarding LSM policies.
LANDLOCK_PERMISSION_CAPABILITY_USE, merged for v7.4, denies capabilities
through the LSM capable hook, but commoncap's mmap_addr, capset and
vm_enough_memory hooks call cap_capable() directly. A domain that
denies a capability can therefore still:
- map memory below vm.mmap_min_addr (CAP_SYS_RAWIO), which defeats a
mitigation for kernel NULL pointer dereferences;
- add inheritable capabilities outside of its permitted set
(CAP_SETPCAP), which lets a privileged sandbox without no_new_privs
gain capabilities through execve(), although the domain still
restricts their use;
- use the admin memory reserve with vm.overcommit_memory=2
(CAP_SYS_ADMIN).
As SELinux does, patches 2 to 4 add Landlock's own check to these hooks,
with commoncap's conditions, so they only deny what commoncap granted
because of the capability. Patch 1 factors out the shared check. Patch
5 adds tests, including audit tests, that fail without the fixes. Patch
6 fixes the documentation, which overstated what the permission
restricts and how its denials show.
Two other direct capability-set checks are fixed separately:
- PSI triggers (CAP_SYS_RESOURCE):
https://patch.msgid.link/20261106120004.90041-1-dennylin0707@gmail.com
- user namespace uid 0 mapping (CAP_SETFCAP):
https://patch.msgid.link/20261110144205.2404088-1-mic@digikod.net
Regards,
Mickaël Salaün (6):
landlock: Factor out capability denial checks
landlock: Mediate CAP_SYS_RAWIO for mappings below mmap_min_addr
landlock: Mediate CAP_SETPCAP for inheritable capabilities
landlock: Mediate CAP_SYS_ADMIN for the admin memory reserve
selftests/landlock: Test capability checks outside the capable hook
landlock: Fix capability restriction documentation
Documentation/security/landlock.rst | 8 +-
Documentation/userspace-api/landlock.rst | 8 +-
include/uapi/linux/landlock.h | 6 +-
security/landlock/cap.c | 110 +++-
tools/testing/selftests/landlock/cap_test.c | 535 ++++++++++++++++++++
tools/testing/selftests/landlock/common.h | 4 +-
6 files changed, 650 insertions(+), 21 deletions(-)
base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
--
2.56.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH 1/6] landlock: Factor out capability denial checks 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 2026-11-10 20:24 ` [PATCH 2/6] landlock: Mediate CAP_SYS_RAWIO for mappings below mmap_min_addr Mickaël Salaün ` (4 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module hook_capable() is the only place that checks a capability against a Landlock domain. commoncap also makes capability decisions from other LSM hooks, and the following commits add Landlock counterparts for them. Move the check and its denial logging into check_cap() so that these hooks share it. No functional change. Cc: Günther Noack <gnoack@google.com> Signed-off-by: Mickaël Salaün <mic@digikod.net> --- security/landlock/cap.c | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/security/landlock/cap.c b/security/landlock/cap.c index b4f491b09baa..fa59118e3486 100644 --- a/security/landlock/cap.c +++ b/security/landlock/cap.c @@ -24,25 +24,21 @@ static const struct access_masks cap_permission = { }; /** - * hook_capable - Deny capability use for Landlock-sandboxed processes + * check_cap - Check a capability use against the domain of @cred * * @cred: Credentials being checked. - * @ns: Target user namespace (intentionally ignored; policy depends on the - * acting credentials and @cap, not namespace ownership). * @cap: Capability number (CAP_*). * @opts: Capability check options. CAP_OPT_NOAUDIT skips the denial record: * no audit record, no trace event, and no denial count. * * Pure bitmask check: denies the capability if it is not in the layer's allowed - * set. This hook is purely restrictive: commoncap is registered with - * LSM_ORDER_FIRST so cap_capable() always runs first, which means Landlock can - * deny capabilities that commoncap would allow, but never grant capabilities - * that commoncap denied. + * set. It does not check that @cred holds @cap: only call it for a capability + * that commoncap already granted. * * Return: 0 if allowed, -EPERM if capability use is denied. */ -static int hook_capable(const struct cred *cred, struct user_namespace *ns, - int cap, unsigned int opts) +static int check_cap(const struct cred *const cred, const int cap, + const unsigned int opts) { const struct landlock_cred_security *subject; size_t denied_layer; @@ -72,6 +68,28 @@ static int hook_capable(const struct cred *cred, struct user_namespace *ns, return -EPERM; } +/** + * hook_capable - Deny capability use for Landlock-sandboxed processes + * + * @cred: Credentials being checked. + * @ns: Target user namespace (intentionally ignored; policy depends on the + * acting credentials and @cap, not namespace ownership). + * @cap: Capability number (CAP_*). + * @opts: Capability check options, see check_cap(). + * + * This hook is purely restrictive: commoncap is registered with LSM_ORDER_FIRST + * so cap_capable() always runs first, which means Landlock can deny + * capabilities that commoncap would allow, but never grant capabilities that + * commoncap denied. + * + * Return: 0 if allowed, -EPERM if capability use is denied. + */ +static int hook_capable(const struct cred *cred, struct user_namespace *ns, + int cap, unsigned int opts) +{ + return check_cap(cred, cap, opts); +} + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(capable, hook_capable), }; -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/6] landlock: Mediate CAP_SYS_RAWIO for mappings below mmap_min_addr 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün 2026-11-10 20:24 ` [PATCH 1/6] landlock: Factor out capability denial checks Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 2026-11-10 20:24 ` [PATCH 3/6] landlock: Mediate CAP_SETPCAP for inheritable capabilities Mickaël Salaün ` (3 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module Until now, a process in a domain that denies CAP_SYS_RAWIO could still map memory below the vm.mmap_min_addr limit if it held this capability. cap_mmap_addr() is commoncap's mmap_addr hook, and it checks CAP_SYS_RAWIO with cap_capable(), not security_capable(), so the check never reaches hook_capable(). Mapping the lowest pages is what this limit protects against, because it helps to exploit kernel NULL pointer dereferences. Add a Landlock mmap_addr hook that checks CAP_SYS_RAWIO against the domain under the same condition as cap_mmap_addr(). With the same vm.mmap_min_addr value, the hook only denies mappings for which commoncap required the capability. security_mmap_addr() stops at commoncap's error, so processes without the capability get the same result as before. This also covers the x86 vm86() syscalls, which use the same check for address 0. Making cap_mmap_addr() call capable() would also fix this, but it would send every low mapping through the capable hook of all LSMs (SELinux, AppArmor, SafeSetID, BPF LSM programs), adding capability checks that their policies never had to handle. For instance, SELinux checks mmap_zero in its own mmap_addr hook and keeps it separate from the capability on purpose. LSMs with their own policy for low mappings implement it in this hook, so Landlock does the same. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Reported-by: Ada Smith <adasmithgt@gmail.com> Fixes: 97c8386f1d74 ("landlock: Enforce capability restrictions") Signed-off-by: Mickaël Salaün <mic@digikod.net> --- security/landlock/cap.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/security/landlock/cap.c b/security/landlock/cap.c index fa59118e3486..10e1ca772b9c 100644 --- a/security/landlock/cap.c +++ b/security/landlock/cap.c @@ -9,6 +9,7 @@ #include <linux/cred.h> #include <linux/lsm_audit.h> #include <linux/lsm_hooks.h> +#include <linux/security.h> #include <uapi/linux/landlock.h> #include "cap.h" @@ -90,8 +91,29 @@ static int hook_capable(const struct cred *cred, struct user_namespace *ns, return check_cap(cred, cap, opts); } +/** + * hook_mmap_addr - Restrict mappings below dac_mmap_min_addr + * + * @addr: Address of the new mapping. + * + * cap_mmap_addr() requires CAP_SYS_RAWIO to map below dac_mmap_min_addr, but + * calls cap_capable() directly, which skips hook_capable(). Check the same + * condition against the domain. commoncap runs first, so this only sees + * mappings that commoncap allowed. + * + * Return: 0 if allowed, -EPERM if CAP_SYS_RAWIO use is denied. + */ +static int hook_mmap_addr(unsigned long addr) +{ + if (addr >= dac_mmap_min_addr) + return 0; + + return check_cap(current_cred(), CAP_SYS_RAWIO, CAP_OPT_NONE); +} + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(capable, hook_capable), + LSM_HOOK_INIT(mmap_addr, hook_mmap_addr), }; __init void landlock_add_cap_hooks(void) -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 3/6] landlock: Mediate CAP_SETPCAP for inheritable capabilities 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün 2026-11-10 20:24 ` [PATCH 1/6] landlock: Factor out capability denial checks Mickaël Salaün 2026-11-10 20:24 ` [PATCH 2/6] landlock: Mediate CAP_SYS_RAWIO for mappings below mmap_min_addr Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 2026-11-10 20:24 ` [PATCH 4/6] landlock: Mediate CAP_SYS_ADMIN for the admin memory reserve Mickaël Salaün ` (2 subsequent siblings) 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module A process in a domain that denies CAP_SETPCAP could still add capabilities outside of its permitted set to its inheritable set with capset(2), if it held CAP_SETPCAP. cap_capset() checks CAP_SETPCAP with cap_capable() in cap_inh_is_capped(), so the check never reaches hook_capable(). Through file inheritable capabilities, such a set can grant new permitted capabilities on execve() when no_new_privs is not set, which is possible for a domain enforced by a task holding CAP_SYS_ADMIN. Add a Landlock capset hook that checks CAP_SETPCAP against the domain under the same condition as cap_capset(). As with the mmap_addr hook, LSMs implement their own policy in this hook: SELinux checks capset with its own permission. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Reported-by: Ada Smith <adasmithgt@gmail.com> Fixes: 97c8386f1d74 ("landlock: Enforce capability restrictions") Signed-off-by: Mickaël Salaün <mic@digikod.net> --- security/landlock/cap.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/security/landlock/cap.c b/security/landlock/cap.c index 10e1ca772b9c..c89e8d76db8c 100644 --- a/security/landlock/cap.c +++ b/security/landlock/cap.c @@ -91,6 +91,35 @@ static int hook_capable(const struct cred *cred, struct user_namespace *ns, return check_cap(cred, cap, opts); } +/** + * hook_capset - Restrict inheritable capability changes + * + * @new: New credentials (unused). + * @old: Current credentials. + * @effective: New effective set (unused). + * @inheritable: New inheritable set. + * @permitted: New permitted set (unused). + * + * cap_capset() requires CAP_SETPCAP to add inheritable capabilities that are + * not in the old permitted set, but calls cap_capable() directly, which skips + * hook_capable(). Check the same condition against the domain. commoncap runs + * first, so this only sees requests that commoncap allowed. capset(2) discards + * @new if this hook fails, so nothing is committed. + * + * Return: 0 if allowed, -EPERM if CAP_SETPCAP use is denied. + */ +static int hook_capset(struct cred *new, const struct cred *old, + const kernel_cap_t *effective, + const kernel_cap_t *inheritable, + const kernel_cap_t *permitted) +{ + if (cap_issubset(*inheritable, + cap_combine(old->cap_inheritable, old->cap_permitted))) + return 0; + + return check_cap(old, CAP_SETPCAP, CAP_OPT_NONE); +} + /** * hook_mmap_addr - Restrict mappings below dac_mmap_min_addr * @@ -113,6 +142,7 @@ static int hook_mmap_addr(unsigned long addr) static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(capable, hook_capable), + LSM_HOOK_INIT(capset, hook_capset), LSM_HOOK_INIT(mmap_addr, hook_mmap_addr), }; -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 4/6] landlock: Mediate CAP_SYS_ADMIN for the admin memory reserve 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün ` (2 preceding siblings ...) 2026-11-10 20:24 ` [PATCH 3/6] landlock: Mediate CAP_SETPCAP for inheritable capabilities Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 2026-11-10 20:24 ` [PATCH 5/6] selftests/landlock: Test capability checks outside the capable hook Mickaël Salaün 2026-11-10 20:24 ` [PATCH 6/6] landlock: Fix capability restriction documentation Mickaël Salaün 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module With vm.overcommit_memory set to 2, __vm_enough_memory() keeps the vm.admin_reserve_kbytes reserve for callers whose LSMs all agree that they hold CAP_SYS_ADMIN. cap_vm_enough_memory() checks it with cap_capable(), so a process in a domain that denies CAP_SYS_ADMIN could still use this reserve, which is meant to let an administrator recover from memory exhaustion. Add a Landlock vm_enough_memory hook that withholds the reserve when the domain denies CAP_SYS_ADMIN. Like commoncap and SELinux, do not audit this check: it only selects a limit, it runs for every accounted mapping, and withholding the reserve can only make an allocation fail with -ENOMEM. As with the previous hooks, LSMs implement their own policy in this hook: SELinux already checks CAP_SYS_ADMIN in its own vm_enough_memory hook, so making commoncap call security_capable() would make SELinux check it twice. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Reported-by: Ada Smith <adasmithgt@gmail.com> Fixes: 97c8386f1d74 ("landlock: Enforce capability restrictions") Signed-off-by: Mickaël Salaün <mic@digikod.net> --- security/landlock/cap.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/security/landlock/cap.c b/security/landlock/cap.c index c89e8d76db8c..c9a900888f3b 100644 --- a/security/landlock/cap.c +++ b/security/landlock/cap.c @@ -140,10 +140,32 @@ static int hook_mmap_addr(unsigned long addr) return check_cap(current_cred(), CAP_SYS_RAWIO, CAP_OPT_NONE); } +/** + * hook_vm_enough_memory - Restrict the admin memory reserve + * + * @mm: Address space being charged (unused). + * @pages: Number of pages being charged (unused). + * + * cap_vm_enough_memory() lets CAP_SYS_ADMIN use the vm.admin_reserve_kbytes + * reserve, but calls cap_capable() directly, which skips hook_capable(). Check + * the same capability against the domain. commoncap runs first, so this only + * sees tasks that commoncap let use the reserve. Like commoncap, do not log + * denials: this only selects a memory limit. + * + * Return: 0 if the domain does not withhold the reserve, -EPERM if + * CAP_SYS_ADMIN use is denied. An error does not fail the allocation by + * itself, but without the reserve, the allocation may fail with -ENOMEM. + */ +static int hook_vm_enough_memory(struct mm_struct *mm, long pages) +{ + return check_cap(current_cred(), CAP_SYS_ADMIN, CAP_OPT_NOAUDIT); +} + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(capable, hook_capable), LSM_HOOK_INIT(capset, hook_capset), LSM_HOOK_INIT(mmap_addr, hook_mmap_addr), + LSM_HOOK_INIT(vm_enough_memory, hook_vm_enough_memory), }; __init void landlock_add_cap_hooks(void) -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 5/6] selftests/landlock: Test capability checks outside the capable hook 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün ` (3 preceding siblings ...) 2026-11-10 20:24 ` [PATCH 4/6] landlock: Mediate CAP_SYS_ADMIN for the admin memory reserve Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 2026-11-10 20:24 ` [PATCH 6/6] landlock: Fix capability restriction documentation Mickaël Salaün 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module The previous commits make Landlock restrict the capability checks that commoncap makes from LSM hooks other than capable, which no test covers: mapping below vm.mmap_min_addr (CAP_SYS_RAWIO), adding inheritable capabilities outside of the permitted set (CAP_SETPCAP), and using the admin memory reserve under vm.overcommit_memory=2 (CAP_SYS_ADMIN). Each test first checks that the operation fails without the capability, so that it exercises the capability check, and then checks it with the capability effective in four variants: unsandboxed, in a domain that denies the capability, in a domain that allows it, and in a domain that does not handle capabilities. Audit tests check that the capset and mmap_addr denials are logged, that the admin reserve check is not, and that allowed uses log nothing. The admin reserve test has to change system-wide overcommit settings: it sets a commit limit that only fits its mapping with the reserve, with margins of 96 MiB for other processes. It changes them after the fixture setup, so that the teardown restores them after any failure, and it only runs as root (EUID 0) while writing them. Add CAP_SYS_RAWIO to the capabilities that the tests may raise, and drop the stale count from the comment of this list. Without the fixes, the three denied variants and the denial audit test fail. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Signed-off-by: Mickaël Salaün <mic@digikod.net> --- tools/testing/selftests/landlock/cap_test.c | 535 ++++++++++++++++++++ tools/testing/selftests/landlock/common.h | 4 +- 2 files changed, 538 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/landlock/cap_test.c b/tools/testing/selftests/landlock/cap_test.c index 3d7fd02e21ed..c79bb42c6282 100644 --- a/tools/testing/selftests/landlock/cap_test.c +++ b/tools/testing/selftests/landlock/cap_test.c @@ -12,7 +12,9 @@ #include <linux/landlock.h> #include <sched.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> +#include <sys/mman.h> #include <sys/mount.h> #include <sys/wait.h> #include <sys/xattr.h> @@ -738,6 +740,451 @@ TEST(cap_target_userns_independent) EXPECT_EQ(0, close(target_userns_fd)); } +/* + * Capability checks that commoncap makes from hooks other than the capable + * hook. Each test first checks that the operation needs the capability, then + * checks it with the capability effective. Variants cover an unsandboxed + * baseline, a domain that denies the capability, a domain that allows it, and a + * domain that does not handle capabilities. + */ +/* clang-format off */ +FIXTURE(cap_other_hooks) {}; +/* clang-format on */ + +FIXTURE_VARIANT(cap_other_hooks) +{ + const bool is_sandboxed; + const bool handle_caps; + const bool is_allowed; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_other_hooks, unsandboxed) { + .is_sandboxed = false, + .handle_caps = false, + .is_allowed = true, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_other_hooks, denied) { + .is_sandboxed = true, + .handle_caps = true, + .is_allowed = false, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_other_hooks, allowed) { + .is_sandboxed = true, + .handle_caps = true, + .is_allowed = true, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_other_hooks, unhandled) { + .is_sandboxed = true, + .handle_caps = false, + .is_allowed = true, +}; +/* clang-format on */ + +FIXTURE_SETUP(cap_other_hooks) +{ + disable_caps(_metadata); +} + +FIXTURE_TEARDOWN(cap_other_hooks) +{ +} + +/* + * Creates a ruleset that handles capabilities and allows @cap if @is_allowed, + * or, if !@handle_caps, a ruleset that only handles a filesystem access right. + * Returns the ruleset FD, or -1 with errno set. + */ +static int create_variant_ruleset(const bool handle_caps, const bool is_allowed, + const int cap) +{ + const struct landlock_ruleset_attr fs_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + int ruleset_fd, err; + + if (!handle_caps) + return landlock_create_ruleset(&fs_attr, sizeof(fs_attr), 0); + + ruleset_fd = create_cap_ruleset(); + if (ruleset_fd < 0 || !is_allowed) + return ruleset_fd; + + if (add_cap_rule(ruleset_fd, cap)) { + err = errno; + close(ruleset_fd); + errno = err; + return -1; + } + return ruleset_fd; +} + +static void +enforce_cap_variant(struct __test_metadata *const _metadata, + const FIXTURE_VARIANT(cap_other_hooks) *const variant, + const int cap) +{ + int ruleset_fd; + + if (!variant->is_sandboxed) + return; + + ruleset_fd = create_variant_ruleset(variant->handle_caps, + variant->is_allowed, cap); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + +static unsigned long read_sysctl(struct __test_metadata *const _metadata, + const char *const path) +{ + char buf[32]; + ssize_t len; + int fd; + + fd = open(path, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open %s: %s", path, strerror(errno)); + } + len = read(fd, buf, sizeof(buf) - 1); + ASSERT_LT(0, len); + EXPECT_EQ(0, close(fd)); + + buf[len] = '\0'; + return strtoul(buf, NULL, 10); +} + +/* Returns 0 on success, or -1 with errno set. */ +static int write_sysctl(const char *const path, const unsigned long value) +{ + char buf[32]; + int fd, len, err = 0; + ssize_t ret; + + len = snprintf(buf, sizeof(buf), "%lu", value); + fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) + return -1; + ret = write(fd, buf, len); + if (ret != len) { + if (ret >= 0) + errno = EIO; + err = -1; + } + if (close(fd) && !err) + err = -1; + return err; +} + +/* + * cap_mmap_addr() requires CAP_SYS_RAWIO to map below the vm.mmap_min_addr + * sysctl (dac_mmap_min_addr). + */ +TEST_F(cap_other_hooks, mmap_min_addr) +{ + const size_t page_size = sysconf(_SC_PAGESIZE); + void *addr; + + if (read_sysctl(_metadata, "/proc/sys/vm/mmap_min_addr") == 0) + SKIP(return, "vm.mmap_min_addr is 0"); + + enforce_cap_variant(_metadata, variant, CAP_SYS_RAWIO); + + addr = mmap(NULL, page_size, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + EXPECT_EQ(MAP_FAILED, addr); + EXPECT_EQ(EPERM, errno); + + set_cap(_metadata, CAP_SYS_RAWIO); + addr = mmap(NULL, page_size, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + if (variant->is_allowed) { + EXPECT_EQ(NULL, addr) + { + TH_LOG("Failed to map page 0: %s", strerror(errno)); + } + if (addr != MAP_FAILED) + EXPECT_EQ(0, munmap(addr, page_size)); + } else { + EXPECT_EQ(MAP_FAILED, addr); + EXPECT_EQ(EPERM, errno); + if (addr != MAP_FAILED) + munmap(addr, page_size); + } + clear_cap(_metadata, CAP_SYS_RAWIO); +} + +static int add_inheritable_cap(const cap_value_t cap) +{ + cap_t cap_p; + int ret, err; + + cap_p = cap_get_proc(); + if (!cap_p) + return -1; + + ret = cap_set_flag(cap_p, CAP_INHERITABLE, 1, &cap, CAP_SET); + if (!ret) + ret = cap_set_proc(cap_p); + err = errno; + cap_free(cap_p); + errno = err; + return ret; +} + +/* + * cap_capset() requires CAP_SETPCAP to add an inheritable capability that is + * not in the permitted set. CAP_NET_RAW is in the bounding set, but + * disable_caps() does not put it in the permitted set. + */ +TEST_F(cap_other_hooks, capset_inheritable) +{ + enforce_cap_variant(_metadata, variant, CAP_SETPCAP); + + EXPECT_EQ(-1, add_inheritable_cap(CAP_NET_RAW)); + EXPECT_EQ(EPERM, errno); + + set_cap(_metadata, CAP_SETPCAP); + if (variant->is_allowed) { + EXPECT_EQ(0, add_inheritable_cap(CAP_NET_RAW)) + { + TH_LOG("Failed to raise CAP_NET_RAW: %s", + strerror(errno)); + } + } else { + EXPECT_EQ(-1, add_inheritable_cap(CAP_NET_RAW)); + EXPECT_EQ(EPERM, errno); + } + clear_cap(_metadata, CAP_SETPCAP); +} + +/* + * With vm.overcommit_memory=2, cap_vm_enough_memory() lets CAP_SYS_ADMIN use + * the vm.admin_reserve_kbytes reserve. The test sets a commit limit that + * leaves room for the test mapping only with the reserve, and keeps some room + * for other processes. The fixture setup only saves the settings, so that the + * teardown restores them after any failure once they are changed. + */ +#define OVERCOMMIT_PATH "/proc/sys/vm/" +#define ADMIN_RESERVE_KB (256UL * 1024) +#define HEADROOM_KB (64UL * 1024) +#define MAPPING_KB (160UL * 1024) + +/* clang-format off */ +FIXTURE(cap_admin_reserve) { + /* clang-format on */ + unsigned long overcommit_memory, overcommit_kbytes, overcommit_ratio; + unsigned long admin_reserve_kbytes, committed; + bool is_set; +}; + +FIXTURE_VARIANT(cap_admin_reserve) +{ + const bool is_sandboxed; + const bool handle_caps; + const bool is_allowed; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_admin_reserve, unsandboxed) { + .is_sandboxed = false, + .handle_caps = false, + .is_allowed = true, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_admin_reserve, denied) { + .is_sandboxed = true, + .handle_caps = true, + .is_allowed = false, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_admin_reserve, allowed) { + .is_sandboxed = true, + .handle_caps = true, + .is_allowed = true, +}; +/* clang-format on */ + +/* clang-format off */ +FIXTURE_VARIANT_ADD(cap_admin_reserve, unhandled) { + .is_sandboxed = true, + .handle_caps = false, + .is_allowed = true, +}; +/* clang-format on */ + +static unsigned long read_committed_kb(struct __test_metadata *const _metadata) +{ + unsigned long committed = 0; + char line[128]; + FILE *file; + + file = fopen("/proc/meminfo", "re"); + ASSERT_NE(NULL, file); + + while (fgets(line, sizeof(line), file)) + if (sscanf(line, "Committed_AS: %lu kB", &committed) == 1) + break; + + EXPECT_EQ(0, fclose(file)); + EXPECT_NE(0, committed); + return committed; +} + +/* + * sysctl_perm() only lets the root user write the vm.* sysctls. Switching to + * EUID 0 raises all permitted capabilities, so drop them again. + */ +static void set_root_euid(struct __test_metadata *const _metadata) +{ + set_cap(_metadata, CAP_SETUID); + ASSERT_EQ(0, seteuid(0)); + disable_caps(_metadata); +} + +/* + * Switching back to the real UID needs no capability. Keep the permitted + * capabilities, which the setuid fixup would otherwise clear. + */ +static void restore_euid(struct __test_metadata *const _metadata) +{ + ASSERT_EQ(0, prctl(PR_SET_KEEPCAPS, 1)); + ASSERT_EQ(0, seteuid(getuid())); + disable_caps(_metadata); +} + +FIXTURE_SETUP(cap_admin_reserve) +{ + self->is_set = false; + disable_caps(_metadata); + + self->overcommit_memory = + read_sysctl(_metadata, OVERCOMMIT_PATH "overcommit_memory"); + self->overcommit_kbytes = + read_sysctl(_metadata, OVERCOMMIT_PATH "overcommit_kbytes"); + self->overcommit_ratio = + read_sysctl(_metadata, OVERCOMMIT_PATH "overcommit_ratio"); + self->admin_reserve_kbytes = + read_sysctl(_metadata, OVERCOMMIT_PATH "admin_reserve_kbytes"); + self->committed = read_committed_kb(_metadata); + ASSERT_NE(0, self->committed); +} + +FIXTURE_TEARDOWN(cap_admin_reserve) +{ + if (!self->is_set) + return; + + /* Restores every value, even if one of the writes fails. */ + set_cap(_metadata, CAP_SETUID); + EXPECT_EQ(0, seteuid(0)); + disable_caps(_metadata); + EXPECT_EQ(0, write_sysctl(OVERCOMMIT_PATH "overcommit_memory", + self->overcommit_memory)); + /* Writing one of these two resets the other to 0. */ + if (self->overcommit_kbytes) { + EXPECT_EQ(0, write_sysctl(OVERCOMMIT_PATH "overcommit_kbytes", + self->overcommit_kbytes)); + } else { + EXPECT_EQ(0, write_sysctl(OVERCOMMIT_PATH "overcommit_ratio", + self->overcommit_ratio)); + } + EXPECT_EQ(0, write_sysctl(OVERCOMMIT_PATH "admin_reserve_kbytes", + self->admin_reserve_kbytes)); + restore_euid(_metadata); +} + +/* + * Called from the test, after the fixture setup, so that the teardown restores + * the settings if any of these writes fails. + */ +static void set_admin_reserve(struct __test_metadata *const _metadata, + FIXTURE_DATA(cap_admin_reserve) *const self) +{ + set_root_euid(_metadata); + self->is_set = true; + ASSERT_EQ(0, write_sysctl(OVERCOMMIT_PATH "admin_reserve_kbytes", + ADMIN_RESERVE_KB)); + ASSERT_EQ(0, write_sysctl(OVERCOMMIT_PATH "overcommit_kbytes", + self->committed + ADMIN_RESERVE_KB + + HEADROOM_KB)); + ASSERT_EQ(0, write_sysctl(OVERCOMMIT_PATH "overcommit_memory", 2)); + restore_euid(_metadata); +} + +/* Returns 0 if a writable private mapping of MAPPING_KB fits, or errno. */ +static int +test_admin_reserve(const FIXTURE_VARIANT(cap_admin_reserve) *const variant, + const bool is_admin) +{ + const size_t size = MAPPING_KB * 1024; + int ruleset_fd, status; + void *addr; + pid_t child; + + child = fork(); + if (child < 0) + return errno; + + if (child == 0) { + if (variant->is_sandboxed) { + ruleset_fd = create_variant_ruleset( + variant->handle_caps, variant->is_allowed, + CAP_SYS_ADMIN); + if (ruleset_fd < 0) + _exit(255); + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || + landlock_restrict_self(ruleset_fd, 0)) + _exit(255); + close(ruleset_fd); + } + + if (is_admin) { + cap_t cap_p = cap_get_proc(); + const cap_value_t cap = CAP_SYS_ADMIN; + + if (!cap_p || + cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &cap, + CAP_SET) || + cap_set_proc(cap_p)) + _exit(255); + cap_free(cap_p); + } + + addr = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + _exit(addr == MAP_FAILED ? errno : 0); + } + + if (waitpid(child, &status, 0) != child || !WIFEXITED(status)) + return 255; + return WEXITSTATUS(status); +} + +TEST_F(cap_admin_reserve, mmap) +{ + set_admin_reserve(_metadata, self); + + EXPECT_EQ(ENOMEM, test_admin_reserve(variant, false)); + EXPECT_EQ(variant->is_allowed ? 0 : ENOMEM, + test_admin_reserve(variant, true)); +} + /* Audit tests */ static int matches_log_cap(int audit_fd, int cap_number) @@ -828,6 +1275,94 @@ TEST_F(cap_audit, denied) EXPECT_EQ(0, records.domain); } +/* + * Denials in the capset and mmap_addr paths are audited like capable hook + * denials. The vm_enough_memory hook only withholds the admin reserve, with + * CAP_OPT_NOAUDIT, so an accounted mapping made with CAP_SYS_ADMIN effective + * leaves no record. + */ +TEST_F(cap_audit, other_hooks) +{ + const size_t page_size = sysconf(_SC_PAGESIZE); + struct audit_records records = {}; + int ruleset_fd; + __u64 domain_id; + void *addr; + + ruleset_fd = create_cap_ruleset(); + ASSERT_LE(0, ruleset_fd); + /* Allow CAP_AUDIT_CONTROL for child-side audit cleanup. */ + ASSERT_EQ(0, add_cap_rule(ruleset_fd, CAP_AUDIT_CONTROL)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + set_cap(_metadata, CAP_SETPCAP); + EXPECT_EQ(-1, add_inheritable_cap(CAP_NET_RAW)); + EXPECT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SETPCAP); + EXPECT_EQ(0, matches_log_cap(self->audit_fd, CAP_SETPCAP)); + EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(), + &domain_id)); + + /* The mmap_addr hook has nothing to check without a limit. */ + if (read_sysctl(_metadata, "/proc/sys/vm/mmap_min_addr")) { + set_cap(_metadata, CAP_SYS_RAWIO); + addr = mmap(NULL, page_size, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + EXPECT_EQ(MAP_FAILED, addr); + EXPECT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SYS_RAWIO); + EXPECT_EQ(0, matches_log_cap(self->audit_fd, CAP_SYS_RAWIO)); + } + + set_cap(_metadata, CAP_SYS_ADMIN); + addr = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + EXPECT_NE(MAP_FAILED, addr); + clear_cap(_metadata, CAP_SYS_ADMIN); + if (addr != MAP_FAILED) + EXPECT_EQ(0, munmap(addr, page_size)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +/* Allowed capability uses in the capset and mmap_addr paths. */ +TEST_F(cap_audit, other_hooks_allowed) +{ + const size_t page_size = sysconf(_SC_PAGESIZE); + struct audit_records records = {}; + int ruleset_fd; + void *addr; + + ruleset_fd = create_cap_ruleset(); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, add_cap_rule(ruleset_fd, CAP_SETPCAP)); + ASSERT_EQ(0, add_cap_rule(ruleset_fd, CAP_SYS_RAWIO)); + /* Allow CAP_AUDIT_CONTROL for child-side audit cleanup. */ + ASSERT_EQ(0, add_cap_rule(ruleset_fd, CAP_AUDIT_CONTROL)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + set_cap(_metadata, CAP_SETPCAP); + EXPECT_EQ(0, add_inheritable_cap(CAP_NET_RAW)); + clear_cap(_metadata, CAP_SETPCAP); + + if (read_sysctl(_metadata, "/proc/sys/vm/mmap_min_addr")) { + set_cap(_metadata, CAP_SYS_RAWIO); + addr = mmap(NULL, page_size, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + EXPECT_EQ(NULL, addr); + clear_cap(_metadata, CAP_SYS_RAWIO); + if (addr != MAP_FAILED) + EXPECT_EQ(0, munmap(addr, page_size)); + } + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); +} + TEST_F(cap_audit, allowed) { struct audit_records records = {}; diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index e29406900c04..a6fdfb422fad 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -36,7 +36,7 @@ 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. */ + /* Only these capabilities are useful for the tests. */ const cap_value_t caps[] = { /* clang-format off */ CAP_AUDIT_CONTROL, @@ -44,9 +44,11 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) CAP_MKNOD, CAP_NET_ADMIN, CAP_NET_BIND_SERVICE, + CAP_SETPCAP, CAP_SETUID, CAP_SYS_ADMIN, CAP_SYS_CHROOT, + CAP_SYS_RAWIO, /* clang-format on */ }; const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED; -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 6/6] landlock: Fix capability restriction documentation 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün ` (4 preceding siblings ...) 2026-11-10 20:24 ` [PATCH 5/6] selftests/landlock: Test capability checks outside the capable hook Mickaël Salaün @ 2026-11-10 20:24 ` Mickaël Salaün 5 siblings, 0 replies; 7+ messages in thread From: Mickaël Salaün @ 2026-11-10 20:24 UTC (permalink / raw) To: Günther Noack Cc: Mickaël Salaün, Ada Smith, Hungyu Lin, James Morris, Josef Bacik, Paul Moore, Serge Hallyn, kernel-team, linux-kernel, linux-security-module Some documented claims about LANDLOCK_PERMISSION_CAPABILITY_USE do not hold for every capability check. Remove them instead of listing their exceptions, and call it a permission instead of a hook. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Fixes: 97c8386f1d74 ("landlock: Enforce capability restrictions") Signed-off-by: Mickaël Salaün <mic@digikod.net> --- Documentation/security/landlock.rst | 8 ++++---- Documentation/userspace-api/landlock.rst | 8 ++++---- include/uapi/linux/landlock.h | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst index d81f1c2cd236..98c2e7139e8d 100644 --- a/Documentation/security/landlock.rst +++ b/Documentation/security/landlock.rst @@ -8,7 +8,7 @@ Landlock LSM: kernel documentation ================================== :Author: Mickaël Salaün -:Date: October 2026 +:Date: November 2026 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, @@ -359,9 +359,9 @@ framework in ``log.c``, compiled under ``CONFIG_SECURITY_LANDLOCK_LOG`` (automatically selected by ``CONFIG_AUDIT`` or ``CONFIG_TRACEPOINTS``). Audit records respect audit configuration, the domain's Landlock log -flags, and ``LANDLOCK_LOG_DISABLED``. Tracepoints fire unconditionally, -independent of these settings. The denial counter (``num_denials``) is -always incremented regardless of logging configuration. +flags, and ``LANDLOCK_LOG_DISABLED``. Tracepoints fire independently of +these settings. The denial counter (``num_denials``) is +incremented regardless of logging configuration. Each denial tracepoint carries a ``logged`` field reporting the audit-logging verdict: whether the denial would be written to the audit diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst index 3d45b956b640..d00e94d8ed37 100644 --- a/Documentation/userspace-api/landlock.rst +++ b/Documentation/userspace-api/landlock.rst @@ -8,7 +8,7 @@ Landlock: unprivileged access control ===================================== :Author: Mickaël Salaün -:Date: October 2026 +:Date: November 2026 The goal of Landlock is to enable restriction of ambient rights (e.g. global filesystem or network access) for a set of processes. Because Landlock @@ -532,7 +532,7 @@ acquisition via :manpage:`open_tree(2)` / :manpage:`fsmount(2)`. Namespaces the process is already a member of when the domain is enforced are implicitly allowed (the process could not continue running otherwise); rules describe which new namespace types the process may acquire. -``LANDLOCK_PERMISSION_CAPABILITY_USE`` gates every exercise of a capability +``LANDLOCK_PERMISSION_CAPABILITY_USE`` gates the exercise of capabilities after the domain is enforced, regardless of how the capability was obtained (inherited credentials, ``CLONE_NEWUSER`` grant, ``setuid``/file-cap-bearing :manpage:`execve(2)`, etc.). Configuring @@ -641,9 +641,9 @@ after a domain is enforced through user namespace entry or :manpage:`capset(2)`; privileged sandboxes that did not set ``PR_SET_NO_NEW_PRIVS`` may also gain capabilities through :manpage:`execve(2)` of binaries with file capabilities. In all cases, :manpage:`capget(2)` will report the credential's capability sets, -but any denied capability will fail with ``EPERM`` when exercised. Do not rely +but any denied capability will fail when exercised. Do not rely on :manpage:`capget(2)` to determine whether the policy permits a given -capability; only the actual operation will return ``EPERM`` upon denial. +capability. Truncating files ---------------- diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index 24510e5d8479..c9e4ce23778b 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -622,9 +622,9 @@ struct landlock_capability_attr { * - %LANDLOCK_PERMISSION_CAPABILITY_USE: Restrict the use of specific Linux * capabilities. A process in a Landlock domain that handles this permission * is denied from exercising capabilities that are not explicitly allowed by a - * %LANDLOCK_RULE_CAPABILITY rule. This hook is purely restrictive: it can - * deny capabilities that the kernel would otherwise grant, but it can never - * grant capabilities that the kernel already denied. Support added in + * %LANDLOCK_RULE_CAPABILITY rule. This permission is purely restrictive: it + * can deny capabilities that the kernel would otherwise grant, but it can + * never grant capabilities that the kernel already denied. Support added in * Landlock ABI version 12. */ #define LANDLOCK_PERMISSION_NAMESPACE_USE (1ULL << 0) -- 2.56.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-10 20:25 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-11-10 20:24 [PATCH 0/6] Fix Landlock mediation of commoncap's capability checks Mickaël Salaün 2026-11-10 20:24 ` [PATCH 1/6] landlock: Factor out capability denial checks Mickaël Salaün 2026-11-10 20:24 ` [PATCH 2/6] landlock: Mediate CAP_SYS_RAWIO for mappings below mmap_min_addr Mickaël Salaün 2026-11-10 20:24 ` [PATCH 3/6] landlock: Mediate CAP_SETPCAP for inheritable capabilities Mickaël Salaün 2026-11-10 20:24 ` [PATCH 4/6] landlock: Mediate CAP_SYS_ADMIN for the admin memory reserve Mickaël Salaün 2026-11-10 20:24 ` [PATCH 5/6] selftests/landlock: Test capability checks outside the capable hook Mickaël Salaün 2026-11-10 20:24 ` [PATCH 6/6] landlock: Fix capability restriction documentation Mickaël Salaün
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®