diff mbox series

[pseudo,1/6] test-landlock: Add a Landlock sandbox test

Message ID 1794500398-31706-2-git-send-email-mark.hatle@kernel.crashing.org
State New
Headers show
Series Add landlock support, fix seccomp | expand

Commit Message

Mark Hatle Nov. 12, 2026, 4:19 p.m. UTC
From: Mark Hatle <mark.hatle@amd.com>

[YOCTO #16493]

AI-Generated: Uses GitHub Copilot

Reported-by: Richard Tollerton <rich.tollerton@ni.com>
Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
 test/test-landlock.c  | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++
 test/test-landlock.sh |  24 +++++++++
 2 files changed, 170 insertions(+)
 create mode 100644 test/test-landlock.c
 create mode 100755 test/test-landlock.sh
diff mbox series

Patch

diff --git a/test/test-landlock.c b/test/test-landlock.c
new file mode 100644
index 0000000..e9bce88
--- /dev/null
+++ b/test/test-landlock.c
@@ -0,0 +1,146 @@ 
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * xz (5.6 and later) sandboxes itself with Landlock when it works on a
+ * pipe, revoking every filesystem access right before it does any work.
+ * libpseudo only connects to the server on the first operation it has to
+ * report, and connecting needs filesystem access (open(".") and fchdir()
+ * to the state directory, the pidfile, spawning a server), so under pseudo
+ * the first operation after the sandbox goes up fails. Do what xz does,
+ * then fstat() a descriptor that was opened beforehand and check that
+ * pseudo still reports the ownership it recorded for it.
+ *
+ * If libpseudo refuses Landlock (as it does seccomp), carry on without the
+ * sandbox the way xz does; only the result of the fstat() matters.
+ */
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include <linux/types.h>
+
+/* matches test-openat2-syscall: the shell wrapper turns 77 into "skipped" */
+#define SKIP 77
+
+/* <linux/landlock.h> and the syscall numbers are missing on hosts with
+ * pre-5.13 kernel headers.  The numbers are the same on every architecture.
+ */
+#ifndef __NR_landlock_create_ruleset
+#define __NR_landlock_create_ruleset 444
+#endif
+#ifndef __NR_landlock_restrict_self
+#define __NR_landlock_restrict_self 446
+#endif
+
+struct test_landlock_ruleset_attr {
+	__u64 handled_access_fs;
+};
+
+#define TEST_LANDLOCK_CREATE_RULESET_VERSION	(1U << 0)
+
+/* ABI 1: 13 rights, then REFER (2), TRUNCATE (3), IOCTL_DEV (5) */
+#define TEST_LANDLOCK_ACCESS_FS_ABI1		((1ULL << 13) - 1)
+#define TEST_LANDLOCK_ACCESS_FS_REFER		(1ULL << 13)
+#define TEST_LANDLOCK_ACCESS_FS_TRUNCATE	(1ULL << 14)
+#define TEST_LANDLOCK_ACCESS_FS_IOCTL_DEV	(1ULL << 15)
+
+static int landlock_abi(void) {
+	return (int) syscall(__NR_landlock_create_ruleset, NULL, 0,
+		TEST_LANDLOCK_CREATE_RULESET_VERSION);
+}
+
+/* Is Landlock there at all? The wrapper runs this with PSEUDO_DISABLED
+ * set, so the answer is the kernel's.
+ */
+static int probe(void) {
+	if (landlock_abi() < 1) {
+		if (errno == ENOSYS || errno == EOPNOTSUPP)
+			return SKIP;
+		perror("landlock_create_ruleset(VERSION)");
+		return 1;
+	}
+	return 0;
+}
+
+/* Revoke every filesystem access right the kernel knows about, as xz's
+ * sandbox_enable_strict_if_allowed() does. Returns 0 if the sandbox is up,
+ * 1 if Landlock is not available, -1 on error.
+ */
+static int sandbox(void) {
+	struct test_landlock_ruleset_attr attr = { 0 };
+	int abi, fd;
+
+	abi = landlock_abi();
+	if (abi < 1) {
+		if (errno == ENOSYS || errno == EOPNOTSUPP)
+			return 1;
+		perror("landlock_create_ruleset(VERSION)");
+		return -1;
+	}
+	attr.handled_access_fs = TEST_LANDLOCK_ACCESS_FS_ABI1;
+	if (abi >= 2)
+		attr.handled_access_fs |= TEST_LANDLOCK_ACCESS_FS_REFER;
+	if (abi >= 3)
+		attr.handled_access_fs |= TEST_LANDLOCK_ACCESS_FS_TRUNCATE;
+	if (abi >= 5)
+		attr.handled_access_fs |= TEST_LANDLOCK_ACCESS_FS_IOCTL_DEV;
+
+	fd = (int) syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0);
+	if (fd == -1) {
+		perror("landlock_create_ruleset");
+		return -1;
+	}
+	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
+		perror("prctl(PR_SET_NO_NEW_PRIVS)");
+		close(fd);
+		return -1;
+	}
+	if (syscall(__NR_landlock_restrict_self, fd, 0) == -1) {
+		perror("landlock_restrict_self");
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+int main(int argc, char **argv) {
+	struct stat st;
+	uid_t uid;
+	gid_t gid;
+	int rc;
+
+	if (argc > 1 && !strcmp(argv[1], "--probe"))
+		return probe();
+	if (argc != 3) {
+		fprintf(stderr, "usage: %s --probe | uid gid < file\n", argv[0]);
+		return 1;
+	}
+	uid = (uid_t) strtoul(argv[1], NULL, 10);
+	gid = (gid_t) strtoul(argv[2], NULL, 10);
+
+	/* nothing above this point has needed the pseudo server */
+	rc = sandbox();
+	if (rc < 0)
+		return 1;
+	if (rc > 0)
+		fprintf(stderr, "Landlock unavailable, running unsandboxed\n");
+
+	if (fstat(0, &st) == -1) {
+		perror("fstat(stdin)");
+		return 1;
+	}
+	if (st.st_uid != uid || st.st_gid != gid) {
+		fprintf(stderr, "fstat(stdin): owner %d:%d, expected %d:%d\n",
+			(int) st.st_uid, (int) st.st_gid, (int) uid, (int) gid);
+		return 1;
+	}
+	return 0;
+}
diff --git a/test/test-landlock.sh b/test/test-landlock.sh
new file mode 100755
index 0000000..1514236
--- /dev/null
+++ b/test/test-landlock.sh
@@ -0,0 +1,24 @@ 
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+# Landlock needs a 5.13 kernel built with it enabled. Ask with pseudo out of
+# the way, so that pseudo refusing Landlock is not mistaken for the kernel
+# lacking it.
+PSEUDO_DISABLED=1 ./test/test-landlock --probe
+case $? in
+0)  ;;
+77) exit 255 ;;
+*)  exit 1 ;;
+esac
+
+file=test-landlock-file
+trap "rm -f $file" 0
+
+echo foo > $file || exit 1
+chown 1234:5678 $file || exit 1
+
+# A client that cannot reach the server retries for about 90 seconds with
+# SIGTERM blocked by the wrapper, so stop it with SIGKILL instead.
+timeout -s KILL 10s ./test/test-landlock 1234 5678 < $file