Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org
Cc: Alexander Viro <viro@zeniv.linux.org.uk>, Jan Kara <jack@suse.cz>,
	 Kees Cook <kees@kernel.org>,
	linux-mm@kvack.org, bpf@vger.kernel.org,
	 Jonathan Corbet <corbet@lwn.net>,
	Farid Zakaria <farid.m.zakaria@gmail.com>,
	 Daniel Borkmann <daniel@iogearbox.net>,
	Alexei Starovoitov <ast@kernel.org>,
	 jannh@google.com, mail@johnericson.me,
	 "Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH 3/9] selftests/exec: test registering an entry disabled
Date: Thu, 30 Jul 2026 15:34:05 +0200	[thread overview]
Message-ID: <20260730-work-binfmt_misc-preopen-v1-3-4a0b0da71f16@kernel.org> (raw)
In-Reply-To: <20260730-work-binfmt_misc-preopen-v1-0-4a0b0da71f16@kernel.org>

A magic entry registered with 'D' and the same entry without it, to pin
down what the flag decides and what it leaves alone:

- the entry reports itself disabled and nothing dispatches until '1' is
  written to it

- without 'D' it dispatches straight away

- 'D' is not read back among the entry's flags

- enabling and disabling afterwards works as it does for any entry

- 'D' composes with the flags that shape the invocation

- '-1' to the status file removes a staged entry like any other

- a file handle held across a removal cannot resurrect the entry

Put the entry write and read-back helpers into binfmt_misc_common.h.
The bpf suite will need them as well.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/Makefile              |   4 +
 tools/testing/selftests/exec/binfmt_misc_common.h  |  39 +++++
 .../testing/selftests/exec/binfmt_misc_disabled.c  | 172 +++++++++++++++++++++
 3 files changed, 215 insertions(+)

diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 390fe11a7bed..ec7894a802e0 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec
 # or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
 TEST_GEN_PROGS += binfmt_misc_selfpin
 
+# 'D' (register disabled) binfmt_misc test: an entry that exists but does
+# not dispatch until it is enabled. Static magic entry, no bpf toolchain.
+TEST_GEN_PROGS += binfmt_misc_disabled
+
 # Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
 # is shared with the bpf harness's transparent case. No bpf toolchain needed.
 TEST_GEN_PROGS += binfmt_misc_transparent
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index e8d67908dbc4..745aff84dc78 100644
--- a/tools/testing/selftests/exec/binfmt_misc_common.h
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -93,6 +93,45 @@ static inline void unregister(const char *name)
 	}
 }
 
