diff options
| author | Mark Brown <broonie@kernel.org> | 2026-07-27 14:04:35 +0100 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-07-27 14:04:35 +0100 |
| commit | 5bc1a474a3d4020c5098c31012e62f4dba263b6d (patch) | |
| tree | a16357a8c7d6ec2367e2caa0cb0fd5e2f4bdc3c8 /tools/testing/selftests/exec/nix_origin.bpf.c | |
| parent | bc02fb733f2db295b00a8aad95c308e1c61c670e (diff) | |
| parent | 345a7b650f48493907ee45d681fff00bccbcc5d4 (diff) | |
| download | linux-next-5bc1a474a3d4020c5098c31012e62f4dba263b6d.tar.gz linux-next-5bc1a474a3d4020c5098c31012e62f4dba263b6d.zip | |
Merge branch 'fs-next' of linux-next
Diffstat (limited to 'tools/testing/selftests/exec/nix_origin.bpf.c')
| -rw-r--r-- | tools/testing/selftests/exec/nix_origin.bpf.c | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/tools/testing/selftests/exec/nix_origin.bpf.c b/tools/testing/selftests/exec/nix_origin.bpf.c new file mode 100644 index 000000000000..378e22a4c43b --- /dev/null +++ b/tools/testing/selftests/exec/nix_origin.bpf.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution + * + * A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF + * binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is + * resolved relative to the directory of the binary being executed and + * selected via bpf_binprm_set_interp(). The match program reads the + * program headers itself, so anything else never commits to this + * handler and passes through untouched. + * + * Activate with: + * bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf + * echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register + */ +#include "vmlinux.h" +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +#define PATH_MAX 4096 +#define EI_CLASS 4 +#define ELFCLASSXX 2 /* ELFCLASS64; flip to 1 for 32-bit */ +#define PT_INTERP 3 +#define MAX_PHDRS 64 + +#define ORIGIN "$ORIGIN" +#define ORIGIN_LEN (sizeof(ORIGIN) - 1) + +#define ENOENT 2 +#define ENOEXEC 8 +#define ENAMETOOLONG 36 + +extern int bpf_dynptr_from_file(struct file *file, __u32 flags, + struct bpf_dynptr *ptr__uninit) __ksym; +extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym; +extern int bpf_path_d_path(const struct path *path, char *buf, + size_t buf__sz) __ksym; +extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, + size_t path__sz) __ksym; + +struct scratch { + char interp[PATH_MAX]; /* PT_INTERP as embedded in the binary */ + char path[PATH_MAX]; /* d_path of the binary, becomes the result */ +}; + +/* Keyed by pid: execs run concurrently and the programs can sleep. */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 512); + __type(key, __u64); + __type(value, struct scratch); +} scratch_map SEC(".maps"); + +static const struct scratch zero_scratch; + +/* An ELF64 binary per the prefetched header? */ +static bool is_elf64(struct linux_binprm *bprm) +{ + return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' && + bprm->buf[2] == 'L' && bprm->buf[3] == 'F' && + bprm->buf[EI_CLASS] == ELFCLASSXX; +} + +/* Locate PT_INTERP; false if the file has none or looks malformed. */ +static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr) +{ + struct elf64_hdr ehdr; + bool found = false; + int i; + + if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0)) + return false; + if (ehdr.e_phentsize != sizeof(struct elf64_phdr)) + return false; + + bpf_for(i, 0, ehdr.e_phnum) { + if (i >= MAX_PHDRS) + break; + if (bpf_dynptr_read(phdr, sizeof(*phdr), dp, + ehdr.e_phoff + i * sizeof(*phdr), 0)) + return false; + if (phdr->p_type == PT_INTERP) { + found = true; + break; + } + } + return found; +} + +/* + * An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The + * match can sleep and read the file, so the decision is made here and + * regular binaries never commit to this handler: later binfmt_misc + * entries and binfmt_elf see them as if we did not exist. + */ +SEC("struct_ops.s/match") +bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm) +{ + char prefix[ORIGIN_LEN + 1] = {}; + struct elf64_phdr phdr; + struct bpf_dynptr dp; + bool ours = false; + + if (!is_elf64(bprm)) + return false; + + /* The dynptr must be discarded on every path once requested. */ + if (bpf_dynptr_from_file(bprm->file, 0, &dp)) + goto out; + if (find_pt_interp(&dp, &phdr) && + phdr.p_filesz > ORIGIN_LEN + 1 && + !bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0)) + ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/"); +out: + bpf_dynptr_file_discard(&dp); + return ours; +} + +/* + * The match is committed and already vetted the "$ORIGIN/" prefix, so + * everything here reads the file again from scratch: -ENOEXEC only + * covers a binary that changed under us and stopped being ours. + */ +SEC("struct_ops.s/load") +int BPF_PROG(nix_origin_load, struct linux_binprm *bprm) +{ + __u32 isz, sfx, rsz, slash; + struct elf64_phdr phdr; + struct bpf_dynptr dp; + struct scratch *sc; + __u64 id; + int ret = -ENOEXEC, len, i; + + if (bpf_dynptr_from_file(bprm->file, 0, &dp)) + goto out; + + if (!find_pt_interp(&dp, &phdr)) + goto out; + + isz = phdr.p_filesz; + if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp)) + goto out; + /* + * The range check above compiles to a test on a zero-extended copy of + * the u64 p_filesz, so the verifier does not carry the bound to the + * dynptr_read() length below ("unbounded memory access"). Mask isz to + * the buffer size (a power of two) and force the masked value to be + * materialized with a barrier so the read uses the bounded register. + */ + isz &= sizeof(sc->interp) - 1; + barrier_var(isz); + + id = bpf_get_current_pid_tgid(); + if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY)) + goto out; + sc = bpf_map_lookup_elem(&scratch_map, &id); + if (!sc) + goto out_del; + + if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0)) + goto out_del; + if (sc->interp[isz - 1] != '\0') + goto out_del; + + /* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */ + if (sc->interp[0] != '$' || sc->interp[1] != 'O' || + sc->interp[2] != 'R' || sc->interp[3] != 'I' || + sc->interp[4] != 'G' || sc->interp[5] != 'I' || + sc->interp[6] != 'N' || sc->interp[7] != '/') + goto out_del; + + /* + * From here on resolution failures fail the exec instead of falling + * back to binfmt_elf, which would resolve the literal "$ORIGIN/..." + * relative to the caller's cwd. + */ + ret = -ENOENT; + len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path)); + if (len <= 0 || len > sizeof(sc->path)) + goto out_del; + /* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */ + if (sc->path[0] != '/') + goto out_del; + + /* $ORIGIN = dirname of the binary. */ + slash = 0; + bpf_for(i, 1, len - 1) { + if (i >= sizeof(sc->path)) + break; + if (sc->path[i] == '/') + slash = i; + } + + /* Splice the suffix (leading '/' and NUL included) onto the dir. */ + sfx = isz - ORIGIN_LEN; + rsz = slash + sfx; + if (rsz > sizeof(sc->path)) { + ret = -ENAMETOOLONG; + goto out_del; + } + bpf_for(i, 0, sfx) { + __u32 s = ORIGIN_LEN + i, d = slash + i; + + if (s >= sizeof(sc->interp) || d >= sizeof(sc->path)) + break; + sc->path[d] = sc->interp[s]; + } + + ret = bpf_binprm_set_interp(bprm, sc->path, rsz); +out_del: + bpf_map_delete_elem(&scratch_map, &id); +out: + bpf_dynptr_file_discard(&dp); + return ret; +} + +SEC(".struct_ops.link") +struct binfmt_misc_ops nix_origin = { + .match = (void *)nix_origin_match, + .load = (void *)nix_origin_load, + .name = "nix_origin", +}; |
