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