BPF List
 help / color / mirror / Atom feed
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


  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