* [PATCH 1/3] landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-08 13:39 [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
@ 2026-07-08 13:39 ` Justin Suess
2026-07-08 13:39 ` [PATCH 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
` (2 subsequent siblings)
3 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-07-08 13:39 UTC (permalink / raw)
To: mic, linux-security-module; +Cc: gnoack, Justin Suess
Add a landlock_restrict_self(2) flag to stage no_new_privs so that it is
only set at the next execve(2) of the calling thread, past its point of
no return. This makes it possible to prepare an execution environment
where the new program runs with no_new_privs, without changing the
behavior of the calling program, and without leaving no_new_privs set if
the execve(2) fails.
The staged bit lives in the Landlock credential blob and is installed by
a new bprm_committing_creds hook. This flag does not relax the
no_new_privs/CAP_SYS_ADMIN requirement of landlock_restrict_self(2), and
may be used with a ruleset_fd of -1 to stage no_new_privs without
enforcing a ruleset. Combined with LANDLOCK_RESTRICT_SELF_TSYNC, the
staged state is propagated to sibling threads like the rest of the
Landlock credential state, and each thread then sets no_new_privs at its
own next execve(2).
Because no_new_privs is only set once the credentials of the new program
have been computed, this flag does not change how the next execve(2)
itself computes credentials: set-user-ID, set-group-ID and file
capabilities are still honored for that execution. The executed program
then runs with no_new_privs set, with its usual effect on subsequent
execve(2) calls.
Bump the Landlock ABI version to 11.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
include/uapi/linux/landlock.h | 36 ++++++++++++++++++++++-
security/landlock/cred.c | 22 ++++++++++++++
security/landlock/cred.h | 8 ++++++
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 54 ++++++++++++++++++++++++++---------
5 files changed, 107 insertions(+), 15 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 7ffe2ef127ee..8d5b41d94c4a 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -190,13 +190,47 @@ struct landlock_ruleset_attr {
* logging configurations on these threads.
*
* If the calling thread is running with no_new_privs, this operation
- * enables no_new_privs on the sibling threads as well.
+ * enables no_new_privs on the sibling threads as well. If the calling
+ * thread has no_new_privs staged with
+ * %LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC (whether by the same call or a
+ * previous one), the staged state is propagated to the sibling threads as
+ * well.
+ *
+ * The following flag stages no_new_privs so that it only takes effect at the
+ * next :manpage:`execve(2)` of the calling thread, rather than immediately.
+ * It does not relax the permission check performed by
+ * sys_landlock_restrict_self(): the calling thread must still be running with
+ * no_new_privs or hold %CAP_SYS_ADMIN in its namespace.
+ *
+ * %LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
+ * Sets the no_new_privs attribute of the calling thread, but only at the
+ * next :manpage:`execve(2)` and past its point of no return, so a failed
+ * :manpage:`execve(2)` leaves the attribute unchanged. Because
+ * sys_landlock_restrict_self() already requires no_new_privs or
+ * %CAP_SYS_ADMIN, this flag cannot be used to defer around that
+ * requirement: when no_new_privs is already set it is effectively a
+ * no-op, and otherwise the caller must hold %CAP_SYS_ADMIN. This flag
+ * may be used with a @ruleset_fd value of -1 to stage no_new_privs
+ * without enforcing a ruleset.
+ *
+ * This flag does not change how the next :manpage:`execve(2)` itself
+ * computes credentials: set-user-ID, set-group-ID and file capabilities
+ * are still honored for that execution. The executed program then runs
+ * with no_new_privs set, with its usual effect on subsequent
+ * :manpage:`execve(2)` calls.
+ *
+ * Staging is per-thread by default. When combined with
+ * %LANDLOCK_RESTRICT_SELF_TSYNC, the staged no_new_privs is propagated
+ * to the sibling threads along with the rest of the Landlock
+ * configuration, and each thread then sets no_new_privs at its own next
+ * :manpage:`execve(2)`.
*/
/* clang-format off */
#define LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF (1U << 0)
#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1)
#define LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF (1U << 2)
#define LANDLOCK_RESTRICT_SELF_TSYNC (1U << 3)
+#define LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC (1U << 4)
/* clang-format on */
/**
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index cc419de75cd6..31fa4f78adef 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -10,6 +10,7 @@
#include <linux/binfmts.h>
#include <linux/cred.h>
#include <linux/lsm_hooks.h>
+#include <linux/sched.h>
#include "common.h"
#include "cred.h"
@@ -52,11 +53,32 @@ static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
#endif /* CONFIG_AUDIT */
+/*
+ * Installs the no_new_privs attribute staged with
+ * LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC.
+ *
+ * This hook runs from begin_new_exec(), past the execution's point of no
+ * return: a binary that cannot be loaded (or any earlier failure) never reaches
+ * this point, so a failed execve(2) leaves the thread's no_new_privs attribute
+ * untouched.
+ */
+static void hook_bprm_committing_creds(const struct linux_binprm *const bprm)
+{
+ struct landlock_cred_security *const llcred = landlock_cred(bprm->cred);
+
+ if (llcred->set_nnp_on_exec) {
+ task_set_no_new_privs(current);
+ llcred->set_nnp_on_exec = 0;
+ }
+}
+
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
LSM_HOOK_INIT(cred_free, hook_cred_free),
+ LSM_HOOK_INIT(bprm_committing_creds, hook_bprm_committing_creds),
+
#ifdef CONFIG_AUDIT
LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
#endif /* CONFIG_AUDIT */
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index f287c56b5fd4..673206d3c3f3 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -35,6 +35,14 @@ struct landlock_cred_security {
*/
struct landlock_ruleset *domain;
+ /**
+ * @set_nnp_on_exec: Set if %LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
+ * requested that no_new_privs be enabled at the next execve(2).
+ * Propagated to sibling threads by %LANDLOCK_RESTRICT_SELF_TSYNC like
+ * the rest of this struct. Consumed by hook_bprm_committing_creds().
+ */
+ u8 set_nnp_on_exec : 1;
+
#ifdef CONFIG_AUDIT
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 08d5f2f6d321..29c190d00540 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -34,7 +34,7 @@
#define LANDLOCK_NUM_ACCESS_MAX \
MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE)
-#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC
+#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
/* clang-format on */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 36b02892c62f..daed1ea80455 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -169,7 +169,7 @@ static const struct file_operations ruleset_fops = {
* If the change involves a fix that requires userspace awareness, also update
* the errata documentation in Documentation/userspace-api/landlock.rst .
*/
-const int landlock_abi_version = 10;
+const int landlock_abi_version = 11;
/**
* sys_landlock_create_ruleset - Create a new ruleset
@@ -502,12 +502,21 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
* - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
* - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF
* - %LANDLOCK_RESTRICT_SELF_TSYNC
+ * - %LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
*
* This system call enforces a Landlock ruleset on the current thread.
* Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its
* namespace or is running with no_new_privs. This avoids scenarios where
* unprivileged tasks can affect the behavior of privileged children.
*
+ * With %LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC, no_new_privs is not set
+ * immediately but staged to be set at the next execve(2), past its point of no
+ * return. This flag does not relax the no_new_privs / %CAP_SYS_ADMIN
+ * requirement above. Staging is per-thread by default; when combined with
+ * %LANDLOCK_RESTRICT_SELF_TSYNC, the staged no_new_privs is propagated to the
+ * sibling threads along with the rest of the Landlock credential state, and
+ * each thread then sets no_new_privs at its own next execve(2).
+ *
* Return: 0 on success, or -errno on failure. Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
@@ -526,6 +535,10 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
+ /* Flags that do not require a ruleset_fd. */
+ const __u32 fd_optional_flags =
+ LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+ LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC;
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
@@ -538,6 +551,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
/*
* Similar checks as for seccomp(2), except that an -EPERM may be
* returned.
+ *
+ * This gate runs before any flag-specific handling, so
+ * LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC cannot be used to defer around the
+ * no_new_privs / CAP_SYS_ADMIN requirement.
*/
if (!task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
@@ -555,14 +572,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF);
/*
- * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
- * -1 as ruleset_fd, optionally combined with
- * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
- * threads. No other flag must be set.
+ * A ruleset is not required when only muting subdomain logs
+ * (LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, optionally combined with
+ * LANDLOCK_RESTRICT_SELF_TSYNC to propagate it to all threads) and/or
+ * staging no_new_privs on exec (LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC).
+ * Such calls may pass -1 as ruleset_fd. Any other flag (or the absence
+ * of these) requires a real ruleset.
*/
- if (!(ruleset_fd == -1 &&
- (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
- LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
+ if (!(ruleset_fd == -1 && (flags & fd_optional_flags) &&
+ !(flags & ~(fd_optional_flags | LANDLOCK_RESTRICT_SELF_TSYNC)))) {
/* Gets and checks the ruleset. */
ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
if (IS_ERR(ruleset))
@@ -583,11 +601,12 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
#endif /* CONFIG_AUDIT */
/*
- * The only case when a ruleset may not be set is if
- * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
- * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1. We could
- * optimize this case by not calling commit_creds() if this flag was
- * already set, but it is not worth the complexity.
+ * A ruleset may legitimately be absent here when only muting subdomain
+ * logs (LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, optionally with
+ * LANDLOCK_RESTRICT_SELF_TSYNC) and/or staging no_new_privs on exec
+ * (LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC) with ruleset_fd == -1. We could
+ * optimize some of these cases by not calling commit_creds(), but it is
+ * not worth the complexity.
*/
if (ruleset) {
/*
@@ -618,6 +637,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
#endif /* CONFIG_AUDIT */
}
+ /*
+ * Stages no_new_privs to be set at the next execve(2). Because the bit
+ * lives in new_cred, TSYNC below propagates it to the sibling threads
+ * (whether it was staged by this call or a previous one), and each
+ * thread then sets no_new_privs at its own next execve(2).
+ */
+ if (flags & LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC)
+ new_llcred->set_nnp_on_exec = 1;
+
if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
const int err = landlock_restrict_sibling_threads(
current_cred(), new_cred);
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-08 13:39 [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
2026-07-08 13:39 ` [PATCH 1/3] landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
@ 2026-07-08 13:39 ` Justin Suess
2026-07-08 13:39 ` [PATCH 3/3] landlock: Document LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
2026-07-09 10:09 ` [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Mickaël Salaün
3 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-07-08 13:39 UTC (permalink / raw)
To: mic, linux-security-module; +Cc: gnoack, Justin Suess
Add an nnp_on_exec fixture checking that staging no_new_privs does not
set it before execve(2) but does set it for the executed program, with
and without a ruleset, and that LANDLOCK_RESTRICT_SELF_TSYNC propagates
the staged state to sibling threads. Also check that the
no_new_privs/CAP_SYS_ADMIN requirement still applies to stage-only
calls.
Update the ABI version and last-flag checks accordingly.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
tools/testing/selftests/landlock/base_test.c | 259 ++++++++++++++++++-
1 file changed, 255 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index cbd3c1669951..55ae26a72752 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -11,9 +11,12 @@
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/landlock.h>
+#include <pthread.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include "common.h"
@@ -76,7 +79,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
- ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+ ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
@@ -288,7 +291,7 @@ TEST(restrict_self_fd)
EXPECT_EQ(EBADFD, errno);
}
-TEST(restrict_self_fd_logging_flags)
+TEST(restrict_self_fd_flags)
{
int fd;
@@ -302,11 +305,16 @@ TEST(restrict_self_fd_logging_flags)
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADFD, errno);
+
+ /* Same for LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC. */
+ EXPECT_EQ(-1, landlock_restrict_self(
+ fd, LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC));
+ EXPECT_EQ(EBADFD, errno);
}
-TEST(restrict_self_logging_flags)
+TEST(restrict_self_flags)
{
- const __u32 last_flag = LANDLOCK_RESTRICT_SELF_TSYNC;
+ const __u32 last_flag = LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC;
/* Tests invalid flag combinations. */
@@ -349,6 +357,12 @@ TEST(restrict_self_logging_flags)
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
+ EXPECT_EQ(-1,
+ landlock_restrict_self(
+ -1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+ LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC));
+ EXPECT_EQ(EBADF, errno);
+
/* Tests with an invalid ruleset_fd. */
EXPECT_EQ(-1, landlock_restrict_self(
@@ -359,6 +373,243 @@ TEST(restrict_self_logging_flags)
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
}
+/*
+ * Exits with the NoNewPrivs value of the executed shell, as read from
+ * /proc/self/status, making the staged no_new_privs observable after an
+ * execve(2).
+ */
+static const char nnp_script[] =
+ "#!/bin/sh\n"
+ "while read -r label value; do\n"
+ " if [ \"$label\" = \"NoNewPrivs:\" ]; then\n"
+ " exit \"$value\"\n"
+ " fi\n"
+ "done < /proc/self/status\n"
+ "exit 42\n";
+
+/* Returns the no_new_privs value inherited through fork(2) and execve(2). */
+static int get_nnp_after_exec(const char *const script_path)
+{
+ int status;
+ const pid_t child = fork();
+
+ if (child < 0)
+ return -errno;
+
+ if (child == 0) {
+ char *const argv[] = { (char *)script_path, NULL };
+
+ execve(script_path, argv, NULL);
+ _exit(127);
+ }
+
+ if (waitpid(child, &status, 0) != child)
+ return -ECHILD;
+
+ if (!WIFEXITED(status))
+ return -EIO;
+
+ return WEXITSTATUS(status);
+}
+
+struct nnp_staging_thread {
+ const char *script_path;
+ int ruleset_fd;
+ __u32 flags;
+ int restrict_ret;
+ int restrict_errno;
+ int open_write_ret;
+ int open_write_errno;
+ int nnp_before_exec;
+ int nnp_after_exec;
+};
+
+/*
+ * Stages no_new_privs from a sibling thread of the test thread, so that the
+ * test thread can observe whether LANDLOCK_RESTRICT_SELF_TSYNC propagated the
+ * staged state.
+ */
+static void *stage_nnp_on_exec(void *data)
+{
+ struct nnp_staging_thread *const ctx = data;
+
+ ctx->restrict_ret = landlock_restrict_self(ctx->ruleset_fd, ctx->flags);
+ ctx->restrict_errno = errno;
+
+ /*
+ * Tries to open the script for writing to check that a ruleset passed
+ * along LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC is enforced immediately,
+ * unlike the staged no_new_privs.
+ */
+ ctx->open_write_ret = open(ctx->script_path, O_WRONLY | O_CLOEXEC);
+ ctx->open_write_errno = errno;
+ if (ctx->open_write_ret >= 0)
+ close(ctx->open_write_ret);
+
+ ctx->nnp_before_exec = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0);
+ ctx->nnp_after_exec = get_nnp_after_exec(ctx->script_path);
+ return NULL;
+}
+
+FIXTURE(nnp_on_exec)
+{
+ char script_path[sizeof("/tmp/landlock_nnp_XXXXXX")];
+};
+
+FIXTURE_VARIANT(nnp_on_exec)
+{
+ const bool with_ruleset;
+ const bool tsync;
+ const bool nnp_before;
+ const bool cap_sys_admin;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, no_ruleset) {
+ /* clang-format on */
+ .cap_sys_admin = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, with_ruleset) {
+ /* clang-format on */
+ .with_ruleset = true,
+ .cap_sys_admin = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, no_ruleset_tsync) {
+ /* clang-format on */
+ .tsync = true,
+ .cap_sys_admin = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, with_ruleset_tsync) {
+ /* clang-format on */
+ .with_ruleset = true,
+ .tsync = true,
+ .cap_sys_admin = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, nnp_already_set) {
+ /* clang-format on */
+ .nnp_before = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, nnp_already_set_tsync) {
+ /* clang-format on */
+ .nnp_before = true,
+ .tsync = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, unprivileged) {
+ /* clang-format on */
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(nnp_on_exec, unprivileged_tsync) {
+ /* clang-format on */
+ .tsync = true,
+};
+
+FIXTURE_SETUP(nnp_on_exec)
+{
+ int script_fd;
+
+ disable_caps(_metadata);
+
+ memcpy(self->script_path, "/tmp/landlock_nnp_XXXXXX",
+ sizeof(self->script_path));
+ script_fd = mkstemp(self->script_path);
+ ASSERT_LE(0, script_fd);
+ ASSERT_EQ(sizeof(nnp_script) - 1,
+ write(script_fd, nnp_script, sizeof(nnp_script) - 1));
+ ASSERT_EQ(0, fchmod(script_fd, 0700));
+ ASSERT_EQ(0, close(script_fd));
+}
+
+FIXTURE_TEARDOWN(nnp_on_exec)
+{
+ EXPECT_EQ(0, unlink(self->script_path));
+}
+
+TEST_F(nnp_on_exec, staging)
+{
+ /* The nnp/CAP_SYS_ADMIN gate applies even to stage-only calls. */
+ const bool expect_success = variant->nnp_before ||
+ variant->cap_sys_admin;
+ struct nnp_staging_thread staging = {
+ .script_path = self->script_path,
+ .ruleset_fd = -1,
+ .flags = LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC,
+ };
+ pthread_t thread;
+
+ if (variant->tsync)
+ staging.flags |= LANDLOCK_RESTRICT_SELF_TSYNC;
+
+ if (variant->with_ruleset) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ };
+
+ staging.ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, staging.ruleset_fd);
+ }
+
+ if (variant->nnp_before)
+ ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+ if (variant->cap_sys_admin)
+ set_cap(_metadata, CAP_SYS_ADMIN);
+
+ /* The staging thread inherits no_new_privs and capabilities. */
+ ASSERT_EQ(0, pthread_create(&thread, NULL, stage_nnp_on_exec,
+ &staging));
+ ASSERT_EQ(0, pthread_join(thread, NULL));
+
+ if (variant->cap_sys_admin)
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ if (expect_success) {
+ EXPECT_EQ(0, staging.restrict_ret);
+ } else {
+ EXPECT_EQ(-1, staging.restrict_ret);
+ EXPECT_EQ(EPERM, staging.restrict_errno);
+ }
+
+ /* A ruleset is enforced immediately, unlike the staged no_new_privs. */
+ if (variant->with_ruleset) {
+ EXPECT_EQ(-1, staging.open_write_ret);
+ EXPECT_EQ(EACCES, staging.open_write_errno);
+ } else {
+ EXPECT_LE(0, staging.open_write_ret);
+ }
+
+ /* Staging never sets no_new_privs before the execve(2)... */
+ EXPECT_EQ(variant->nnp_before, staging.nnp_before_exec);
+ /* ...but a successful call sets it for the executed program. */
+ EXPECT_EQ(expect_success, staging.nnp_after_exec);
+
+ /*
+ * On this sibling thread, the staged state is only visible if it was
+ * propagated with LANDLOCK_RESTRICT_SELF_TSYNC, and it is still not
+ * set before the execve(2).
+ */
+ EXPECT_EQ(variant->nnp_before,
+ prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+ EXPECT_EQ((variant->nnp_before || (expect_success && variant->tsync)),
+ get_nnp_after_exec(self->script_path));
+
+ if (variant->with_ruleset)
+ EXPECT_EQ(0, close(staging.ruleset_fd));
+}
+
TEST(ruleset_fd_io)
{
struct landlock_ruleset_attr ruleset_attr = {
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 3/3] landlock: Document LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-08 13:39 [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
2026-07-08 13:39 ` [PATCH 1/3] landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
2026-07-08 13:39 ` [PATCH 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
@ 2026-07-08 13:39 ` Justin Suess
2026-07-09 10:09 ` [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Mickaël Salaün
3 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-07-08 13:39 UTC (permalink / raw)
To: mic, linux-security-module; +Cc: gnoack, Justin Suess
Document staged no_new_privs on exec, following the same compatibility
section style as previous ABI additions.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Documentation/userspace-api/landlock.rst | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 5a63d4476c1c..1d779dd7bb5f 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: June 2026
+:Date: July 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
@@ -789,6 +789,25 @@ when at least one sys_landlock_add_rule() call is made for it with the
``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same
object without this flag do not clear it.
+Staged no_new_privs on exec (ABI < 11)
+--------------------------------------
+
+Starting with the Landlock ABI version 11, it is possible to stage
+no_new_privs so that it is only set at the next :manpage:`execve(2)` of the
+calling thread, past its point of no return, using the
+``LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC`` flag passed to
+sys_landlock_restrict_self(). This flag may be used with a ``ruleset_fd``
+value of -1 to stage no_new_privs without enforcing a ruleset, and does not
+relax the no_new_privs / ``CAP_SYS_ADMIN`` requirement of the system call.
+This flag does not change how the next :manpage:`execve(2)` itself computes
+credentials: set-user-ID, set-group-ID and file capabilities are still
+honored for that execution. The executed program then runs with
+no_new_privs set, with its usual effect on subsequent :manpage:`execve(2)`
+calls. Staging is per-thread by default: when combined with
+``LANDLOCK_RESTRICT_SELF_TSYNC``, the staged no_new_privs is propagated to
+the sibling threads along with the rest of the Landlock configuration, and
+each thread then sets no_new_privs at its own next :manpage:`execve(2)`.
+
.. _kernel_support:
Kernel support
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-08 13:39 [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
` (2 preceding siblings ...)
2026-07-08 13:39 ` [PATCH 3/3] landlock: Document LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Justin Suess
@ 2026-07-09 10:09 ` Mickaël Salaün
2026-07-09 11:22 ` Simon McVittie
2026-07-10 13:56 ` Justin Suess
3 siblings, 2 replies; 8+ messages in thread
From: Mickaël Salaün @ 2026-07-09 10:09 UTC (permalink / raw)
To: Justin Suess; +Cc: linux-security-module, gnoack
Hi Justin,
Thanks for this patch series, I like the underlying idea, but I'm not
convince we should mix NNP and ON_EXEC. Why not a
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag instead?
One issue with this NNP_ON_EXEC is that it might be confusing for one
thread to set it and expect the next exec to be NNP, but if the exec is
requested by another thread that would not happen. With a
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag, we could have an atomic
Landlock and NNP enforcement, which can also be combined with the TSYNC
flag, both useful cases.
If we want an enforcement to happen at exec time, we could add a
dedicated flag, similar to the TSYNC one, but I'm not sure it would be
useful for now.
On Wed, Jul 08, 2026 at 09:39:24AM -0400, Justin Suess wrote:
> Good morning,
>
> This series adds a new landlock_restrict_self(2) flag:
> LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC.
>
> This flag stages a bit in the Landlock credentials indicating that the
> next successful execution will set the no_new_privs attribute while
> committing its new credentials.
>
> Differences from prctl(PR_SET_NO_NEW_PRIVS):
>
> PR_SET_NO_NEW_PRIVS takes effect immediately: it prevents gaining
> privileges through set-user-ID, set-group-ID and file capabilities
> starting with the very next execve(2).
>
> LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC instead only sets the task's
> no_new_privs attribute once the next execve(2) is guaranteed to succeed
> (past its point of no return, in bprm_committing_creds), after any
> privilege gain through set-user-ID, set-group-ID or file capabilities
> has already taken place. The executed program then runs with
> no_new_privs, so subsequent executions cannot gain privileges. A
> failed execve(2) leaves no_new_privs untouched.
>
> Use cases:
>
> Enforcing a Landlock ruleset requires that the calling process either
> already has no_new_privs set or possesses CAP_SYS_ADMIN. This series
> does not grant any exception to this rule; for a caller that already
> has no_new_privs set, the flag is effectively a no-op. It is therefore
> mostly useful for CAP_SYS_ADMIN callers that need to execute programs
> that legitimately escalate privileges (e.g. transition to another
> user), while ensuring that further executions cannot gain privileges.
That's a good point, but why not just set NNP for the whole process at
landlock_restrict_self() call time? There is a partial answer below
but...
>
> Consider a sandbox launcher that depends on a set-user-ID helper, such
> as launching applications through bubblewrap on distributions where
> unprivileged user namespaces are disabled and bwrap is installed
> set-user-ID root. The launcher cannot set PR_SET_NO_NEW_PRIVS before
> the execution, as that would neuter the very helper it depends on:
>
> sandbox launcher (CAP_SYS_ADMIN)
> |
> | landlock_restrict_self(-1, LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC)
> V
> /usr/bin/bwrap (set-user-ID root: honored for this execution;
> | no_new_privs set once the execution is past its
> | point of no return)
This should only be allowed if the launcher has CAP_SYS_ADMIN, otherwise
it would be like changing the state of a more privileged process.
Anyway, even with this safeguard, this approach looks risky.
> V
> sandboxed application (runs with no_new_privs; cannot gain
> privileges through any further execution, and
> may enforce its own Landlock ruleset without
> CAP_SYS_ADMIN)
>
> This flag also closes a gap for CAP_SYS_ADMIN callers of
> landlock_restrict_self(2) itself. The no_new_privs/CAP_SYS_ADMIN
> requirement exists to keep set-user-ID programs from running confused
> inside a sandbox they do not expect. However, a privileged process
> that enforces a domain without setting no_new_privs leaves that hole
> open for all of its descendants: anything running in the domain may
> still execute a set-user-ID binary, which then runs privileged under a
> restricted view of the system. A privileged process that needs one
> legitimate set-user-ID/set-group-ID transition currently has to choose
> between breaking that transition (setting no_new_privs first) or
> leaving the hole open for the lifetime of the domain.
> LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC allows the one intended transition
> and then closes the hole.
In a nutshell, not setting NNP might be risky, even when not strictly
needed. We might want to update the Landlock doc with that.
>
> Design:
>
> This flag is implemented simply: a bit is stored in the Landlock
> credential blob (struct landlock_cred_security) indicating whether the
> next execution should set no_new_privs when it commits its new
> credentials.
Also, if we don't have the ON_EXEC part, there is no need to store
anything in the cred.
>
> The bit is not coupled to any ruleset and, like
> LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, may be passed with no
> ruleset (i.e. ruleset_fd = -1). It may also be combined with
> LANDLOCK_RESTRICT_SELF_TSYNC to propagate the staged state to sibling
> threads, each thread then setting no_new_privs at its own next
> execve(2).
>
> The staged bit is inherited across fork(2) and persists in the
> credentials until the next successful execution. To consume it,
> Landlock handles the bprm_committing_creds hook, which runs while the
> credentials of the new program are being committed: if the bit is set,
> task_set_no_new_privs(current) is called and the bit is cleared.
>
> Again, this flag does not bypass the requirement to either have
> CAP_SYS_ADMIN or no_new_privs already set to call
> landlock_restrict_self(2).
>
> The Landlock ABI version is bumped to 11.
>
> Test coverage:
>
> The new nnp_on_exec fixture generates a shell script that reads the
> NoNewPrivs value from /proc/self/status and exits with it. The
> variants select the conditions under which the flag is tested (with
> and without a ruleset, with and without CAP_SYS_ADMIN/no_new_privs
> already set, with and without TSYNC, etc.), then compare no_new_privs
> before the execution and in the executed script. The ruleset variants
> also check that a ruleset passed along the flag is enforced
> immediately, unlike the staged no_new_privs.
>
> Justin Suess (3):
> landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
> selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
> landlock: Document LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
>
> Documentation/userspace-api/landlock.rst | 21 +-
> include/uapi/linux/landlock.h | 36 ++-
> security/landlock/cred.c | 22 ++
> security/landlock/cred.h | 8 +
> security/landlock/limits.h | 2 +-
> security/landlock/syscalls.c | 54 +++-
> tools/testing/selftests/landlock/base_test.c | 259 ++++++++++++++++++-
> 7 files changed, 382 insertions(+), 20 deletions(-)
>
> --
> 2.54.0
>
>
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-09 10:09 ` [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Mickaël Salaün
@ 2026-07-09 11:22 ` Simon McVittie
2026-07-11 17:00 ` Justin Suess
2026-07-10 13:56 ` Justin Suess
1 sibling, 1 reply; 8+ messages in thread
From: Simon McVittie @ 2026-07-09 11:22 UTC (permalink / raw)
To: Mickaël Salaün; +Cc: Justin Suess, linux-security-module, gnoack
On Thu, 09 Jul 2026 at 12:09:25 +0200, Mickaël Salaün wrote:
> On Wed, Jul 08, 2026 at 09:39:24AM -0400, Justin Suess wrote:
> > Consider a sandbox launcher that depends on a set-user-ID helper, such
> > as launching applications through bubblewrap on distributions where
> > unprivileged user namespaces are disabled and bwrap is installed
> > set-user-ID root.
It's perhaps worth noting that the current version 0.11.2 of bubblewrap
deprecates this mode of use (it will refuse to run while setuid unless
that was explicitly enabled at compile-time), and the next release 0.12.0
will also remove the ability to enable it at compile-time.
When bubblewrap was first written, having it be setuid was a necessary
workaround for kernels/distros not letting it do its sandboxing job any
other way; but now that unprivileged user namespaces are more widespread,
its maintainers have come to the conclusion that when it's setuid,
the risk of vulnerabilities that allow a root privilege escalation
(CVE-2020-5291, CVE-2026-41163) is unacceptably high, so being able to
make it setuid is no longer a good trade-off.
> > This flag also closes a gap for CAP_SYS_ADMIN callers of
> > landlock_restrict_self(2) itself. The no_new_privs/CAP_SYS_ADMIN
> > requirement exists to keep set-user-ID programs from running confused
> > inside a sandbox they do not expect. However, a privileged process
> > that enforces a domain without setting no_new_privs leaves that hole
> > open for all of its descendants
>
> In a nutshell, not setting NNP might be risky, even when not strictly
> needed. We might want to update the Landlock doc with that.
If the CAP_SYS_ADMIN caller is setuid or setcap, then it has been
granted special privileges by the sysadmin or distro, and part of
the "contract" between the sysadmin/distro and the setuid program is
that setuid/setcap must only be set on executables that have taken
responsibility for ensuring that they can't create insecure situations
(for example bubblewrap always sets PR_SET_NO_NEW_PRIVS, unconditionally,
for this reason).
A large part of why bubblewrap no longer supports being setuid is that
its maintainers don't want it to have this heavy responsibility.
smcv
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-09 11:22 ` Simon McVittie
@ 2026-07-11 17:00 ` Justin Suess
0 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-07-11 17:00 UTC (permalink / raw)
To: Simon McVittie; +Cc: Mickaël Salaün, linux-security-module, gnoack
On Thu, Jul 09, 2026 at 12:22:40PM +0100, Simon McVittie wrote:
> On Thu, 09 Jul 2026 at 12:09:25 +0200, Mickaël Salaün wrote:
> > On Wed, Jul 08, 2026 at 09:39:24AM -0400, Justin Suess wrote:
> > > Consider a sandbox launcher that depends on a set-user-ID helper, such
> > > as launching applications through bubblewrap on distributions where
> > > unprivileged user namespaces are disabled and bwrap is installed
> > > set-user-ID root.
>
> It's perhaps worth noting that the current version 0.11.2 of bubblewrap
> deprecates this mode of use (it will refuse to run while setuid unless
> that was explicitly enabled at compile-time), and the next release 0.12.0
> will also remove the ability to enable it at compile-time.
>
> When bubblewrap was first written, having it be setuid was a necessary
> workaround for kernels/distros not letting it do its sandboxing job any
> other way; but now that unprivileged user namespaces are more widespread,
> its maintainers have come to the conclusion that when it's setuid,
> the risk of vulnerabilities that allow a root privilege escalation
> (CVE-2020-5291, CVE-2026-41163) is unacceptably high, so being able to
> make it setuid is no longer a good trade-off.
>
> > > This flag also closes a gap for CAP_SYS_ADMIN callers of
> > > landlock_restrict_self(2) itself. The no_new_privs/CAP_SYS_ADMIN
> > > requirement exists to keep set-user-ID programs from running confused
> > > inside a sandbox they do not expect. However, a privileged process
> > > that enforces a domain without setting no_new_privs leaves that hole
> > > open for all of its descendants
> >
> > In a nutshell, not setting NNP might be risky, even when not strictly
> > needed. We might want to update the Landlock doc with that.
>
> If the CAP_SYS_ADMIN caller is setuid or setcap, then it has been
> granted special privileges by the sysadmin or distro, and part of
> the "contract" between the sysadmin/distro and the setuid program is
> that setuid/setcap must only be set on executables that have taken
> responsibility for ensuring that they can't create insecure situations
> (for example bubblewrap always sets PR_SET_NO_NEW_PRIVS, unconditionally,
> for this reason).
>
> A large part of why bubblewrap no longer supports being setuid is that
> its maintainers don't want it to have this heavy responsibility.
>
Understandable. I guess my usecase proposed was narrower than thought.
And we probably want to avoid creating risky confused deputy scenarios.
So now I'm leaning towards the LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS path,
dropping the "ON_EXEC" part as Mickaël proposed.
But I'm struggling to see where the value is added over the prctl call.
Sure atomic enforcement with the ruleset is nice, but not a huge value added
on it's own.
Perhaps the setting of this bit with a -1 ruleset_fd could enable auditing
of the (failed or successful) privilege acquisition for the domain via the
fcaps/suid/guid methods w/o setting NNP? (otherwise this would be no
different from a Landlock specific prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
call).
And setting it with a non -1 ruleset_fd would set NNP *and* enable the
same auditing.
That way we are making this a "value-added" flag instead of just a landlock-flavored
prctl call.
This would work well with the LANDLOCK_PERM_CAPABILITY_USE proposal,
which allows us to audit both methods of acquiring new capabilities.
(the clone/unshare path, and the fcaps/setuid/setgid path).
What do you think Mickaël?
Justin
> smcv
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
2026-07-09 10:09 ` [PATCH 0/3] Implement LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC Mickaël Salaün
2026-07-09 11:22 ` Simon McVittie
@ 2026-07-10 13:56 ` Justin Suess
1 sibling, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-07-10 13:56 UTC (permalink / raw)
To: Mickaël Salaün; +Cc: linux-security-module, gnoack
On Thu, Jul 09, 2026 at 12:09:25PM +0200, Mickaël Salaün wrote:
> Hi Justin,
>
> Thanks for this patch series, I like the underlying idea, but I'm not
> convince we should mix NNP and ON_EXEC. Why not a
> LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag instead?
>
tldr I think your idea is better, see below.
> One issue with this NNP_ON_EXEC is that it might be confusing for one
> thread to set it and expect the next exec to be NNP, but if the exec is
> requested by another thread that would not happen. With a
> LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS flag, we could have an atomic
> Landlock and NNP enforcement, which can also be combined with the TSYNC
> flag, both useful cases.
>
> If we want an enforcement to happen at exec time, we could add a
> dedicated flag, similar to the TSYNC one, but I'm not sure it would be
> useful for now.
>
> On Wed, Jul 08, 2026 at 09:39:24AM -0400, Justin Suess wrote:
> > Good morning,
> >
> > This series adds a new landlock_restrict_self(2) flag:
> > LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC.
> >
> > This flag stages a bit in the Landlock credentials indicating that the
> > next successful execution will set the no_new_privs attribute while
> > committing its new credentials.
> >
> > Differences from prctl(PR_SET_NO_NEW_PRIVS):
> >
> > PR_SET_NO_NEW_PRIVS takes effect immediately: it prevents gaining
> > privileges through set-user-ID, set-group-ID and file capabilities
> > starting with the very next execve(2).
> >
> > LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC instead only sets the task's
> > no_new_privs attribute once the next execve(2) is guaranteed to succeed
> > (past its point of no return, in bprm_committing_creds), after any
> > privilege gain through set-user-ID, set-group-ID or file capabilities
> > has already taken place. The executed program then runs with
> > no_new_privs, so subsequent executions cannot gain privileges. A
> > failed execve(2) leaves no_new_privs untouched.
> >
> > Use cases:
> >
> > Enforcing a Landlock ruleset requires that the calling process either
> > already has no_new_privs set or possesses CAP_SYS_ADMIN. This series
> > does not grant any exception to this rule; for a caller that already
> > has no_new_privs set, the flag is effectively a no-op. It is therefore
> > mostly useful for CAP_SYS_ADMIN callers that need to execute programs
> > that legitimately escalate privileges (e.g. transition to another
> > user), while ensuring that further executions cannot gain privileges.
>
> That's a good point, but why not just set NNP for the whole process at
> landlock_restrict_self() call time? There is a partial answer below
> but...
>
Atomically with respect to the ruleset enforcement I presume?
I guess that makes this even easier... just run it at the end after all
possible errors in the landlock_restrict_self path.
In this case then, we must consider whether this would be sufficient to
serve as a substitute for NNP || cap_sys_admin check.
> >
> > Consider a sandbox launcher that depends on a set-user-ID helper, such
> > as launching applications through bubblewrap on distributions where
> > unprivileged user namespaces are disabled and bwrap is installed
> > set-user-ID root. The launcher cannot set PR_SET_NO_NEW_PRIVS before
> > the execution, as that would neuter the very helper it depends on:
> >
> > sandbox launcher (CAP_SYS_ADMIN)
> > |
> > | landlock_restrict_self(-1, LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC)
> > V
> > /usr/bin/bwrap (set-user-ID root: honored for this execution;
> > | no_new_privs set once the execution is past its
> > | point of no return)
>
> This should only be allowed if the launcher has CAP_SYS_ADMIN, otherwise
> it would be like changing the state of a more privileged process.
> Anyway, even with this safeguard, this approach looks risky.
>
Yes, and it does have CAP_SYS_ADMIN in this example. It "feels" risky
though to mess with further escalations. But if you are CAP_SYS_ADMIN,
you can load kernel modules, potentially write to any memory, etc.
So that is less the argument I would make.
I think the main argument that hasn't been highlighted yet against *my
proposal* (I like to red team my own proposals) is that this whole
thing has nothing to do with Landlock. My feature doesn't touch Landlock
rulesets, or anything Landlock specific, so if this were implemented,
why not just make it a prctl? Why tie it to an LSM at all?
So I like your proposal better (atomic nnp with Landlock) because it's
actually relevant to Landlock rulesets.
The last remaining questions:
1. Shall the NNP || cap_sys_admin check remain in place for the NNP Landlock flag?
2. Should we allow enforcement with ruleset_fd being -1 (would basically
be equivalent to the prctl version of set no new privs)?
Justin
> > V
> > sandboxed application (runs with no_new_privs; cannot gain
> > privileges through any further execution, and
> > may enforce its own Landlock ruleset without
> > CAP_SYS_ADMIN)
> >
> > This flag also closes a gap for CAP_SYS_ADMIN callers of
> > landlock_restrict_self(2) itself. The no_new_privs/CAP_SYS_ADMIN
> > requirement exists to keep set-user-ID programs from running confused
> > inside a sandbox they do not expect. However, a privileged process
> > that enforces a domain without setting no_new_privs leaves that hole
> > open for all of its descendants: anything running in the domain may
> > still execute a set-user-ID binary, which then runs privileged under a
> > restricted view of the system. A privileged process that needs one
> > legitimate set-user-ID/set-group-ID transition currently has to choose
> > between breaking that transition (setting no_new_privs first) or
> > leaving the hole open for the lifetime of the domain.
> > LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC allows the one intended transition
> > and then closes the hole.
>
> In a nutshell, not setting NNP might be risky, even when not strictly
> needed. We might want to update the Landlock doc with that.
>
Yes, I
> >
> > Design:
> >
> > This flag is implemented simply: a bit is stored in the Landlock
> > credential blob (struct landlock_cred_security) indicating whether the
> > next execution should set no_new_privs when it commits its new
> > credentials.
>
> Also, if we don't have the ON_EXEC part, there is no need to store
> anything in the cred.
>
> >
> > The bit is not coupled to any ruleset and, like
> > LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, may be passed with no
> > ruleset (i.e. ruleset_fd = -1). It may also be combined with
> > LANDLOCK_RESTRICT_SELF_TSYNC to propagate the staged state to sibling
> > threads, each thread then setting no_new_privs at its own next
> > execve(2).
> >
> > The staged bit is inherited across fork(2) and persists in the
> > credentials until the next successful execution. To consume it,
> > Landlock handles the bprm_committing_creds hook, which runs while the
> > credentials of the new program are being committed: if the bit is set,
> > task_set_no_new_privs(current) is called and the bit is cleared.
> >
> > Again, this flag does not bypass the requirement to either have
> > CAP_SYS_ADMIN or no_new_privs already set to call
> > landlock_restrict_self(2).
> >
> > The Landlock ABI version is bumped to 11.
> >
> > Test coverage:
> >
> > The new nnp_on_exec fixture generates a shell script that reads the
> > NoNewPrivs value from /proc/self/status and exits with it. The
> > variants select the conditions under which the flag is tested (with
> > and without a ruleset, with and without CAP_SYS_ADMIN/no_new_privs
> > already set, with and without TSYNC, etc.), then compare no_new_privs
> > before the execution and in the executed script. The ruleset variants
> > also check that a ruleset passed along the flag is enforced
> > immediately, unlike the staged no_new_privs.
> >
> > Justin Suess (3):
> > landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
> > selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
> > landlock: Document LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
> >
> > Documentation/userspace-api/landlock.rst | 21 +-
> > include/uapi/linux/landlock.h | 36 ++-
> > security/landlock/cred.c | 22 ++
> > security/landlock/cred.h | 8 +
> > security/landlock/limits.h | 2 +-
> > security/landlock/syscalls.c | 54 +++-
> > tools/testing/selftests/landlock/base_test.c | 259 ++++++++++++++++++-
> > 7 files changed, 382 insertions(+), 20 deletions(-)
> >
> > --
> > 2.54.0
> >
> >
^ permalink raw reply [flat|nested] 8+ messages in thread