Created
July 13, 2026 03:25
-
-
Save fzakaria/2e1e1c44fa488a951674f8761c672366 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * shebang_origin.bpf.c - $ORIGIN-relative shebang (#!) interpreter resolution. | |
| * | |
| * A binfmt_misc_ops handler for scripts whose interpreter line is | |
| * "#!$ORIGIN/...": the interpreter is resolved relative to the directory of | |
| * the script and selected via bpf_binprm_set_interp(). Ordinary scripts are | |
| * declined so binfmt_script handles them. | |
| * | |
| * The whole shebang line is in the prefetched bprm->buf, so no file read is | |
| * needed. bprm->buf is a trusted pointer that can only be read at constant | |
| * offsets, and the interpreter is variable-length, so the fixed-size buffer | |
| * (always BINPRM_BUF_SIZE, zero-padded) is copied at unrolled constant | |
| * offsets into a map value that can then be scanned with a variable index. | |
| * | |
| * Only the no-argument form is handled; a single shebang argument | |
| * ("#!interp arg") cannot be passed through set_interp(). | |
| */ | |
| #include "vmlinux.h" | |
| #include <bpf/bpf_helpers.h> | |
| #include <bpf/bpf_tracing.h> | |
| char _license[] SEC("license") = "GPL"; | |
| #define PATH_MAX 4096 | |
| #define BINPRM_BUF_SIZE 256 | |
| #define ORIGIN_LEN 7 /* strlen("$ORIGIN") */ | |
| #define ENOENT 2 | |
| #define ENAMETOOLONG 36 | |
| 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 line[BINPRM_BUF_SIZE]; /* copy of bprm->buf (holds the #! line) */ | |
| char interp[PATH_MAX]; /* the shebang interpreter path */ | |
| char path[PATH_MAX]; /* d_path of the script, becomes the result */ | |
| }; | |
| 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; | |
| SEC("struct_ops.s/load") | |
| int BPF_PROG(shebang_origin_load, struct linux_binprm *bprm) | |
| { | |
| __u32 ilen = 0, slash, sfx, rsz; | |
| struct scratch *sc; | |
| int ret = 0, len, i; | |
| __u64 id; | |
| /* Cheap reject straight from the prefetched header (fixed offsets). */ | |
| if (bprm->buf[0] != '#' || bprm->buf[1] != '!') | |
| return 0; | |
| id = bpf_get_current_pid_tgid(); | |
| if (bpf_map_update_elem(&scratch_map, &id, &zero_scratch, BPF_ANY)) | |
| return 0; | |
| sc = bpf_map_lookup_elem(&scratch_map, &id); | |
| if (!sc) | |
| return 0; | |
| /* | |
| * Copy bprm->buf into a map value at constant offsets so the | |
| * variable-length interpreter can be scanned with a variable index. | |
| */ | |
| #pragma clang loop unroll(full) | |
| for (i = 0; i < BINPRM_BUF_SIZE; i++) | |
| sc->line[i] = bprm->buf[i]; | |
| /* Copy the interpreter that follows "#!", up to whitespace/NUL. */ | |
| bpf_for(i, 0, sizeof(sc->interp) - 1) { | |
| __u32 si = 2 + i; | |
| char c; | |
| if (si >= sizeof(sc->line)) | |
| break; | |
| c = sc->line[si]; | |
| if (c == '\n' || c == ' ' || c == '\t' || c == '\0') | |
| break; | |
| sc->interp[i] = c; | |
| ilen = i + 1; | |
| } | |
| if (ilen >= sizeof(sc->interp)) | |
| goto out; | |
| sc->interp[ilen] = '\0'; | |
| /* Only "$ORIGIN/..." is ours; anything else falls to binfmt_script. */ | |
| if (ilen <= ORIGIN_LEN + 1 || | |
| 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; | |
| /* From here the script is ours: a resolution failure fails the exec. */ | |
| ret = -ENOENT; | |
| len = bpf_path_d_path(&bprm->file->f_path, sc->path, sizeof(sc->path)); | |
| if (len <= 0 || len > sizeof(sc->path) || sc->path[0] != '/') | |
| goto out; | |
| /* $ORIGIN = dirname of the script. */ | |
| slash = 0; | |
| bpf_for(i, 1, len - 1) { | |
| if (i >= sizeof(sc->path)) | |
| break; | |
| if (sc->path[i] == '/') | |
| slash = i; | |
| } | |
| /* Splice the suffix after "$ORIGIN" (leading '/' included) onto the dir. */ | |
| sfx = ilen - ORIGIN_LEN; | |
| rsz = slash + sfx; | |
| if (rsz >= sizeof(sc->path)) { | |
| ret = -ENAMETOOLONG; | |
| goto out; | |
| } | |
| rsz &= sizeof(sc->path) - 1; | |
| barrier_var(rsz); | |
| 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]; | |
| } | |
| sc->path[rsz] = '\0'; | |
| ret = bpf_binprm_set_interp(bprm, sc->path, rsz + 1); | |
| if (!ret) | |
| ret = 1; | |
| out: | |
| bpf_map_delete_elem(&scratch_map, &id); | |
| return ret; | |
| } | |
| SEC(".struct_ops.link") | |
| struct binfmt_misc_ops shebang_origin = { | |
| .load = (void *)shebang_origin_load, | |
| .name = "shebang_origin", | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment