[PATCH 1/3] landlock: Add LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
Justin Suess
utilityemal77 at gmail.com
Wed Jul 8 13:39:25 UTC 2026
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 at digikod.net>
Signed-off-by: Justin Suess <utilityemal77 at 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
More information about the Linux-security-module-archive
mailing list