Skip to content

Instantly share code, notes, and snippets.

@fzakaria
Created July 13, 2026 03:25
Show Gist options
  • Select an option

  • Save fzakaria/2e1e1c44fa488a951674f8761c672366 to your computer and use it in GitHub Desktop.

Select an option

Save fzakaria/2e1e1c44fa488a951674f8761c672366 to your computer and use it in GitHub Desktop.
// 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