Something went wrong. Try again.
(I'm so sorry!) Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225// 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",};