mirror of
https://github.com/linux-msm/laptops-kernel.git
synced 2026-08-13 14:19:53 -07:00
selftests/exec: check that a binfmt_misc instance cannot be pinned
An 'F' entry whose interpreter keeps the binfmt_misc superblock alive pins the instance that owns it forever. Cover both ways to build that: - an interpreter on the instance's own files, control file and entry file alike - and an instance used as an overlayfs lower layer. Check that an ordinary 'F' registration still succeeds so the fix stays honest about not changing what 'F' promises. Link: https://patch.msgid.link/20260728-work-binfmt_misc-selfpin-v1-2-74df5daeca5b@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
@@ -21,6 +21,10 @@ TEST_GEN_PROGS += recursion-depth
|
||||
TEST_GEN_PROGS += null-argv
|
||||
TEST_GEN_PROGS += check-exec
|
||||
|
||||
# binfmt_misc must not be reachable as an exec source or as a stacking layer,
|
||||
# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
|
||||
TEST_GEN_PROGS += binfmt_misc_selfpin
|
||||
|
||||
# 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
|
||||
@@ -91,6 +95,12 @@ $(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
|
||||
$(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
|
||||
cp $< $@
|
||||
|
||||
# Reuses setup_userns()/write_file() from the filesystems selftests. Their
|
||||
# wrappers.h wants the uapi headers, so ask for them here rather than widening
|
||||
# CFLAGS for every program in this directory.
|
||||
$(OUTPUT)/binfmt_misc_selfpin: CFLAGS += $(TOOLS_INCLUDES)
|
||||
$(OUTPUT)/binfmt_misc_selfpin: ../filesystems/utils.c
|
||||
|
||||
# --- binfmt_misc bpf ('B') handler test ---------------------------------
|
||||
# The struct_ops bpf objects are compiled against the running kernel's BTF.
|
||||
# CLANG/BPFTOOL/VMLINUX_BTF are set above next to the toolchain check;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* An 'F' entry keeps its interpreter open for as long as the entry exists,
|
||||
* and the entry only goes away when the binfmt_misc superblock is destroyed.
|
||||
* An interpreter that lives on a mount which in turn keeps that superblock
|
||||
* alive therefore pins the instance that owns it, and nothing can break the
|
||||
* cycle. Check the two ways userspace could arrange for that: an interpreter
|
||||
* on the binfmt_misc instance itself, and one on a filesystem stacked on it.
|
||||
*
|
||||
* Runs unprivileged in a user namespace; binfmt_misc is FS_USERNS_MOUNT.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <sched.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "../filesystems/utils.h"
|
||||
#include "kselftest_harness.h"
|
||||
|
||||
#define MNT "/tmp/binfmt_selfpin"
|
||||
#define BACKING "/tmp/binfmt_selfpin_back"
|
||||
#define LOWER BACKING "/lower"
|
||||
#define MERGED "/tmp/binfmt_selfpin_merged"
|
||||
|
||||
#define MAGIC "\\xde\\xad"
|
||||
#define RULE(interp) ":selfpin:M::" MAGIC "::" interp ":F"
|
||||
/* Not on the instance, and unlike /bin/true it always exists. */
|
||||
#define INTERP "/proc/self/exe"
|
||||
|
||||
#define OPTS_MAX (3 * PATH_MAX + 64)
|
||||
|
||||
static int ensure_dir(const char *path)
|
||||
{
|
||||
if (mkdir(path, 0755) && errno != EEXIST)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write @rule to this instance's register file, preserving write(2)'s errno. */
|
||||
static int register_at(struct __test_metadata *_metadata, const char *rule)
|
||||
{
|
||||
int fd, saved;
|
||||
ssize_t n;
|
||||
|
||||
fd = open(MNT "/register", O_WRONLY);
|
||||
ASSERT_GE(fd, 0);
|
||||
n = write(fd, rule, strlen(rule));
|
||||
saved = errno;
|
||||
close(fd);
|
||||
errno = saved;
|
||||
return n < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mount an overlay over @lower using a private upper/work pair, so the two
|
||||
* mounts this test performs cannot interfere with each other and neither
|
||||
* overlaps the lower layer.
|
||||
*/
|
||||
static int mount_overlay(const char *lower, int nr)
|
||||
{
|
||||
char opts[OPTS_MAX], upper[PATH_MAX], work[PATH_MAX];
|
||||
|
||||
snprintf(upper, sizeof(upper), "%s/upper%d", BACKING, nr);
|
||||
snprintf(work, sizeof(work), "%s/work%d", BACKING, nr);
|
||||
if (mkdir(upper, 0755) || mkdir(work, 0755))
|
||||
return -1;
|
||||
|
||||
snprintf(opts, sizeof(opts), "lowerdir=%s,upperdir=%s,workdir=%s",
|
||||
lower, upper, work);
|
||||
return mount("ovl", MERGED, "overlay", 0, opts);
|
||||
}
|
||||
|
||||
FIXTURE(selfpin) {
|
||||
};
|
||||
|
||||
FIXTURE_SETUP(selfpin)
|
||||
{
|
||||
/* setup_userns() exits rather than returns if this is not there. */
|
||||
if (access("/proc/self/ns/user", F_OK))
|
||||
SKIP(return, "kernel without user namespaces");
|
||||
ASSERT_EQ(setup_userns(), 0);
|
||||
|
||||
ASSERT_EQ(ensure_dir(MNT), 0);
|
||||
if (mount("binfmt_misc", MNT, "binfmt_misc", 0, NULL)) {
|
||||
int saved = errno;
|
||||
|
||||
/* Teardown doesn't run when setup skips, so clean up here. */
|
||||
rmdir(MNT);
|
||||
SKIP(return, "no binfmt_misc: %s", strerror(saved));
|
||||
}
|
||||
}
|
||||
|
||||
FIXTURE_TEARDOWN(selfpin)
|
||||
{
|
||||
/* The namespaces go with the process; just don't litter /tmp. */
|
||||
umount2(MERGED, MNT_DETACH);
|
||||
umount2(BACKING, MNT_DETACH);
|
||||
umount2(MNT, MNT_DETACH);
|
||||
rmdir(MERGED);
|
||||
rmdir(BACKING);
|
||||
rmdir(MNT);
|
||||
}
|
||||
|
||||
/*
|
||||
* The instance's own files are regular files the mounter owns, so they can be
|
||||
* made executable. Opening one for exec still has to fail, otherwise the entry
|
||||
* pins the very superblock it lives in.
|
||||
*/
|
||||
TEST_F(selfpin, interpreter_on_the_instance)
|
||||
{
|
||||
ASSERT_EQ(chmod(MNT "/status", 0755), 0);
|
||||
|
||||
ASSERT_NE(register_at(_metadata, RULE(MNT "/status")), 0);
|
||||
EXPECT_EQ(errno, EACCES);
|
||||
}
|
||||
|
||||
/* Same for an entry file rather than one of the control files. */
|
||||
TEST_F(selfpin, interpreter_on_an_entry)
|
||||
{
|
||||
ASSERT_EQ(register_at(_metadata, ":victim:M::" MAGIC "::" INTERP ":"), 0);
|
||||
ASSERT_EQ(chmod(MNT "/victim", 0755), 0);
|
||||
|
||||
ASSERT_NE(register_at(_metadata, RULE(MNT "/victim")), 0);
|
||||
EXPECT_EQ(errno, EACCES);
|
||||
}
|
||||
|
||||
/*
|
||||
* A stacking filesystem holds a private clone of each layer for its whole
|
||||
* lifetime, so an instance used as a layer can be pinned by an interpreter
|
||||
* that does not live on it at all. Refuse to be a layer.
|
||||
*/
|
||||
TEST_F(selfpin, refuses_to_be_stacked_on)
|
||||
{
|
||||
ASSERT_EQ(ensure_dir(BACKING), 0);
|
||||
ASSERT_EQ(mount("tmpfs", BACKING, "tmpfs", 0, NULL), 0);
|
||||
ASSERT_EQ(mkdir(LOWER, 0755), 0);
|
||||
ASSERT_EQ(ensure_dir(MERGED), 0);
|
||||
|
||||
/* Nothing to prove unless overlayfs works here at all. */
|
||||
if (mount_overlay(LOWER, 1)) {
|
||||
if (errno == ENODEV || errno == EPERM)
|
||||
SKIP(return, "no unprivileged overlayfs");
|
||||
SKIP(return, "overlayfs unusable here: %s", strerror(errno));
|
||||
}
|
||||
ASSERT_EQ(umount(MERGED), 0);
|
||||
|
||||
EXPECT_NE(mount_overlay(MNT, 2), 0);
|
||||
}
|
||||
|
||||
/* An ordinary interpreter still registers with 'F'. */
|
||||
TEST_F(selfpin, ordinary_interpreter_still_works)
|
||||
{
|
||||
EXPECT_EQ(register_at(_metadata, RULE(INTERP)), 0);
|
||||
}
|
||||
|
||||
TEST_HARNESS_MAIN
|
||||
@@ -7,3 +7,6 @@ CONFIG_BPF_SYSCALL=y
|
||||
CONFIG_DEBUG_INFO=y
|
||||
CONFIG_DEBUG_INFO_BTF=y
|
||||
CONFIG_DEBUG_INFO_DWARF4=y
|
||||
CONFIG_OVERLAY_FS=y
|
||||
CONFIG_TMPFS=y
|
||||
CONFIG_USER_NS=y
|
||||
|
||||
Reference in New Issue
Block a user