]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/sysupdate/sysupdate-resource.c
tree-wide: introduce PIPE_EBADF macro
[thirdparty/systemd.git] / src / sysupdate / sysupdate-resource.c
CommitLineData
43cc7a3e
LP
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include <fcntl.h>
4#include <sys/stat.h>
5#include <unistd.h>
6
7#include "alloc-util.h"
8#include "blockdev-util.h"
9#include "chase-symlinks.h"
ca822829 10#include "device-util.h"
f8a7112c 11#include "devnum-util.h"
43cc7a3e
LP
12#include "dirent-util.h"
13#include "env-util.h"
14#include "fd-util.h"
15#include "fileio.h"
16#include "glyph-util.h"
17#include "gpt.h"
18#include "hexdecoct.h"
19#include "import-util.h"
20#include "macro.h"
21#include "process-util.h"
22#include "sort-util.h"
43cc7a3e
LP
23#include "string-table.h"
24#include "sysupdate-cache.h"
25#include "sysupdate-instance.h"
26#include "sysupdate-pattern.h"
27#include "sysupdate-resource.h"
28#include "sysupdate.h"
29#include "utf8.h"
30
31void resource_destroy(Resource *rr) {
32 assert(rr);
33
34 free(rr->path);
35 strv_free(rr->patterns);
36
37 for (size_t i = 0; i < rr->n_instances; i++)
38 instance_free(rr->instances[i]);
39 free(rr->instances);
40}
41
42static int resource_add_instance(
43 Resource *rr,
44 const char *path,
45 const InstanceMetadata *f,
46 Instance **ret) {
47
48 Instance *i;
49 int r;
50
51 assert(rr);
52 assert(path);
53 assert(f);
54 assert(f->version);
55
56 if (!GREEDY_REALLOC(rr->instances, rr->n_instances + 1))
57 return log_oom();
58
59 r = instance_new(rr, path, f, &i);
60 if (r < 0)
61 return r;
62
63 rr->instances[rr->n_instances++] = i;
64
65 if (ret)
66 *ret = i;
67
68 return 0;
69}
70
71static int resource_load_from_directory(
72 Resource *rr,
73 mode_t m) {
74
75 _cleanup_(closedirp) DIR *d = NULL;
76 int r;
77
78 assert(rr);
79 assert(IN_SET(rr->type, RESOURCE_TAR, RESOURCE_REGULAR_FILE, RESOURCE_DIRECTORY, RESOURCE_SUBVOLUME));
80 assert(IN_SET(m, S_IFREG, S_IFDIR));
81
82 d = opendir(rr->path);
83 if (!d) {
84 if (errno == ENOENT) {
85 log_debug("Directory %s does not exist, not loading any resources.", rr->path);
86 return 0;
87 }
88
89 return log_error_errno(errno, "Failed to open directory '%s': %m", rr->path);
90 }
91
92 for (;;) {
93 _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
94 _cleanup_free_ char *joined = NULL;
95 Instance *instance;
96 struct dirent *de;
97 struct stat st;
98
99 errno = 0;
100 de = readdir_no_dot(d);
101 if (!de) {
102 if (errno != 0)
103 return log_error_errno(errno, "Failed to read directory '%s': %m", rr->path);
104 break;
105 }
106
107 switch (de->d_type) {
108
109 case DT_UNKNOWN:
110 break;
111
112 case DT_DIR:
113 if (m != S_IFDIR)
114 continue;
115
116 break;
117
118 case DT_REG:
119 if (m != S_IFREG)
120 continue;
121 break;
122
123 default:
124 continue;
125 }
126
127 if (fstatat(dirfd(d), de->d_name, &st, AT_NO_AUTOMOUNT) < 0) {
128 if (errno == ENOENT) /* Gone by now? */
129 continue;
130
131 return log_error_errno(errno, "Failed to stat %s/%s: %m", rr->path, de->d_name);
132 }
133
134 if ((st.st_mode & S_IFMT) != m)
135 continue;
136
137 r = pattern_match_many(rr->patterns, de->d_name, &extracted_fields);
138 if (r < 0)
139 return log_error_errno(r, "Failed to match pattern: %m");
140 if (r == 0)
141 continue;
142
143 joined = path_join(rr->path, de->d_name);
144 if (!joined)
145 return log_oom();
146
147 r = resource_add_instance(rr, joined, &extracted_fields, &instance);
148 if (r < 0)
149 return r;
150
151 /* Inherit these from the source, if not explicitly overwritten */
152 if (instance->metadata.mtime == USEC_INFINITY)
153 instance->metadata.mtime = timespec_load(&st.st_mtim) ?: USEC_INFINITY;
154
155 if (instance->metadata.mode == MODE_INVALID)
156 instance->metadata.mode = st.st_mode & 0775; /* mask out world-writability and suid and stuff, for safety */
157 }
158
159 return 0;
160}
161
162static int resource_load_from_blockdev(Resource *rr) {
163 _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
164 _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
165 size_t n_partitions;
166 int r;
167
168 assert(rr);
169
170 c = fdisk_new_context();
171 if (!c)
172 return log_oom();
173
174 r = fdisk_assign_device(c, rr->path, /* readonly= */ true);
175 if (r < 0)
176 return log_error_errno(r, "Failed to open device '%s': %m", rr->path);
177
178 if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
179 return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Disk %s has no GPT disk label, not suitable.", rr->path);
180
181 r = fdisk_get_partitions(c, &t);
182 if (r < 0)
183 return log_error_errno(r, "Failed to acquire partition table: %m");
184
185 n_partitions = fdisk_table_get_nents(t);
186 for (size_t i = 0; i < n_partitions; i++) {
187 _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
188 _cleanup_(partition_info_destroy) PartitionInfo pinfo = PARTITION_INFO_NULL;
189 Instance *instance;
190
191 r = read_partition_info(c, t, i, &pinfo);
192 if (r < 0)
193 return r;
194 if (r == 0) /* not assigned */
195 continue;
196
197 /* Check if partition type matches */
22e932f4 198 if (rr->partition_type_set && !sd_id128_equal(pinfo.type, rr->partition_type.uuid))
43cc7a3e
LP
199 continue;
200
201 /* A label of "_empty" means "not used so far" for us */
202 if (streq_ptr(pinfo.label, "_empty")) {
203 rr->n_empty++;
204 continue;
205 }
206
207 r = pattern_match_many(rr->patterns, pinfo.label, &extracted_fields);
208 if (r < 0)
209 return log_error_errno(r, "Failed to match pattern: %m");
210 if (r == 0)
211 continue;
212
213 r = resource_add_instance(rr, pinfo.device, &extracted_fields, &instance);
214 if (r < 0)
215 return r;
216
217 instance->partition_info = pinfo;
218 pinfo = (PartitionInfo) PARTITION_INFO_NULL;
219
220 /* Inherit data from source if not configured explicitly */
221 if (!instance->metadata.partition_uuid_set) {
222 instance->metadata.partition_uuid = instance->partition_info.uuid;
223 instance->metadata.partition_uuid_set = true;
224 }
225
226 if (!instance->metadata.partition_flags_set) {
227 instance->metadata.partition_flags = instance->partition_info.flags;
228 instance->metadata.partition_flags_set = true;
229 }
230
231 if (instance->metadata.read_only < 0)
232 instance->metadata.read_only = instance->partition_info.read_only;
233 }
234
235 return 0;
236}
237
238static int download_manifest(
239 const char *url,
240 bool verify_signature,
241 char **ret_buffer,
242 size_t *ret_size) {
243
244 _cleanup_free_ char *buffer = NULL, *suffixed_url = NULL;
19ee48a6 245 _cleanup_(close_pairp) int pfd[2] = PIPE_EBADF;
43cc7a3e
LP
246 _cleanup_fclose_ FILE *manifest = NULL;
247 size_t size = 0;
248 pid_t pid;
249 int r;
250
251 assert(url);
252 assert(ret_buffer);
253 assert(ret_size);
254
255 /* Download a SHA256SUMS file as manifest */
256
257 r = import_url_append_component(url, "SHA256SUMS", &suffixed_url);
258 if (r < 0)
259 return log_error_errno(r, "Failed to append SHA256SUMS to URL: %m");
260
261 if (pipe2(pfd, O_CLOEXEC) < 0)
262 return log_error_errno(errno, "Failed to allocate pipe: %m");
263
28e5e1e9
DT
264 log_info("%s Acquiring manifest file %s%s", special_glyph(SPECIAL_GLYPH_DOWNLOAD),
265 suffixed_url, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
43cc7a3e
LP
266
267 r = safe_fork("(sd-pull)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
268 if (r < 0)
269 return r;
270 if (r == 0) {
271 /* Child */
272
273 const char *cmdline[] = {
274 "systemd-pull",
275 "raw",
276 "--direct", /* just download the specified URL, don't download anything else */
277 "--verify", verify_signature ? "signature" : "no", /* verify the manifest file */
278 suffixed_url,
279 "-", /* write to stdout */
280 NULL
281 };
282
283 pfd[0] = safe_close(pfd[0]);
284
285 r = rearrange_stdio(-1, pfd[1], STDERR_FILENO);
286 if (r < 0) {
287 log_error_errno(r, "Failed to rearrange stdin/stdout: %m");
288 _exit(EXIT_FAILURE);
289 }
290
291 (void) unsetenv("NOTIFY_SOCKET");
292 execv(pull_binary_path(), (char *const*) cmdline);
293 log_error_errno(errno, "Failed to execute %s tool: %m", pull_binary_path());
294 _exit(EXIT_FAILURE);
295 };
296
297 pfd[1] = safe_close(pfd[1]);
298
299 /* We'll first load the entire manifest into memory before parsing it. That's because the
300 * systemd-pull tool can validate the download only after its completion, but still pass the data to
301 * us as it runs. We thus need to check the return value of the process *before* parsing, to be
302 * reasonably safe. */
303
304 manifest = fdopen(pfd[0], "r");
305 if (!manifest)
306 return log_error_errno(errno, "Failed allocate FILE object for manifest file: %m");
307
308 TAKE_FD(pfd[0]);
309
310 r = read_full_stream(manifest, &buffer, &size);
311 if (r < 0)
312 return log_error_errno(r, "Failed to read manifest file from child: %m");
313
314 manifest = safe_fclose(manifest);
315
316 r = wait_for_terminate_and_check("(sd-pull)", pid, WAIT_LOG);
317 if (r < 0)
318 return r;
319 if (r != 0)
320 return -EPROTO;
321
322 *ret_buffer = TAKE_PTR(buffer);
323 *ret_size = size;
324
325 return 0;
326}
327
328static int resource_load_from_web(
329 Resource *rr,
330 bool verify,
331 Hashmap **web_cache) {
332
333 size_t manifest_size = 0, left = 0;
334 _cleanup_free_ char *buf = NULL;
335 const char *manifest, *p;
336 size_t line_nr = 1;
337 WebCacheItem *ci;
338 int r;
339
340 assert(rr);
341
342 ci = web_cache ? web_cache_get_item(*web_cache, rr->path, verify) : NULL;
343 if (ci) {
344 log_debug("Manifest web cache hit for %s.", rr->path);
345
346 manifest = (char*) ci->data;
347 manifest_size = ci->size;
348 } else {
349 log_debug("Manifest web cache miss for %s.", rr->path);
350
351 r = download_manifest(rr->path, verify, &buf, &manifest_size);
352 if (r < 0)
353 return r;
354
355 manifest = buf;
356 }
357
358 if (memchr(manifest, 0, manifest_size))
359 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file has embedded NUL byte, refusing.");
360 if (!utf8_is_valid_n(manifest, manifest_size))
361 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Manifest file is not valid UTF-8, refusing.");
362
363 p = manifest;
364 left = manifest_size;
365
366 while (left > 0) {
367 _cleanup_(instance_metadata_destroy) InstanceMetadata extracted_fields = INSTANCE_METADATA_NULL;
368 _cleanup_free_ char *fn = NULL;
369 _cleanup_free_ void *h = NULL;
370 Instance *instance;
371 const char *e;
372 size_t hlen;
373
374 /* 64 character hash + separator + filename + newline */
375 if (left < 67)
376 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Corrupt manifest at line %zu, refusing.", line_nr);
377
378 if (p[0] == '\\')
379 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr);
380
381 r = unhexmem(p, 64, &h, &hlen);
382 if (r < 0)
383 return log_error_errno(r, "Failed to parse digest at manifest line %zu, refusing.", line_nr);
384
385 p += 64, left -= 64;
386
387 if (*p != ' ')
388 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing space separator at manifest line %zu, refusing.", line_nr);
389 p++, left--;
390
391 if (!IN_SET(*p, '*', ' '))
392 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr);
393 p++, left--;
394
395 e = memchr(p, '\n', left);
396 if (!e)
397 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Truncated manifest file at line %zu, refusing.", line_nr);
398 if (e == p)
399 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty filename specified at manifest line %zu, refusing.", line_nr);
400
401 fn = strndup(p, e - p);
402 if (!fn)
403 return log_oom();
404
405 if (!filename_is_valid(fn))
406 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid filename specified at manifest line %zu, refusing.", line_nr);
407 if (string_has_cc(fn, NULL))
408 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Filename contains control characters at manifest line %zu, refusing.", line_nr);
409
410 r = pattern_match_many(rr->patterns, fn, &extracted_fields);
411 if (r < 0)
412 return log_error_errno(r, "Failed to match pattern: %m");
413 if (r > 0) {
414 _cleanup_free_ char *path = NULL;
415
416 r = import_url_append_component(rr->path, fn, &path);
417 if (r < 0)
418 return log_error_errno(r, "Failed to build instance URL: %m");
419
420 r = resource_add_instance(rr, path, &extracted_fields, &instance);
421 if (r < 0)
422 return r;
423
424 assert(hlen == sizeof(instance->metadata.sha256sum));
425
426 if (instance->metadata.sha256sum_set) {
427 if (memcmp(instance->metadata.sha256sum, h, hlen) != 0)
428 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr);
429 } else {
430 memcpy(instance->metadata.sha256sum, h, hlen);
431 instance->metadata.sha256sum_set = true;
432 }
433 }
434
435 left -= (e - p) + 1;
436 p = e + 1;
437
438 line_nr++;
439 }
440
441 if (!ci && web_cache) {
442 r = web_cache_add_item(web_cache, rr->path, verify, manifest, manifest_size);
443 if (r < 0)
444 log_debug_errno(r, "Failed to add manifest '%s' to cache, ignoring: %m", rr->path);
445 else
446 log_debug("Added manifest '%s' to cache.", rr->path);
447 }
448
449 return 0;
450}
451
452static int instance_cmp(Instance *const*a, Instance *const*b) {
453 int r;
454
455 assert(a);
456 assert(b);
457 assert(*a);
458 assert(*b);
459 assert((*a)->metadata.version);
460 assert((*b)->metadata.version);
461
462 /* Newest version at the beginning */
463 r = strverscmp_improved((*a)->metadata.version, (*b)->metadata.version);
464 if (r != 0)
465 return -r;
466
467 /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all,
468 * and since we allow multiple matching patterns not even in directories they are unique). Hence
469 * let's order by path as secondary ordering key. */
470 return path_compare((*a)->path, (*b)->path);
471}
472
473int resource_load_instances(Resource *rr, bool verify, Hashmap **web_cache) {
474 int r;
475
476 assert(rr);
477
478 switch (rr->type) {
479
480 case RESOURCE_TAR:
481 case RESOURCE_REGULAR_FILE:
482 r = resource_load_from_directory(rr, S_IFREG);
483 break;
484
485 case RESOURCE_DIRECTORY:
486 case RESOURCE_SUBVOLUME:
487 r = resource_load_from_directory(rr, S_IFDIR);
488 break;
489
490 case RESOURCE_PARTITION:
491 r = resource_load_from_blockdev(rr);
492 break;
493
494 case RESOURCE_URL_FILE:
495 case RESOURCE_URL_TAR:
496 r = resource_load_from_web(rr, verify, web_cache);
497 break;
498
499 default:
500 assert_not_reached();
501 }
502 if (r < 0)
503 return r;
504
505 typesafe_qsort(rr->instances, rr->n_instances, instance_cmp);
506 return 0;
507}
508
509Instance* resource_find_instance(Resource *rr, const char *version) {
510 Instance key = {
511 .metadata.version = (char*) version,
512 }, *k = &key;
513
514 return typesafe_bsearch(&k, rr->instances, rr->n_instances, instance_cmp);
515}
516
517int resource_resolve_path(
518 Resource *rr,
519 const char *root,
520 const char *node) {
521
522 _cleanup_free_ char *p = NULL;
523 dev_t d;
524 int r;
525
526 assert(rr);
527
528 if (rr->path_auto) {
f8a7112c 529 struct stat orig_root_stats;
43cc7a3e 530
f8a7112c 531 /* NB: If the root mount has been replaced by some form of volatile file system (overlayfs),
532 * the original root block device node is symlinked in /run/systemd/volatile-root. Let's
533 * follow that link here. If that doesn't exist, we check the backing device of "/usr". We
534 * don't actually check the backing device of the root fs "/", in order to support
535 * environments where the root fs is a tmpfs, and the OS itself placed exclusively in
536 * /usr/. */
43cc7a3e
LP
537
538 if (rr->type != RESOURCE_PARTITION)
539 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
540 "Automatic root path discovery only supported for partition resources.");
541
542 if (node) { /* If --image= is specified, directly use the loopback device */
543 r = free_and_strdup_warn(&rr->path, node);
544 if (r < 0)
545 return r;
546
547 return 0;
548 }
549
550 if (root)
551 return log_error_errno(SYNTHETIC_ERRNO(EPERM),
552 "Block device is not allowed when using --root= mode.");
553
f8a7112c 554 r = stat("/run/systemd/volatile-root", &orig_root_stats);
555 if (r < 0) {
556 if (errno == -ENOENT) /* volatile-root not found */
557 r = get_block_device_harder("/usr/", &d);
558 else
559 return log_error_errno(r, "Failed to stat /run/systemd/volatile-root: %m");
560 } else if (!S_ISBLK(orig_root_stats.st_mode)) /* symlink was present but not block device */
561 return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "/run/systemd/volatile-root is not linked to a block device.");
562 else /* symlink was present and a block device */
563 d = orig_root_stats.st_rdev;
43cc7a3e
LP
564
565 } else if (rr->type == RESOURCE_PARTITION) {
254d1313 566 _cleanup_close_ int fd = -EBADF, real_fd = -EBADF;
43cc7a3e
LP
567 _cleanup_free_ char *resolved = NULL;
568 struct stat st;
569
570 r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, &fd);
571 if (r < 0)
572 return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
573
574 if (fstat(fd, &st) < 0)
7a692931 575 return log_error_errno(errno, "Failed to stat '%s': %m", resolved);
43cc7a3e
LP
576
577 if (S_ISBLK(st.st_mode) && root)
578 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "When using --root= or --image= access to device nodes is prohibited.");
579
580 if (S_ISREG(st.st_mode) || S_ISBLK(st.st_mode)) {
581 /* Not a directory, hence no need to find backing block device for the path */
582 free_and_replace(rr->path, resolved);
583 return 0;
584 }
585
586 if (!S_ISDIR(st.st_mode))
587 return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Target path '%s' does not refer to regular file, directory or block device, refusing.", rr->path);
588
589 if (node) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */
590 r = free_and_strdup_warn(&rr->path, node);
591 if (r < 0)
592 return r;
593
594 return 0;
595 }
596
597 real_fd = fd_reopen(fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
598 if (real_fd < 0)
599 return log_error_errno(real_fd, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr->path);
600
601 r = get_block_device_harder_fd(fd, &d);
602
603 } else if (RESOURCE_IS_FILESYSTEM(rr->type) && root) {
604 _cleanup_free_ char *resolved = NULL;
605
606 r = chase_symlinks(rr->path, root, CHASE_PREFIX_ROOT, &resolved, NULL);
607 if (r < 0)
608 return log_error_errno(r, "Failed to resolve '%s': %m", rr->path);
609
610 free_and_replace(rr->path, resolved);
611 return 0;
612 } else
613 return 0; /* Otherwise assume there's nothing to resolve */
614
615 if (r < 0)
616 return log_error_errno(r, "Failed to determine block device of file system: %m");
617
618 r = block_get_whole_disk(d, &d);
619 if (r < 0)
620 return log_error_errno(r, "Failed to find whole disk device for partition backing file system: %m");
621 if (r == 0)
622 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
623 "File system is not placed on a partition block device, cannot determine whole block device backing root file system.");
624
4fe46c34 625 r = devname_from_devnum(S_IFBLK, d, &p);
43cc7a3e
LP
626 if (r < 0)
627 return r;
628
629 if (rr->path)
630 log_info("Automatically discovered block device '%s' from '%s'.", p, rr->path);
631 else
632 log_info("Automatically discovered root block device '%s'.", p);
633
634 free_and_replace(rr->path, p);
635 return 1;
636}
637
638static const char *resource_type_table[_RESOURCE_TYPE_MAX] = {
639 [RESOURCE_URL_FILE] = "url-file",
640 [RESOURCE_URL_TAR] = "url-tar",
641 [RESOURCE_TAR] = "tar",
642 [RESOURCE_PARTITION] = "partition",
643 [RESOURCE_REGULAR_FILE] = "regular-file",
644 [RESOURCE_DIRECTORY] = "directory",
645 [RESOURCE_SUBVOLUME] = "subvolume",
646};
647
648DEFINE_STRING_TABLE_LOOKUP(resource_type, ResourceType);