+/* Write @line to @entry's file, reporting the errno it was refused with. */
+static inline int entry_command(const char *entry, const char *line)
+{
+	char path[PATH_MAX];
+	int fd, retval = 0;
+	size_t len = strlen(line);
+
+	snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+	fd = open(path, O_WRONLY | O_CLOEXEC);
+	if (fd < 0)
+		return -errno;
+	if (write(fd, line, len) != (ssize_t)len)
+		retval = -errno;
+	close(fd);
+	return retval;
+}
+
+/* Does @entry's file report @line? */
+static inline bool entry_shows(const char *entry, const char *line)
+{
+	char path[PATH_MAX], buf[PATH_MAX];
+	bool found = false;
+	FILE *fp;
+
+	snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+	fp = fopen(path, "r");
+	if (!fp)
+		return false;
+	while (fgets(buf, sizeof(buf), fp)) {
+		buf[strcspn(buf, "\n")] = '\0';
+		if (!strcmp(buf, line)) {
+			found = true;
+			break;
+		}
+	}
+	fclose(fp);
+	return found;
+}
+
 /* Mount binfmt_misc unless it already is, and report whether it is usable. */
 static inline bool binfmt_misc_available(void)
 {
diff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c
new file mode 100644
index 000000000000..47c9e8a4ee42
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the 'D' (register disabled) flag of binfmt_misc. An entry
+ * registered with it exists but cannot be matched until userspace enables
+ * it, which splits a registration into create and activate.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define MAGIC		"#DISABLED-SELFTEST#"
+#define TARGET_PATH	"/tmp/binfmt_disabled_target"
+#define INTERP_PATH	"/tmp/binfmt_disabled_interp.sh"
+#define ENTRY		"test_disabled"
+#define RULE(flags)	":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
+
+/* The interpreter exits with a code the harness can recognise. */
+#define EXIT_INTERP	7
+
+/* The target only has to carry the magic; it is never actually loaded. */
+static int create_target(void)
+{
+	char buf[128] = MAGIC "\n";
+	int fd;
+
+	unlink(TARGET_PATH);
+	fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+	if (fd < 0)
+		return -1;
+	if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+static int create_interp(void)
+{
+	char buf[64];
+	int fd;
+
+	unlink(INTERP_PATH);
+	fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+	if (fd < 0)
+		return -1;
+	snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP);
+	if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
+		close(fd);
+		return -1;
+	}
+	return close(fd);
+}
+
+FIXTURE(disabled) {
+};
+
+FIXTURE_SETUP(disabled)
+{
+	if (getuid() != 0)
+		SKIP(return, "test must be run as root");
+	if (!binfmt_misc_available())
+		SKIP(return, "no binfmt_misc");
+
+	/* Skip the whole suite on a kernel that does not know 'D'. */
+	if (!binfmt_flag_supported('D')) {
+		ASSERT_EQ(errno, EINVAL);
+		SKIP(return, "kernel without the 'D' flag");
+	}
+
+	ASSERT_EQ(create_interp(), 0);
+	ASSERT_EQ(create_target(), 0);
+}
+
+FIXTURE_TEARDOWN(disabled)
+{
+	unregister(ENTRY);
+	unlink(TARGET_PATH);
+	unlink(INTERP_PATH);
+}
+
+/* The entry exists but does not dispatch until it is enabled. */
+TEST_F(disabled, inert_until_enabled)
+{
+	ASSERT_EQ(write_reg(RULE("D")), 0);
+	EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+	/* Nothing matches it, so no binary format claims the target. */
+	EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+
+	ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+	EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+	EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* Without 'D' an entry is matchable the moment it is registered. */
+TEST_F(disabled, enabled_without_the_flag)
+{
+	ASSERT_EQ(write_reg(RULE("")), 0);
+	EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+	EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' is spent on the registration: the entry does not report it back. */
+TEST_F(disabled, flag_not_reported)
+{
+	ASSERT_EQ(write_reg(RULE("D")), 0);
+	EXPECT_FALSE(entry_shows(ENTRY, "flags: D"));
+	EXPECT_TRUE(entry_shows(ENTRY, "flags: "));
+}
+
+/* A disabled entry can be disabled and enabled like any other. */
+TEST_F(disabled, toggles_like_any_entry)
+{
+	ASSERT_EQ(write_reg(RULE("D")), 0);
+
+	ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+	ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+	ASSERT_EQ(entry_command(ENTRY, "0\n"), 0);
+	EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+	ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+	EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' composes with the invocation flags a static entry can carry. */
+TEST_F(disabled, composes_with_invocation_flags)
+{
+	ASSERT_EQ(write_reg(RULE("PD")), 0);
+	EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+	EXPECT_TRUE(entry_shows(ENTRY, "flags: P"));
+}
+
+/* '-1' to the status file sweeps a staged entry with everything else. */
+TEST_F(disabled, removed_by_remove_all)
+{
+	int fd;
+
+	ASSERT_EQ(write_reg(RULE("D")), 0);
+	EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+	fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(write(fd, "-1", 2), 2);
+	close(fd);
+
+	EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+}
+
+/* A file handle held across a removal cannot resurrect the entry. */
+TEST_F(disabled, no_resurrection_after_remove)
+{
+	int fd;
+
+	ASSERT_EQ(write_reg(RULE("D")), 0);
+	fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+
+	ASSERT_EQ(write(fd, "-1", 2), 2);
+	EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+
+	/* Accepted like any toggle of a removed entry, but publishes nothing. */
+	EXPECT_EQ(write(fd, "1", 1), 1);
+	EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+	close(fd);
+}
+
+TEST_HARNESS_MAIN

-- 
2.53.0



  parent reply	other threads:[~2026-07-30 13:34 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-30 13:34 [PATCH 0/9] binfmt_misc: bind interpreters to a bpf-backed entry Christian Brauner
2026-07-30 13:34 ` [PATCH 1/9] binfmt_misc: let a register string create an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 2/9] selftests/exec: let binfmt_flag_supported() return a bool Christian Brauner
2026-07-30 13:34 ` Christian Brauner [this message]
2026-07-30 13:34 ` [PATCH 4/9] binfmt_misc: document registering an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 5/9] selftests/exec: share the bpf handler preconditions Christian Brauner
2026-07-30 13:34 ` [PATCH 6/9] binfmt_misc: carry pre-opened interpreters in struct binfmt_misc_interp Christian Brauner
2026-07-30 13:34 ` [PATCH 7/9] binfmt_misc: let a 'B' entry bind its interpreters Christian Brauner
2026-07-30 13:34 ` [PATCH 8/9] selftests/exec: test interpreters bound to a 'B' entry Christian Brauner
2026-07-30 13:34 ` [PATCH 9/9] binfmt_misc: document interpreters bound by " Christian Brauner

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=20260730-work-binfmt_misc-preopen-v1-3-4a0b0da71f16@kernel.org \
    --to=brauner@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=corbet@lwn.net \
    --cc=daniel@iogearbox.net \
    --cc=farid.m.zakaria@gmail.com \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=mail@johnericson.me \
    --cc=viro@zeniv.linux.org.uk \
    /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