1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
7 #include "alloc-util.h"
8 #include "blockdev-util.h"
9 #include "chase-symlinks.h"
10 #include "dirent-util.h"
14 #include "glyph-util.h"
16 #include "hexdecoct.h"
17 #include "import-util.h"
19 #include "process-util.h"
20 #include "sort-util.h"
21 #include "stat-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"
30 void resource_destroy(Resource
*rr
) {
34 strv_free(rr
->patterns
);
36 for (size_t i
= 0; i
< rr
->n_instances
; i
++)
37 instance_free(rr
->instances
[i
]);
41 static int resource_add_instance(
44 const InstanceMetadata
*f
,
55 if (!GREEDY_REALLOC(rr
->instances
, rr
->n_instances
+ 1))
58 r
= instance_new(rr
, path
, f
, &i
);
62 rr
->instances
[rr
->n_instances
++] = i
;
70 static int resource_load_from_directory(
74 _cleanup_(closedirp
) DIR *d
= NULL
;
78 assert(IN_SET(rr
->type
, RESOURCE_TAR
, RESOURCE_REGULAR_FILE
, RESOURCE_DIRECTORY
, RESOURCE_SUBVOLUME
));
79 assert(IN_SET(m
, S_IFREG
, S_IFDIR
));
81 d
= opendir(rr
->path
);
83 if (errno
== ENOENT
) {
84 log_debug("Directory %s does not exist, not loading any resources.", rr
->path
);
88 return log_error_errno(errno
, "Failed to open directory '%s': %m", rr
->path
);
92 _cleanup_(instance_metadata_destroy
) InstanceMetadata extracted_fields
= INSTANCE_METADATA_NULL
;
93 _cleanup_free_
char *joined
= NULL
;
99 de
= readdir_no_dot(d
);
102 return log_error_errno(errno
, "Failed to read directory '%s': %m", rr
->path
);
106 switch (de
->d_type
) {
126 if (fstatat(dirfd(d
), de
->d_name
, &st
, AT_NO_AUTOMOUNT
) < 0) {
127 if (errno
== ENOENT
) /* Gone by now? */
130 return log_error_errno(errno
, "Failed to stat %s/%s: %m", rr
->path
, de
->d_name
);
133 if ((st
.st_mode
& S_IFMT
) != m
)
136 r
= pattern_match_many(rr
->patterns
, de
->d_name
, &extracted_fields
);
138 return log_error_errno(r
, "Failed to match pattern: %m");
142 joined
= path_join(rr
->path
, de
->d_name
);
146 r
= resource_add_instance(rr
, joined
, &extracted_fields
, &instance
);
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
;
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 */
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
;
169 c
= fdisk_new_context();
173 r
= fdisk_assign_device(c
, rr
->path
, /* readonly= */ true);
175 return log_error_errno(r
, "Failed to open device '%s': %m", rr
->path
);
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
);
180 r
= fdisk_get_partitions(c
, &t
);
182 return log_error_errno(r
, "Failed to acquire partition table: %m");
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
;
190 r
= read_partition_info(c
, t
, i
, &pinfo
);
193 if (r
== 0) /* not assigned */
196 /* Check if partition type matches */
197 if (rr
->partition_type_set
&& !sd_id128_equal(pinfo
.type
, rr
->partition_type
))
200 /* A label of "_empty" means "not used so far" for us */
201 if (streq_ptr(pinfo
.label
, "_empty")) {
206 r
= pattern_match_many(rr
->patterns
, pinfo
.label
, &extracted_fields
);
208 return log_error_errno(r
, "Failed to match pattern: %m");
212 r
= resource_add_instance(rr
, pinfo
.device
, &extracted_fields
, &instance
);
216 instance
->partition_info
= pinfo
;
217 pinfo
= (PartitionInfo
) PARTITION_INFO_NULL
;
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;
225 if (!instance
->metadata
.partition_flags_set
) {
226 instance
->metadata
.partition_flags
= instance
->partition_info
.flags
;
227 instance
->metadata
.partition_flags_set
= true;
230 if (instance
->metadata
.read_only
< 0)
231 instance
->metadata
.read_only
= instance
->partition_info
.read_only
;
237 static int download_manifest(
239 bool verify_signature
,
243 _cleanup_free_
char *buffer
= NULL
, *suffixed_url
= NULL
;
244 _cleanup_(close_pairp
) int pfd
[2] = { -1, -1 };
245 _cleanup_fclose_
FILE *manifest
= NULL
;
254 /* Download a SHA256SUMS file as manifest */
256 r
= import_url_append_component(url
, "SHA256SUMS", &suffixed_url
);
258 return log_error_errno(r
, "Failed to append SHA256SUMS to URL: %m");
260 if (pipe2(pfd
, O_CLOEXEC
) < 0)
261 return log_error_errno(errno
, "Failed to allocate pipe: %m");
263 log_info("%s Acquiring manifest file %s…", special_glyph(SPECIAL_GLYPH_DOWNLOAD
), suffixed_url
);
265 r
= safe_fork("(sd-pull)", FORK_RESET_SIGNALS
|FORK_DEATHSIG
|FORK_LOG
, &pid
);
271 const char *cmdline
[] = {
274 "--direct", /* just download the specified URL, don't download anything else */
275 "--verify", verify_signature
? "signature" : "no", /* verify the manifest file */
277 "-", /* write to stdout */
281 pfd
[0] = safe_close(pfd
[0]);
283 r
= rearrange_stdio(-1, pfd
[1], STDERR_FILENO
);
285 log_error_errno(r
, "Failed to rearrange stdin/stdout: %m");
289 (void) unsetenv("NOTIFY_SOCKET");
290 execv(pull_binary_path(), (char *const*) cmdline
);
291 log_error_errno(errno
, "Failed to execute %s tool: %m", pull_binary_path());
295 pfd
[1] = safe_close(pfd
[1]);
297 /* We'll first load the entire manifest into memory before parsing it. That's because the
298 * systemd-pull tool can validate the download only after its completion, but still pass the data to
299 * us as it runs. We thus need to check the return value of the process *before* parsing, to be
300 * reasonably safe. */
302 manifest
= fdopen(pfd
[0], "r");
304 return log_error_errno(errno
, "Failed allocate FILE object for manifest file: %m");
308 r
= read_full_stream(manifest
, &buffer
, &size
);
310 return log_error_errno(r
, "Failed to read manifest file from child: %m");
312 manifest
= safe_fclose(manifest
);
314 r
= wait_for_terminate_and_check("(sd-pull)", pid
, WAIT_LOG
);
320 *ret_buffer
= TAKE_PTR(buffer
);
326 static int resource_load_from_web(
329 Hashmap
**web_cache
) {
331 size_t manifest_size
= 0, left
= 0;
332 _cleanup_free_
char *buf
= NULL
;
333 const char *manifest
, *p
;
340 ci
= web_cache
? web_cache_get_item(*web_cache
, rr
->path
, verify
) : NULL
;
342 log_debug("Manifest web cache hit for %s.", rr
->path
);
344 manifest
= (char*) ci
->data
;
345 manifest_size
= ci
->size
;
347 log_debug("Manifest web cache miss for %s.", rr
->path
);
349 r
= download_manifest(rr
->path
, verify
, &buf
, &manifest_size
);
356 if (memchr(manifest
, 0, manifest_size
))
357 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Manifest file has embedded NUL byte, refusing.");
358 if (!utf8_is_valid_n(manifest
, manifest_size
))
359 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Manifest file is not valid UTF-8, refusing.");
362 left
= manifest_size
;
365 _cleanup_(instance_metadata_destroy
) InstanceMetadata extracted_fields
= INSTANCE_METADATA_NULL
;
366 _cleanup_free_
char *fn
= NULL
;
367 _cleanup_free_
void *h
= NULL
;
372 /* 64 character hash + separator + filename + newline */
374 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Corrupt manifest at line %zu, refusing.", line_nr
);
377 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
), "File names with escapes not supported in manifest at line %zu, refusing.", line_nr
);
379 r
= unhexmem(p
, 64, &h
, &hlen
);
381 return log_error_errno(r
, "Failed to parse digest at manifest line %zu, refusing.", line_nr
);
386 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Missing space separator at manifest line %zu, refusing.", line_nr
);
389 if (!IN_SET(*p
, '*', ' '))
390 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Missing binary/text input marker at manifest line %zu, refusing.", line_nr
);
393 e
= memchr(p
, '\n', left
);
395 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Truncated manifest file at line %zu, refusing.", line_nr
);
397 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Empty filename specified at manifest line %zu, refusing.", line_nr
);
399 fn
= strndup(p
, e
- p
);
403 if (!filename_is_valid(fn
))
404 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Invalid filename specified at manifest line %zu, refusing.", line_nr
);
405 if (string_has_cc(fn
, NULL
))
406 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Filename contains control characters at manifest line %zu, refusing.", line_nr
);
408 r
= pattern_match_many(rr
->patterns
, fn
, &extracted_fields
);
410 return log_error_errno(r
, "Failed to match pattern: %m");
412 _cleanup_free_
char *path
= NULL
;
414 r
= import_url_append_component(rr
->path
, fn
, &path
);
416 return log_error_errno(r
, "Failed to build instance URL: %m");
418 r
= resource_add_instance(rr
, path
, &extracted_fields
, &instance
);
422 assert(hlen
== sizeof(instance
->metadata
.sha256sum
));
424 if (instance
->metadata
.sha256sum_set
) {
425 if (memcmp(instance
->metadata
.sha256sum
, h
, hlen
) != 0)
426 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "SHA256 sum parsed from filename and manifest don't match at line %zu, refusing.", line_nr
);
428 memcpy(instance
->metadata
.sha256sum
, h
, hlen
);
429 instance
->metadata
.sha256sum_set
= true;
439 if (!ci
&& web_cache
) {
440 r
= web_cache_add_item(web_cache
, rr
->path
, verify
, manifest
, manifest_size
);
442 log_debug_errno(r
, "Failed to add manifest '%s' to cache, ignoring: %m", rr
->path
);
444 log_debug("Added manifest '%s' to cache.", rr
->path
);
450 static int instance_cmp(Instance
*const*a
, Instance
*const*b
) {
457 assert((*a
)->metadata
.version
);
458 assert((*b
)->metadata
.version
);
460 /* Newest version at the beginning */
461 r
= strverscmp_improved((*a
)->metadata
.version
, (*b
)->metadata
.version
);
465 /* Instances don't have to be uniquely named (uniqueness on partition tables is not enforced at all,
466 * and since we allow multiple matching patterns not even in directories they are unique). Hence
467 * let's order by path as secondary ordering key. */
468 return path_compare((*a
)->path
, (*b
)->path
);
471 int resource_load_instances(Resource
*rr
, bool verify
, Hashmap
**web_cache
) {
479 case RESOURCE_REGULAR_FILE
:
480 r
= resource_load_from_directory(rr
, S_IFREG
);
483 case RESOURCE_DIRECTORY
:
484 case RESOURCE_SUBVOLUME
:
485 r
= resource_load_from_directory(rr
, S_IFDIR
);
488 case RESOURCE_PARTITION
:
489 r
= resource_load_from_blockdev(rr
);
492 case RESOURCE_URL_FILE
:
493 case RESOURCE_URL_TAR
:
494 r
= resource_load_from_web(rr
, verify
, web_cache
);
498 assert_not_reached();
503 typesafe_qsort(rr
->instances
, rr
->n_instances
, instance_cmp
);
507 Instance
* resource_find_instance(Resource
*rr
, const char *version
) {
509 .metadata
.version
= (char*) version
,
512 return typesafe_bsearch(&k
, rr
->instances
, rr
->n_instances
, instance_cmp
);
515 int resource_resolve_path(
520 _cleanup_free_
char *p
= NULL
;
528 /* NB: we don't actually check the backing device of the root fs "/", but of "/usr", in order
529 * to support environments where the root fs is a tmpfs, and the OS itself placed exclusively
532 if (rr
->type
!= RESOURCE_PARTITION
)
533 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
534 "Automatic root path discovery only supported for partition resources.");
536 if (node
) { /* If --image= is specified, directly use the loopback device */
537 r
= free_and_strdup_warn(&rr
->path
, node
);
545 return log_error_errno(SYNTHETIC_ERRNO(EPERM
),
546 "Block device is not allowed when using --root= mode.");
548 r
= get_block_device_harder("/usr/", &d
);
550 } else if (rr
->type
== RESOURCE_PARTITION
) {
551 _cleanup_close_
int fd
= -1, real_fd
= -1;
552 _cleanup_free_
char *resolved
= NULL
;
555 r
= chase_symlinks(rr
->path
, root
, CHASE_PREFIX_ROOT
, &resolved
, &fd
);
557 return log_error_errno(r
, "Failed to resolve '%s': %m", rr
->path
);
559 if (fstat(fd
, &st
) < 0)
560 return log_error_errno(errno
, "Failed to stat '%s': %m", resolved
);
562 if (S_ISBLK(st
.st_mode
) && root
)
563 return log_error_errno(SYNTHETIC_ERRNO(EPERM
), "When using --root= or --image= access to device nodes is prohibited.");
565 if (S_ISREG(st
.st_mode
) || S_ISBLK(st
.st_mode
)) {
566 /* Not a directory, hence no need to find backing block device for the path */
567 free_and_replace(rr
->path
, resolved
);
571 if (!S_ISDIR(st
.st_mode
))
572 return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR
), "Target path '%s' does not refer to regular file, directory or block device, refusing.", rr
->path
);
574 if (node
) { /* If --image= is specified all file systems are backed by the same loopback device, hence shortcut things. */
575 r
= free_and_strdup_warn(&rr
->path
, node
);
582 real_fd
= fd_reopen(fd
, O_RDONLY
|O_CLOEXEC
|O_DIRECTORY
);
584 return log_error_errno(real_fd
, "Failed to convert O_PATH file descriptor for %s to regular file descriptor: %m", rr
->path
);
586 r
= get_block_device_harder_fd(fd
, &d
);
588 } else if (RESOURCE_IS_FILESYSTEM(rr
->type
) && root
) {
589 _cleanup_free_
char *resolved
= NULL
;
591 r
= chase_symlinks(rr
->path
, root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
593 return log_error_errno(r
, "Failed to resolve '%s': %m", rr
->path
);
595 free_and_replace(rr
->path
, resolved
);
598 return 0; /* Otherwise assume there's nothing to resolve */
601 return log_error_errno(r
, "Failed to determine block device of file system: %m");
603 r
= block_get_whole_disk(d
, &d
);
605 return log_error_errno(r
, "Failed to find whole disk device for partition backing file system: %m");
607 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
608 "File system is not placed on a partition block device, cannot determine whole block device backing root file system.");
610 r
= device_path_make_canonical(S_IFBLK
, d
, &p
);
615 log_info("Automatically discovered block device '%s' from '%s'.", p
, rr
->path
);
617 log_info("Automatically discovered root block device '%s'.", p
);
619 free_and_replace(rr
->path
, p
);
623 static const char *resource_type_table
[_RESOURCE_TYPE_MAX
] = {
624 [RESOURCE_URL_FILE
] = "url-file",
625 [RESOURCE_URL_TAR
] = "url-tar",
626 [RESOURCE_TAR
] = "tar",
627 [RESOURCE_PARTITION
] = "partition",
628 [RESOURCE_REGULAR_FILE
] = "regular-file",
629 [RESOURCE_DIRECTORY
] = "directory",
630 [RESOURCE_SUBVOLUME
] = "subvolume",
633 DEFINE_STRING_TABLE_LOOKUP(resource_type
, ResourceType
);