xref: /linux/tools/testing/selftests/exec/nix_origin.bpf.c (revision b9cba7ebfe539f3e4bbdd03a1e0efa3b30b3f592)
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