1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include <linux/loop.h>
11 #include "sd-device.h"
13 #include "architecture.h"
14 #include "blockdev-util.h"
16 #include "chase-symlinks.h"
18 #include "device-util.h"
19 #include "devnum-util.h"
20 #include "dissect-image.h"
25 #include "format-table.h"
26 #include "format-util.h"
28 #include "hexdecoct.h"
30 #include "loop-util.h"
31 #include "main-func.h"
33 #include "mount-util.h"
34 #include "mountpoint-util.h"
35 #include "namespace-util.h"
36 #include "parse-argument.h"
37 #include "parse-util.h"
38 #include "path-util.h"
39 #include "pretty-print.h"
40 #include "process-util.h"
41 #include "recurse-dir.h"
43 #include "stat-util.h"
44 #include "string-util.h"
46 #include "terminal-util.h"
47 #include "tmpfile-util.h"
48 #include "user-util.h"
59 } arg_action
= ACTION_DISSECT
;
60 static const char *arg_image
= NULL
;
61 static const char *arg_path
= NULL
;
62 static const char *arg_source
= NULL
;
63 static const char *arg_target
= NULL
;
64 static DissectImageFlags arg_flags
=
65 DISSECT_IMAGE_GENERIC_ROOT
|
66 DISSECT_IMAGE_DISCARD_ON_LOOP
|
67 DISSECT_IMAGE_RELAX_VAR_CHECK
|
69 DISSECT_IMAGE_USR_NO_ROOT
|
71 static VeritySettings arg_verity_settings
= VERITY_SETTINGS_DEFAULT
;
72 static JsonFormatFlags arg_json_format_flags
= JSON_FORMAT_OFF
;
73 static PagerFlags arg_pager_flags
= 0;
74 static bool arg_legend
= true;
75 static bool arg_rmdir
= false;
76 static char **arg_argv
= NULL
;
78 STATIC_DESTRUCTOR_REGISTER(arg_verity_settings
, verity_settings_done
);
79 STATIC_DESTRUCTOR_REGISTER(arg_argv
, strv_freep
);
81 static int help(void) {
82 _cleanup_free_
char *link
= NULL
;
85 r
= terminal_urlify_man("systemd-dissect", "1", &link
);
89 printf("%1$s [OPTIONS...] IMAGE\n"
90 "%1$s [OPTIONS...] --mount IMAGE PATH\n"
91 "%1$s [OPTIONS...] --umount PATH\n"
92 "%1$s [OPTIONS...] --list IMAGE\n"
93 "%1$s [OPTIONS...] --mtree IMAGE\n"
94 "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n"
95 "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n"
96 "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n"
97 "%5$sDissect a Discoverable Disk Image (DDI).%6$s\n\n"
99 " --no-pager Do not pipe output into a pager\n"
100 " --no-legend Do not show the headers and footers\n"
101 " -r --read-only Mount read-only\n"
102 " --fsck=BOOL Run fsck before mounting\n"
103 " --growfs=BOOL Grow file system to partition size, if marked\n"
104 " --mkdir Make mount directory before mounting, if missing\n"
105 " --rmdir Remove mount directory after unmounting\n"
106 " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n"
107 " --root-hash=HASH Specify root hash for verity\n"
108 " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n"
109 " as a DER encoded PKCS7, either as a path to a file\n"
110 " or as an ASCII base64 encoded string prefixed by\n"
112 " --verity-data=PATH Specify data file with hash tree for verity if it is\n"
113 " not embedded in IMAGE\n"
114 " --json=pretty|short|off\n"
115 " Generate JSON output\n"
116 "\n%3$sCommands:%4$s\n"
117 " -h --help Show this help\n"
118 " --version Show package version\n"
119 " -m --mount Mount the image to the specified directory\n"
120 " -M Shortcut for --mount --mkdir\n"
121 " -u --umount Unmount the image from the specified directory\n"
122 " -U Shortcut for --umount --rmdir\n"
123 " -l --list List all the files and directories of the specified\n"
125 " --mtree Show BSD mtree manifest of OS image\n"
126 " --with Mount, run command, unmount\n"
127 " -x --copy-from Copy files from image to host\n"
128 " -a --copy-to Copy files from host to image\n"
129 "\nSee the %2$s for details.\n",
130 program_invocation_short_name
,
140 static int patch_argv(int *argc
, char ***argv
, char ***buf
) {
141 _cleanup_free_
char **l
= NULL
;
150 /* Ugly hack: if --with is included in command line, also insert "--" immediately after it, to make
151 * getopt_long() stop processing switches */
153 for (e
= *argv
+ 1; e
< *argv
+ *argc
; e
++) {
156 if (streq(*e
, "--with"))
160 if (e
>= *argv
+ *argc
|| streq_ptr(e
[1], "--")) {
161 /* No --with used? Or already followed by "--"? Then don't do anything */
166 /* Insert the extra "--" right after the --with */
167 l
= new(char*, *argc
+ 2);
171 size_t idx
= e
- *argv
+ 1;
172 memcpy(l
, *argv
, sizeof(char*) * idx
); /* copy everything up to and including the --with */
173 l
[idx
] = (char*) "--"; /* insert "--" */
174 memcpy(l
+ idx
+ 1, e
+ 1, sizeof(char*) * (*argc
- idx
+ 1)); /* copy the rest, including trailing NULL entry */
183 static int parse_argv(int argc
, char *argv
[]) {
202 static const struct option options
[] = {
203 { "help", no_argument
, NULL
, 'h' },
204 { "version", no_argument
, NULL
, ARG_VERSION
},
205 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
206 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
207 { "mount", no_argument
, NULL
, 'm' },
208 { "umount", no_argument
, NULL
, 'u' },
209 { "with", no_argument
, NULL
, ARG_WITH
},
210 { "read-only", no_argument
, NULL
, 'r' },
211 { "discard", required_argument
, NULL
, ARG_DISCARD
},
212 { "fsck", required_argument
, NULL
, ARG_FSCK
},
213 { "growfs", required_argument
, NULL
, ARG_GROWFS
},
214 { "root-hash", required_argument
, NULL
, ARG_ROOT_HASH
},
215 { "root-hash-sig", required_argument
, NULL
, ARG_ROOT_HASH_SIG
},
216 { "verity-data", required_argument
, NULL
, ARG_VERITY_DATA
},
217 { "mkdir", no_argument
, NULL
, ARG_MKDIR
},
218 { "rmdir", no_argument
, NULL
, ARG_RMDIR
},
219 { "list", no_argument
, NULL
, 'l' },
220 { "mtree", no_argument
, NULL
, ARG_MTREE
},
221 { "copy-from", no_argument
, NULL
, 'x' },
222 { "copy-to", no_argument
, NULL
, 'a' },
223 { "json", required_argument
, NULL
, ARG_JSON
},
227 _cleanup_free_
char **buf
= NULL
; /* we use free(), not strv_free() here, as we don't copy the strings here */
233 r
= patch_argv(&argc
, &argv
, &buf
);
237 while ((c
= getopt_long(argc
, argv
, "hmurMUlxa", options
, NULL
)) >= 0) {
248 arg_pager_flags
|= PAGER_DISABLE
;
256 arg_action
= ACTION_MOUNT
;
260 arg_flags
|= DISSECT_IMAGE_MKDIR
;
264 /* Shortcut combination of the above two */
265 arg_action
= ACTION_MOUNT
;
266 arg_flags
|= DISSECT_IMAGE_MKDIR
;
270 arg_action
= ACTION_UMOUNT
;
278 /* Shortcut combination of the above two */
279 arg_action
= ACTION_UMOUNT
;
284 arg_action
= ACTION_LIST
;
285 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
289 arg_action
= ACTION_MTREE
;
290 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
294 arg_action
= ACTION_WITH
;
298 arg_action
= ACTION_COPY_FROM
;
299 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
303 arg_action
= ACTION_COPY_TO
;
307 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
311 DissectImageFlags flags
;
313 if (streq(optarg
, "disabled"))
315 else if (streq(optarg
, "loop"))
316 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
;
317 else if (streq(optarg
, "all"))
318 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
| DISSECT_IMAGE_DISCARD
;
319 else if (streq(optarg
, "crypt"))
320 flags
= DISSECT_IMAGE_DISCARD_ANY
;
321 else if (streq(optarg
, "list")) {
328 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
329 "Unknown --discard= parameter: %s",
331 arg_flags
= (arg_flags
& ~DISSECT_IMAGE_DISCARD_ANY
) | flags
;
336 case ARG_ROOT_HASH
: {
337 _cleanup_free_
void *p
= NULL
;
340 r
= unhexmem(optarg
, strlen(optarg
), &p
, &l
);
342 return log_error_errno(r
, "Failed to parse root hash '%s': %m", optarg
);
343 if (l
< sizeof(sd_id128_t
))
344 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
345 "Root hash must be at least 128bit long: %s", optarg
);
347 free_and_replace(arg_verity_settings
.root_hash
, p
);
348 arg_verity_settings
.root_hash_size
= l
;
352 case ARG_ROOT_HASH_SIG
: {
357 if ((value
= startswith(optarg
, "base64:"))) {
358 r
= unbase64mem(value
, strlen(value
), &p
, &l
);
360 return log_error_errno(r
, "Failed to parse root hash signature '%s': %m", optarg
);
362 r
= read_full_file(optarg
, (char**) &p
, &l
);
364 return log_error_errno(r
, "Failed to read root hash signature file '%s': %m", optarg
);
367 free_and_replace(arg_verity_settings
.root_hash_sig
, p
);
368 arg_verity_settings
.root_hash_sig_size
= l
;
372 case ARG_VERITY_DATA
:
373 r
= parse_path_argument(optarg
, false, &arg_verity_settings
.data_path
);
379 r
= parse_boolean(optarg
);
381 return log_error_errno(r
, "Failed to parse --fsck= parameter: %s", optarg
);
383 SET_FLAG(arg_flags
, DISSECT_IMAGE_FSCK
, r
);
387 r
= parse_boolean(optarg
);
389 return log_error_errno(r
, "Failed to parse --growfs= parameter: %s", optarg
);
391 SET_FLAG(arg_flags
, DISSECT_IMAGE_GROWFS
, r
);
395 r
= parse_json_argument(optarg
, &arg_json_format_flags
);
405 assert_not_reached();
409 switch (arg_action
) {
412 if (optind
+ 1 != argc
)
413 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
414 "Expected an image file path as only argument.");
416 arg_image
= argv
[optind
];
417 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
421 if (optind
+ 2 != argc
)
422 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
423 "Expected an image file path and mount point path as only arguments.");
425 arg_image
= argv
[optind
];
426 arg_path
= argv
[optind
+ 1];
427 arg_flags
|= DISSECT_IMAGE_REQUIRE_ROOT
;
431 if (optind
+ 1 != argc
)
432 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
433 "Expected a mount point path as only argument.");
435 arg_path
= argv
[optind
];
440 if (optind
+ 1 != argc
)
441 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
442 "Expected an image file path as only argument.");
444 arg_image
= argv
[optind
];
445 arg_flags
|= DISSECT_IMAGE_READ_ONLY
| DISSECT_IMAGE_REQUIRE_ROOT
;
448 case ACTION_COPY_FROM
:
449 if (argc
< optind
+ 2 || argc
> optind
+ 3)
450 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
451 "Expected an image file path, a source path and an optional destination path as only arguments.");
453 arg_image
= argv
[optind
];
454 arg_source
= argv
[optind
+ 1];
455 arg_target
= argc
> optind
+ 2 ? argv
[optind
+ 2] : "-" /* this means stdout */ ;
457 arg_flags
|= DISSECT_IMAGE_READ_ONLY
| DISSECT_IMAGE_REQUIRE_ROOT
;
461 if (argc
< optind
+ 2 || argc
> optind
+ 3)
462 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
463 "Expected an image file path, an optional source path and a destination path as only arguments.");
465 arg_image
= argv
[optind
];
467 if (argc
> optind
+ 2) {
468 arg_source
= argv
[optind
+ 1];
469 arg_target
= argv
[optind
+ 2];
471 arg_source
= "-"; /* this means stdin */
472 arg_target
= argv
[optind
+ 1];
475 arg_flags
|= DISSECT_IMAGE_REQUIRE_ROOT
;
480 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
481 "Expected an image file path and an optional command line.");
483 arg_image
= argv
[optind
];
484 if (argc
> optind
+ 1) {
485 arg_argv
= strv_copy(argv
+ optind
+ 1);
493 assert_not_reached();
499 static int strv_pair_to_json(char **l
, JsonVariant
**ret
) {
500 _cleanup_strv_free_
char **jl
= NULL
;
502 STRV_FOREACH_PAIR(a
, b
, l
) {
505 j
= strjoin(*a
, "=", *b
);
509 if (strv_consume(&jl
, j
) < 0)
513 return json_variant_new_array_strv(ret
, jl
);
516 static void strv_pair_print(char **l
, const char *prefix
) {
519 STRV_FOREACH_PAIR(p
, q
, l
)
521 printf("%s %s=%s\n", prefix
, *p
, *q
);
523 printf("%*s %s=%s\n", (int) strlen(prefix
), "", *p
, *q
);
526 static int get_sysext_scopes(DissectedImage
*m
, char ***ret_scopes
) {
527 _cleanup_strv_free_
char **l
= NULL
;
533 /* If there's no extension-release file its not a system extension. Otherwise the SYSEXT_SCOPE field
534 * indicates which scope it is for — and it defaults to "system" + "portable" if unset. */
536 if (!m
->extension_release
) {
541 e
= strv_env_pairs_get(m
->extension_release
, "SYSEXT_SCOPE");
543 l
= strv_split(e
, WHITESPACE
);
545 l
= strv_new("system", "portable");
549 *ret_scopes
= TAKE_PTR(l
);
553 static int action_dissect(DissectedImage
*m
, LoopDevice
*d
) {
554 _cleanup_(json_variant_unrefp
) JsonVariant
*v
= NULL
;
555 _cleanup_(table_unrefp
) Table
*t
= NULL
;
556 _cleanup_free_
char *bn
= NULL
;
557 uint64_t size
= UINT64_MAX
;
563 r
= path_extract_filename(arg_image
, &bn
);
565 return log_error_errno(r
, "Failed to extract file name from image path '%s': %m", arg_image
);
567 if (arg_json_format_flags
& (JSON_FORMAT_OFF
|JSON_FORMAT_PRETTY
|JSON_FORMAT_PRETTY_AUTO
))
568 pager_open(arg_pager_flags
);
570 if (arg_json_format_flags
& JSON_FORMAT_OFF
)
571 printf(" Name: %s\n", bn
);
573 if (ioctl(d
->fd
, BLKGETSIZE64
, &size
) < 0)
574 log_debug_errno(errno
, "Failed to query size of loopback device: %m");
575 else if (arg_json_format_flags
& JSON_FORMAT_OFF
)
576 printf(" Size: %s\n", FORMAT_BYTES(size
));
578 if (arg_json_format_flags
& JSON_FORMAT_OFF
)
581 r
= dissected_image_acquire_metadata(m
, 0);
583 return log_error_errno(r
, "No root partition discovered.");
585 return log_error_errno(r
, "File system check of image failed.");
586 if (r
== -EMEDIUMTYPE
)
587 log_warning_errno(r
, "Not a valid OS image, no os-release file included. Proceeding anyway.");
588 else if (r
== -EUNATCH
)
589 log_warning_errno(r
, "OS image is encrypted, proceeding without showing OS image metadata.");
590 else if (r
== -EBUSY
)
591 log_warning_errno(r
, "OS image is currently in use, proceeding without showing OS image metadata.");
593 return log_error_errno(r
, "Failed to acquire image metadata: %m");
594 else if (arg_json_format_flags
& JSON_FORMAT_OFF
) {
595 _cleanup_strv_free_
char **sysext_scopes
= NULL
;
598 printf(" Hostname: %s\n", m
->hostname
);
600 if (!sd_id128_is_null(m
->machine_id
))
601 printf("Machine ID: " SD_ID128_FORMAT_STR
"\n", SD_ID128_FORMAT_VAL(m
->machine_id
));
603 strv_pair_print(m
->machine_info
,
605 strv_pair_print(m
->os_release
,
607 strv_pair_print(m
->initrd_release
,
609 strv_pair_print(m
->extension_release
,
613 !sd_id128_is_null(m
->machine_id
) ||
614 !strv_isempty(m
->machine_info
) ||
615 !strv_isempty(m
->os_release
) ||
616 !strv_isempty(m
->initrd_release
) ||
617 !strv_isempty(m
->extension_release
))
620 printf(" Use As: %s bootable system for UEFI\n", COLOR_MARK_BOOL(m
->partitions
[PARTITION_ESP
].found
));
622 if (m
->has_init_system
>= 0)
623 printf(" %s bootable system for container\n", COLOR_MARK_BOOL(m
->has_init_system
));
625 printf(" %s portable service\n",
626 COLOR_MARK_BOOL(strv_env_pairs_get(m
->os_release
, "PORTABLE_PREFIXES")));
627 printf(" %s initrd\n",
628 COLOR_MARK_BOOL(!strv_isempty(m
->initrd_release
)));
630 r
= get_sysext_scopes(m
, &sysext_scopes
);
632 return log_error_errno(r
, "Failed to parse SYSEXT_SCOPE: %m");
634 printf(" %s extension for system\n",
635 COLOR_MARK_BOOL(strv_contains(sysext_scopes
, "system")));
636 printf(" %s extension for initrd\n",
637 COLOR_MARK_BOOL(strv_contains(sysext_scopes
, "initrd")));
638 printf(" %s extension for portable service\n",
639 COLOR_MARK_BOOL(strv_contains(sysext_scopes
, "portable")));
643 _cleanup_(json_variant_unrefp
) JsonVariant
*mi
= NULL
, *osr
= NULL
, *irdr
= NULL
, *exr
= NULL
;
644 _cleanup_strv_free_
char **sysext_scopes
= NULL
;
646 if (!strv_isempty(m
->machine_info
)) {
647 r
= strv_pair_to_json(m
->machine_info
, &mi
);
652 if (!strv_isempty(m
->os_release
)) {
653 r
= strv_pair_to_json(m
->os_release
, &osr
);
658 if (!strv_isempty(m
->initrd_release
)) {
659 r
= strv_pair_to_json(m
->initrd_release
, &irdr
);
664 if (!strv_isempty(m
->extension_release
)) {
665 r
= strv_pair_to_json(m
->extension_release
, &exr
);
670 r
= get_sysext_scopes(m
, &sysext_scopes
);
672 return log_error_errno(r
, "Failed to parse SYSEXT_SCOPE: %m");
674 r
= json_build(&v
, JSON_BUILD_OBJECT(
675 JSON_BUILD_PAIR("name", JSON_BUILD_STRING(bn
)),
676 JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(size
)),
677 JSON_BUILD_PAIR_CONDITION(m
->hostname
, "hostname", JSON_BUILD_STRING(m
->hostname
)),
678 JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m
->machine_id
), "machineId", JSON_BUILD_ID128(m
->machine_id
)),
679 JSON_BUILD_PAIR_CONDITION(mi
, "machineInfo", JSON_BUILD_VARIANT(mi
)),
680 JSON_BUILD_PAIR_CONDITION(osr
, "osRelease", JSON_BUILD_VARIANT(osr
)),
681 JSON_BUILD_PAIR_CONDITION(osr
, "initrdRelease", JSON_BUILD_VARIANT(irdr
)),
682 JSON_BUILD_PAIR_CONDITION(exr
, "extensionRelease", JSON_BUILD_VARIANT(exr
)),
683 JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(m
->partitions
[PARTITION_ESP
].found
)),
684 JSON_BUILD_PAIR_CONDITION(m
->has_init_system
>= 0, "useBootableContainer", JSON_BUILD_BOOLEAN(m
->has_init_system
)),
685 JSON_BUILD_PAIR("useInitrd", JSON_BUILD_BOOLEAN(!strv_isempty(m
->initrd_release
))),
686 JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(strv_env_pairs_get(m
->os_release
, "PORTABLE_MATCHES"))),
687 JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "system"))),
688 JSON_BUILD_PAIR("useInitRDExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "initrd"))),
689 JSON_BUILD_PAIR("usePortableExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "portable")))));
694 t
= table_new("rw", "designator", "partition uuid", "partition label", "fstype", "architecture", "verity", "growfs", "node", "partno");
698 table_set_ersatz_string(t
, TABLE_ERSATZ_DASH
);
699 (void) table_set_align_percent(t
, table_get_cell(t
, 0, 7), 100);
701 for (PartitionDesignator i
= 0; i
< _PARTITION_DESIGNATOR_MAX
; i
++) {
702 DissectedPartition
*p
= m
->partitions
+ i
;
709 TABLE_STRING
, p
->rw
? "rw" : "ro",
710 TABLE_STRING
, partition_designator_to_string(i
));
712 return table_log_add_error(r
);
714 if (sd_id128_is_null(p
->uuid
))
715 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
717 r
= table_add_cell(t
, NULL
, TABLE_UUID
, &p
->uuid
);
719 return table_log_add_error(r
);
723 TABLE_STRING
, p
->label
,
724 TABLE_STRING
, p
->fstype
,
725 TABLE_STRING
, architecture_to_string(p
->architecture
));
727 return table_log_add_error(r
);
729 if (arg_verity_settings
.data_path
)
730 r
= table_add_cell(t
, NULL
, TABLE_STRING
, "external");
731 else if (dissected_image_verity_candidate(m
, i
))
732 r
= table_add_cell(t
, NULL
, TABLE_STRING
,
733 dissected_image_verity_sig_ready(m
, i
) ? "signed" :
734 yes_no(dissected_image_verity_ready(m
, i
)));
736 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
738 return table_log_add_error(r
);
740 r
= table_add_many(t
, TABLE_BOOLEAN
, (int) p
->growfs
);
742 return table_log_add_error(r
);
744 if (p
->partno
< 0) /* no partition table, naked file system */ {
745 r
= table_add_cell(t
, NULL
, TABLE_STRING
, arg_image
);
747 return table_log_add_error(r
);
749 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
751 r
= table_add_cell(t
, NULL
, TABLE_STRING
, p
->node
);
753 return table_log_add_error(r
);
755 r
= table_add_cell(t
, NULL
, TABLE_INT
, &p
->partno
);
758 return table_log_add_error(r
);
761 if (arg_json_format_flags
& JSON_FORMAT_OFF
) {
762 (void) table_set_header(t
, arg_legend
);
764 r
= table_print(t
, NULL
);
766 return table_log_print_error(r
);
768 _cleanup_(json_variant_unrefp
) JsonVariant
*jt
= NULL
;
770 r
= table_to_json(t
, &jt
);
772 return log_error_errno(r
, "Failed to convert table to JSON: %m");
774 r
= json_variant_set_field(&v
, "mounts", jt
);
778 json_variant_dump(v
, arg_json_format_flags
, stdout
, NULL
);
784 static int action_mount(DissectedImage
*m
, LoopDevice
*d
) {
790 r
= dissected_image_decrypt_interactively(
792 &arg_verity_settings
,
797 r
= dissected_image_mount_and_warn(m
, arg_path
, UID_INVALID
, UID_INVALID
, arg_flags
);
801 r
= loop_device_flock(d
, LOCK_UN
);
803 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
805 r
= dissected_image_relinquish(m
);
807 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
812 static int list_print_item(
813 RecurseDirEvent event
,
817 const struct dirent
*de
,
818 const struct statx
*sx
,
823 if (event
== RECURSE_DIR_ENTER
)
824 printf("%s%s/%s\n", path
, ansi_grey(), ansi_normal());
825 else if (event
== RECURSE_DIR_ENTRY
)
826 printf("%s\n", path
);
828 return RECURSE_DIR_CONTINUE
;
831 static int get_file_sha256(int inode_fd
, uint8_t ret
[static SHA256_DIGEST_SIZE
]) {
832 _cleanup_close_
int fd
= -1;
833 struct sha256_ctx ctx
;
835 /* convert O_PATH fd into a regular one */
836 fd
= fd_reopen(inode_fd
, O_RDONLY
|O_CLOEXEC
);
840 /* Calculating the SHA sum might be slow, hence let's flush STDOUT first, to give user an idea where we are slow. */
843 sha256_init_ctx(&ctx
);
846 uint8_t buffer
[64 * 1024];
849 n
= read(fd
, buffer
, sizeof(buffer
));
855 sha256_process_bytes(buffer
, n
, &ctx
);
858 sha256_finish_ctx(&ctx
, ret
);
862 static int mtree_print_item(
863 RecurseDirEvent event
,
867 const struct dirent
*de
,
868 const struct statx
*sx
,
876 if (IN_SET(event
, RECURSE_DIR_ENTER
, RECURSE_DIR_ENTRY
)) {
877 _cleanup_free_
char *escaped
= NULL
;
882 /* BSD mtree uses either C or octal escaping, and covers whitespace, comments and glob characters. We use C style escaping and follow suit */
883 escaped
= xescape(path
, WHITESPACE COMMENTS GLOB_CHARS
);
890 printf("%s", isempty(path
) ? "." : path
);
892 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
)) {
893 if (S_ISDIR(sx
->stx_mode
))
894 printf("%s/%s", ansi_grey(), ansi_normal());
896 printf(" %stype=%s%s%s%s",
899 S_ISDIR(sx
->stx_mode
) ? ansi_highlight_blue() :
900 S_ISLNK(sx
->stx_mode
) ? ansi_highlight_cyan() :
901 (S_ISFIFO(sx
->stx_mode
) || S_ISCHR(sx
->stx_mode
) || S_ISBLK(sx
->stx_mode
)) ? ansi_highlight_yellow4() :
902 S_ISSOCK(sx
->stx_mode
) ? ansi_highlight_magenta() : "",
903 ASSERT_PTR(S_ISDIR(sx
->stx_mode
) ? "dir" :
904 S_ISREG(sx
->stx_mode
) ? "file" :
905 S_ISLNK(sx
->stx_mode
) ? "link" :
906 S_ISFIFO(sx
->stx_mode
) ? "fifo" :
907 S_ISBLK(sx
->stx_mode
) ? "block" :
908 S_ISCHR(sx
->stx_mode
) ? "char" :
909 S_ISSOCK(sx
->stx_mode
) ? "socket" : NULL
),
913 if (FLAGS_SET(sx
->stx_mask
, STATX_MODE
) && (!FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) || !S_ISLNK(sx
->stx_mode
)))
914 printf(" %smode=%s%04o",
917 (unsigned) (sx
->stx_mode
& 0777));
919 if (FLAGS_SET(sx
->stx_mask
, STATX_UID
))
920 printf(" %suid=%s" UID_FMT
,
925 if (FLAGS_SET(sx
->stx_mask
, STATX_GID
))
926 printf(" %sgid=%s" GID_FMT
,
931 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
|STATX_SIZE
) && S_ISREG(sx
->stx_mode
)) {
932 printf(" %ssize=%s%" PRIu64
,
935 (uint64_t) sx
->stx_size
);
937 if (inode_fd
>= 0 && sx
->stx_size
> 0) {
938 uint8_t hash
[SHA256_DIGEST_SIZE
];
940 r
= get_file_sha256(inode_fd
, hash
);
942 log_warning_errno(r
, "Failed to calculate file SHA256 sum for '%s', ignoring: %m", path
);
944 _cleanup_free_
char *h
= NULL
;
946 h
= hexmem(hash
, sizeof(hash
));
950 printf(" %ssha256sum=%s%s",
958 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) && S_ISLNK(sx
->stx_mode
) && inode_fd
>= 0) {
959 _cleanup_free_
char *target
= NULL
;
961 r
= readlinkat_malloc(inode_fd
, "", &target
);
963 log_warning_errno(r
, "Failed to read symlink '%s', ignoring: %m", path
);
965 _cleanup_free_
char *target_escaped
= NULL
;
967 target_escaped
= xescape(target
, WHITESPACE COMMENTS GLOB_CHARS
);
971 printf(" %slink=%s%s",
978 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) && (S_ISBLK(sx
->stx_mode
) || S_ISCHR(sx
->stx_mode
)))
979 printf(" %sdevice=%slinux,%" PRIu64
",%" PRIu64
,
982 (uint64_t) sx
->stx_rdev_major
,
983 (uint64_t) sx
->stx_rdev_minor
);
988 return RECURSE_DIR_CONTINUE
;
991 static int action_list_or_mtree_or_copy(DissectedImage
*m
, LoopDevice
*d
) {
992 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
993 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
994 _cleanup_free_
char *temp
= NULL
;
1000 r
= dissected_image_decrypt_interactively(
1002 &arg_verity_settings
,
1007 r
= detach_mount_namespace();
1009 return log_error_errno(r
, "Failed to detach mount namespace: %m");
1011 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
1013 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
1015 r
= mkdir_p(temp
, 0700);
1017 return log_error_errno(r
, "Failed to create mount point: %m");
1019 created_dir
= TAKE_PTR(temp
);
1021 r
= dissected_image_mount_and_warn(m
, created_dir
, UID_INVALID
, UID_INVALID
, arg_flags
);
1025 mounted_dir
= TAKE_PTR(created_dir
);
1027 r
= loop_device_flock(d
, LOCK_UN
);
1029 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
1031 r
= dissected_image_relinquish(m
);
1033 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1035 if (arg_action
== ACTION_COPY_FROM
) {
1036 _cleanup_close_
int source_fd
= -1, target_fd
= -1;
1038 source_fd
= chase_symlinks_and_open(arg_source
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
, NULL
);
1040 return log_error_errno(source_fd
, "Failed to open source path '%s' in image '%s': %m", arg_source
, arg_image
);
1042 /* Copying to stdout? */
1043 if (streq(arg_target
, "-")) {
1044 r
= copy_bytes(source_fd
, STDOUT_FILENO
, UINT64_MAX
, COPY_REFLINK
);
1046 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to stdout: %m", arg_source
, arg_image
);
1048 /* When we copy to stdout we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
1052 /* Try to copy as directory? */
1053 r
= copy_directory_fd(source_fd
, arg_target
, COPY_REFLINK
|COPY_MERGE_EMPTY
|COPY_SIGINT
|COPY_HARDLINKS
);
1057 return log_error_errno(r
, "Failed to copy %s in image '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
1059 r
= fd_verify_regular(source_fd
);
1061 return log_error_errno(r
, "Target '%s' exists already and is not a directory.", arg_target
);
1063 return log_error_errno(r
, "Source path %s in image '%s' is neither regular file nor directory, refusing: %m", arg_source
, arg_image
);
1065 /* Nah, it's a plain file! */
1066 target_fd
= open(arg_target
, O_WRONLY
|O_CREAT
|O_EXCL
|O_CLOEXEC
|O_NOCTTY
|O_NOFOLLOW
, 0600);
1068 return log_error_errno(errno
, "Failed to create regular file at target path '%s': %m", arg_target
);
1070 r
= copy_bytes(source_fd
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1072 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
1074 (void) copy_xattr(source_fd
, target_fd
, 0);
1075 (void) copy_access(source_fd
, target_fd
);
1076 (void) copy_times(source_fd
, target_fd
, 0);
1078 /* When this is a regular file we don't copy ownership! */
1080 } else if (arg_action
== ACTION_COPY_TO
) {
1081 _cleanup_close_
int source_fd
= -1, target_fd
= -1, dfd
= -1;
1082 _cleanup_free_
char *dn
= NULL
, *bn
= NULL
;
1084 r
= path_extract_directory(arg_target
, &dn
);
1086 return log_error_errno(r
, "Failed to extract directory from target path '%s': %m", arg_target
);
1087 r
= path_extract_filename(arg_target
, &bn
);
1089 return log_error_errno(r
, "Failed to extract filename from target path '%s': %m", arg_target
);
1091 r
= chase_symlinks(dn
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, NULL
, &dfd
);
1093 return log_error_errno(r
, "Failed to open '%s': %m", dn
);
1095 /* Are we reading from stdin? */
1096 if (streq(arg_source
, "-")) {
1097 target_fd
= openat(dfd
, bn
, O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0644);
1099 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
1101 r
= copy_bytes(STDIN_FILENO
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1103 return log_error_errno(r
, "Failed to copy bytes from stdin to '%s' in image '%s': %m", arg_target
, arg_image
);
1105 /* When we copy from stdin we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
1109 source_fd
= open(arg_source
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
);
1111 return log_error_errno(source_fd
, "Failed to open source path '%s': %m", arg_source
);
1113 r
= fd_verify_regular(source_fd
);
1116 return log_error_errno(r
, "Source '%s' is neither regular file nor directory: %m", arg_source
);
1118 /* We are looking at a directory. */
1120 target_fd
= openat(dfd
, bn
, O_RDONLY
|O_DIRECTORY
|O_CLOEXEC
);
1121 if (target_fd
< 0) {
1122 if (errno
!= ENOENT
)
1123 return log_error_errno(errno
, "Failed to open destination '%s': %m", arg_target
);
1125 r
= copy_tree_at(source_fd
, ".", dfd
, bn
, UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
, NULL
);
1127 r
= copy_tree_at(source_fd
, ".", target_fd
, ".", UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
, NULL
);
1129 return log_error_errno(r
, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
1134 /* We area looking at a regular file */
1135 target_fd
= openat(dfd
, basename(arg_target
), O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0600);
1137 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
1139 r
= copy_bytes(source_fd
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1141 return log_error_errno(r
, "Failed to copy bytes from '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
1143 (void) copy_xattr(source_fd
, target_fd
, 0);
1144 (void) copy_access(source_fd
, target_fd
);
1145 (void) copy_times(source_fd
, target_fd
, 0);
1147 /* When this is a regular file we don't copy ownership! */
1150 _cleanup_close_
int dfd
= -1;
1152 dfd
= open(mounted_dir
, O_DIRECTORY
|O_CLOEXEC
|O_RDONLY
);
1154 return log_error_errno(errno
, "Failed to open mount directory: %m");
1156 pager_open(arg_pager_flags
);
1158 if (arg_action
== ACTION_LIST
)
1159 r
= recurse_dir(dfd
, NULL
, 0, UINT_MAX
, RECURSE_DIR_SORT
, list_print_item
, NULL
);
1160 else if (arg_action
== ACTION_MTREE
)
1161 r
= recurse_dir(dfd
, ".", STATX_TYPE
|STATX_MODE
|STATX_UID
|STATX_GID
|STATX_SIZE
, UINT_MAX
, RECURSE_DIR_SORT
|RECURSE_DIR_INODE_FD
|RECURSE_DIR_TOPLEVEL
, mtree_print_item
, NULL
);
1163 assert_not_reached();
1165 return log_error_errno(r
, "Failed to list image: %m");
1171 static int action_umount(const char *path
) {
1172 _cleanup_close_
int fd
= -1;
1173 _cleanup_free_
char *canonical
= NULL
, *devname
= NULL
;
1174 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
1178 fd
= chase_symlinks_and_open(path
, NULL
, 0, O_DIRECTORY
, &canonical
);
1180 return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR
), "'%s' is not a directory", path
);
1182 return log_error_errno(fd
, "Failed to resolve path '%s': %m", path
);
1184 r
= fd_is_mount_point(fd
, NULL
, 0);
1186 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "'%s' is not a mount point", canonical
);
1188 return log_error_errno(r
, "Failed to determine whether '%s' is a mount point: %m", canonical
);
1190 r
= fd_get_whole_disk(fd
, /*backing=*/ true, &devno
);
1192 return log_error_errno(r
, "Failed to find backing block device for '%s': %m", canonical
);
1194 r
= devname_from_devnum(S_IFBLK
, devno
, &devname
);
1196 return log_error_errno(r
, "Failed to get devname of block device " DEVNUM_FORMAT_STR
": %m",
1197 DEVNUM_FORMAT_VAL(devno
));
1199 r
= loop_device_open_from_path(devname
, 0, LOCK_EX
, &d
);
1201 return log_error_errno(r
, "Failed to open loop device '%s': %m", devname
);
1203 /* We've locked the loop device, now we're ready to unmount. To allow the unmount to succeed, we have
1204 * to close the O_PATH fd we opened earlier. */
1205 fd
= safe_close(fd
);
1207 r
= umount_recursive(canonical
, 0);
1209 return log_error_errno(r
, "Failed to unmount '%s': %m", canonical
);
1211 /* We managed to lock and unmount successfully? That means we can try to remove the loop device.*/
1212 loop_device_unrelinquish(d
);
1215 r
= RET_NERRNO(rmdir(canonical
));
1217 return log_error_errno(r
, "Failed to remove mount directory '%s': %m", canonical
);
1223 static int action_with(DissectedImage
*m
, LoopDevice
*d
) {
1224 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
1225 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
1226 _cleanup_free_
char *temp
= NULL
;
1229 r
= dissected_image_decrypt_interactively(
1231 &arg_verity_settings
,
1236 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
1238 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
1240 r
= mkdir_p(temp
, 0700);
1242 return log_error_errno(r
, "Failed to create mount point: %m");
1244 created_dir
= TAKE_PTR(temp
);
1246 r
= dissected_image_mount_and_warn(m
, created_dir
, UID_INVALID
, UID_INVALID
, arg_flags
);
1250 mounted_dir
= TAKE_PTR(created_dir
);
1252 r
= dissected_image_relinquish(m
);
1254 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1256 r
= loop_device_flock(d
, LOCK_UN
);
1258 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
1260 rcode
= safe_fork("(with)", FORK_CLOSE_ALL_FDS
|FORK_LOG
|FORK_WAIT
, NULL
);
1264 if (chdir(mounted_dir
) < 0) {
1265 log_error_errno(errno
, "Failed to change to '%s' directory: %m", mounted_dir
);
1266 _exit(EXIT_FAILURE
);
1269 if (setenv("SYSTEMD_DISSECT_ROOT", mounted_dir
, /* overwrite= */ true) < 0) {
1270 log_error_errno(errno
, "Failed to set $SYSTEMD_DISSECT_ROOT: %m");
1271 _exit(EXIT_FAILURE
);
1274 if (strv_isempty(arg_argv
)) {
1277 sh
= secure_getenv("SHELL");
1279 execvp(sh
, STRV_MAKE(sh
));
1280 log_warning_errno(errno
, "Failed to execute $SHELL, falling back to /bin/sh: %m");
1283 execl("/bin/sh", "sh", NULL
);
1284 log_error_errno(errno
, "Failed to invoke /bin/sh: %m");
1286 execvp(arg_argv
[0], arg_argv
);
1287 log_error_errno(errno
, "Failed to execute '%s': %m", arg_argv
[0]);
1290 _exit(EXIT_FAILURE
);
1293 /* Let's manually detach everything, to make things synchronous */
1294 r
= loop_device_flock(d
, LOCK_SH
);
1296 log_warning_errno(r
, "Failed to lock loopback block device, ignoring: %m");
1298 r
= umount_recursive(mounted_dir
, 0);
1300 log_warning_errno(r
, "Failed to unmount '%s', ignoring: %m", mounted_dir
);
1302 loop_device_unrelinquish(d
); /* Let's try to destroy the loopback device */
1304 created_dir
= TAKE_PTR(mounted_dir
);
1306 if (rmdir(created_dir
) < 0)
1307 log_warning_errno(r
, "Failed to remove directory '%s', ignoring: %m", created_dir
);
1309 temp
= TAKE_PTR(created_dir
);
1314 static int run(int argc
, char *argv
[]) {
1315 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
1316 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
1319 log_parse_environment();
1322 r
= parse_argv(argc
, argv
);
1326 if (arg_action
== ACTION_UMOUNT
)
1327 return action_umount(arg_path
);
1329 r
= verity_settings_load(
1330 &arg_verity_settings
,
1331 arg_image
, NULL
, NULL
);
1333 return log_error_errno(r
, "Failed to read verity artifacts for %s: %m", arg_image
);
1335 if (arg_verity_settings
.data_path
)
1336 arg_flags
|= DISSECT_IMAGE_NO_PARTITION_TABLE
; /* We only support Verity per file system,
1337 * hence if there's external Verity data
1338 * available we turn off partition table
1341 r
= loop_device_make_by_path(
1343 FLAGS_SET(arg_flags
, DISSECT_IMAGE_DEVICE_READ_ONLY
) ? O_RDONLY
: O_RDWR
,
1344 FLAGS_SET(arg_flags
, DISSECT_IMAGE_NO_PARTITION_TABLE
) ? 0 : LO_FLAGS_PARTSCAN
,
1348 return log_error_errno(r
, "Failed to set up loopback device for %s: %m", arg_image
);
1350 r
= dissect_loop_device_and_warn(
1352 &arg_verity_settings
,
1359 r
= dissected_image_load_verity_sig_partition(
1362 &arg_verity_settings
);
1364 return log_error_errno(r
, "Failed to load verity signature partition: %m");
1366 switch (arg_action
) {
1368 case ACTION_DISSECT
:
1369 r
= action_dissect(m
, d
);
1373 r
= action_mount(m
, d
);
1378 case ACTION_COPY_FROM
:
1379 case ACTION_COPY_TO
:
1380 r
= action_list_or_mtree_or_copy(m
, d
);
1384 r
= action_with(m
, d
);
1388 assert_not_reached();
1394 DEFINE_MAIN_FUNCTION(run
);