| // 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", |
| }; |