From bf5ba73c9bdd5e872eb35e801e429a3b526f8f04 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 26 Oct 2026 21:53:29 +0100 Subject: [PATCH 1/2] userns: don't drop the exec securebits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SECBIT_EXEC_RESTRICT_FILE and SECBIT_EXEC_DENY_INTERACTIVE tell script interpreters to only run what AT_EXECVE_CHECK allows and to refuse interactive input. A service manager sets them together with their lock bits so that nothing below it can turn them off again. But set_cred_user_ns() resets all securebits to SECUREBITS_DEFAULT. So any task that can create a user namespace sheds the locked exec bits with a single unshare(CLONE_NEWUSER) and joining one with setns() does the same. The lock is pointless if anyone who may create a user namespace can drop it, and that is pretty much everyone. Resetting the other bits is fine. They are about how capabilities are computed and a new user namespace starts out with a full set of capabilities anyway. The exec bits grant nothing though. They are restrictions a task placed on itself, just like no_new_privs which a user namespace doesn't touch either. So keep them and their locks. Reproducer: // SPDX-License-Identifier: GPL-2.0 /* Lock the exec securebits, then create a user namespace and look at them again. */ #define _GNU_SOURCE #include #include #include #define SECBIT_EXEC_RESTRICT_FILE (1 << 8) #define SECBIT_EXEC_RESTRICT_FILE_LOCKED (1 << 9) #define SECBIT_EXEC_DENY_INTERACTIVE (1 << 10) #define SECBIT_EXEC_DENY_INTERACTIVE_LOCKED (1 << 11) int main(void) { int bits = SECBIT_EXEC_RESTRICT_FILE | SECBIT_EXEC_RESTRICT_FILE_LOCKED | SECBIT_EXEC_DENY_INTERACTIVE | SECBIT_EXEC_DENY_INTERACTIVE_LOCKED; if (prctl(PR_SET_SECUREBITS, bits, 0, 0, 0)) { perror("PR_SET_SECUREBITS"); return 1; } printf("before: 0x%x\n", prctl(PR_GET_SECUREBITS, 0, 0, 0, 0)); printf("clear locked bits: %s\n", prctl(PR_SET_SECUREBITS, 0, 0, 0, 0) ? "refused" : "ALLOWED"); if (unshare(CLONE_NEWUSER)) { perror("unshare(CLONE_NEWUSER)"); return 1; } printf("after unshare(CLONE_NEWUSER): 0x%x\n", prctl(PR_GET_SECUREBITS, 0, 0, 0, 0)); return 0; } On v7.1 this prints "before: 0xf00", "clear locked bits: refused" and "after unshare(CLONE_NEWUSER): 0x0". With this patch the last line reads 0xf00 as well. Fixes: a0623b2a1d59 ("security: Add EXEC_RESTRICT_FILE and EXEC_DENY_INTERACTIVE securebits") Cc: stable@vger.kernel.org # v6.14+ Link: https://patch.msgid.link/20261026-work-securebits-userns-v1-1-b35473fa1a34@kernel.org Reviewed-by: Mickaël Salaün Signed-off-by: Christian Brauner (Amutable) --- Documentation/userspace-api/check_exec.rst | 4 ++++ kernel/user_namespace.c | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/userspace-api/check_exec.rst b/Documentation/userspace-api/check_exec.rst index 05dfe3b56f71..0904810e1d8d 100644 --- a/Documentation/userspace-api/check_exec.rst +++ b/Documentation/userspace-api/check_exec.rst @@ -99,6 +99,10 @@ This secure bit may be set by user session managers, service managers, container runtimes, sandboxer tools... Except for test environments, the related ``SECBIT_EXEC_DENY_INTERACTIVE_LOCKED`` bit should also be set. +Both secure bits and their locks are inherited across :manpage:`fork(2)` and +:manpage:`execve(2)`, and they are kept when a task creates or joins a user +namespace. + Here is the expected behavior for a script interpreter according to combination of any exec securebits: diff --git a/kernel/user_namespace.c b/kernel/user_namespace.c index 50c374ed4687..cd748e1cbace 100644 --- a/kernel/user_namespace.c +++ b/kernel/user_namespace.c @@ -46,8 +46,11 @@ static void set_cred_user_ns(struct cred *cred, struct user_namespace *user_ns) { /* Start with the same capabilities as init but useless for doing * anything as the capabilities are bound to the new user namespace. + * The exec securebits grant nothing, they are restrictions the task + * placed on itself. Keep them and their locks. */ - cred->securebits = SECUREBITS_DEFAULT; + cred->securebits = SECUREBITS_DEFAULT | + (cred->securebits & (SECURE_ALL_UNPRIVILEGED | SECURE_ALL_UNPRIVILEGED << 1)); cred->cap_inheritable = CAP_EMPTY_SET; cred->cap_permitted = CAP_FULL_SET; cred->cap_effective = CAP_FULL_SET; From 9084efd0a2b49c61d588b14a9b49ac8ee5d92080 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 26 Oct 2026 21:53:30 +0100 Subject: [PATCH 2/2] selftests/exec: check that user namespaces keep the exec securebits Check that the exec securebits survive creating a user namespace with unshare(CLONE_NEWUSER) and joining one with setns(): - both bits with their locks, which can't be changed in there - both bits without their locks, which can still be cleared in there - only the locks, which keep the bits from being set in there The other securebits are reset as before. Link: https://patch.msgid.link/20261026-work-securebits-userns-v1-2-b35473fa1a34@kernel.org Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/exec/check-exec.c | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tools/testing/selftests/exec/check-exec.c b/tools/testing/selftests/exec/check-exec.c index f2397e75aa7c..5753c657808e 100644 --- a/tools/testing/selftests/exec/check-exec.c +++ b/tools/testing/selftests/exec/check-exec.c @@ -10,11 +10,13 @@ * Author: Mickaël Salaün */ +#define _GNU_SOURCE #include #include #include #include #include +#include #include #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #include /* Defines AT_EXECVE_CHECK without type conflicts. */ @@ -460,4 +463,84 @@ TEST_F(secbits, restrict_locked_unset) CHILD(EXPECT_EQ(EPERM, test_secbits_set(0))); } +static const unsigned int exec_bits = SECBIT_EXEC_RESTRICT_FILE | + SECBIT_EXEC_DENY_INTERACTIVE; +static const unsigned int exec_locks = SECBIT_EXEC_RESTRICT_FILE_LOCKED | + SECBIT_EXEC_DENY_INTERACTIVE_LOCKED; + +/* + * Checks that the exec securebits in @exec survive creating and joining a user + * namespace, and that in there they can be changed exactly when they are not + * locked. + */ +static void test_userns(struct __test_metadata *const _metadata, + const unsigned int exec) +{ + const int error = (exec & exec_locks) ? EPERM : 0; + int ready[2], done[2], userns_fd, status; + char path[64], c = 0; + pid_t pid; + + /* A user namespace owned by us to join later. */ + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(done)); + pid = fork(); + ASSERT_LE(0, pid); + if (pid == 0) { + close(ready[0]); + close(done[1]); + if (unshare(CLONE_NEWUSER)) + _exit(1); + if (write(ready[1], &c, 1) != 1) + _exit(1); + if (read(done[0], &c, 1) < 0) + _exit(1); + _exit(0); + } + close(ready[1]); + close(done[0]); + if (read(ready[0], &c, 1) != 1) { + waitpid(pid, &status, 0); + SKIP(return, "Cannot create a user namespace"); + } + + snprintf(path, sizeof(path), "/proc/%d/ns/user", pid); + userns_fd = open(path, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, userns_fd); + + EXPECT_EQ(0, test_secbits_set(prctl(PR_GET_SECUREBITS) | exec)); + + /* Creating a user namespace keeps them... */ + CHILD(EXPECT_EQ(0, unshare(CLONE_NEWUSER)); + EXPECT_EQ(exec, prctl(PR_GET_SECUREBITS)); + EXPECT_EQ(error, test_secbits_set(exec ^ exec_bits)); + EXPECT_EQ(error, test_secbits_set(0))); + + /* ...and so does joining one. */ + CHILD(EXPECT_EQ(0, setns(userns_fd, CLONE_NEWUSER)); + EXPECT_EQ(exec, prctl(PR_GET_SECUREBITS)); + EXPECT_EQ(error, test_secbits_set(exec ^ exec_bits)); + EXPECT_EQ(error, test_secbits_set(0))); + + close(userns_fd); + close(done[1]); + EXPECT_EQ(pid, waitpid(pid, &status, 0)); + close(ready[0]); +} + +TEST_F(secbits, userns_locked) +{ + test_userns(_metadata, exec_bits | exec_locks); +} + +TEST_F(secbits, userns_unlocked) +{ + test_userns(_metadata, exec_bits); +} + +TEST_F(secbits, userns_lock_only) +{ + test_userns(_metadata, exec_locks); +} + TEST_HARNESS_MAIN