1 /* SPDX-License-Identifier: LGPL-2.1+ */
5 #include <linux/loop.h>
10 #include "architecture.h"
12 #include "dissect-image.h"
14 #include "format-table.h"
15 #include "format-util.h"
17 #include "hexdecoct.h"
19 #include "loop-util.h"
20 #include "main-func.h"
22 #include "mount-util.h"
23 #include "namespace-util.h"
24 #include "parse-util.h"
25 #include "path-util.h"
26 #include "pretty-print.h"
27 #include "stat-util.h"
28 #include "string-util.h"
30 #include "terminal-util.h"
31 #include "tmpfile-util.h"
32 #include "user-util.h"
40 } arg_action
= ACTION_DISSECT
;
41 static const char *arg_image
= NULL
;
42 static const char *arg_path
= NULL
;
43 static const char *arg_source
= NULL
;
44 static const char *arg_target
= NULL
;
45 static DissectImageFlags arg_flags
= DISSECT_IMAGE_REQUIRE_ROOT
|DISSECT_IMAGE_DISCARD_ON_LOOP
|DISSECT_IMAGE_RELAX_VAR_CHECK
|DISSECT_IMAGE_FSCK
;
46 static void *arg_root_hash
= NULL
;
47 static char *arg_verity_data
= NULL
;
48 static size_t arg_root_hash_size
= 0;
49 static char *arg_root_hash_sig_path
= NULL
;
50 static void *arg_root_hash_sig
= NULL
;
51 static size_t arg_root_hash_sig_size
= 0;
53 STATIC_DESTRUCTOR_REGISTER(arg_root_hash
, freep
);
54 STATIC_DESTRUCTOR_REGISTER(arg_verity_data
, freep
);
55 STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig_path
, freep
);
56 STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig
, freep
);
58 static int help(void) {
59 _cleanup_free_
char *link
= NULL
;
62 r
= terminal_urlify_man("systemd-dissect", "1", &link
);
66 printf("%1$s [OPTIONS...] IMAGE\n"
67 "%1$s [OPTIONS...] --mount IMAGE PATH\n"
68 "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n"
69 "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n"
70 "%5$sDissect a file system OS image.%6$s\n\n"
72 " -r --read-only Mount read-only\n"
73 " --fsck=BOOL Run fsck before mounting\n"
74 " --mkdir Make mount directory before mounting, if missing\n"
75 " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n"
76 " --root-hash=HASH Specify root hash for verity\n"
77 " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n"
78 " as a DER encoded PKCS7, either as a path to a file\n"
79 " or as an ASCII base64 encoded string prefixed by\n"
81 " --verity-data=PATH Specify data file with hash tree for verity if it is\n"
82 " not embedded in IMAGE\n"
83 "\n%3$sCommands:%4$s\n"
84 " -h --help Show this help\n"
85 " --version Show package version\n"
86 " -m --mount Mount the image to the specified directory\n"
87 " -M Shortcut for --mount --mkdir\n"
88 " -x --copy-from Copy files from image to host\n"
89 " -a --copy-to Copy files from host to image\n"
90 "\nSee the %2$s for details.\n"
91 , program_invocation_short_name
93 , ansi_underline(), ansi_normal()
94 , ansi_highlight(), ansi_normal());
99 static int parse_argv(int argc
, char *argv
[]) {
111 static const struct option options
[] = {
112 { "help", no_argument
, NULL
, 'h' },
113 { "version", no_argument
, NULL
, ARG_VERSION
},
114 { "mount", no_argument
, NULL
, 'm' },
115 { "read-only", no_argument
, NULL
, 'r' },
116 { "discard", required_argument
, NULL
, ARG_DISCARD
},
117 { "root-hash", required_argument
, NULL
, ARG_ROOT_HASH
},
118 { "fsck", required_argument
, NULL
, ARG_FSCK
},
119 { "verity-data", required_argument
, NULL
, ARG_VERITY_DATA
},
120 { "root-hash-sig", required_argument
, NULL
, ARG_ROOT_HASH_SIG
},
121 { "mkdir", no_argument
, NULL
, ARG_MKDIR
},
122 { "copy-from", no_argument
, NULL
, 'x' },
123 { "copy-to", no_argument
, NULL
, 'a' },
132 while ((c
= getopt_long(argc
, argv
, "hmrMxa", options
, NULL
)) >= 0) {
143 arg_action
= ACTION_MOUNT
;
147 arg_flags
|= DISSECT_IMAGE_MKDIR
;
151 /* Shortcut combination of the above two */
152 arg_action
= ACTION_MOUNT
;
153 arg_flags
|= DISSECT_IMAGE_MKDIR
;
157 arg_action
= ACTION_COPY_FROM
;
158 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
162 arg_action
= ACTION_COPY_TO
;
166 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
170 DissectImageFlags flags
;
172 if (streq(optarg
, "disabled"))
174 else if (streq(optarg
, "loop"))
175 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
;
176 else if (streq(optarg
, "all"))
177 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
| DISSECT_IMAGE_DISCARD
;
178 else if (streq(optarg
, "crypt"))
179 flags
= DISSECT_IMAGE_DISCARD_ANY
;
180 else if (streq(optarg
, "list")) {
187 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
188 "Unknown --discard= parameter: %s",
190 arg_flags
= (arg_flags
& ~DISSECT_IMAGE_DISCARD_ANY
) | flags
;
195 case ARG_ROOT_HASH
: {
199 r
= unhexmem(optarg
, strlen(optarg
), &p
, &l
);
201 return log_error_errno(r
, "Failed to parse root hash '%s': %m", optarg
);
202 if (l
< sizeof(sd_id128_t
)) {
203 log_error("Root hash must be at least 128bit long: %s", optarg
);
210 arg_root_hash_size
= l
;
214 case ARG_VERITY_DATA
:
215 r
= parse_path_argument_and_warn(optarg
, false, &arg_verity_data
);
220 case ARG_ROOT_HASH_SIG
: {
223 if ((value
= startswith(optarg
, "base64:"))) {
227 r
= unbase64mem(value
, strlen(value
), &p
, &l
);
229 return log_error_errno(r
, "Failed to parse root hash signature '%s': %m", optarg
);
231 free_and_replace(arg_root_hash_sig
, p
);
232 arg_root_hash_sig_size
= l
;
233 arg_root_hash_sig_path
= mfree(arg_root_hash_sig_path
);
235 r
= parse_path_argument_and_warn(optarg
, false, &arg_root_hash_sig_path
);
238 arg_root_hash_sig
= mfree(arg_root_hash_sig
);
239 arg_root_hash_sig_size
= 0;
246 r
= parse_boolean(optarg
);
248 return log_error_errno(r
, "Failed to parse --fsck= parameter: %s", optarg
);
250 SET_FLAG(arg_flags
, DISSECT_IMAGE_FSCK
, r
);
257 assert_not_reached("Unhandled option");
262 switch (arg_action
) {
265 if (optind
+ 1 != argc
)
266 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
267 "Expected an image file path as only argument.");
269 arg_image
= argv
[optind
];
270 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
274 if (optind
+ 2 != argc
)
275 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
276 "Expected an image file path and mount point path as only arguments.");
278 arg_image
= argv
[optind
];
279 arg_path
= argv
[optind
+ 1];
282 case ACTION_COPY_FROM
:
283 if (argc
< optind
+ 2 || argc
> optind
+ 3)
284 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
285 "Expected an image file path, a source path and an optional destination path as only arguments.");
287 arg_image
= argv
[optind
];
288 arg_source
= argv
[optind
+ 1];
289 arg_target
= argc
> optind
+ 2 ? argv
[optind
+ 2] : "-" /* this means stdout */ ;
291 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
295 if (argc
< optind
+ 2 || argc
> optind
+ 3)
296 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
297 "Expected an image file path, an optional source path and a destination path as only arguments.");
299 arg_image
= argv
[optind
];
301 if (argc
> optind
+ 2) {
302 arg_source
= argv
[optind
+ 1];
303 arg_target
= argv
[optind
+ 2];
305 arg_source
= "-"; /* this means stdin */
306 arg_target
= argv
[optind
+ 1];
312 assert_not_reached("Unknown action.");
318 static int action_dissect(DissectedImage
*m
, LoopDevice
*d
) {
319 _cleanup_(table_unrefp
) Table
*t
= NULL
;
326 printf(" Name: %s\n", basename(arg_image
));
328 if (ioctl(d
->fd
, BLKGETSIZE64
, &size
) < 0)
329 log_debug_errno(errno
, "Failed to query size of loopback device: %m");
331 char s
[FORMAT_BYTES_MAX
];
332 printf(" Size: %s\n", format_bytes(s
, sizeof(s
), size
));
337 r
= dissected_image_acquire_metadata(m
);
339 return log_error_errno(r
, "No root partition discovered.");
340 if (r
== -EMEDIUMTYPE
)
341 return log_error_errno(r
, "Not a valid OS image, no os-release file included.");
343 return log_error_errno(r
, "File system check of image failed.");
345 log_warning_errno(r
, "OS image is encrypted, proceeding without showing OS image metadata.");
346 else if (r
== -EBUSY
)
347 log_warning_errno(r
, "OS image is currently in use, proceeding without showing OS image metadata.");
349 return log_error_errno(r
, "Failed to acquire image metadata: %m");
352 printf(" Hostname: %s\n", m
->hostname
);
354 if (!sd_id128_is_null(m
->machine_id
))
355 printf("Machine ID: " SD_ID128_FORMAT_STR
"\n", SD_ID128_FORMAT_VAL(m
->machine_id
));
357 if (!strv_isempty(m
->machine_info
)) {
360 STRV_FOREACH_PAIR(p
, q
, m
->machine_info
)
362 p
== m
->machine_info
? "Mach. Info:" : " ",
366 if (!strv_isempty(m
->os_release
)) {
369 STRV_FOREACH_PAIR(p
, q
, m
->os_release
)
371 p
== m
->os_release
? "OS Release:" : " ",
378 t
= table_new("rw", "designator", "partition uuid", "fstype", "architecture", "verity", "node", "partno");
382 (void) table_set_empty_string(t
, "-");
383 (void) table_set_align_percent(t
, table_get_cell(t
, 0, 7), 100);
385 for (unsigned i
= 0; i
< _PARTITION_DESIGNATOR_MAX
; i
++) {
386 DissectedPartition
*p
= m
->partitions
+ i
;
393 TABLE_STRING
, p
->rw
? "rw" : "ro",
394 TABLE_STRING
, partition_designator_to_string(i
));
396 return table_log_add_error(r
);
398 if (sd_id128_is_null(p
->uuid
))
399 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
401 r
= table_add_cell(t
, NULL
, TABLE_UUID
, &p
->uuid
);
403 return table_log_add_error(r
);
407 TABLE_STRING
, p
->fstype
,
408 TABLE_STRING
, architecture_to_string(p
->architecture
));
410 return table_log_add_error(r
);
413 r
= table_add_cell(t
, NULL
, TABLE_STRING
, "external");
414 else if (dissected_image_can_do_verity(m
, i
))
415 r
= table_add_cell(t
, NULL
, TABLE_STRING
, yes_no(dissected_image_has_verity(m
, i
)));
417 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
419 return table_log_add_error(r
);
421 if (p
->partno
< 0) /* no partition table, naked file system */ {
422 r
= table_add_cell(t
, NULL
, TABLE_STRING
, arg_image
);
424 return table_log_add_error(r
);
426 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
428 r
= table_add_cell(t
, NULL
, TABLE_STRING
, p
->node
);
430 return table_log_add_error(r
);
432 r
= table_add_cell(t
, NULL
, TABLE_INT
, &p
->partno
);
435 return table_log_add_error(r
);
438 r
= table_print(t
, stdout
);
440 return log_error_errno(r
, "Failed to dump table: %m");
445 static int action_mount(DissectedImage
*m
, LoopDevice
*d
) {
446 _cleanup_(decrypted_image_unrefp
) DecryptedImage
*di
= NULL
;
452 r
= dissected_image_decrypt_interactively(
454 arg_root_hash
, arg_root_hash_size
,
456 arg_root_hash_sig_path
, arg_root_hash_sig
, arg_root_hash_sig_size
,
462 r
= dissected_image_mount_and_warn(m
, arg_path
, UID_INVALID
, arg_flags
);
467 r
= decrypted_image_relinquish(di
);
469 return log_error_errno(r
, "Failed to relinquish DM devices: %m");
472 loop_device_relinquish(d
);
476 static int action_copy(DissectedImage
*m
, LoopDevice
*d
) {
477 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
478 _cleanup_(decrypted_image_unrefp
) DecryptedImage
*di
= NULL
;
479 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
480 _cleanup_free_
char *temp
= NULL
;
486 r
= dissected_image_decrypt_interactively(
488 arg_root_hash
, arg_root_hash_size
,
490 arg_root_hash_sig_path
, arg_root_hash_sig
, arg_root_hash_sig_size
,
496 r
= detach_mount_namespace();
498 return log_error_errno(r
, "Failed to detach mount namespace: %m");
500 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
502 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
504 r
= mkdir_p(temp
, 0700);
506 return log_error_errno(r
, "Failed to create mount point: %m");
508 created_dir
= TAKE_PTR(temp
);
510 r
= dissected_image_mount_and_warn(m
, created_dir
, UID_INVALID
, arg_flags
);
514 mounted_dir
= TAKE_PTR(created_dir
);
517 r
= decrypted_image_relinquish(di
);
519 return log_error_errno(r
, "Failed to relinquish DM devices: %m");
522 loop_device_relinquish(d
);
524 if (arg_action
== ACTION_COPY_FROM
) {
525 _cleanup_close_
int source_fd
= -1, target_fd
= -1;
527 source_fd
= chase_symlinks_and_open(arg_source
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
, NULL
);
529 return log_error_errno(source_fd
, "Failed to open source path '%s' in image '%s': %m", arg_source
, arg_image
);
531 /* Copying to stdout? */
532 if (streq(arg_target
, "-")) {
533 r
= copy_bytes(source_fd
, STDOUT_FILENO
, (uint64_t) -1, COPY_REFLINK
);
535 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to stdout: %m", arg_source
, arg_image
);
537 /* When we copy to stdou we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
541 /* Try to copy as directory? */
542 r
= copy_directory_fd(source_fd
, arg_target
, COPY_REFLINK
|COPY_MERGE_EMPTY
|COPY_SIGINT
);
546 return log_error_errno(r
, "Failed to copy %s in image '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
548 r
= fd_verify_regular(source_fd
);
550 return log_error_errno(r
, "Target '%s' exists already and is not a directory.", arg_target
);
552 return log_error_errno(r
, "Source path %s in image '%s' is neither regular file nor directory, refusing: %m", arg_source
, arg_image
);
554 /* Nah, it's a plain file! */
555 target_fd
= open(arg_target
, O_WRONLY
|O_CREAT
|O_EXCL
|O_CLOEXEC
|O_NOCTTY
|O_NOFOLLOW
, 0600);
557 return log_error_errno(errno
, "Failed to create regular file at target path '%s': %m", arg_target
);
559 r
= copy_bytes(source_fd
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
561 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
563 (void) copy_xattr(source_fd
, target_fd
);
564 (void) copy_access(source_fd
, target_fd
);
565 (void) copy_times(source_fd
, target_fd
, 0);
567 /* When this is a regular file we don't copy ownership! */
570 _cleanup_close_
int source_fd
= -1, target_fd
= -1;
571 _cleanup_close_
int dfd
= -1;
572 _cleanup_free_
char *dn
= NULL
;
574 assert(arg_action
== ACTION_COPY_TO
);
576 dn
= dirname_malloc(arg_target
);
580 r
= chase_symlinks(dn
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, NULL
, &dfd
);
582 return log_error_errno(r
, "Failed to open '%s': %m", dn
);
584 /* Are we reading from stdin? */
585 if (streq(arg_source
, "-")) {
586 target_fd
= openat(dfd
, basename(arg_target
), O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0644);
588 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
590 r
= copy_bytes(STDIN_FILENO
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
592 return log_error_errno(r
, "Failed to copy bytes from stdin to '%s' in image '%s': %m", arg_target
, arg_image
);
594 /* When we copy from stdin we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
598 source_fd
= open(arg_source
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
);
600 return log_error_errno(source_fd
, "Failed to open source path '%s': %m", arg_source
);
602 r
= fd_verify_regular(source_fd
);
605 return log_error_errno(r
, "Source '%s' is neither regular file nor directory: %m", arg_source
);
607 /* We are looking at a directory. */
609 target_fd
= openat(dfd
, basename(arg_target
), O_RDONLY
|O_DIRECTORY
|O_CLOEXEC
);
612 return log_error_errno(errno
, "Failed to open destination '%s': %m", arg_target
);
614 r
= copy_tree_at(source_fd
, ".", dfd
, basename(arg_target
), UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
);
616 r
= copy_tree_at(source_fd
, ".", target_fd
, ".", UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
);
618 return log_error_errno(r
, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
623 /* We area looking at a regular file */
624 target_fd
= openat(dfd
, basename(arg_target
), O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0600);
626 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
628 r
= copy_bytes(source_fd
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
630 return log_error_errno(r
, "Failed to copy bytes from '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
632 (void) copy_xattr(source_fd
, target_fd
);
633 (void) copy_access(source_fd
, target_fd
);
634 (void) copy_times(source_fd
, target_fd
, 0);
636 /* When this is a regular file we don't copy ownership! */
642 static int run(int argc
, char *argv
[]) {
643 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
644 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
647 log_parse_environment();
650 r
= parse_argv(argc
, argv
);
654 r
= verity_metadata_load(
656 arg_root_hash
? NULL
: &arg_root_hash
,
658 arg_verity_data
? NULL
: &arg_verity_data
,
659 arg_root_hash_sig_path
|| arg_root_hash_sig
? NULL
: &arg_root_hash_sig_path
);
661 return log_error_errno(r
, "Failed to read verity artifacts for %s: %m", arg_image
);
663 r
= loop_device_make_by_path(
665 (arg_flags
& DISSECT_IMAGE_READ_ONLY
) ? O_RDONLY
: O_RDWR
,
666 arg_verity_data
? 0 : LO_FLAGS_PARTSCAN
,
669 return log_error_errno(r
, "Failed to set up loopback device: %m");
672 arg_flags
|= DISSECT_IMAGE_NO_PARTITION_TABLE
; /* We only support Verity per file system,
673 * hence if there's external Verity data
674 * available we turn off partition table
676 r
= dissect_image_and_warn(
688 switch (arg_action
) {
691 r
= action_dissect(m
, d
);
695 r
= action_mount(m
, d
);
698 case ACTION_COPY_FROM
:
700 r
= action_copy(m
, d
);
704 assert_not_reached("Unknown action.");
710 DEFINE_MAIN_FUNCTION(run
);