projects/linux·blob master · refs

224 lines · 6.4 KiB

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
20char _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
35extern int bpf_dynptr_from_file(struct file *file, __u32 flags,
36 struct bpf_dynptr *ptr__uninit) __ksym;
37extern int bpf_dynptr_file_discard(struct bpf_dynptr *dynptr) __ksym;
38extern int bpf_path_d_path(const struct path *path, char *buf,
39 size_t buf__sz) __ksym;
40extern int bpf_binprm_set_interp(struct linux_binprm *bprm, const char *path,
41 size_t path__sz) __ksym;
42
43struct 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. */
49struct {
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
56static const struct scratch zero_scratch;
57
58/* An ELF64 binary per the prefetched header? */
59static 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. */
67static 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 */
98SEC("struct_ops.s/match")
99bool 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 "/");
116out:
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 */
126SEC("struct_ops.s/load")
127int 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);
212out_del:
213 bpf_map_delete_elem(&scratch_map, &id);
214out:
215 bpf_dynptr_file_discard(&dp);
216 return ret;
217}
218
219SEC(".struct_ops.link")
220struct binfmt_misc_ops nix_origin = {
221 .match = (void *)nix_origin_match,
222 .load = (void *)nix_origin_load,
223 .name = "nix_origin",
224};