From: Justin Suess <utilityemal77@gmail.com>
To: mic@digikod.net, linux-security-module@vger.kernel.org
Cc: gnoack@google.com, Justin Suess <utilityemal77@gmail.com>
Subject: [PATCH 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
Date: Wed, 8 Jul 2026 09:39:26 -0400 [thread overview]
Message-ID: <20260708133928.852999-3-utilityemal77@gmail.com> (raw)
In-Reply-To: <20260708133928.852999-1-utilityemal77@gmail.com>
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
next prev parent reply other threads:[~2026-07-08 13:39 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
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
2026-07-09 11:22 ` Simon McVittie
2026-07-11 17:00 ` Justin Suess
2026-07-10 13:56 ` Justin Suess
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260708133928.852999-3-utilityemal77@gmail.com \
--to=utilityemal77@gmail.com \
--cc=gnoack@google.com \
--cc=linux-security-module@vger.kernel.org \
--cc=mic@digikod.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.