* [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
@ 2026-08-03 22:31 Justin Suess
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Justin Suess @ 2026-08-03 22:31 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Howdy
This series adds a new landlock_restrict_self(2) flag:
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS.
The flag sets the no_new_privs attribute of the calling thread only
once the enforcement of the ruleset succeeded: no_new_privs is set if
and only if the landlock_restrict_self(2) call succeeds. Following
Mickaël's feedback [2] on v2 [1], the "atomic" framing is gone: this is
an ordering guarantee, not atomicity, and the wording now reflects that
throughout the series.
Semantics:
A single call replaces the usual prctl(PR_SET_NO_NEW_PRIVS) +
landlock_restrict_self(2) pair. Because no_new_privs is set by the
call itself, the no_new_privs/CAP_SYS_ADMIN precondition is fulfilled
by construction, so the flag is usable by unprivileged processes. This
is safe for the same reason the prctl(2) pair is: the executed programs
can either gain privileges or be restricted, never both.
The two states cannot diverge. A failed call (invalid ruleset FD,
E2BIG, ENOMEM, interrupted TSYNC, ...) leaves no_new_privs unchanged,
and a successful call never returns without no_new_privs set: the
attribute is set past the last point of failure, right before
commit_creds(), which cannot fail.
Combined with LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on all
threads with the same guarantee: each sibling thread sets it in the
commit phase of the TSYNC protocol, after its all-or-nothing barrier,
so either every thread gets both the domain and no_new_privs, or none
does. This also makes it possible to set no_new_privs process-wide in
one call, which prctl(2) cannot do.
The flag requires a ruleset: calls with a ruleset_fd of -1 are
rejected. Such a call would be nothing more than a Landlock-flavored
prctl(PR_SET_NO_NEW_PRIVS), and rejecting it keeps the option of giving
it a meaning later. As a consequence of the fulfilled precondition, an
unprivileged caller passing unknown flag bits together with this flag
receives EINVAL instead of EPERM; the selftests pin this error ordering
as well.
The Landlock ABI version is bumped to 11.
Test coverage:
base_test checks that a successful call sets no_new_privs without a
prior prctl(2) nor CAP_SYS_ADMIN, that a failed call (invalid ruleset
FD or layer maximum) leaves it unchanged, that the flag requires a
ruleset FD, and the updated EPERM/EINVAL ordering. tsync_test checks
that TSYNC sets no_new_privs on sibling threads along with the domain,
and that a TSYNC call failing on the layer maximum leaves it unset on
every thread.
Changes since v2:
- Reworded "atomically" to the ordering guarantee in the commit
messages, kdocs and documentation, per Mickaël's feedback.
- Explained the valid-ruleset requirement in the first patch's commit
message.
- Ran clang-format on the selftests and added max-layers failure tests
checking E2BIG and that no_new_privs stays unset.
- Mentioned the selftest renames in the commit message.
- Updated the documentation tutorial (restrict_flags gated on the ABI
version, prctl(2) call skipped when the flag is used) and explained
that not setting no_new_privs is risky even when it is not required.
- New patch: the sandboxer sample now uses the flag by default, gated
on the ABI version, with a prctl(2) fallback.
Per-patch changelogs are below each patch.
[1] https://lore.kernel.org/linux-security-module/20260717220320.1030123-1-utilityemal77@gmail.com/
[2] https://lore.kernel.org/linux-security-module/20260731.at2Zilei1ech@digikod.net/
Justin Suess (4):
landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler
Documentation/userspace-api/landlock.rst | 47 +++++++--
include/uapi/linux/landlock.h | 13 +++
samples/landlock/sandboxer.c | 16 ++-
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 28 ++++--
security/landlock/tsync.c | 8 +-
security/landlock/tsync.h | 4 +-
tools/testing/selftests/landlock/base_test.c | 99 ++++++++++++++++++-
tools/testing/selftests/landlock/tsync_test.c | 72 ++++++++++++++
9 files changed, 263 insertions(+), 26 deletions(-)
base-commit: 308bc78f1e577439ca61ec77842a5a3ba7e8bbce
--
2.54.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-03 22:31 ` Justin Suess
2026-08-07 10:51 ` Mickaël Salaün
2026-08-07 13:18 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
` (2 subsequent siblings)
3 siblings, 2 replies; 9+ messages in thread
From: Justin Suess @ 2026-08-03 22:31 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Add a landlock_restrict_self(2) flag to set the no_new_privs attribute
of the calling thread only after enforcement of the ruleset:
no_new_privs is set if and only if the call succeeds. This removes the
need for a prior prctl(2) PR_SET_NO_NEW_PRIVS call and guarantees that
a failed enforcement leaves the attribute unchanged.
Because no_new_privs is set by the call itself, the no_new_privs /
CAP_SYS_ADMIN requirement of landlock_restrict_self(2) is fulfilled by
construction, and the related EPERM check is skipped. As a consequence,
an unprivileged caller passing unknown flags along with this flag gets
EINVAL instead of EPERM.
Unlike LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, this flag always
requires a valid ruleset: with a ruleset_fd of -1, such a call would be
nothing more than a Landlock-flavored prctl(2) PR_SET_NO_NEW_PRIVS, and
there is no valid use case for setting no_new_privs (possibly with
LANDLOCK_RESTRICT_SELF_TSYNC) without also enforcing Landlock
restrictions. Rejecting these calls also keeps the option of giving
them a meaning later.
The attribute is only set past the last point of failure, just before
committing the new credentials. When combined with
LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the sibling threads
as well, in their commit phase, with the same ordering.
Bump the Landlock ABI version to 11.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- Reword "atomically" to the ordering guarantee (no_new_privs is only set
once enforcement succeeded) in the commit message and both kdocs
- Explain in the commit message why the flag requires a valid ruleset
include/uapi/linux/landlock.h | 13 +++++++++++++
security/landlock/limits.h | 2 +-
security/landlock/syscalls.c | 28 +++++++++++++++++++++-------
security/landlock/tsync.c | 8 ++++++--
security/landlock/tsync.h | 4 +++-
5 files changed, 44 insertions(+), 11 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 27ae3f39cafb..11bf600698f0 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -191,12 +191,25 @@ struct landlock_ruleset_attr {
*
* If the calling thread is running with no_new_privs, this operation
* enables no_new_privs on the sibling threads as well.
+ *
+ * The following flag ties the no_new_privs attribute to the ruleset
+ * enforcement:
+ *
+ * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
+ * Sets the no_new_privs attribute of the calling thread only once the
+ * enforcement of the ruleset succeeded: no_new_privs is set if and only
+ * if sys_landlock_restrict_self() succeeds. This removes the need for a
+ * prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS`` call, and with it the
+ * %CAP_SYS_ADMIN requirement. This flag requires a ruleset. When
+ * combined with %LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the
+ * sibling threads as well.
*/
/* 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_NO_NEW_PRIVS (1U << 4)
/* clang-format on */
/**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 08d5f2f6d321..1a7c5fb8f6fd 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_NO_NEW_PRIVS
#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..e97f944109f9 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,21 +502,28 @@ 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_NO_NEW_PRIVS
*
* 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_NO_NEW_PRIVS, the no_new_privs attribute of the
+ * calling thread is set only once the enforcement of the ruleset succeeded,
+ * which fulfills the above requirement: no_new_privs is set if and only if the
+ * call succeeds.
+ *
* Return: 0 on success, or -errno on failure. Possible returned errors are:
*
* - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
* - %EINVAL: @flags contains an unknown bit.
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
* - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
- * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
- * current thread is not running with no_new_privs, or it doesn't have
- * %CAP_SYS_ADMIN in its namespace.
+ * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
+ * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
+ * is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
+ * namespace.
* - %E2BIG: The maximum number of stacked rulesets is reached for the current
* thread.
*
@@ -529,6 +536,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
+ const bool set_no_new_privs =
+ !!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS);
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
prev_log_subdomains;
@@ -537,9 +546,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.
+ * returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
+ * requirement.
*/
- if (!task_no_new_privs(current) &&
+ if (!set_no_new_privs && !task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
@@ -620,12 +630,16 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
const int err = landlock_restrict_sibling_threads(
- current_cred(), new_cred);
+ current_cred(), new_cred, flags);
if (err) {
abort_creds(new_cred);
return err;
}
}
+ /* Sets no_new_privs past the last point of failure. */
+ if (set_no_new_privs)
+ task_set_no_new_privs(current);
+
return commit_creds(new_cred);
}
diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
index c5730bbd9ed3..0b71e158c3f5 100644
--- a/security/landlock/tsync.c
+++ b/security/landlock/tsync.c
@@ -17,6 +17,7 @@
#include <linux/sched/task.h>
#include <linux/slab.h>
#include <linux/task_work.h>
+#include <uapi/linux/landlock.h>
#include "cred.h"
#include "tsync.h"
@@ -466,7 +467,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
* restrict_sibling_threads - enables a Landlock policy for all sibling threads
*/
int landlock_restrict_sibling_threads(const struct cred *old_cred,
- const struct cred *new_cred)
+ const struct cred *new_cred,
+ const u32 restrict_flags)
{
int err;
struct tsync_shared_context shared_ctx;
@@ -481,7 +483,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
init_completion(&shared_ctx.all_finished);
shared_ctx.old_cred = old_cred;
shared_ctx.new_cred = new_cred;
- shared_ctx.set_no_new_privs = task_no_new_privs(current);
+ shared_ctx.set_no_new_privs =
+ (restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
+ task_no_new_privs(current);
/*
* Serialize concurrent TSYNC operations to prevent deadlocks when
diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h
index ef86bb61c2f6..2ae4f938ca00 100644
--- a/security/landlock/tsync.h
+++ b/security/landlock/tsync.h
@@ -9,8 +9,10 @@
#define _SECURITY_LANDLOCK_TSYNC_H
#include <linux/cred.h>
+#include <linux/types.h>
int landlock_restrict_sibling_threads(const struct cred *old_cred,
- const struct cred *new_cred);
+ const struct cred *new_cred,
+ u32 restrict_flags);
#endif /* _SECURITY_LANDLOCK_TSYNC_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-03 22:31 ` Justin Suess
2026-08-07 10:48 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-03 22:31 ` [PATCH v3 4/4] samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler Justin Suess
3 siblings, 1 reply; 9+ messages in thread
From: Justin Suess @ 2026-08-03 22:31 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Check that a successful landlock_restrict_self(2) call with
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS sets no_new_privs without a prior
prctl(2) call nor CAP_SYS_ADMIN, that a failed call from both an
invalid ruleset and hitting the layer maximum leaves the attribute
unchanged, and that LANDLOCK_RESTRICT_SELF_TSYNC extends it to sibling
threads. Also check that this flag requires a ruleset, and update the
restrict_self_checks_ordering EPERM checks since this flag is now
checked before the flags validity.
Finally, rename restrict_self_fd_logging_flags to
restrict_self_fd_flags, and restrict_self_logging_flags to
restrict_self_flags to indicate that non-logging flags are now tested.
Update the ABI version and last-flag checks accordingly.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- Run clang-format
- Add max-layers tests (base_test and tsync_test) checking E2BIG and
that a failed call leaves no_new_privs unchanged
- Mention the test renames in the commit message
- Match comment style of surrounding tests
tools/testing/selftests/landlock/base_test.c | 99 ++++++++++++++++++-
tools/testing/selftests/landlock/tsync_test.c | 72 ++++++++++++++
2 files changed, 166 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index cbd3c1669951..c8ed165a32ed 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,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,
@@ -255,8 +255,15 @@ TEST(restrict_self_checks_ordering)
/* Checks unprivileged enforcement without no_new_privs. */
drop_caps(_metadata);
- ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
+ ASSERT_EQ(-1, landlock_restrict_self(
+ -1, ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
ASSERT_EQ(EPERM, errno);
+ /*
+ * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills the no_new_privs /
+ * CAP_SYS_ADMIN requirement, so the invalid flags are checked first.
+ */
+ ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
+ ASSERT_EQ(EINVAL, errno);
ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
ASSERT_EQ(EPERM, errno);
ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
@@ -277,6 +284,41 @@ TEST(restrict_self_checks_ordering)
ASSERT_EQ(0, close(ruleset_fd));
}
+TEST(restrict_self_max_layers)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
+ };
+ struct landlock_path_beneath_attr path_beneath_attr = {
+ .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
+ .parent_fd = -1,
+ };
+ const int ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ path_beneath_attr.parent_fd =
+ open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath_attr.parent_fd);
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath_attr, 0));
+ ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
+
+ /* Enforces the maximum number of allowed layers. */
+ for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
+ ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+
+ /* Enforces one too many rulesets. */
+ drop_caps(_metadata);
+ ASSERT_EQ(-1, landlock_restrict_self(
+ ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ ASSERT_EQ(E2BIG, errno);
+
+ /* Checks that the failed call did not set no_new_privs. */
+ ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
TEST(restrict_self_fd)
{
int fd;
@@ -288,7 +330,7 @@ TEST(restrict_self_fd)
EXPECT_EQ(EBADFD, errno);
}
-TEST(restrict_self_fd_logging_flags)
+TEST(restrict_self_fd_flags)
{
int fd;
@@ -302,11 +344,16 @@ TEST(restrict_self_fd_logging_flags)
EXPECT_EQ(-1, landlock_restrict_self(
fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
EXPECT_EQ(EBADFD, errno);
+
+ /* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
+ EXPECT_EQ(-1, landlock_restrict_self(
+ fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ 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_NO_NEW_PRIVS;
/* Tests invalid flag combinations. */
@@ -349,6 +396,17 @@ TEST(restrict_self_logging_flags)
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
EXPECT_EQ(EBADF, errno);
+ /* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
+
+ EXPECT_EQ(-1, landlock_restrict_self(
+ -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ EXPECT_EQ(EBADF, errno);
+
+ EXPECT_EQ(-1, landlock_restrict_self(
+ -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+ LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ EXPECT_EQ(EBADF, errno);
+
/* Tests with an invalid ruleset_fd. */
EXPECT_EQ(-1, landlock_restrict_self(
@@ -359,6 +417,37 @@ TEST(restrict_self_logging_flags)
-1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
}
+TEST(restrict_self_no_new_privs)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+ };
+ const int ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+
+ ASSERT_LE(0, ruleset_fd);
+
+ /*
+ * The calling thread does not need CAP_SYS_ADMIN nor an explicit
+ * prctl(2) PR_SET_NO_NEW_PRIVS call.
+ */
+ drop_caps(_metadata);
+ ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ /* Checks that a failed call does not set no_new_privs. */
+ EXPECT_EQ(-1, landlock_restrict_self(
+ -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ EXPECT_EQ(EBADF, errno);
+ EXPECT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ /* Checks that a successful call sets no_new_privs. */
+ ASSERT_EQ(0, landlock_restrict_self(
+ ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+ EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
TEST(ruleset_fd_io)
{
struct landlock_ruleset_attr ruleset_attr = {
diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
index 9cf1491bbaaf..afa4a8222248 100644
--- a/tools/testing/selftests/landlock/tsync_test.c
+++ b/tools/testing/selftests/landlock/tsync_test.c
@@ -90,6 +90,78 @@ TEST(multi_threaded_success)
EXPECT_EQ(0, close(ruleset_fd));
}
+TEST(multi_threaded_no_new_privs)
+{
+ pthread_t t1, t2;
+ bool no_new_privs1, no_new_privs2;
+ const int ruleset_fd = create_ruleset(_metadata);
+
+ disable_caps(_metadata);
+
+ ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
+ ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
+
+ /* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
+ ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ EXPECT_EQ(0, landlock_restrict_self(
+ ruleset_fd,
+ LANDLOCK_RESTRICT_SELF_TSYNC |
+ LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
+
+ EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ ASSERT_EQ(0, pthread_cancel(t1));
+ ASSERT_EQ(0, pthread_cancel(t2));
+ ASSERT_EQ(0, pthread_join(t1, NULL));
+ ASSERT_EQ(0, pthread_join(t2, NULL));
+
+ /* The no_new_privs flag was enabled on all threads. */
+ EXPECT_TRUE(no_new_privs1);
+ EXPECT_TRUE(no_new_privs2);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+TEST(multi_threaded_no_new_privs_max_layers)
+{
+ pthread_t t1, t2;
+ bool no_new_privs1, no_new_privs2;
+ const int ruleset_fd = create_ruleset(_metadata);
+
+ /* Enforces the maximum number of allowed layers. */
+ for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
+ ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+
+ ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
+ ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
+
+ disable_caps(_metadata);
+
+ /* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
+ ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ ASSERT_EQ(-1,
+ landlock_restrict_self(ruleset_fd,
+ LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS |
+ LANDLOCK_RESTRICT_SELF_TSYNC));
+ ASSERT_EQ(E2BIG, errno);
+
+ /* Checks that the failed call did not set no_new_privs. */
+ ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
+
+ ASSERT_EQ(0, pthread_cancel(t1));
+ ASSERT_EQ(0, pthread_cancel(t2));
+ ASSERT_EQ(0, pthread_join(t1, NULL));
+ ASSERT_EQ(0, pthread_join(t2, NULL));
+
+ /* The no_new_privs flag was not enabled on any thread. */
+ EXPECT_FALSE(no_new_privs1);
+ EXPECT_FALSE(no_new_privs2);
+
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
TEST(multi_threaded_success_despite_diverging_domains)
{
pthread_t t1, t2;
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-03 22:31 ` [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-03 22:31 ` Justin Suess
2026-08-07 10:57 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 4/4] samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler Justin Suess
3 siblings, 1 reply; 9+ messages in thread
From: Justin Suess @ 2026-08-03 22:31 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Document setting no_new_privs with ruleset enforcement, following the
same compatibility section style as previous ABI additions.
Include a section explaining the tradeoffs of setting no_new_privs
through any means for privileged users of Landlock.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- Update the tutorial: restrict_flags per ABI version and prctl call
skipped when LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is used
- Drop "Atomic" from the section title; describe the ordering instead
- Explain that not setting no_new_privs is risky even when not required
- Fix ABI 8/9 switch coverage (case 8 ... 10) and indentation
Documentation/userspace-api/landlock.rst | 47 +++++++++++++++++++++---
1 file changed, 41 insertions(+), 6 deletions(-)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 5085822d8930..0e4a73fd5ea4 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: July 2026
+:Date: August 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
@@ -250,7 +250,8 @@ similar backwards compatibility check is needed for the restrict flags
__u32 restrict_flags =
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
- LANDLOCK_RESTRICT_SELF_TSYNC;
+ LANDLOCK_RESTRICT_SELF_TSYNC |
+ LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
switch (abi) {
case 1 ... 6:
/* Removes logging flags for ABI < 7 */
@@ -269,16 +270,36 @@ similar backwards compatibility check is needed for the restrict flags
* children (and not for all threads, including parents and siblings).
*/
restrict_flags &= ~LANDLOCK_RESTRICT_SELF_TSYNC;
+ __attribute__((fallthrough));
+ case 8 ... 10:
+ /* Removes no new privs flag for ABI < 11 */
+ restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
}
The next step is to restrict the current thread from gaining more privileges
-(e.g. through a SUID binary). We now have a ruleset with the first rule
-allowing read and execute access to ``/usr`` while denying all other handled
-accesses for the filesystem, and two more rules allowing DNS queries.
+(e.g. through a SUID binary). For unprivileged processes, setting the
+no_new_privs attribute is required by Landlock.
+
+Processes with ``CAP_SYS_ADMIN`` in their namespace can enforce a ruleset
+without it, but not setting no_new_privs is risky even when it is not
+required: sandboxed processes could still execute set-user-ID, set-group-ID
+or file-capability binaries, which would then run with elevated privileges
+while being restricted by a Landlock domain they may not expect, making them
+potential confused deputies. Setting no_new_privs should only be avoided if
+such a privilege transition is expected.
+
+We now have a ruleset with the first rule allowing read and execute access to
+``/usr`` while denying all other handled accesses for the filesystem, and two
+more rules allowing DNS queries.
.. code-block:: c
- if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
+ /*
+ * If the ABI > 10, we can tie setting no_new_privs with successful ruleset
+ * enforcement and skip the manual prctl(PR_SET_NO_NEW_PRIVS, ...) call.
+ */
+ if (!(restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
close(ruleset_fd);
return 1;
@@ -792,6 +813,20 @@ 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.
+no_new_privs flag (ABI < 11)
+----------------------------
+
+Starting with the Landlock ABI version 11, sys_landlock_restrict_self()
+accepts the ``LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS`` flag, which sets the
+no_new_privs attribute of the calling thread only once the enforcement of
+the ruleset succeeded: no_new_privs is set if and only if the call
+succeeds. This removes the need for a prior :manpage:`prctl(2)`
+``PR_SET_NO_NEW_PRIVS`` call, and with it the ``CAP_SYS_ADMIN``
+requirement. When combined with ``LANDLOCK_RESTRICT_SELF_TSYNC``,
+no_new_privs is set on all threads of the process. As explained in the
+tutorial above, not setting no_new_privs is risky even when it is not
+required.
+
.. _kernel_support:
Kernel support
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v3 4/4] samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler
2026-08-03 22:31 [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
` (2 preceding siblings ...)
2026-08-03 22:31 ` [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-03 22:31 ` Justin Suess
3 siblings, 0 replies; 9+ messages in thread
From: Justin Suess @ 2026-08-03 22:31 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to the default flag setting.
Gate the flag on the ABI version, but do not expose any userspace
control over this flag as it has no practical effect on the resulting
sandbox.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- New patch: use LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS by default in the
sandboxer, gated on the ABI version, with a prctl fallback
samples/landlock/sandboxer.c | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index ac71019e6212..030583273f3f 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -369,7 +369,7 @@ static int add_quiet_access(const char *const env_var,
return 0;
}
-#define LANDLOCK_ABI_LAST 10
+#define LANDLOCK_ABI_LAST 11
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -453,8 +453,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
.quiet_scoped = 0,
};
bool quiet_supported = true;
- int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
- int set_restrict_flags = 0;
+ int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
+ LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ int set_restrict_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
if (argc < 2) {
fprintf(stderr, help, argv[0]);
@@ -545,6 +546,12 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
/* Removes quiet flags for ABI < 10 later on. */
quiet_supported = false;
+ __attribute__((fallthrough));
+ case 10:
+ /* Removes LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS for ABI < 11 */
+ supported_restrict_flags &=
+ ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ set_restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
@@ -673,7 +680,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
}
}
- if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
+ if (!(set_restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
goto err_close_ruleset;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 ` [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-07 10:48 ` Mickaël Salaün
0 siblings, 0 replies; 9+ messages in thread
From: Mickaël Salaün @ 2026-08-07 10:48 UTC (permalink / raw)
To: Justin Suess; +Cc: gnoack3000, linux-kernel, linux-security-module
On Mon, Aug 03, 2026 at 06:31:06PM -0400, Justin Suess wrote:
> Check that a successful landlock_restrict_self(2) call with
> LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS sets no_new_privs without a prior
> prctl(2) call nor CAP_SYS_ADMIN, that a failed call from both an
> invalid ruleset and hitting the layer maximum leaves the attribute
> unchanged, and that LANDLOCK_RESTRICT_SELF_TSYNC extends it to sibling
> threads. Also check that this flag requires a ruleset, and update the
> restrict_self_checks_ordering EPERM checks since this flag is now
> checked before the flags validity.
>
> Finally, rename restrict_self_fd_logging_flags to
> restrict_self_fd_flags, and restrict_self_logging_flags to
> restrict_self_flags to indicate that non-logging flags are now tested.
>
> Update the ABI version and last-flag checks accordingly.
>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Notes:
> v2->v3:
> - Run clang-format
> - Add max-layers tests (base_test and tsync_test) checking E2BIG and
> that a failed call leaves no_new_privs unchanged
> - Mention the test renames in the commit message
> - Match comment style of surrounding tests
>
> tools/testing/selftests/landlock/base_test.c | 99 ++++++++++++++++++-
> tools/testing/selftests/landlock/tsync_test.c | 72 ++++++++++++++
> 2 files changed, 166 insertions(+), 5 deletions(-)
>
> diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
> index cbd3c1669951..c8ed165a32ed 100644
> --- a/tools/testing/selftests/landlock/base_test.c
> +++ b/tools/testing/selftests/landlock/base_test.c
> @@ -76,7 +76,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,
> @@ -255,8 +255,15 @@ TEST(restrict_self_checks_ordering)
>
> /* Checks unprivileged enforcement without no_new_privs. */
> drop_caps(_metadata);
> - ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
> + ASSERT_EQ(-1, landlock_restrict_self(
> + -1, ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> ASSERT_EQ(EPERM, errno);
> + /*
> + * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills the no_new_privs /
> + * CAP_SYS_ADMIN requirement, so the invalid flags are checked first.
> + */
> + ASSERT_EQ(-1, landlock_restrict_self(-1, -1));
> + ASSERT_EQ(EINVAL, errno);
> ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
> ASSERT_EQ(EPERM, errno);
> ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0));
> @@ -277,6 +284,41 @@ TEST(restrict_self_checks_ordering)
> ASSERT_EQ(0, close(ruleset_fd));
> }
>
> +TEST(restrict_self_max_layers)
> +{
> + const struct landlock_ruleset_attr ruleset_attr = {
> + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
> + };
> + struct landlock_path_beneath_attr path_beneath_attr = {
> + .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
> + .parent_fd = -1,
> + };
> + const int ruleset_fd =
> + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> + ASSERT_LE(0, ruleset_fd);
> +
> + path_beneath_attr.parent_fd =
> + open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC);
> + ASSERT_LE(0, path_beneath_attr.parent_fd);
> + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
> + &path_beneath_attr, 0));
> + ASSERT_EQ(0, close(path_beneath_attr.parent_fd));
> +
> + /* Enforces the maximum number of allowed layers. */
> + for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
> + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> +
> + /* Enforces one too many rulesets. */
> + drop_caps(_metadata);
> + ASSERT_EQ(-1, landlock_restrict_self(
> + ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + ASSERT_EQ(E2BIG, errno);
> +
> + /* Checks that the failed call did not set no_new_privs. */
> + ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> + ASSERT_EQ(0, close(ruleset_fd));
> +}
> +
> TEST(restrict_self_fd)
> {
> int fd;
> @@ -288,7 +330,7 @@ TEST(restrict_self_fd)
> EXPECT_EQ(EBADFD, errno);
> }
>
> -TEST(restrict_self_fd_logging_flags)
> +TEST(restrict_self_fd_flags)
> {
> int fd;
>
> @@ -302,11 +344,16 @@ TEST(restrict_self_fd_logging_flags)
> EXPECT_EQ(-1, landlock_restrict_self(
> fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
> EXPECT_EQ(EBADFD, errno);
> +
> + /* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
> + EXPECT_EQ(-1, landlock_restrict_self(
> + fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + 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_NO_NEW_PRIVS;
>
> /* Tests invalid flag combinations. */
>
> @@ -349,6 +396,17 @@ TEST(restrict_self_logging_flags)
> LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON));
> EXPECT_EQ(EBADF, errno);
>
> + /* LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS requires a ruleset FD. */
> +
> + EXPECT_EQ(-1, landlock_restrict_self(
> + -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + EXPECT_EQ(EBADF, errno);
> +
> + EXPECT_EQ(-1, landlock_restrict_self(
> + -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
> + LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + EXPECT_EQ(EBADF, errno);
> +
> /* Tests with an invalid ruleset_fd. */
>
> EXPECT_EQ(-1, landlock_restrict_self(
> @@ -359,6 +417,37 @@ TEST(restrict_self_logging_flags)
> -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF));
> }
>
> +TEST(restrict_self_no_new_privs)
> +{
> + const struct landlock_ruleset_attr ruleset_attr = {
> + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
> + };
> + const int ruleset_fd =
> + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
> +
> + ASSERT_LE(0, ruleset_fd);
> +
> + /*
> + * The calling thread does not need CAP_SYS_ADMIN nor an explicit
> + * prctl(2) PR_SET_NO_NEW_PRIVS call.
> + */
> + drop_caps(_metadata);
> + ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + /* Checks that a failed call does not set no_new_privs. */
> + EXPECT_EQ(-1, landlock_restrict_self(
> + -1, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + EXPECT_EQ(EBADF, errno);
> + EXPECT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + /* Checks that a successful call sets no_new_privs. */
> + ASSERT_EQ(0, landlock_restrict_self(
> + ruleset_fd, LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> + EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + EXPECT_EQ(0, close(ruleset_fd));
> +}
> +
> TEST(ruleset_fd_io)
> {
> struct landlock_ruleset_attr ruleset_attr = {
> diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c
> index 9cf1491bbaaf..afa4a8222248 100644
> --- a/tools/testing/selftests/landlock/tsync_test.c
> +++ b/tools/testing/selftests/landlock/tsync_test.c
> @@ -90,6 +90,78 @@ TEST(multi_threaded_success)
> EXPECT_EQ(0, close(ruleset_fd));
> }
>
> +TEST(multi_threaded_no_new_privs)
> +{
> + pthread_t t1, t2;
> + bool no_new_privs1, no_new_privs2;
> + const int ruleset_fd = create_ruleset(_metadata);
> +
> + disable_caps(_metadata);
> +
> + ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
> + ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
> +
> + /* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
> + ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + EXPECT_EQ(0, landlock_restrict_self(
> + ruleset_fd,
> + LANDLOCK_RESTRICT_SELF_TSYNC |
> + LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS));
> +
> + EXPECT_EQ(1, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + ASSERT_EQ(0, pthread_cancel(t1));
> + ASSERT_EQ(0, pthread_cancel(t2));
> + ASSERT_EQ(0, pthread_join(t1, NULL));
> + ASSERT_EQ(0, pthread_join(t2, NULL));
> +
> + /* The no_new_privs flag was enabled on all threads. */
> + EXPECT_TRUE(no_new_privs1);
> + EXPECT_TRUE(no_new_privs2);
> +
> + EXPECT_EQ(0, close(ruleset_fd));
> +}
> +
> +TEST(multi_threaded_no_new_privs_max_layers)
> +{
> + pthread_t t1, t2;
> + bool no_new_privs1, no_new_privs2;
> + const int ruleset_fd = create_ruleset(_metadata);
> +
> + /* Enforces the maximum number of allowed layers. */
> + for (int i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++)
> + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
> +
> + ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1));
> + ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2));
> +
> + disable_caps(_metadata);
> +
> + /* No prior prctl(2) PR_SET_NO_NEW_PRIVS call. */
> + ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + ASSERT_EQ(-1,
> + landlock_restrict_self(ruleset_fd,
> + LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS |
> + LANDLOCK_RESTRICT_SELF_TSYNC));
> + ASSERT_EQ(E2BIG, errno);
> +
> + /* Checks that the failed call did not set no_new_privs. */
> + ASSERT_EQ(0, prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
> +
> + ASSERT_EQ(0, pthread_cancel(t1));
> + ASSERT_EQ(0, pthread_cancel(t2));
> + ASSERT_EQ(0, pthread_join(t1, NULL));
> + ASSERT_EQ(0, pthread_join(t2, NULL));
> +
> + /* The no_new_privs flag was not enabled on any thread. */
> + EXPECT_FALSE(no_new_privs1);
> + EXPECT_FALSE(no_new_privs2);
> +
> + ASSERT_EQ(0, close(ruleset_fd));
> +}
multi_threaded_{success,no_new_privs{,_max_layers} should be test
variants to factor out the code.
> +
> TEST(multi_threaded_success_despite_diverging_domains)
> {
> pthread_t t1, t2;
> --
> 2.54.0
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-07 10:51 ` Mickaël Salaün
2026-08-07 13:18 ` Mickaël Salaün
1 sibling, 0 replies; 9+ messages in thread
From: Mickaël Salaün @ 2026-08-07 10:51 UTC (permalink / raw)
To: Justin Suess; +Cc: gnoack3000, linux-kernel, linux-security-module
This patch must also include the minimal test changes (from the next
patch) to be bisectable (e.g. the ABI version check, the flag
compat/errno). See the commit I tweaked in my next branch.
On Mon, Aug 03, 2026 at 06:31:05PM -0400, Justin Suess wrote:
> Add a landlock_restrict_self(2) flag to set the no_new_privs attribute
> of the calling thread only after enforcement of the ruleset:
> no_new_privs is set if and only if the call succeeds. This removes the
> need for a prior prctl(2) PR_SET_NO_NEW_PRIVS call and guarantees that
> a failed enforcement leaves the attribute unchanged.
>
> Because no_new_privs is set by the call itself, the no_new_privs /
> CAP_SYS_ADMIN requirement of landlock_restrict_self(2) is fulfilled by
> construction, and the related EPERM check is skipped. As a consequence,
> an unprivileged caller passing unknown flags along with this flag gets
> EINVAL instead of EPERM.
>
> Unlike LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, this flag always
> requires a valid ruleset: with a ruleset_fd of -1, such a call would be
> nothing more than a Landlock-flavored prctl(2) PR_SET_NO_NEW_PRIVS, and
> there is no valid use case for setting no_new_privs (possibly with
> LANDLOCK_RESTRICT_SELF_TSYNC) without also enforcing Landlock
> restrictions. Rejecting these calls also keeps the option of giving
> them a meaning later.
>
> The attribute is only set past the last point of failure, just before
> committing the new credentials. When combined with
> LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the sibling threads
> as well, in their commit phase, with the same ordering.
>
> Bump the Landlock ABI version to 11.
>
> Cc: Mickaël Salaün <mic@digikod.net>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Notes:
> v2->v3:
> - Reword "atomically" to the ordering guarantee (no_new_privs is only set
> once enforcement succeeded) in the commit message and both kdocs
> - Explain in the commit message why the flag requires a valid ruleset
>
> include/uapi/linux/landlock.h | 13 +++++++++++++
> security/landlock/limits.h | 2 +-
> security/landlock/syscalls.c | 28 +++++++++++++++++++++-------
> security/landlock/tsync.c | 8 ++++++--
> security/landlock/tsync.h | 4 +++-
> 5 files changed, 44 insertions(+), 11 deletions(-)
>
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 27ae3f39cafb..11bf600698f0 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -191,12 +191,25 @@ struct landlock_ruleset_attr {
> *
> * If the calling thread is running with no_new_privs, this operation
> * enables no_new_privs on the sibling threads as well.
> + *
> + * The following flag ties the no_new_privs attribute to the ruleset
> + * enforcement:
> + *
> + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
> + * Sets the no_new_privs attribute of the calling thread only once the
> + * enforcement of the ruleset succeeded: no_new_privs is set if and only
> + * if sys_landlock_restrict_self() succeeds. This removes the need for a
> + * prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS`` call, and with it the
> + * %CAP_SYS_ADMIN requirement. This flag requires a ruleset. When
> + * combined with %LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the
> + * sibling threads as well.
> */
> /* 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_NO_NEW_PRIVS (1U << 4)
> /* clang-format on */
>
> /**
> diff --git a/security/landlock/limits.h b/security/landlock/limits.h
> index 08d5f2f6d321..1a7c5fb8f6fd 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_NO_NEW_PRIVS
> #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..e97f944109f9 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,21 +502,28 @@ 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_NO_NEW_PRIVS
> *
> * 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_NO_NEW_PRIVS, the no_new_privs attribute of the
> + * calling thread is set only once the enforcement of the ruleset succeeded,
> + * which fulfills the above requirement: no_new_privs is set if and only if the
> + * call succeeds.
> + *
> * Return: 0 on success, or -errno on failure. Possible returned errors are:
> *
> * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
> * - %EINVAL: @flags contains an unknown bit.
> * - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
> * - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
> - * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
> - * current thread is not running with no_new_privs, or it doesn't have
> - * %CAP_SYS_ADMIN in its namespace.
> + * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
> + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
> + * is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
> + * namespace.
> * - %E2BIG: The maximum number of stacked rulesets is reached for the current
> * thread.
> *
> @@ -529,6 +536,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
> struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
> struct cred *new_cred;
> struct landlock_cred_security *new_llcred;
> + const bool set_no_new_privs =
> + !!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS);
> bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
> prev_log_subdomains;
>
> @@ -537,9 +546,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.
> + * returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
> + * requirement.
> */
> - if (!task_no_new_privs(current) &&
> + if (!set_no_new_privs && !task_no_new_privs(current) &&
> !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
> return -EPERM;
>
> @@ -620,12 +630,16 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
>
> if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
> const int err = landlock_restrict_sibling_threads(
> - current_cred(), new_cred);
> + current_cred(), new_cred, flags);
> if (err) {
> abort_creds(new_cred);
> return err;
> }
> }
>
> + /* Sets no_new_privs past the last point of failure. */
> + if (set_no_new_privs)
> + task_set_no_new_privs(current);
> +
> return commit_creds(new_cred);
> }
> diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
> index c5730bbd9ed3..0b71e158c3f5 100644
> --- a/security/landlock/tsync.c
> +++ b/security/landlock/tsync.c
> @@ -17,6 +17,7 @@
> #include <linux/sched/task.h>
> #include <linux/slab.h>
> #include <linux/task_work.h>
> +#include <uapi/linux/landlock.h>
>
> #include "cred.h"
> #include "tsync.h"
> @@ -466,7 +467,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
> * restrict_sibling_threads - enables a Landlock policy for all sibling threads
> */
> int landlock_restrict_sibling_threads(const struct cred *old_cred,
> - const struct cred *new_cred)
> + const struct cred *new_cred,
> + const u32 restrict_flags)
> {
> int err;
> struct tsync_shared_context shared_ctx;
> @@ -481,7 +483,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
> init_completion(&shared_ctx.all_finished);
> shared_ctx.old_cred = old_cred;
> shared_ctx.new_cred = new_cred;
> - shared_ctx.set_no_new_privs = task_no_new_privs(current);
> + shared_ctx.set_no_new_privs =
> + (restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
> + task_no_new_privs(current);
>
> /*
> * Serialize concurrent TSYNC operations to prevent deadlocks when
> diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h
> index ef86bb61c2f6..2ae4f938ca00 100644
> --- a/security/landlock/tsync.h
> +++ b/security/landlock/tsync.h
> @@ -9,8 +9,10 @@
> #define _SECURITY_LANDLOCK_TSYNC_H
>
> #include <linux/cred.h>
> +#include <linux/types.h>
>
> int landlock_restrict_sibling_threads(const struct cred *old_cred,
> - const struct cred *new_cred);
> + const struct cred *new_cred,
> + u32 restrict_flags);
>
> #endif /* _SECURITY_LANDLOCK_TSYNC_H */
> --
> 2.54.0
>
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 ` [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
@ 2026-08-07 10:57 ` Mickaël Salaün
0 siblings, 0 replies; 9+ messages in thread
From: Mickaël Salaün @ 2026-08-07 10:57 UTC (permalink / raw)
To: Justin Suess; +Cc: gnoack3000, linux-kernel, linux-security-module
On Mon, Aug 03, 2026 at 06:31:07PM -0400, Justin Suess wrote:
> Document setting no_new_privs with ruleset enforcement, following the
> same compatibility section style as previous ABI additions.
>
> Include a section explaining the tradeoffs of setting no_new_privs
> through any means for privileged users of Landlock.
>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Notes:
> v2->v3:
> - Update the tutorial: restrict_flags per ABI version and prctl call
> skipped when LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is used
> - Drop "Atomic" from the section title; describe the ordering instead
> - Explain that not setting no_new_privs is risky even when not required
> - Fix ABI 8/9 switch coverage (case 8 ... 10) and indentation
>
> Documentation/userspace-api/landlock.rst | 47 +++++++++++++++++++++---
> 1 file changed, 41 insertions(+), 6 deletions(-)
>
> diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
> index 5085822d8930..0e4a73fd5ea4 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: July 2026
> +:Date: August 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
> @@ -250,7 +250,8 @@ similar backwards compatibility check is needed for the restrict flags
>
> __u32 restrict_flags =
> LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
> - LANDLOCK_RESTRICT_SELF_TSYNC;
> + LANDLOCK_RESTRICT_SELF_TSYNC |
> + LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
> switch (abi) {
> case 1 ... 6:
> /* Removes logging flags for ABI < 7 */
> @@ -269,16 +270,36 @@ similar backwards compatibility check is needed for the restrict flags
> * children (and not for all threads, including parents and siblings).
> */
> restrict_flags &= ~LANDLOCK_RESTRICT_SELF_TSYNC;
> + __attribute__((fallthrough));
> + case 8 ... 10:
> + /* Removes no new privs flag for ABI < 11 */
> + restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
> }
>
> The next step is to restrict the current thread from gaining more privileges
> -(e.g. through a SUID binary). We now have a ruleset with the first rule
> -allowing read and execute access to ``/usr`` while denying all other handled
> -accesses for the filesystem, and two more rules allowing DNS queries.
> +(e.g. through a SUID binary). For unprivileged processes, setting the
> +no_new_privs attribute is required by Landlock.
> +
> +Processes with ``CAP_SYS_ADMIN`` in their namespace can enforce a ruleset
> +without it, but not setting no_new_privs is risky even when it is not
This is a new paragraph and understanding the "it" requires some
stretches.
> +required: sandboxed processes could still execute set-user-ID, set-group-ID
> +or file-capability binaries, which would then run with elevated privileges
> +while being restricted by a Landlock domain they may not expect, making them
> +potential confused deputies. Setting no_new_privs should only be avoided if
> +such a privilege transition is expected.
> +
> +We now have a ruleset with the first rule allowing read and execute access to
> +``/usr`` while denying all other handled accesses for the filesystem, and two
> +more rules allowing DNS queries.
>
> .. code-block:: c
>
> - if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
> + /*
> + * If the ABI > 10, we can tie setting no_new_privs with successful ruleset
> + * enforcement and skip the manual prctl(PR_SET_NO_NEW_PRIVS, ...) call.
> + */
> + if (!(restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
> + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
> perror("Failed to restrict privileges");
> close(ruleset_fd);
> return 1;
> @@ -792,6 +813,20 @@ 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.
>
> +no_new_privs flag (ABI < 11)
> +----------------------------
> +
> +Starting with the Landlock ABI version 11, sys_landlock_restrict_self()
> +accepts the ``LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS`` flag, which sets the
> +no_new_privs attribute of the calling thread only once the enforcement of
> +the ruleset succeeded: no_new_privs is set if and only if the call
> +succeeds. This removes the need for a prior :manpage:`prctl(2)`
> +``PR_SET_NO_NEW_PRIVS`` call, and with it the ``CAP_SYS_ADMIN``
> +requirement.
Reading this looks like CAP_SYS_ADMIN is always a requirement... What
about:
+``PR_SET_NO_NEW_PRIVS`` call (or ``CAP_SYS_ADMIN`` use).
> When combined with ``LANDLOCK_RESTRICT_SELF_TSYNC``,
> +no_new_privs is set on all threads of the process. As explained in the
> +tutorial above, not setting no_new_privs is risky even when it is not
> +required.
> +
> .. _kernel_support:
>
> Kernel support
> --
> 2.54.0
>
>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-07 10:51 ` Mickaël Salaün
@ 2026-08-07 13:18 ` Mickaël Salaün
1 sibling, 0 replies; 9+ messages in thread
From: Mickaël Salaün @ 2026-08-07 13:18 UTC (permalink / raw)
To: Justin Suess; +Cc: gnoack3000, linux-kernel, linux-security-module
On Mon, Aug 03, 2026 at 06:31:05PM -0400, Justin Suess wrote:
> Add a landlock_restrict_self(2) flag to set the no_new_privs attribute
> of the calling thread only after enforcement of the ruleset:
> no_new_privs is set if and only if the call succeeds. This removes the
> need for a prior prctl(2) PR_SET_NO_NEW_PRIVS call and guarantees that
> a failed enforcement leaves the attribute unchanged.
>
> Because no_new_privs is set by the call itself, the no_new_privs /
> CAP_SYS_ADMIN requirement of landlock_restrict_self(2) is fulfilled by
> construction, and the related EPERM check is skipped. As a consequence,
> an unprivileged caller passing unknown flags along with this flag gets
> EINVAL instead of EPERM.
>
> Unlike LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, this flag always
> requires a valid ruleset: with a ruleset_fd of -1, such a call would be
> nothing more than a Landlock-flavored prctl(2) PR_SET_NO_NEW_PRIVS, and
> there is no valid use case for setting no_new_privs (possibly with
> LANDLOCK_RESTRICT_SELF_TSYNC) without also enforcing Landlock
> restrictions. Rejecting these calls also keeps the option of giving
> them a meaning later.
>
> The attribute is only set past the last point of failure, just before
> committing the new credentials. When combined with
> LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the sibling threads
> as well, in their commit phase, with the same ordering.
>
> Bump the Landlock ABI version to 11.
>
> Cc: Mickaël Salaün <mic@digikod.net>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Notes:
> v2->v3:
> - Reword "atomically" to the ordering guarantee (no_new_privs is only set
> once enforcement succeeded) in the commit message and both kdocs
> - Explain in the commit message why the flag requires a valid ruleset
>
> include/uapi/linux/landlock.h | 13 +++++++++++++
> security/landlock/limits.h | 2 +-
> security/landlock/syscalls.c | 28 +++++++++++++++++++++-------
> security/landlock/tsync.c | 8 ++++++--
> security/landlock/tsync.h | 4 +++-
> 5 files changed, 44 insertions(+), 11 deletions(-)
>
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 27ae3f39cafb..11bf600698f0 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -191,12 +191,25 @@ struct landlock_ruleset_attr {
> *
> * If the calling thread is running with no_new_privs, this operation
> * enables no_new_privs on the sibling threads as well.
> + *
> + * The following flag ties the no_new_privs attribute to the ruleset
> + * enforcement:
> + *
> + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
> + * Sets the no_new_privs attribute of the calling thread only once the
> + * enforcement of the ruleset succeeded: no_new_privs is set if and only
> + * if sys_landlock_restrict_self() succeeds. This removes the need for a
> + * prior :manpage:`prctl(2)` ``PR_SET_NO_NEW_PRIVS`` call, and with it the
> + * %CAP_SYS_ADMIN requirement. This flag requires a ruleset. When
> + * combined with %LANDLOCK_RESTRICT_SELF_TSYNC, no_new_privs is set on the
> + * sibling threads as well.
> */
> /* 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_NO_NEW_PRIVS (1U << 4)
> /* clang-format on */
>
> /**
> diff --git a/security/landlock/limits.h b/security/landlock/limits.h
> index 08d5f2f6d321..1a7c5fb8f6fd 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_NO_NEW_PRIVS
> #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..e97f944109f9 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,21 +502,28 @@ 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_NO_NEW_PRIVS
> *
> * 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_NO_NEW_PRIVS, the no_new_privs attribute of the
> + * calling thread is set only once the enforcement of the ruleset succeeded,
> + * which fulfills the above requirement: no_new_privs is set if and only if the
> + * call succeeds.
> + *
> * Return: 0 on success, or -errno on failure. Possible returned errors are:
> *
> * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
> * - %EINVAL: @flags contains an unknown bit.
> * - %EBADF: @ruleset_fd is not a file descriptor for the current thread;
> * - %EBADFD: @ruleset_fd is not a ruleset file descriptor;
> - * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the
> - * current thread is not running with no_new_privs, or it doesn't have
> - * %CAP_SYS_ADMIN in its namespace.
> + * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or
> + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread
> + * is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its
> + * namespace.
> * - %E2BIG: The maximum number of stacked rulesets is reached for the current
> * thread.
> *
> @@ -529,6 +536,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
> struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
> struct cred *new_cred;
> struct landlock_cred_security *new_llcred;
> + const bool set_no_new_privs =
> + !!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS);
This variable should be set later (or not exist at all), see my next
comment.
> bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
> prev_log_subdomains;
>
> @@ -537,9 +546,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.
> + * returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this
> + * requirement.
> */
> - if (!task_no_new_privs(current) &&
> + if (!set_no_new_privs && !task_no_new_privs(current) &&
This is correct according to the current code, but kind of inconsistent
wrt previous kernels (e.g. an unprivileged caller *without* NNP already
set would get EPERM if it sets LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
whereas it will now get EINVAL). In fact, a dedicated patch should move
the NNP/CAP checks just after the flags check. This is a visible change
but I think it would be cleaner this way. BTW, seccomp check flags in
the same order.
> !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
> return -EPERM;
>
> @@ -620,12 +630,16 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
>
> if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
> const int err = landlock_restrict_sibling_threads(
> - current_cred(), new_cred);
> + current_cred(), new_cred, flags);
> if (err) {
> abort_creds(new_cred);
> return err;
> }
> }
>
> + /* Sets no_new_privs past the last point of failure. */
> + if (set_no_new_privs)
> + task_set_no_new_privs(current);
> +
> return commit_creds(new_cred);
> }
> diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
> index c5730bbd9ed3..0b71e158c3f5 100644
> --- a/security/landlock/tsync.c
> +++ b/security/landlock/tsync.c
> @@ -17,6 +17,7 @@
> #include <linux/sched/task.h>
> #include <linux/slab.h>
> #include <linux/task_work.h>
> +#include <uapi/linux/landlock.h>
>
> #include "cred.h"
> #include "tsync.h"
> @@ -466,7 +467,8 @@ static void cancel_tsync_works(const struct tsync_works *works,
> * restrict_sibling_threads - enables a Landlock policy for all sibling threads
> */
> int landlock_restrict_sibling_threads(const struct cred *old_cred,
> - const struct cred *new_cred)
> + const struct cred *new_cred,
> + const u32 restrict_flags)
> {
> int err;
> struct tsync_shared_context shared_ctx;
> @@ -481,7 +483,9 @@ int landlock_restrict_sibling_threads(const struct cred *old_cred,
> init_completion(&shared_ctx.all_finished);
> shared_ctx.old_cred = old_cred;
> shared_ctx.new_cred = new_cred;
> - shared_ctx.set_no_new_privs = task_no_new_privs(current);
> + shared_ctx.set_no_new_privs =
> + (restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) ||
> + task_no_new_privs(current);
>
> /*
> * Serialize concurrent TSYNC operations to prevent deadlocks when
> diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h
> index ef86bb61c2f6..2ae4f938ca00 100644
> --- a/security/landlock/tsync.h
> +++ b/security/landlock/tsync.h
> @@ -9,8 +9,10 @@
> #define _SECURITY_LANDLOCK_TSYNC_H
>
> #include <linux/cred.h>
> +#include <linux/types.h>
>
> int landlock_restrict_sibling_threads(const struct cred *old_cred,
> - const struct cred *new_cred);
> + const struct cred *new_cred,
> + u32 restrict_flags);
>
> #endif /* _SECURITY_LANDLOCK_TSYNC_H */
> --
> 2.54.0
>
>
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-08-07 13:18 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03 22:31 [PATCH v3 0/4] Implement LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-03 22:31 ` [PATCH v3 1/4] landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-07 10:51 ` Mickaël Salaün
2026-08-07 13:18 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 2/4] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-07 10:48 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 3/4] landlock: Document LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS Justin Suess
2026-08-07 10:57 ` Mickaël Salaün
2026-08-03 22:31 ` [PATCH v3 4/4] samples/landlock: Add LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS to sampler Justin Suess
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox