linux-mm.kvack.org archive mirror
 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 8/9] selftests/exec: test interpreters bound to a 'B' entry
Date: Thu, 30 Jul 2026 15:34:10 +0200	[thread overview]
Message-ID: <20260730-work-binfmt_misc-preopen-v1-8-4a0b0da71f16@kernel.org> (raw)
In-Reply-To: <20260730-work-binfmt_misc-preopen-v1-0-4a0b0da71f16@kernel.org>

One handler, one entry registered disabled, an interpreter per guest
architecture bound to a file one write at a time. The load program picks
one by name per exec:

- an aarch64 binary runs the interpreter bound as "first" and a riscv one
  the interpreter bound as "second", from a single entry and a single
  handler

- unlinking a bound interpreter and putting a different binary in its
  place changes nothing, which is what the binding exists for

- the entry reports what it bound, under the names it bound them as

- a name the entry did not bind fails the exec with -ENOENT rather than
  falling back to anything

- activating the entry refuses further binding with -EBUSY, a later
  disable does not undo that, and an entry registered without 'D' never
  accepted a '+' write to begin with

- a name binds one interpreter, and control characters are refused

- the command has to end at the write, bytes past an embedded nul are
  refused

- an entry binds at most 100 interpreters, the next one is refused with
  -ENOSPC

The test interpreter prints its argv[0], which is the path the kernel ran
that copy under, so one binary installed at two paths tells the harness
which of them the program picked.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/exec/Makefile             |   7 +-
 tools/testing/selftests/exec/binfmt_bind_interp.c |  14 ++
 tools/testing/selftests/exec/binfmt_misc_bpf.c    | 261 +++++++++++++++++++++-
 tools/testing/selftests/exec/interp_bind.bpf.c    |  76 +++++++
 4 files changed, 349 insertions(+), 9 deletions(-)

diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index ec7894a802e0..410c93606a0c 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -56,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
 ifeq ($(HAVE_BPF_TOOLCHAIN),y)
 TEST_GEN_PROGS += binfmt_misc_bpf
 TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
-TEST_GEN_FILES += loader.bpf.o
-TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
+TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
+TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
 else
 $(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
 endif
@@ -127,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
 $(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
 	$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
 
+$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c
+	$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
 $(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
 	$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
 
diff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c
new file mode 100644
index 000000000000..06d65062856b
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bind_interp.c
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test interpreter for the bound-interpreter case of the binfmt_misc_bpf
+ * selftest. Two copies are installed at different paths and bound to one
+ * entry under different names; printing argv[0] - the path the kernel ran
+ * this copy under - tells the harness which of them the load program picked.
+ */
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+	printf("BIND_RAN %s\n", argc > 0 ? argv[0] : "");
+	return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 71bb6d8b4517..2c7b63075f1d 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -9,7 +9,7 @@
  *
  *     echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
  *
- * Three self-contained cases are exercised:
+ * Five self-contained cases are exercised:
  *
  *   1. bpf_interp: the match program matches a synthetic aarch64 ELF header
  *      from the prefetched bprm->buf and the load program routes it to a
@@ -26,6 +26,11 @@
  *      (binfmt_loader_payload) runs as the main image with the selected
  *      interpreter substituted for its PT_INTERP and asserts the native
  *      identity from inside.
+ *   5. interp_bind: an entry registered disabled with 'D' is given its
+ *      interpreters one write at a time, and the load program picks one by
+ *      name per exec. Replacing what the path holds afterwards changes
+ *      nothing, which is the point of binding a file rather than resolving
+ *      a name at exec time. Enabling the entry seals it.
  *
  * The first two route to a test interpreter that prints BPF_INTERP_RAN,
  * proving the program's chosen interpreter actually ran.
@@ -54,6 +59,12 @@
 #define TRANS_EXPECT	"TRANSPARENT_OK"
 #define LOADER_INTERP	"/tmp/binfmt_loader_interp"
 #define LOADER_PATH	"/tmp/binfmt_bpf_loader.ldrtest"
+#define BIND_FIRST	"/tmp/binfmt_bind_first"
+#define BIND_SECOND	"/tmp/binfmt_bind_second"
+#define BIND_ARM_PATH	"/tmp/binfmt_bind_arm"
+#define BIND_RISCV_PATH	"/tmp/binfmt_bind_riscv"
+#define BIND_EXPECT	"BIND_RAN "
+#define BIND_MAX	100
 
 /* A minimal 64-bit little-endian ELF header, padded to the read size. */
 static int create_fake_elf(const char *path, unsigned short machine)
@@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine)
 	return 0;
 }
 
-static int register_entry(const char *name, const char *handler)
+/*
+ * Register a 'B' entry for @handler. With @flags "D" the entry is created
+ * disabled, which is what leaves it open to being given interpreters.
+ */
+static int register_entry(const char *name, const char *handler,
+			  const char *flags)
 {
 	char rule[PATH_MAX];
 
-	snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
+	snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
+		 flags ? flags : "");
 	return write_reg(rule);
 }
 
@@ -139,10 +156,12 @@ struct bpf_case {
 
 /*
  * Load @objfile, attach its struct_ops map @handler (which publishes the
- * handler) and activate a 'B' entry named @entry that references it.
+ * handler) and register a 'B' entry named @entry that references it, with
+ * @flags as the entry's register-string flags.
  */
-static int bpf_case_start(struct bpf_case *c, const char *objfile,
-			  const char *handler, const char *entry)
+static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
+				const char *handler, const char *entry,
+				const char *flags)
 {
 	struct bpf_map *map;
 
@@ -172,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
 		c->link = NULL;
 		goto fail;
 	}
-	if (register_entry(entry, handler)) {
+	if (register_entry(entry, handler, flags)) {
 		fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
 		goto fail;
 	}
@@ -186,6 +205,12 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
 	return -1;
 }
 
+static int bpf_case_start(struct bpf_case *c, const char *objfile,
+			  const char *handler, const char *entry)
+{
+	return bpf_case_start_flags(c, objfile, handler, entry, NULL);
+}
+
 static void bpf_case_stop(struct bpf_case *c)
 {
 	unregister(c->entry);
@@ -318,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution)
 	unlink(LOADER_INTERP);
 }
 
+/* The errno an exec of @path fails with, 0 if it succeeded. */
+static int exec_errno(const char *path)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (pid == 0) {
+		execl(path, path, (char *)NULL);
+		_exit(errno);
+	}
+	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+		return -1;
+	return WEXITSTATUS(status);
+}
+
+/* Install a copy of the bound-interpreter test binary at @path. */
+static int install_interp(const char *path)
+{
+	char src[PATH_MAX];
+
+	if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
+		return -1;
+	return copy_file(src, path);
+}
+
+/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
+static int entry_bind(const char *entry, const char *name, const char *path)
+{
+	char cmd[PATH_MAX];
+
+	snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
+	return entry_command(entry, cmd);
+}
+
+FIXTURE(bound_interp) {
+	char obj[PATH_MAX];
+	struct bpf_case c;
+	bool started;
+};
+
+FIXTURE_SETUP(bound_interp)
+{
+	const char *why = bpf_handler_unsupported();
+
+	if (why)
+		SKIP(return, "%s", why);
+	if (!binfmt_flag_supported('D')) {
+		ASSERT_EQ(errno, EINVAL);
+		SKIP(return, "kernel without the 'D' flag");
+	}
+
+	ASSERT_EQ(install_interp(BIND_FIRST), 0);
+	ASSERT_EQ(install_interp(BIND_SECOND), 0);
+
+	ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+				"interp_bind.bpf.o"), 0);
+
+	/*
+	 * Registered disabled, so it cannot be matched yet and can still be
+	 * given interpreters. Each path is resolved once, by its write(2);
+	 * from here on the entry holds the files themselves.
+	 */
+	ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
+				       "test_interp_bind", "D"), 0);
+	self->started = true;
+
+	ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
+	ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
+}
+
+FIXTURE_TEARDOWN(bound_interp)
+{
+	if (self->started)
+		bpf_case_stop(&self->c);
+	unlink(BIND_FIRST);
+	unlink(BIND_SECOND);
+	unlink(AARCH64_PATH);
+	unlink(BIND_RISCV_PATH);
+	unlink(BIND_ARM_PATH);
+}
+
+/* Enabling is what makes the configured entry matchable. */
+static int activate(const char *entry)
+{
+	return entry_command(entry, "1\n");
+}
+
+/* One entry, one interpreter per guest architecture, picked per exec. */
+TEST_F(bound_interp, selects_by_name)
+{
+	ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+	ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
+
+	/* Disabled, so it does not match and no format claims the binary. */
+	EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
+
+	ASSERT_EQ(activate("test_interp_bind"), 0);
+	EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+	EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
+}
+
+/* What was bound is what runs, whatever the path holds afterwards. */
+TEST_F(bound_interp, path_no_longer_decides)
+{
+	char other[PATH_MAX];
+
+	ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+	ASSERT_EQ(activate("test_interp_bind"), 0);
+
+	/* Bound interpreters are pinned against writes, exactly like 'F'. */
+	EXPECT_TRUE(write_denied(BIND_FIRST));
+
+	/* Replace the path with a different binary: a new file, new inode. */
+	ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
+	ASSERT_EQ(unlink(BIND_FIRST), 0);
+	ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
+
+	EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+}
+
+/* The entry reports what it bound, under the names it bound them as. */
+TEST_F(bound_interp, entry_reports_bindings)
+{
+	EXPECT_TRUE(entry_shows("test_interp_bind",
+				"bpf-interpreter first " BIND_FIRST));
+	EXPECT_TRUE(entry_shows("test_interp_bind",
+				"bpf-interpreter second " BIND_SECOND));
+}
+
+/* Selecting a name the entry did not bind fails the exec. */
+TEST_F(bound_interp, unbound_name_fails)
+{
+	ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
+	ASSERT_EQ(activate("test_interp_bind"), 0);
+
+	EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
+}
+
+/* Activating seals it: what can be matched cannot be changed. */
+TEST_F(bound_interp, sealed_once_active)
+{
+	ASSERT_EQ(activate("test_interp_bind"), 0);
+
+	EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+	EXPECT_FALSE(entry_shows("test_interp_bind",
+				 "bpf-interpreter third " BIND_SECOND));
+}
+
+/* The seal is for good: disabling the entry again reopens nothing. */
+TEST_F(bound_interp, disable_does_not_unseal)
+{
+	ASSERT_EQ(activate("test_interp_bind"), 0);
+	ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
+
+	EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+}
+
+/* An entry registered without 'D' is sealed from the start. */
+TEST_F(bound_interp, born_sealed)
+{
+	/* A second entry for the handler the fixture already published. */
+	ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
+
+	EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
+	unregister("test_born_sealed");
+}
+
+/* A name is bound once; a second use of it is refused. */
+TEST_F(bound_interp, duplicate_name_refused)
+{
+	EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
+}
+
+/* A name is a printable word: the entry file reports 'name path' lines. */
+TEST_F(bound_interp, name_must_be_printable)
+{
+	/* A control character would forge a line into the entry file. */
+	EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
+	EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
+
+	/* A space cannot even be spelled: the path starts after the first one. */
+	EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
+}
+
+/* The command ends at the write: bytes past an embedded nul are refused. */
+TEST_F(bound_interp, trailing_bytes_refused)
+{
+	char cmd[PATH_MAX];
+	size_t len;
+	int fd;
+
+	/* entry_command() cannot spell a nul, so write the buffer raw. */
+	snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
+	len = strlen(cmd) + 1;
+	memcpy(cmd + len, "junk", sizeof("junk"));
+	len += sizeof("junk");
+
+	fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(write(fd, cmd, len), -1);
+	EXPECT_EQ(errno, EINVAL);
+	close(fd);
+
+	EXPECT_FALSE(entry_shows("test_interp_bind",
+				 "bpf-interpreter nul " BIND_FIRST));
+}
+
+/* An entry binds at most BIND_MAX interpreters. */
+TEST_F(bound_interp, capped_bindings)
+{
+	char name[16];
+	int i;
+
+	/* The fixture bound "first" and "second" already. */
+	for (i = 2; i < BIND_MAX; i++) {
+		snprintf(name, sizeof(name), "n%d", i);
+		ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
+	}
+	EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c
new file mode 100644
index 000000000000..1ce45cca215f
--- /dev/null
+++ b/tools/testing/selftests/exec/interp_bind.bpf.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the selftest's bound-interpreter case: one
+ * handler, one entry, an interpreter per guest architecture - each bound to
+ * a file when the entry was registered rather than to a path resolved at
+ * exec time. The load program names the one it wants; a name the entry did
+ * not bind fails the exec, which the harness checks too.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+#define EI_CLASS	4
+#define ELFCLASS64	2
+#define E_MACHINE_OFF	18
+#define EM_ARM		40
+#define EM_AARCH64	183
+#define EM_RISCV	243
+
+extern int bpf_binprm_select_interp(struct linux_binprm *bprm,
+				    const char *name, size_t name__sz) __ksym;
+
+/* The guest architecture of a 64-bit ELF, or zero if it is not one. */
+static __u16 elf_machine(struct linux_binprm *bprm)
+{
+	if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
+	    bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
+	    bprm->buf[EI_CLASS] != ELFCLASS64)
+		return 0;
+
+	/* Little-endian 16-bit field, read byte-wise for the verifier. */
+	return (__u8)bprm->buf[E_MACHINE_OFF] |
+	       ((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8);
+}
+
+SEC("struct_ops.s/match")
+bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)
+{
+	__u16 machine = elf_machine(bprm);
+
+	return machine == EM_AARCH64 || machine == EM_RISCV ||
+	       machine == EM_ARM;
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)
+{
+	/*
+	 * Names, not paths: each one selects a file the entry pre-opened, so
+	 * nothing is resolved here or later, in any namespace. The buffers
+	 * are on the stack because the verifier rejects .rodata for a sized
+	 * memory argument.
+	 */
+	char first[] = "first";
+	char second[] = "second";
+	char unbound[] = "unbound";
+
+	switch (elf_machine(bprm)) {
+	case EM_AARCH64:
+		return bpf_binprm_select_interp(bprm, first, sizeof(first));
+	case EM_RISCV:
+		return bpf_binprm_select_interp(bprm, second, sizeof(second));
+	}
+
+	/* The entry bound nothing under this name: -ENOENT fails the exec. */
+	return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops interp_bind = {
+	.match	= (void *)interp_bind_match,
+	.load	= (void *)interp_bind_load,
+	.name	= "interp_bind",
+};

-- 
2.53.0



  parent reply	other threads:[~2026-07-30 13:35 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 ` [PATCH 3/9] selftests/exec: test registering an entry disabled Christian Brauner
2026-07-30 13:34 ` [PATCH 4/9] binfmt_misc: document " 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 ` Christian Brauner [this message]
2026-07-30 13:34 ` [PATCH 9/9] binfmt_misc: document interpreters bound by a 'B' entry 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-8-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;
as well as URLs for NNTP newsgroup(s).