All of lore.kernel.org
 help / color / mirror / Atom feed
From: Justin Suess <utilityemal77@gmail.com>
To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
	kpsingh@kernel.org, matt@bobrowski.net, paul@paul-moore.com,
	mic@digikod.net, viro@zeniv.linux.org.uk, brauner@kernel.org,
	kees@kernel.org
Cc: casey@schaufler-ca.com, gnoack@google.com, jack@suse.cz,
	song@kernel.org, yonghong.song@linux.dev, martin.lau@linux.dev,
	eddyz87@gmail.com, memxor@gmail.com, jolsa@kernel.org,
	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 bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
Date: Wed,  9 Sep 2026 15:37:17 -0400	[thread overview]
Message-ID: <20260909193719.518517-15-utilityemal77@gmail.com> (raw)
In-Reply-To: <20260909193719.518517-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>
---

Notes:
    v2->v3:
        - Fix a pipe fd leak on the fork() error path of
          test_restrict_binprm_discard().

 tools/testing/selftests/bpf/config            |   1 +
 tools/testing/selftests/bpf/config.x86_64     |   2 +-
 .../bpf/prog_tests/lsm_policy_landlock.c      | 525 ++++++++++++++++++
 .../selftests/bpf/progs/lsm_policy_landlock.c | 142 +++++
 4 files changed, 669 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 2f79688dcf7c..42e20d245f90 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -122,6 +122,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 523e0d29bbd4..2c4d857f69f5 100644
--- a/tools/testing/selftests/bpf/config.x86_64
+++ b/tools/testing/selftests/bpf/config.x86_64
@@ -125,7 +125,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..f01ae5be8aef
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
@@ -0,0 +1,525 @@
+// 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")) {
+		close(pipe_fds[0]);
+		close(pipe_fds[1]);
+		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


  parent reply	other threads:[~2026-09-09 19:38 UTC|newest]

Thread overview: 29+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
2026-09-09 20:29   ` bot+bpf-ci
2026-09-09 21:34   ` Paul Moore
2026-09-09 22:20     ` Justin Suess
2026-09-09 23:08       ` Paul Moore
2026-09-09 19:37 ` [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
2026-09-09 20:46   ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
2026-09-09 20:30   ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
2026-09-09 19:55   ` sashiko-bot
2026-09-09 20:20     ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 08/15] lsm: Document the LSM policy object interface Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
2026-09-09 20:30   ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
2026-09-09 20:29   ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
2026-09-09 20:46   ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks Justin Suess
2026-09-09 20:46   ` bot+bpf-ci
2026-09-09 19:37 ` Justin Suess [this message]
2026-09-09 20:46   ` [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 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=20260909193719.518517-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=casey@schaufler-ca.com \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=gnoack@google.com \
    --cc=jack@suse.cz \
    --cc=jolsa@kernel.org \
    --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=matt@bobrowski.net \
    --cc=memxor@gmail.com \
    --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 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.