From: Justin Suess <utilityemal77@gmail.com>
To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
kpsingh@kernel.org, paul@paul-moore.com, mic@digikod.net,
viro@zeniv.linux.org.uk, brauner@kernel.org, kees@kernel.org
Cc: gnoack@google.com, jack@suse.cz, song@kernel.org,
yonghong.song@linux.dev, martin.lau@linux.dev, m@maowtm.org,
bpf@vger.kernel.org, linux-security-module@vger.kernel.org,
linux-kernel@vger.kernel.org,
Justin Suess <utilityemal77@gmail.com>
Subject: [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
Date: Mon, 31 Aug 2026 10:58:56 -0400 [thread overview]
Message-ID: <20260831145858.3869191-15-utilityemal77@gmail.com> (raw)
In-Reply-To: <20260831145858.3869191-1-utilityemal77@gmail.com>
Exercise the policy object kfuncs against an LSM actually providing
policy objects, complementing the LSM-independent tests of the
verifier-side and from_fd contracts.
The programs mirror the intended usage: a syscall program acquires a
Landlock ruleset with bpf_lsm_policy_from_fd() and parks it in a map
kptr field; an LSM program on bprm_creds_for_exec() loads the field
under bpf_rcu_read_lock(), takes its own reference with
bpf_lsm_policy_acquire(), and applies the ruleset with
bpf_lsm_policy_apply_bprm(). The prog_tests runner checks that:
- a monitored execution starts confined (a handled-but-not-allowed
write fails) while an unmonitored one is untouched,
- two concurrent monitored executions are both restricted from the
one shared map slot,
- the landlock_restrict_self(2) log flags are accepted while
LANDLOCK_RESTRICT_SELF_TSYNC is rejected with -EINVAL and leaves
the execution unrestricted,
- LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS makes the executed program
start with no_new_privs set, while an execution without it stays
non-nnp,
- a second apply call on the same execution replaces the staged
restriction rather than failing,
- an execution failing past the bprm hook (ENOEXEC) discards the
staged restriction and leaves the caller unconfined,
- the object's identity is BTF-readable off the trusted kptr: the
LSM-private type tag is nonzero, and the lsmid is LSM_ID_LANDLOCK
with the fd alone having routed the translation,
- the bprm application emits a single landlock_enforce_domain event,
observed by a tp_btf program: complete == 1, process_wide == 1, and
no_new_privs reporting the post-flag state.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
tools/testing/selftests/bpf/config | 1 +
tools/testing/selftests/bpf/config.x86_64 | 2 +-
.../bpf/prog_tests/lsm_policy_landlock.c | 522 ++++++++++++++++++
.../selftests/bpf/progs/lsm_policy_landlock.c | 142 +++++
4 files changed, 666 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..2fa734497461 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -120,6 +120,7 @@ CONFIG_SAMPLES=y
CONFIG_SAMPLE_LIVEPATCH=m
CONFIG_SECURITY=y
CONFIG_SECURITYFS=y
+CONFIG_SECURITY_LANDLOCK=y
CONFIG_SYN_COOKIES=y
CONFIG_TEST_BPF=m
CONFIG_UDMABUF=y
diff --git a/tools/testing/selftests/bpf/config.x86_64 b/tools/testing/selftests/bpf/config.x86_64
index 42ad817b00ae..13ca4906b67f 100644
--- a/tools/testing/selftests/bpf/config.x86_64
+++ b/tools/testing/selftests/bpf/config.x86_64
@@ -126,7 +126,7 @@ CONFIG_LEGACY_VSYSCALL_NONE=y
CONFIG_LOG_BUF_SHIFT=21
CONFIG_LOG_CPU_MAX_BUF_SHIFT=0
CONFIG_LOGO=y
-CONFIG_LSM="selinux,bpf,integrity"
+CONFIG_LSM="landlock,selinux,bpf,integrity"
CONFIG_MAC_PARTITION=y
CONFIG_MAGIC_SYSRQ=y
CONFIG_MCORE2=y
diff --git a/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
new file mode 100644
index 000000000000..9270b39f5e3a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
@@ -0,0 +1,522 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <test_progs.h>
+#include <errno.h>
+#include <linux/landlock.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "lsm_policy_landlock.skel.h"
+
+/* Fallbacks for old system headers. */
+#ifndef LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
+#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1)
+#endif
+#ifndef LANDLOCK_RESTRICT_SELF_TSYNC
+#define LANDLOCK_RESTRICT_SELF_TSYNC (1U << 3)
+#endif
+#ifndef LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
+#define LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS (1U << 4)
+#endif
+#ifndef LSM_ID_LANDLOCK
+#define LSM_ID_LANDLOCK 110 /* uapi/linux/lsm.h */
+#endif
+
+struct policy_test_env {
+ struct lsm_policy_landlock *skel;
+ char tmp_path[64];
+ int tmp_fd;
+ int ruleset_fd;
+};
+
+static int create_ruleset(void)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ };
+
+ return syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0);
+}
+
+static void reset_prog_state(struct lsm_policy_landlock *skel)
+{
+ skel->bss->called = false;
+ skel->bss->no_policy = false;
+ skel->bss->restrict_err = -1;
+ skel->bss->restrict2_err = -1;
+ skel->bss->restrict_ok_count = 0;
+ skel->bss->kfunc_flags = 0;
+ skel->bss->double_call = false;
+ skel->bss->monitored_pid = 0;
+ skel->bss->monitored_pid2 = 0;
+ skel->bss->enforce_domain_id = 0;
+ skel->bss->enforce_count = 0;
+ skel->bss->enforce_complete = false;
+ skel->bss->enforce_process_wide = false;
+ skel->bss->enforce_no_new_privs = false;
+}
+
+/*
+ * Runs the syscall program that acquires the ruleset from
+ * @ruleset_fd, in the runner's fd table, and parks it in the map kptr
+ * slot for the LSM program.
+ */
+static int load_ruleset_into_map(struct lsm_policy_landlock *skel)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int err;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.load_policy),
+ &opts);
+ if (!ASSERT_OK(err, "load_policy_run"))
+ return -1;
+ if (!ASSERT_OK(opts.retval, "load_policy_retval"))
+ return -1;
+ /* Landlock's type tag, read off the trusted kptr, is never 0. */
+ ASSERT_NEQ(skel->bss->policy_type, 0, "policy_type_nonzero");
+ /* The fd, not a kfunc argument, routed the call to Landlock. */
+ ASSERT_EQ(skel->bss->policy_lsmid, LSM_ID_LANDLOCK, "policy_lsmid");
+ return 0;
+}
+
+/*
+ * Creates the target tmp file and the ruleset, loads and attaches the
+ * skeleton, and parks the ruleset in the map. Returns 0 on success;
+ * on failure (or skip), the caller must still run teardown_env().
+ */
+static int setup_env(struct policy_test_env *env)
+{
+ env->skel = NULL;
+ env->ruleset_fd = -1;
+ strcpy(env->tmp_path, "/tmp/lsm_policy_landlock_XXXXXX");
+ env->tmp_fd = mkstemp(env->tmp_path);
+ if (!ASSERT_GE(env->tmp_fd, 0, "mkstemp"))
+ return -1;
+
+ env->ruleset_fd = create_ruleset();
+ if (env->ruleset_fd < 0) {
+ if (errno == EOPNOTSUPP || errno == ENOSYS)
+ test__skip();
+ else
+ ASSERT_GE(env->ruleset_fd, 0,
+ "landlock_create_ruleset");
+ return -1;
+ }
+
+ env->skel = lsm_policy_landlock__open_and_load();
+ if (!ASSERT_OK_PTR(env->skel, "skel_open_and_load"))
+ return -1;
+ env->skel->bss->ruleset_fd = env->ruleset_fd;
+ reset_prog_state(env->skel);
+
+ if (!ASSERT_OK(lsm_policy_landlock__attach(env->skel),
+ "skel_attach"))
+ return -1;
+
+ return load_ruleset_into_map(env->skel);
+}
+
+static void teardown_env(struct policy_test_env *env)
+{
+ lsm_policy_landlock__destroy(env->skel);
+ if (env->ruleset_fd >= 0)
+ close(env->ruleset_fd);
+ if (env->tmp_fd >= 0)
+ close(env->tmp_fd);
+ unlink(env->tmp_path);
+}
+
+/*
+ * Forks a child that blocks on a pipe, then execs @path with @argv.
+ * Returns the child's pid, or -1 on error. @release_fd receives the
+ * pipe's write end: release_exec_child() lets the child exec, after
+ * its pid has been published to the BPF program.
+ */
+static pid_t spawn_exec_child(const char *path, char *const argv[],
+ int *release_fd)
+{
+ int pipe_fds[2];
+ char buf = 0;
+ pid_t pid;
+
+ if (!ASSERT_OK(pipe(pipe_fds), "pipe"))
+ return -1;
+
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork")) {
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ return -1;
+ }
+ if (pid == 0) {
+ close(pipe_fds[1]);
+ read(pipe_fds[0], &buf, 1);
+ close(pipe_fds[0]);
+ execv(path, argv);
+ exit(127);
+ }
+ close(pipe_fds[0]);
+ *release_fd = pipe_fds[1];
+ return pid;
+}
+
+static void release_exec_child(int release_fd)
+{
+ char buf = 0;
+
+ write(release_fd, &buf, 1);
+ close(release_fd);
+}
+
+/* Returns the child's exit status, or -1 on error. */
+static int wait_exec_child(pid_t pid)
+{
+ int status;
+
+ if (!ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid"))
+ return -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+static int run_exec_child(struct lsm_policy_landlock *skel,
+ bool monitored, const char *shell_cmd)
+{
+ char *argv[] = { "sh", "-c", (char *)shell_cmd, NULL };
+ int release_fd;
+ pid_t pid;
+
+ pid = spawn_exec_child("/bin/sh", argv, &release_fd);
+ if (pid < 0)
+ return -1;
+ skel->bss->monitored_pid = monitored ? pid : 0;
+ release_exec_child(release_fd);
+ return wait_exec_child(pid);
+}
+
+/*
+ * Exit codes: 4 = unexpected write outcome, 0 = everything as
+ * expected.
+ */
+static void format_child_cmd(char *cmd, size_t len, bool expect_write_ok,
+ const char *tmp_path)
+{
+ if (expect_write_ok)
+ snprintf(cmd, len, "echo x > %s || exit 4; exit 0", tmp_path);
+ else
+ snprintf(cmd, len,
+ "if echo x > %s 2>/dev/null; then exit 4; fi; exit 0",
+ tmp_path);
+}
+
+static void test_restrict_binprm(void)
+{
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[256];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ /* Control: an unmonitored execution may write to the tmp file. */
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), true, env.tmp_path);
+ ret = run_exec_child(skel, false, cmd);
+ if (!ASSERT_EQ(ret, 0, "control_child_exit"))
+ goto out;
+ ASSERT_FALSE(skel->bss->called, "control_not_monitored");
+
+ /*
+ * A monitored execution starts landlocked: the ruleset handles
+ * LANDLOCK_ACCESS_FS_WRITE_FILE without any rule, so the write
+ * must fail.
+ */
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "restricted_child_exit"))
+ goto out;
+ ASSERT_TRUE(skel->bss->called, "lsm_prog_called");
+ ASSERT_FALSE(skel->bss->no_policy, "ruleset_in_map");
+ ASSERT_EQ(skel->bss->restrict_err, 0, "restrict_binprm");
+
+ /* The audit log flags of landlock_restrict_self(2) apply too. */
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "log_flags_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "log_flags_restrict_binprm");
+
+ /*
+ * LANDLOCK_RESTRICT_SELF_TSYNC targets the calling threads, not
+ * an execution: the kfunc must reject it and the execution must
+ * stay unrestricted.
+ */
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_TSYNC;
+ format_child_cmd(cmd, sizeof(cmd), true, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "tsync_child_exit"))
+ goto out;
+ ASSERT_TRUE(skel->bss->called, "tsync_prog_called");
+ ASSERT_EQ(skel->bss->restrict_err, -EINVAL, "tsync_rejected");
+
+ /*
+ * A second call on the same execution replaces the staged
+ * domain (and releases the first one): the result is a single
+ * restriction, not an error.
+ */
+ reset_prog_state(skel);
+ skel->bss->double_call = true;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "double_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "double_restrict_first");
+ ASSERT_EQ(skel->bss->restrict2_err, 0, "double_restrict_second");
+out:
+ teardown_env(&env);
+}
+
+/*
+ * Two monitored executions, released together, must both be
+ * restricted from the one shared map kptr slot.
+ */
+static void test_restrict_binprm_concurrent(void)
+{
+ struct policy_test_env env;
+ char *argv[4];
+ int release_fds[2] = { -1, -1 };
+ pid_t pids[2] = { -1, -1 };
+ char cmd[256];
+ int i;
+
+ if (setup_env(&env))
+ goto out;
+
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ argv[0] = "sh";
+ argv[1] = "-c";
+ argv[2] = cmd;
+ argv[3] = NULL;
+
+ for (i = 0; i < 2; i++) {
+ pids[i] = spawn_exec_child("/bin/sh", argv, &release_fds[i]);
+ if (pids[i] < 0)
+ goto out_kill;
+ }
+
+ env.skel->bss->monitored_pid = pids[0];
+ env.skel->bss->monitored_pid2 = pids[1];
+
+ /* Releases both children only once both pids are published. */
+ for (i = 0; i < 2; i++) {
+ release_exec_child(release_fds[i]);
+ release_fds[i] = -1;
+ }
+
+ for (i = 0; i < 2; i++) {
+ ASSERT_EQ(wait_exec_child(pids[i]), 0,
+ "concurrent_child_exit");
+ pids[i] = -1;
+ }
+
+ ASSERT_FALSE(env.skel->bss->no_policy, "ruleset_in_map");
+ ASSERT_EQ(env.skel->bss->restrict_ok_count, 2,
+ "both_execs_restricted");
+
+out_kill:
+ for (i = 0; i < 2; i++) {
+ if (pids[i] > 0) {
+ kill(pids[i], SIGKILL);
+ waitpid(pids[i], NULL, 0);
+ }
+ if (release_fds[i] >= 0)
+ close(release_fds[i]);
+ }
+out:
+ teardown_env(&env);
+}
+
+/*
+ * Checks that a staged restriction is discarded, and the staged
+ * domain released, when the execution fails after the bprm hook: the
+ * calling task must not end up landlocked.
+ */
+static void test_restrict_binprm_discard(void)
+{
+ struct policy_test_env env;
+ char garbage_path[] = "/tmp/lsm_policy_garbage_XXXXXX";
+ int garbage_fd, pipe_fds[2];
+ char buf = 0;
+ pid_t pid;
+
+ if (setup_env(&env))
+ goto out;
+
+ /*
+ * An executable file that no binfmt handler accepts: the exec
+ * fails with ENOEXEC after bprm_creds_for_exec() has run.
+ */
+ garbage_fd = mkstemp(garbage_path);
+ if (!ASSERT_GE(garbage_fd, 0, "mkstemp_garbage"))
+ goto out;
+ if (!ASSERT_EQ(write(garbage_fd, "junk\n", 5), 5, "write_garbage") ||
+ !ASSERT_OK(fchmod(garbage_fd, 0700), "chmod_garbage")) {
+ close(garbage_fd);
+ goto out_unlink;
+ }
+ close(garbage_fd);
+
+ if (!ASSERT_OK(pipe(pipe_fds), "pipe"))
+ goto out_unlink;
+
+ /*
+ * Cannot use spawn_exec_child(): the same process must test its
+ * write access after the failed exec.
+ */
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork"))
+ goto out_unlink;
+ if (pid == 0) {
+ char *argv[] = { "garbage", NULL };
+ int fd;
+
+ close(pipe_fds[1]);
+ read(pipe_fds[0], &buf, 1);
+ close(pipe_fds[0]);
+ execv(garbage_path, argv);
+ /*
+ * The failed execution must leave no trace: no
+ * Landlock domain, i.e. writing must still work
+ * (exit 6).
+ */
+ fd = open(env.tmp_path, O_WRONLY | O_TRUNC);
+ if (fd < 0)
+ exit(6);
+ close(fd);
+ exit(0);
+ }
+ close(pipe_fds[0]);
+ env.skel->bss->monitored_pid = pid;
+ release_exec_child(pipe_fds[1]);
+
+ ASSERT_EQ(wait_exec_child(pid), 0, "discard_child_exit");
+ ASSERT_TRUE(env.skel->bss->called, "lsm_prog_called");
+ ASSERT_EQ(env.skel->bss->restrict_err, 0, "restrict_binprm");
+out_unlink:
+ unlink(garbage_path);
+out:
+ teardown_env(&env);
+}
+
+/*
+ * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS makes the executed program start
+ * with no_new_privs set; without it, a non-nnp parent's execution
+ * stays non-nnp. Child exit code 5: unexpected NoNewPrivs value.
+ */
+static void test_restrict_binprm_nnp(void)
+{
+ static const char nnp_cmd[] =
+ "grep -q '^NoNewPrivs:[[:space:]]*%d' /proc/self/status || exit 5";
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[sizeof(nnp_cmd)];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ if (!ASSERT_OK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0),
+ "runner_not_nnp"))
+ goto out;
+
+ reset_prog_state(skel);
+ snprintf(cmd, sizeof(cmd), nnp_cmd, 0);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "no_flag_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "no_flag_restrict_binprm");
+
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ snprintf(cmd, sizeof(cmd), nnp_cmd, 1);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "nnp_flag_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "nnp_flag_restrict_binprm");
+out:
+ teardown_env(&env);
+}
+
+/*
+ * The bprm application emits landlock_enforce_domain, observed here by
+ * a tp_btf program: the single event concludes the operation
+ * (complete == 1), covers the whole post-de_thread() process
+ * (process_wide == 1), and reports the post-flag no_new_privs state.
+ */
+static void test_restrict_binprm_trace(void)
+{
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[256];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ if (!ASSERT_OK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0),
+ "runner_not_nnp"))
+ goto out;
+
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "trace_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "restrict_binprm");
+ ASSERT_EQ(skel->bss->enforce_count, 1, "one_enforce_event");
+ ASSERT_TRUE(skel->bss->enforce_complete, "enforce_complete");
+ ASSERT_TRUE(skel->bss->enforce_process_wide, "enforce_process_wide");
+ ASSERT_NEQ(skel->bss->enforce_domain_id, 0, "enforce_domain_id");
+ ASSERT_FALSE(skel->bss->enforce_no_new_privs, "enforce_nnp_off");
+
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "trace_nnp_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->enforce_count, 1, "one_enforce_event_nnp");
+ ASSERT_TRUE(skel->bss->enforce_no_new_privs, "enforce_nnp_on");
+out:
+ teardown_env(&env);
+}
+
+void test_lsm_policy_landlock(void)
+{
+ if (test__start_subtest("restrict_binprm"))
+ test_restrict_binprm();
+ if (test__start_subtest("restrict_binprm_concurrent"))
+ test_restrict_binprm_concurrent();
+ if (test__start_subtest("restrict_binprm_discard"))
+ test_restrict_binprm_discard();
+ if (test__start_subtest("restrict_binprm_nnp"))
+ test_restrict_binprm_nnp();
+ if (test__start_subtest("restrict_binprm_trace"))
+ test_restrict_binprm_trace();
+}
diff --git a/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c b/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
new file mode 100644
index 000000000000..231b24b87dd4
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+extern struct lsm_policy_object *
+bpf_lsm_policy_acquire(struct lsm_policy_object *object) __ksym;
+extern int bpf_lsm_policy_apply_bprm(struct lsm_policy_object *object,
+ struct linux_binprm *bprm,
+ u32 flags) __ksym;
+extern struct lsm_policy_object *
+bpf_lsm_policy_from_fd(int fd, u32 flags) __ksym;
+extern void bpf_lsm_policy_release(struct lsm_policy_object *object) __ksym;
+void bpf_rcu_read_lock(void) __ksym;
+void bpf_rcu_read_unlock(void) __ksym;
+
+struct policy_slot {
+ struct lsm_policy_object __kptr *object;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, int);
+ __type(value, struct policy_slot);
+} policy_map SEC(".maps");
+
+int monitored_pid;
+int monitored_pid2;
+int ruleset_fd;
+u32 kfunc_flags;
+bool double_call;
+u32 policy_type;
+u64 policy_lsmid;
+bool no_policy;
+int restrict_err;
+int restrict2_err;
+int restrict_ok_count;
+bool called;
+u64 enforce_domain_id;
+int enforce_count;
+bool enforce_complete;
+bool enforce_process_wide;
+bool enforce_no_new_privs;
+
+/*
+ * Runs in the test runner's context through BPF_PROG_RUN, where
+ * @ruleset_fd is meaningful.
+ */
+SEC("syscall")
+int load_policy(void *ctx)
+{
+ struct lsm_policy_object *object, *old;
+ struct policy_slot *slot;
+ int key = 0;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 1;
+
+ object = bpf_lsm_policy_from_fd(ruleset_fd, 0);
+ if (!object)
+ return 2;
+
+ /*
+ * The object's identity is BTF-readable off the trusted kptr: a
+ * program that expects a policy of one specific LSM can check
+ * the lsmid the fd resolved to.
+ */
+ policy_type = object->type;
+ policy_lsmid = object->lsmid;
+
+ old = bpf_kptr_xchg(&slot->object, object);
+ if (old)
+ bpf_lsm_policy_release(old);
+ return 0;
+}
+
+SEC("lsm.s/bprm_creds_for_exec")
+int BPF_PROG(restrict_exec, struct linux_binprm *bprm)
+{
+ struct lsm_policy_object *object;
+ struct policy_slot *slot;
+ int pid = bpf_get_current_pid_tgid() >> 32;
+ int key = 0;
+
+ if (pid != monitored_pid && pid != monitored_pid2)
+ return 0;
+
+ called = true;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 0;
+
+ /*
+ * RCU load + acquire instead of bpf_kptr_xchg(): the slot is
+ * never emptied, so concurrent executions can share it.
+ */
+ bpf_rcu_read_lock();
+ object = slot->object;
+ if (object)
+ object = bpf_lsm_policy_acquire(object);
+ bpf_rcu_read_unlock();
+
+ if (!object) {
+ no_policy = true;
+ return 0;
+ }
+
+ restrict_err = bpf_lsm_policy_apply_bprm(object, bprm, kfunc_flags);
+ if (!restrict_err)
+ __sync_fetch_and_add(&restrict_ok_count, 1);
+ if (double_call)
+ /* Replaces the domain staged by the first call. */
+ restrict2_err = bpf_lsm_policy_apply_bprm(object, bprm,
+ kfunc_flags);
+
+ bpf_lsm_policy_release(object);
+ return 0;
+}
+
+SEC("tp_btf/landlock_enforce_domain")
+int BPF_PROG(on_enforce_domain, struct landlock_domain *domain, bool complete,
+ bool process_wide, bool no_new_privs)
+{
+ int pid = bpf_get_current_pid_tgid() >> 32;
+
+ if (pid != monitored_pid && pid != monitored_pid2)
+ return 0;
+
+ __sync_fetch_and_add(&enforce_count, 1);
+ enforce_domain_id = domain->hierarchy->id;
+ enforce_complete = complete;
+ enforce_process_wide = process_wide;
+ enforce_no_new_privs = no_new_privs;
+ return 0;
+}
--
2.55.0
next prev parent reply other threads:[~2026-08-31 15:00 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-08-31 14:58 ` [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-08-31 17:17 ` Casey Schaufler
2026-08-31 17:41 ` Justin Suess
2026-09-02 13:05 ` Justin Suess
2026-09-02 17:51 ` Casey Schaufler
2026-09-02 18:28 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
2026-08-31 14:58 ` [PATCH v2 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
2026-08-31 14:58 ` [PATCH v2 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
2026-08-31 14:58 ` [PATCH v2 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
2026-08-31 14:58 ` [PATCH v2 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
2026-08-31 14:58 ` [PATCH v2 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
2026-08-31 14:58 ` [PATCH v2 08/15] lsm: Document the LSM policy object interface Justin Suess
2026-08-31 14:58 ` [PATCH v2 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
2026-08-31 14:58 ` [PATCH v2 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
2026-08-31 14:58 ` [PATCH v2 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
2026-08-31 14:58 ` [PATCH v2 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
2026-08-31 14:58 ` [PATCH v2 13/15] landlock: Implement the LSM policy object hooks Justin Suess
2026-08-31 14:58 ` Justin Suess [this message]
2026-08-31 19:53 ` [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock sashiko-bot
2026-09-02 12:24 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 15/15] landlock: Document the BPF policy interface 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=20260831145858.3869191-15-utilityemal77@gmail.com \
--to=utilityemal77@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=brauner@kernel.org \
--cc=daniel@iogearbox.net \
--cc=gnoack@google.com \
--cc=jack@suse.cz \
--cc=kees@kernel.org \
--cc=kpsingh@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=m@maowtm.org \
--cc=martin.lau@linux.dev \
--cc=mic@digikod.net \
--cc=paul@paul-moore.com \
--cc=song@kernel.org \
--cc=viro@zeniv.linux.org.uk \
--cc=yonghong.song@linux.dev \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox