[PATCH 2/3] selftests/landlock: Test LANDLOCK_RESTRICT_SELF_NNP_ON_EXEC
Justin Suess <[email protected]>
| Newsgroups | org.kernel.vger.linux-security-module |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- 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