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