1*277d787fSFarid Zakaria // SPDX-License-Identifier: GPL-2.0 2*277d787fSFarid Zakaria /* 3*277d787fSFarid Zakaria * nix_origin.bpf.c - $ORIGIN-relative PT_INTERP resolution 4*277d787fSFarid Zakaria * 5*277d787fSFarid Zakaria * A binfmt_misc_ops handler that makes relocatable (Nix-style) ELF 6*277d787fSFarid Zakaria * binaries work: if PT_INTERP starts with "$ORIGIN/", the loader is 7*277d787fSFarid Zakaria * resolved relative to the directory of the binary being executed and 8*277d787fSFarid Zakaria * selected via bpf_binprm_set_interp(). The match program reads the 9*277d787fSFarid Zakaria * program headers itself, so anything else never commits to this 10*277d787fSFarid Zakaria * handler and passes through untouched. 11*277d787fSFarid Zakaria * 12*277d787fSFarid Zakaria * Activate with: 13*277d787fSFarid Zakaria * bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf 14*277d787fSFarid Zakaria * echo ':nix-origin:B::::nix_origin:' > /proc/sys/fs/binfmt_misc/register 15*277d787fSFarid Zakaria */ 16*277d787fSFarid Zakaria #include "vmlinux.h" 17*277d787fSFarid Zakaria #include <bpf/bpf_helpers.h> 18*277d787fSFarid Zakaria #include <bpf/bpf_tracing.h> 19*277d787fSFarid Zakaria 20*277d787fSFarid Zakaria char _license[] SEC("license") = "GPL"; 21*277d787fSFarid Zakaria 22*277d787fSFarid Zakaria #define PATH_MAX 4096 23*277d787fSFarid Zakaria #define EI_CLASS 4 24*277d787fSFarid Zakaria #define ELFCLASSXX 2 /* ELFCLASS64; flip to 1 for 32-bit */ 25*277d787fSFarid Zakaria #define PT_INTERP 3 26*277d787fSFarid Zakaria #define MAX_PHDRS 64 27*277d787fSFarid Zakaria 28*277d787fSFarid Zakaria #define ORIGIN "$ORIGIN" 29*277d787fSFarid Zakaria #define ORIGIN_LEN (sizeof(ORIGIN) - 1) 30*277d787fSFarid Zakaria 31*277d787fSFarid Zakaria #define ENOENT 2 32*277d787fSFarid Zakaria #define ENOEXEC 8 33*277d787fSFarid Zakaria #define ENAMETOOLONG 36 34*277d787fSFarid Zakaria 35*277d787fSFarid Zakaria extern int bpf_dynptr_from_file(struct file *file, __u32 flags, 36*277d787fSFarid Zakaria struct bpf_dynptr *ptr__uninit) __ksym; 37*277d787fSFarid Zakaria extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym; 38*277d787fSFarid Zakaria extern int bpf_path_d_path(const struct path *path, char *buf, 39*277d787fSFarid Zakaria size_t buf__sz) __ksym; 40*277d787fSFarid Zakaria extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path, 41*277d787fSFarid Zakaria size_t path__sz) __ksym; 42*277d787fSFarid Zakaria 43*277d787fSFarid Zakaria struct scratch { 44*277d787fSFarid Zakaria char interp[PATH_MAX]; /* PT_INTERP as embedded in the binary */ 45*277d787fSFarid Zakaria char path[PATH_MAX]; /* d_path of the binary, becomes the result */ 46*277d787fSFarid Zakaria }; 47*277d787fSFarid Zakaria 48*277d787fSFarid Zakaria /* Keyed by pid: execs run concurrently and the programs can sleep. */ 49*277d787fSFarid Zakaria struct { 50*277d787fSFarid Zakaria __uint(type, BPF_MAP_TYPE_HASH); 51*277d787fSFarid Zakaria __uint(max_entries, 512); 52*277d787fSFarid Zakaria __type(key, __u64); 53*277d787fSFarid Zakaria __type(value, struct scratch); 54*277d787fSFarid Zakaria } scratch_map SEC(".maps"); 55*277d787fSFarid Zakaria 56*277d787fSFarid Zakaria static const struct scratch zero_scratch; 57*277d787fSFarid Zakaria 58*277d787fSFarid Zakaria /* An ELF64 binary per the prefetched header? */ 59*277d787fSFarid Zakaria static bool is_elf64(struct linux_binprm *bprm) 60*277d787fSFarid Zakaria { 61*277d787fSFarid Zakaria return bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' && 62*277d787fSFarid Zakaria bprm->buf[2] == 'L' && bprm->buf[3] == 'F' && 63*277d787fSFarid Zakaria bprm->buf[EI_CLASS] == ELFCLASSXX; 64*277d787fSFarid Zakaria } 65*277d787fSFarid Zakaria 66*277d787fSFarid Zakaria /* Locate PT_INTERP; false if the file has none or looks malformed. */ 67*277d787fSFarid Zakaria static bool find_pt_interp(struct bpf_dynptr *dp, struct elf64_phdr *phdr) 68*277d787fSFarid Zakaria { 69*277d787fSFarid Zakaria struct elf64_hdr ehdr; 70*277d787fSFarid Zakaria bool found = false; 71*277d787fSFarid Zakaria int i; 72*277d787fSFarid Zakaria 73*277d787fSFarid Zakaria if (bpf_dynptr_read(&ehdr, sizeof(ehdr), dp, 0, 0)) 74*277d787fSFarid Zakaria return false; 75*277d787fSFarid Zakaria if (ehdr.e_phentsize != sizeof(struct elf64_phdr)) 76*277d787fSFarid Zakaria return false; 77*277d787fSFarid Zakaria 78*277d787fSFarid Zakaria bpf_for(i, 0, ehdr.e_phnum) { 79*277d787fSFarid Zakaria if (i >= MAX_PHDRS) 80*277d787fSFarid Zakaria break; 81*277d787fSFarid Zakaria if (bpf_dynptr_read(phdr, sizeof(*phdr), dp, 82*277d787fSFarid Zakaria ehdr.e_phoff + i * sizeof(*phdr), 0)) 83*277d787fSFarid Zakaria return false; 84*277d787fSFarid Zakaria if (phdr->p_type == PT_INTERP) { 85*277d787fSFarid Zakaria found = true; 86*277d787fSFarid Zakaria break; 87*277d787fSFarid Zakaria } 88*277d787fSFarid Zakaria } 89*277d787fSFarid Zakaria return found; 90*277d787fSFarid Zakaria } 91*277d787fSFarid Zakaria 92*277d787fSFarid Zakaria /* 93*277d787fSFarid Zakaria * An ELF64 binary whose PT_INTERP starts with "$ORIGIN/" is ours. The 94*277d787fSFarid Zakaria * match can sleep and read the file, so the decision is made here and 95*277d787fSFarid Zakaria * regular binaries never commit to this handler: later binfmt_misc 96*277d787fSFarid Zakaria * entries and binfmt_elf see them as if we did not exist. 97*277d787fSFarid Zakaria */ 98*277d787fSFarid Zakaria SEC("struct_ops.s/match") 99*277d787fSFarid Zakaria bool BPF_PROG(nix_origin_match, struct linux_binprm *bprm) 100*277d787fSFarid Zakaria { 101*277d787fSFarid Zakaria char prefix[ORIGIN_LEN + 1] = {}; 102*277d787fSFarid Zakaria struct elf64_phdr phdr; 103*277d787fSFarid Zakaria struct bpf_dynptr dp; 104*277d787fSFarid Zakaria bool ours = false; 105*277d787fSFarid Zakaria 106*277d787fSFarid Zakaria if (!is_elf64(bprm)) 107*277d787fSFarid Zakaria return false; 108*277d787fSFarid Zakaria 109*277d787fSFarid Zakaria /* The dynptr must be discarded on every path once requested. */ 110*277d787fSFarid Zakaria if (bpf_dynptr_from_file(bprm->file, 0, &dp)) 111*277d787fSFarid Zakaria goto out; 112*277d787fSFarid Zakaria if (find_pt_interp(&dp, &phdr) && 113*277d787fSFarid Zakaria phdr.p_filesz > ORIGIN_LEN + 1 && 114*277d787fSFarid Zakaria !bpf_dynptr_read(prefix, sizeof(prefix), &dp, phdr.p_offset, 0)) 115*277d787fSFarid Zakaria ours = !bpf_strncmp(prefix, sizeof(prefix), ORIGIN "/"); 116*277d787fSFarid Zakaria out: 117*277d787fSFarid Zakaria bpf_dynptr_file_discard(&dp); 118*277d787fSFarid Zakaria return ours; 119*277d787fSFarid Zakaria } 120*277d787fSFarid Zakaria 121*277d787fSFarid Zakaria /* 122*277d787fSFarid Zakaria * The match is committed and already vetted the "$ORIGIN/" prefix, so 123*277d787fSFarid Zakaria * everything here reads the file again from scratch: -ENOEXEC only 124*277d787fSFarid Zakaria * covers a binary that changed under us and stopped being ours. 125*277d787fSFarid Zakaria */ 126*277d787fSFarid Zakaria SEC("struct_ops.s/load") 127*277d787fSFarid Zakaria int BPF_PROG(nix_origin_load, struct linux_binprm *bprm) 128*277d787fSFarid Zakaria { 129*277d787fSFarid Zakaria __u32 isz, sfx, rsz, slash; 130*277d787fSFarid Zakaria struct elf64_phdr phdr; 131*277d787fSFarid Zakaria struct bpf_dynptr dp; 132*277d787fSFarid Zakaria struct scratch *sc; 133*277d787fSFarid Zakaria __u64 id; 134*277d787fSFarid Zakaria int ret = -ENOEXEC, len, i; 135*277d787fSFarid Zakaria 136*277d787fSFarid Zakaria if (bpf_dynptr_from_file(bprm->file, 0, &dp)) 137*277d787fSFarid Zakaria goto out; 138*277d787fSFarid Zakaria 139*277d787fSFarid Zakaria if (!find_pt_interp(&dp, &phdr)) 140*277d787fSFarid Zakaria goto out; 141*277d787fSFarid Zakaria 142*277d787fSFarid Zakaria isz = phdr.p_filesz; 143*277d787fSFarid Zakaria if (isz <= ORIGIN_LEN + 1 || isz >= sizeof(sc->interp)) 144*277d787fSFarid Zakaria goto out; 145*277d787fSFarid Zakaria /* 146*277d787fSFarid Zakaria * The range check above compiles to a test on a zero-extended copy of 147*277d787fSFarid Zakaria * the u64 p_filesz, so the verifier does not carry the bound to the 148*277d787fSFarid Zakaria * dynptr_read() length below ("unbounded memory access"). Mask isz to 149*277d787fSFarid Zakaria * the buffer size (a power of two) and force the masked value to be 150*277d787fSFarid Zakaria * materialized with a barrier so the read uses the bounded register. 151*277d787fSFarid Zakaria */ 152*277d787fSFarid Zakaria isz &= sizeof(sc->interp) - 1; 153*277d787fSFarid Zakaria barrier_var(isz); 154*277d787fSFarid Zakaria 155*277d787fSFarid Zakaria id = bpf_get_current_pid_tgid(); 156*277d787fSFarid Zakaria if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY)) 157*277d787fSFarid Zakaria goto out; 158*277d787fSFarid Zakaria sc = bpf_map_lookup_elem(&scratch_map, &id); 159*277d787fSFarid Zakaria if (!sc) 160*277d787fSFarid Zakaria goto out_del; 161*277d787fSFarid Zakaria 162*277d787fSFarid Zakaria if (bpf_dynptr_read(sc->interp, isz, &dp, phdr.p_offset, 0)) 163*277d787fSFarid Zakaria goto out_del; 164*277d787fSFarid Zakaria if (sc->interp[isz - 1] != '\0') 165*277d787fSFarid Zakaria goto out_del; 166*277d787fSFarid Zakaria 167*277d787fSFarid Zakaria /* Not "$ORIGIN/..." anymore? Then it is not ours anymore either. */ 168*277d787fSFarid Zakaria if (sc->interp[0] != '$' || sc->interp[1] != 'O' || 169*277d787fSFarid Zakaria sc->interp[2] != 'R' || sc->interp[3] != 'I' || 170*277d787fSFarid Zakaria sc->interp[4] != 'G' || sc->interp[5] != 'I' || 171*277d787fSFarid Zakaria sc->interp[6] != 'N' || sc->interp[7] != '/') 172*277d787fSFarid Zakaria goto out_del; 173*277d787fSFarid Zakaria 174*277d787fSFarid Zakaria /* 175*277d787fSFarid Zakaria * From here on resolution failures fail the exec instead of falling 176*277d787fSFarid Zakaria * back to binfmt_elf, which would resolve the literal "$ORIGIN/..." 177*277d787fSFarid Zakaria * relative to the caller's cwd. 178*277d787fSFarid Zakaria */ 179*277d787fSFarid Zakaria ret = -ENOENT; 180*277d787fSFarid Zakaria len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path)); 181*277d787fSFarid Zakaria if (len <= 0 || len > sizeof(sc->path)) 182*277d787fSFarid Zakaria goto out_del; 183*277d787fSFarid Zakaria /* Unreachable or unlinked ("... (deleted)") binaries can't resolve. */ 184*277d787fSFarid Zakaria if (sc->path[0] != '/') 185*277d787fSFarid Zakaria goto out_del; 186*277d787fSFarid Zakaria 187*277d787fSFarid Zakaria /* $ORIGIN = dirname of the binary. */ 188*277d787fSFarid Zakaria slash = 0; 189*277d787fSFarid Zakaria bpf_for(i, 1, len - 1) { 190*277d787fSFarid Zakaria if (i >= sizeof(sc->path)) 191*277d787fSFarid Zakaria break; 192*277d787fSFarid Zakaria if (sc->path[i] == '/') 193*277d787fSFarid Zakaria slash = i; 194*277d787fSFarid Zakaria } 195*277d787fSFarid Zakaria 196*277d787fSFarid Zakaria /* Splice the suffix (leading '/' and NUL included) onto the dir. */ 197*277d787fSFarid Zakaria sfx = isz - ORIGIN_LEN; 198*277d787fSFarid Zakaria rsz = slash + sfx; 199*277d787fSFarid Zakaria if (rsz > sizeof(sc->path)) { 200*277d787fSFarid Zakaria ret = -ENAMETOOLONG; 201*277d787fSFarid Zakaria goto out_del; 202*277d787fSFarid Zakaria } 203*277d787fSFarid Zakaria bpf_for(i, 0, sfx) { 204*277d787fSFarid Zakaria __u32 s = ORIGIN_LEN + i, d = slash + i; 205*277d787fSFarid Zakaria 206*277d787fSFarid Zakaria if (s >= sizeof(sc->interp) || d >= sizeof(sc->path)) 207*277d787fSFarid Zakaria break; 208*277d787fSFarid Zakaria sc->path[d] = sc->interp[s]; 209*277d787fSFarid Zakaria } 210*277d787fSFarid Zakaria 211*277d787fSFarid Zakaria ret = bpf_binprm_set_interp(bprm, sc->path, rsz); 212*277d787fSFarid Zakaria out_del: 213*277d787fSFarid Zakaria bpf_map_delete_elem(&scratch_map, &id); 214*277d787fSFarid Zakaria out: 215*277d787fSFarid Zakaria bpf_dynptr_file_discard(&dp); 216*277d787fSFarid Zakaria return ret; 217*277d787fSFarid Zakaria } 218*277d787fSFarid Zakaria 219*277d787fSFarid Zakaria SEC(".struct_ops.link") 220*277d787fSFarid Zakaria struct binfmt_misc_ops nix_origin = { 221*277d787fSFarid Zakaria .match = (void *)nix_origin_match, 222*277d787fSFarid Zakaria .load = (void *)nix_origin_load, 223*277d787fSFarid Zakaria .name = "nix_origin", 224*277d787fSFarid Zakaria }; 225