/src/systemd/src/core/bpf-devices.c
Line | Count | Source |
1 | | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | | |
3 | | #include <fnmatch.h> |
4 | | #include <linux/bpf.h> |
5 | | #include <linux/bpf_insn.h> |
6 | | #include <sys/stat.h> |
7 | | |
8 | | #include "alloc-util.h" |
9 | | #include "bpf-devices.h" |
10 | | #include "bpf-program.h" |
11 | | #include "cgroup.h" |
12 | | #include "devnum-util.h" |
13 | | #include "fd-util.h" |
14 | | #include "fileio.h" |
15 | | #include "log.h" |
16 | | #include "nulstr-util.h" |
17 | | #include "parse-util.h" |
18 | | #include "path-util.h" |
19 | | #include "stat-util.h" |
20 | | #include "string-util.h" |
21 | | |
22 | 0 | #define PASS_JUMP_OFF 4096 |
23 | | |
24 | | /* Ensure the high level flags we use and the low-level BPF flags exposed on the kernel are defined the same way */ |
25 | | assert_cc((unsigned) BPF_DEVCG_ACC_MKNOD == (unsigned) CGROUP_DEVICE_MKNOD); |
26 | | assert_cc((unsigned) BPF_DEVCG_ACC_READ == (unsigned) CGROUP_DEVICE_READ); |
27 | | assert_cc((unsigned) BPF_DEVCG_ACC_WRITE == (unsigned) CGROUP_DEVICE_WRITE); |
28 | | |
29 | | static int bpf_prog_allow_list_device( |
30 | | BPFProgram *prog, |
31 | | char type, |
32 | | unsigned major, |
33 | | unsigned minor, |
34 | 0 | CGroupDevicePermissions p) { |
35 | |
|
36 | 0 | int r; |
37 | |
|
38 | 0 | assert(prog); |
39 | |
|
40 | 0 | log_trace("%s: %c %u:%u %s", __func__, type, major, minor, cgroup_device_permissions_to_string(p)); |
41 | |
|
42 | 0 | if (p <= 0 || p >= _CGROUP_DEVICE_PERMISSIONS_MAX) |
43 | 0 | return -EINVAL; |
44 | | |
45 | 0 | assert(IN_SET(type, 'b', 'c')); |
46 | 0 | const int bpf_type = type == 'c' ? BPF_DEVCG_DEV_CHAR : BPF_DEVCG_DEV_BLOCK; |
47 | |
|
48 | 0 | const struct bpf_insn insn[] = { |
49 | 0 | BPF_MOV32_REG(BPF_REG_1, BPF_REG_3), |
50 | 0 | BPF_ALU32_IMM(BPF_AND, BPF_REG_1, p), |
51 | 0 | BPF_JMP_REG(BPF_JNE, BPF_REG_1, BPF_REG_3, 4), /* compare access type */ |
52 | |
|
53 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_2, bpf_type, 3), /* compare device type */ |
54 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_4, major, 2), /* compare major */ |
55 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_5, minor, 1), /* compare minor */ |
56 | 0 | BPF_JMP_A(PASS_JUMP_OFF), /* jump to PASS */ |
57 | 0 | }; |
58 | |
|
59 | 0 | if (p == _CGROUP_DEVICE_PERMISSIONS_ALL) |
60 | 0 | r = bpf_program_add_instructions(prog, insn + 3, ELEMENTSOF(insn) - 3); |
61 | 0 | else |
62 | 0 | r = bpf_program_add_instructions(prog, insn, ELEMENTSOF(insn)); |
63 | 0 | if (r < 0) |
64 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
65 | | |
66 | 0 | return 1; /* return 1 → we did something */ |
67 | 0 | } |
68 | | |
69 | | static int bpf_prog_allow_list_major( |
70 | | BPFProgram *prog, |
71 | | char type, |
72 | | unsigned major, |
73 | 0 | CGroupDevicePermissions p) { |
74 | |
|
75 | 0 | int r; |
76 | |
|
77 | 0 | assert(prog); |
78 | |
|
79 | 0 | log_trace("%s: %c %u:* %s", __func__, type, major, cgroup_device_permissions_to_string(p)); |
80 | |
|
81 | 0 | if (p <= 0 || p >= _CGROUP_DEVICE_PERMISSIONS_MAX) |
82 | 0 | return -EINVAL; |
83 | | |
84 | 0 | assert(IN_SET(type, 'b', 'c')); |
85 | 0 | const int bpf_type = type == 'c' ? BPF_DEVCG_DEV_CHAR : BPF_DEVCG_DEV_BLOCK; |
86 | |
|
87 | 0 | const struct bpf_insn insn[] = { |
88 | 0 | BPF_MOV32_REG(BPF_REG_1, BPF_REG_3), |
89 | 0 | BPF_ALU32_IMM(BPF_AND, BPF_REG_1, p), |
90 | 0 | BPF_JMP_REG(BPF_JNE, BPF_REG_1, BPF_REG_3, 3), /* compare access type */ |
91 | |
|
92 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_2, bpf_type, 2), /* compare device type */ |
93 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_4, major, 1), /* compare major */ |
94 | 0 | BPF_JMP_A(PASS_JUMP_OFF), /* jump to PASS */ |
95 | 0 | }; |
96 | |
|
97 | 0 | if (p == _CGROUP_DEVICE_PERMISSIONS_ALL) |
98 | 0 | r = bpf_program_add_instructions(prog, insn + 3, ELEMENTSOF(insn) - 3); |
99 | 0 | else |
100 | 0 | r = bpf_program_add_instructions(prog, insn, ELEMENTSOF(insn)); |
101 | 0 | if (r < 0) |
102 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
103 | | |
104 | 0 | return 1; /* return 1 → we did something */ |
105 | 0 | } |
106 | | |
107 | | static int bpf_prog_allow_list_class( |
108 | | BPFProgram *prog, |
109 | | char type, |
110 | 0 | CGroupDevicePermissions p) { |
111 | |
|
112 | 0 | int r; |
113 | |
|
114 | 0 | assert(prog); |
115 | |
|
116 | 0 | log_trace("%s: %c *:* %s", __func__, type, cgroup_device_permissions_to_string(p)); |
117 | |
|
118 | 0 | if (p <= 0 || p >= _CGROUP_DEVICE_PERMISSIONS_MAX) |
119 | 0 | return -EINVAL; |
120 | | |
121 | 0 | assert(IN_SET(type, 'b', 'c')); |
122 | 0 | const int bpf_type = type == 'c' ? BPF_DEVCG_DEV_CHAR : BPF_DEVCG_DEV_BLOCK; |
123 | |
|
124 | 0 | const struct bpf_insn insn[] = { |
125 | 0 | BPF_MOV32_REG(BPF_REG_1, BPF_REG_3), |
126 | 0 | BPF_ALU32_IMM(BPF_AND, BPF_REG_1, p), |
127 | 0 | BPF_JMP_REG(BPF_JNE, BPF_REG_1, BPF_REG_3, 2), /* compare access type */ |
128 | |
|
129 | 0 | BPF_JMP_IMM(BPF_JNE, BPF_REG_2, bpf_type, 1), /* compare device type */ |
130 | 0 | BPF_JMP_A(PASS_JUMP_OFF), /* jump to PASS */ |
131 | 0 | }; |
132 | |
|
133 | 0 | if (p == _CGROUP_DEVICE_PERMISSIONS_ALL) |
134 | 0 | r = bpf_program_add_instructions(prog, insn + 3, ELEMENTSOF(insn) - 3); |
135 | 0 | else |
136 | 0 | r = bpf_program_add_instructions(prog, insn, ELEMENTSOF(insn)); |
137 | 0 | if (r < 0) |
138 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
139 | | |
140 | 0 | return 1; /* return 1 → we did something */ |
141 | 0 | } |
142 | | |
143 | | int bpf_devices_cgroup_init( |
144 | | BPFProgram **ret, |
145 | | CGroupDevicePolicy policy, |
146 | 0 | bool allow_list) { |
147 | |
|
148 | 0 | const struct bpf_insn pre_insn[] = { |
149 | | /* load device type to r2 */ |
150 | 0 | BPF_LDX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, |
151 | 0 | offsetof(struct bpf_cgroup_dev_ctx, access_type)), |
152 | 0 | BPF_ALU32_IMM(BPF_AND, BPF_REG_2, 0xFFFF), |
153 | | |
154 | | /* load access type to r3 */ |
155 | 0 | BPF_LDX_MEM(BPF_W, BPF_REG_3, BPF_REG_1, |
156 | 0 | offsetof(struct bpf_cgroup_dev_ctx, access_type)), |
157 | 0 | BPF_ALU32_IMM(BPF_RSH, BPF_REG_3, 16), |
158 | | |
159 | | /* load major number to r4 */ |
160 | 0 | BPF_LDX_MEM(BPF_W, BPF_REG_4, BPF_REG_1, |
161 | 0 | offsetof(struct bpf_cgroup_dev_ctx, major)), |
162 | | |
163 | | /* load minor number to r5 */ |
164 | 0 | BPF_LDX_MEM(BPF_W, BPF_REG_5, BPF_REG_1, |
165 | 0 | offsetof(struct bpf_cgroup_dev_ctx, minor)), |
166 | 0 | }; |
167 | |
|
168 | 0 | _cleanup_(bpf_program_freep) BPFProgram *prog = NULL; |
169 | 0 | int r; |
170 | |
|
171 | 0 | assert(ret); |
172 | |
|
173 | 0 | if (policy == CGROUP_DEVICE_POLICY_AUTO && !allow_list) { |
174 | 0 | *ret = NULL; |
175 | 0 | return 0; |
176 | 0 | } |
177 | | |
178 | 0 | r = bpf_program_new(BPF_PROG_TYPE_CGROUP_DEVICE, "sd_devices", &prog); |
179 | 0 | if (r < 0) |
180 | 0 | return log_error_errno(r, "Loading device control BPF program failed: %m"); |
181 | | |
182 | 0 | if (policy == CGROUP_DEVICE_POLICY_CLOSED || allow_list) { |
183 | 0 | r = bpf_program_add_instructions(prog, pre_insn, ELEMENTSOF(pre_insn)); |
184 | 0 | if (r < 0) |
185 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
186 | 0 | } |
187 | | |
188 | 0 | *ret = TAKE_PTR(prog); |
189 | 0 | return 1; |
190 | 0 | } |
191 | | |
192 | | int bpf_devices_apply_policy( |
193 | | BPFProgram **prog, |
194 | | CGroupDevicePolicy policy, |
195 | | bool allow_list, |
196 | | const char *cgroup_path, |
197 | 0 | BPFProgram **prog_installed) { |
198 | |
|
199 | 0 | _cleanup_free_ char *controller_path = NULL; |
200 | 0 | int r; |
201 | | |
202 | | /* This will assign *prog_installed if everything goes well. */ |
203 | |
|
204 | 0 | assert(prog); |
205 | 0 | if (!*prog) |
206 | 0 | goto finish; |
207 | | |
208 | 0 | const bool deny_everything = policy == CGROUP_DEVICE_POLICY_STRICT && !allow_list; |
209 | |
|
210 | 0 | const struct bpf_insn post_insn[] = { |
211 | | /* return DENY */ |
212 | 0 | BPF_MOV64_IMM(BPF_REG_0, 0), |
213 | 0 | BPF_JMP_A(1), |
214 | 0 | }; |
215 | |
|
216 | 0 | const struct bpf_insn exit_insn[] = { |
217 | | /* finally return DENY if deny_everything else ALLOW */ |
218 | 0 | BPF_MOV64_IMM(BPF_REG_0, deny_everything ? 0 : 1), |
219 | 0 | BPF_EXIT_INSN() |
220 | 0 | }; |
221 | |
|
222 | 0 | if (!deny_everything) { |
223 | 0 | r = bpf_program_add_instructions(*prog, post_insn, ELEMENTSOF(post_insn)); |
224 | 0 | if (r < 0) |
225 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
226 | | |
227 | | /* Fixup PASS_JUMP_OFF jump offsets. */ |
228 | 0 | for (size_t off = 0; off < (*prog)->n_instructions; off++) { |
229 | 0 | struct bpf_insn *ins = &((*prog)->instructions[off]); |
230 | |
|
231 | 0 | if (ins->code == (BPF_JMP | BPF_JA) && ins->off == PASS_JUMP_OFF) |
232 | 0 | ins->off = (*prog)->n_instructions - off - 1; |
233 | 0 | } |
234 | 0 | } |
235 | | |
236 | 0 | r = bpf_program_add_instructions(*prog, exit_insn, ELEMENTSOF(exit_insn)); |
237 | 0 | if (r < 0) |
238 | 0 | return log_error_errno(r, "Extending device control BPF program failed: %m"); |
239 | | |
240 | 0 | r = cg_get_path(cgroup_path, /* suffix= */ NULL, &controller_path); |
241 | 0 | if (r < 0) |
242 | 0 | return log_error_errno(r, "Failed to determine cgroup path: %m"); |
243 | | |
244 | 0 | r = bpf_program_cgroup_attach(*prog, BPF_CGROUP_DEVICE, controller_path, BPF_F_ALLOW_MULTI); |
245 | 0 | if (r < 0) |
246 | 0 | return log_error_errno(r, "Attaching device control BPF program to cgroup %s failed: %m", |
247 | 0 | empty_to_root(cgroup_path)); |
248 | | |
249 | 0 | finish: |
250 | | /* Unref the old BPF program (which will implicitly detach it) right before attaching the new program. */ |
251 | 0 | if (prog_installed) { |
252 | 0 | bpf_program_free(*prog_installed); |
253 | 0 | *prog_installed = TAKE_PTR(*prog); |
254 | 0 | } |
255 | 0 | return 0; |
256 | 0 | } |
257 | | |
258 | | static int allow_list_device_pattern( |
259 | | BPFProgram *prog, |
260 | | const char *path, |
261 | | char type, |
262 | | unsigned major, |
263 | | unsigned minor, |
264 | 0 | CGroupDevicePermissions p) { |
265 | |
|
266 | 0 | assert(IN_SET(type, 'b', 'c')); |
267 | |
|
268 | 0 | if (!prog) |
269 | 0 | return 0; |
270 | | |
271 | 0 | if (major != UINT_MAX && minor != UINT_MAX) |
272 | 0 | return bpf_prog_allow_list_device(prog, type, major, minor, p); |
273 | 0 | if (major != UINT_MAX) |
274 | 0 | return bpf_prog_allow_list_major(prog, type, major, p); |
275 | | |
276 | 0 | return bpf_prog_allow_list_class(prog, type, p); |
277 | 0 | } |
278 | | |
279 | | int bpf_devices_allow_list_device( |
280 | | BPFProgram *prog, |
281 | | const char *path, |
282 | | const char *node, |
283 | 0 | CGroupDevicePermissions p) { |
284 | |
|
285 | 0 | mode_t mode; |
286 | 0 | dev_t rdev; |
287 | 0 | int r; |
288 | |
|
289 | 0 | assert(path); |
290 | 0 | assert(p >= 0 && p < _CGROUP_DEVICE_PERMISSIONS_MAX); |
291 | |
|
292 | 0 | log_trace("%s: %s %s", __func__, node, cgroup_device_permissions_to_string(p)); |
293 | | |
294 | | /* Some special handling for /dev/block/%u:%u, /dev/char/%u:%u, /run/systemd/inaccessible/chr and |
295 | | * /run/systemd/inaccessible/blk paths. Instead of stat()ing these we parse out the major/minor directly. This |
296 | | * means clients can use these path without the device node actually around */ |
297 | 0 | r = device_path_parse_major_minor(node, &mode, &rdev); |
298 | 0 | if (r < 0) { |
299 | 0 | if (r != -ENODEV) |
300 | 0 | return log_warning_errno(r, "Couldn't parse major/minor from device path '%s': %m", node); |
301 | | |
302 | 0 | struct stat st; |
303 | 0 | if (stat(node, &st) < 0) { |
304 | 0 | if (errno == ENOENT) { |
305 | 0 | log_debug_errno(errno, "Device '%s' does not exist, skipping.", node); |
306 | 0 | return 0; /* returning 0 means → skipped */ |
307 | 0 | } |
308 | | |
309 | 0 | return log_warning_errno(errno, "Couldn't stat device %s: %m", node); |
310 | 0 | } |
311 | | |
312 | 0 | r = stat_verify_device_node(&st); |
313 | 0 | if (r < 0) |
314 | 0 | return log_warning_errno(r, "'%s' is not a device node.", node); |
315 | | |
316 | 0 | mode = st.st_mode; |
317 | 0 | rdev = (dev_t) st.st_rdev; |
318 | 0 | } |
319 | | |
320 | 0 | return allow_list_device_pattern(prog, path, S_ISCHR(mode) ? 'c' : 'b', major(rdev), minor(rdev), p); |
321 | 0 | } |
322 | | |
323 | | int bpf_devices_allow_list_major( |
324 | | BPFProgram *prog, |
325 | | const char *path, |
326 | | const char *name, |
327 | | char type, |
328 | 0 | CGroupDevicePermissions permissions) { |
329 | |
|
330 | 0 | unsigned major; |
331 | 0 | int r; |
332 | |
|
333 | 0 | assert(path); |
334 | 0 | assert(IN_SET(type, 'b', 'c')); |
335 | 0 | assert(permissions >= 0 && permissions < _CGROUP_DEVICE_PERMISSIONS_MAX); |
336 | |
|
337 | 0 | if (streq(name, "*")) |
338 | | /* If the name is a wildcard, then apply this list to all devices of this type */ |
339 | 0 | return allow_list_device_pattern(prog, path, type, /* major= */ UINT_MAX, /* minor= */ UINT_MAX, permissions); |
340 | | |
341 | 0 | if (safe_atou(name, &major) >= 0 && DEVICE_MAJOR_VALID(major)) |
342 | | /* The name is numeric and suitable as major. In that case, let's take its major, and create |
343 | | * the entry directly. */ |
344 | 0 | return allow_list_device_pattern(prog, path, type, major, /* minor= */ UINT_MAX, permissions); |
345 | | |
346 | 0 | _cleanup_fclose_ FILE *f = NULL; |
347 | 0 | bool good = false, any = false; |
348 | |
|
349 | 0 | f = fopen("/proc/devices", "re"); |
350 | 0 | if (!f) |
351 | 0 | return log_warning_errno(errno, "Cannot open /proc/devices to resolve %s: %m", name); |
352 | | |
353 | 0 | for (;;) { |
354 | 0 | _cleanup_free_ char *line = NULL; |
355 | 0 | char *w, *p; |
356 | |
|
357 | 0 | r = read_line(f, LONG_LINE_MAX, &line); |
358 | 0 | if (r < 0) |
359 | 0 | return log_warning_errno(r, "Failed to read /proc/devices: %m"); |
360 | 0 | if (r == 0) |
361 | 0 | break; |
362 | | |
363 | 0 | if (type == 'c' && streq(line, "Character devices:")) { |
364 | 0 | good = true; |
365 | 0 | continue; |
366 | 0 | } |
367 | | |
368 | 0 | if (type == 'b' && streq(line, "Block devices:")) { |
369 | 0 | good = true; |
370 | 0 | continue; |
371 | 0 | } |
372 | | |
373 | 0 | if (isempty(line)) { |
374 | 0 | good = false; |
375 | 0 | continue; |
376 | 0 | } |
377 | | |
378 | 0 | if (!good) |
379 | 0 | continue; |
380 | | |
381 | 0 | p = strstrip(line); |
382 | |
|
383 | 0 | w = strpbrk(p, WHITESPACE); |
384 | 0 | if (!w) |
385 | 0 | continue; |
386 | 0 | *w = 0; |
387 | |
|
388 | 0 | r = safe_atou(p, &major); |
389 | 0 | if (r < 0) |
390 | 0 | continue; |
391 | 0 | if (major <= 0) |
392 | 0 | continue; |
393 | | |
394 | 0 | w++; |
395 | 0 | w += strspn(w, WHITESPACE); |
396 | |
|
397 | 0 | if (fnmatch(name, w, 0) != 0) |
398 | 0 | continue; |
399 | | |
400 | 0 | if (allow_list_device_pattern(prog, path, type, major, /* minor= */ UINT_MAX, permissions) > 0) |
401 | 0 | any = true; |
402 | 0 | } |
403 | | |
404 | 0 | if (!any) |
405 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), |
406 | 0 | "Device allow list pattern \"%s\" did not match anything.", name); |
407 | | |
408 | 0 | return any; |
409 | 0 | } |
410 | | |
411 | | int bpf_devices_allow_list_static( |
412 | | BPFProgram *prog, |
413 | 0 | const char *path) { |
414 | |
|
415 | 0 | static const char auto_devices[] = |
416 | 0 | "/dev/null\0" "rwm\0" |
417 | 0 | "/dev/zero\0" "rwm\0" |
418 | 0 | "/dev/full\0" "rwm\0" |
419 | 0 | "/dev/random\0" "rwm\0" |
420 | 0 | "/dev/urandom\0" "rwm\0" |
421 | 0 | "/dev/tty\0" "rwm\0" |
422 | 0 | "/dev/ptmx\0" "rwm\0" |
423 | | /* Allow /run/systemd/inaccessible/{chr,blk} devices for mapping InaccessiblePaths */ |
424 | 0 | "/run/systemd/inaccessible/chr\0" "rwm\0" |
425 | 0 | "/run/systemd/inaccessible/blk\0" "rwm\0"; |
426 | 0 | int r = 0, k; |
427 | |
|
428 | 0 | NULSTR_FOREACH_PAIR(node, acc, auto_devices) { |
429 | 0 | k = bpf_devices_allow_list_device(prog, path, node, cgroup_device_permissions_from_string(acc)); |
430 | 0 | if ((r >= 0 && k < 0) || (r >= 0 && k > 0)) |
431 | 0 | r = k; |
432 | 0 | } |
433 | | |
434 | | /* PTS (/dev/pts) devices may not be duplicated, but accessed */ |
435 | 0 | k = bpf_devices_allow_list_major(prog, path, "pts", 'c', CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE); |
436 | 0 | if ((r >= 0 && k < 0) || (r >= 0 && k > 0)) |
437 | 0 | r = k; |
438 | |
|
439 | 0 | return r; |
440 | 0 | } |