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