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"
18 #include "device-util.h"
19 #include "devnum-util.h"
20 #include "discover-image.h"
21 #include "dissect-image.h"
26 #include "format-table.h"
27 #include "format-util.h"
29 #include "hexdecoct.h"
31 #include "loop-util.h"
32 #include "main-func.h"
34 #include "mount-util.h"
35 #include "mountpoint-util.h"
36 #include "namespace-util.h"
37 #include "parse-argument.h"
38 #include "parse-util.h"
39 #include "path-util.h"
40 #include "pretty-print.h"
41 #include "process-util.h"
42 #include "recurse-dir.h"
44 #include "stat-util.h"
45 #include "string-util.h"
47 #include "terminal-util.h"
48 #include "tmpfile-util.h"
49 #include "uid-classification.h"
50 #include "user-util.h"
66 } arg_action
= ACTION_DISSECT
;
67 static char *arg_image
= NULL
;
68 static char *arg_root
= NULL
;
69 static char *arg_path
= NULL
;
70 static const char *arg_source
= NULL
;
71 static const char *arg_target
= NULL
;
72 static DissectImageFlags arg_flags
=
73 DISSECT_IMAGE_GENERIC_ROOT
|
74 DISSECT_IMAGE_DISCARD_ON_LOOP
|
75 DISSECT_IMAGE_RELAX_VAR_CHECK
|
77 DISSECT_IMAGE_USR_NO_ROOT
|
78 DISSECT_IMAGE_GROWFS
|
79 DISSECT_IMAGE_PIN_PARTITION_DEVICES
|
80 DISSECT_IMAGE_ADD_PARTITION_DEVICES
;
81 static VeritySettings arg_verity_settings
= VERITY_SETTINGS_DEFAULT
;
82 static JsonFormatFlags arg_json_format_flags
= JSON_FORMAT_OFF
;
83 static PagerFlags arg_pager_flags
= 0;
84 static bool arg_legend
= true;
85 static bool arg_rmdir
= false;
86 static bool arg_in_memory
= false;
87 static char **arg_argv
= NULL
;
88 static char *arg_loop_ref
= NULL
;
89 static ImagePolicy
* arg_image_policy
= NULL
;
90 static bool arg_mtree_hash
= true;
92 STATIC_DESTRUCTOR_REGISTER(arg_image
, freep
);
93 STATIC_DESTRUCTOR_REGISTER(arg_root
, freep
);
94 STATIC_DESTRUCTOR_REGISTER(arg_path
, freep
);
95 STATIC_DESTRUCTOR_REGISTER(arg_verity_settings
, verity_settings_done
);
96 STATIC_DESTRUCTOR_REGISTER(arg_argv
, strv_freep
);
97 STATIC_DESTRUCTOR_REGISTER(arg_loop_ref
, freep
);
99 static int help(void) {
100 _cleanup_free_
char *link
= NULL
;
103 pager_open(arg_pager_flags
);
105 r
= terminal_urlify_man("systemd-dissect", "1", &link
);
109 printf("%1$s [OPTIONS...] IMAGE\n"
110 "%1$s [OPTIONS...] --mount IMAGE PATH\n"
111 "%1$s [OPTIONS...] --umount PATH\n"
112 "%1$s [OPTIONS...] --attach IMAGE\n"
113 "%1$s [OPTIONS...] --detach PATH\n"
114 "%1$s [OPTIONS...] --list IMAGE\n"
115 "%1$s [OPTIONS...] --mtree IMAGE\n"
116 "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n"
117 "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n"
118 "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n"
119 "%1$s [OPTIONS...] --discover\n"
120 "%1$s [OPTIONS...] --validate IMAGE\n"
121 "\n%5$sDissect a Discoverable Disk Image (DDI).%6$s\n\n"
123 " --no-pager Do not pipe output into a pager\n"
124 " --no-legend Do not show the headers and footers\n"
125 " -r --read-only Mount read-only\n"
126 " --fsck=BOOL Run fsck before mounting\n"
127 " --growfs=BOOL Grow file system to partition size, if marked\n"
128 " --mkdir Make mount directory before mounting, if missing\n"
129 " --rmdir Remove mount directory after unmounting\n"
130 " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n"
131 " --in-memory Copy image into memory\n"
132 " --root-hash=HASH Specify root hash for verity\n"
133 " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n"
134 " as a DER encoded PKCS7, either as a path to a file\n"
135 " or as an ASCII base64 encoded string prefixed by\n"
137 " --verity-data=PATH Specify data file with hash tree for verity if it is\n"
138 " not embedded in IMAGE\n"
139 " --image-policy=POLICY\n"
140 " Specify image dissection policy\n"
141 " --json=pretty|short|off\n"
142 " Generate JSON output\n"
143 " --loop-ref=NAME Set reference string for loopback device\n"
144 " --mtree-hash=BOOL Whether to include SHA256 hash in the mtree output\n"
145 "\n%3$sCommands:%4$s\n"
146 " -h --help Show this help\n"
147 " --version Show package version\n"
148 " -m --mount Mount the image to the specified directory\n"
149 " -M Shortcut for --mount --mkdir\n"
150 " -u --umount Unmount the image from the specified directory\n"
151 " -U Shortcut for --umount --rmdir\n"
152 " --attach Attach the disk image to a loopback block device\n"
153 " --detach Detach a loopback block device gain\n"
154 " -l --list List all the files and directories of the specified\n"
156 " --mtree Show BSD mtree manifest of OS image\n"
157 " --with Mount, run command, unmount\n"
158 " -x --copy-from Copy files from image to host\n"
159 " -a --copy-to Copy files from host to image\n"
160 " --discover Discover DDIs in well known directories\n"
161 " --validate Validate image and image policy\n"
162 "\nSee the %2$s for details.\n",
163 program_invocation_short_name
,
173 static int patch_argv(int *argc
, char ***argv
, char ***buf
) {
174 _cleanup_free_
char **l
= NULL
;
183 /* Ugly hack: if --with is included in command line, also insert "--" immediately after it, to make
184 * getopt_long() stop processing switches */
186 for (e
= *argv
+ 1; e
< *argv
+ *argc
; e
++) {
189 if (streq(*e
, "--with"))
193 if (e
>= *argv
+ *argc
|| streq_ptr(e
[1], "--")) {
194 /* No --with used? Or already followed by "--"? Then don't do anything */
199 /* Insert the extra "--" right after the --with */
200 l
= new(char*, *argc
+ 2);
204 size_t idx
= e
- *argv
+ 1;
205 memcpy(l
, *argv
, sizeof(char*) * idx
); /* copy everything up to and including the --with */
206 l
[idx
] = (char*) "--"; /* insert "--" */
207 memcpy(l
+ idx
+ 1, e
+ 1, sizeof(char*) * (*argc
- idx
+ 1)); /* copy the rest, including trailing NULL entry */
216 static int parse_image_path_argument(const char *path
, char **ret_root
, char **ret_image
) {
217 _cleanup_free_
char *p
= NULL
;
223 r
= parse_path_argument(path
, /* suppress_root= */ false, &p
);
227 if (stat(p
, &st
) < 0)
228 return log_error_errno(errno
, "Failed to stat %s: %m", p
);
230 if (S_ISDIR(st
.st_mode
)) {
232 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "%s is not an image file.", p
);
234 *ret_root
= TAKE_PTR(p
);
236 *ret_image
= TAKE_PTR(p
);
241 static int parse_argv(int argc
, char *argv
[]) {
268 static const struct option options
[] = {
269 { "help", no_argument
, NULL
, 'h' },
270 { "version", no_argument
, NULL
, ARG_VERSION
},
271 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
272 { "no-legend", no_argument
, NULL
, ARG_NO_LEGEND
},
273 { "mount", no_argument
, NULL
, 'm' },
274 { "umount", no_argument
, NULL
, 'u' },
275 { "attach", no_argument
, NULL
, ARG_ATTACH
},
276 { "detach", no_argument
, NULL
, ARG_DETACH
},
277 { "with", no_argument
, NULL
, ARG_WITH
},
278 { "read-only", no_argument
, NULL
, 'r' },
279 { "discard", required_argument
, NULL
, ARG_DISCARD
},
280 { "fsck", required_argument
, NULL
, ARG_FSCK
},
281 { "growfs", required_argument
, NULL
, ARG_GROWFS
},
282 { "root-hash", required_argument
, NULL
, ARG_ROOT_HASH
},
283 { "root-hash-sig", required_argument
, NULL
, ARG_ROOT_HASH_SIG
},
284 { "verity-data", required_argument
, NULL
, ARG_VERITY_DATA
},
285 { "mkdir", no_argument
, NULL
, ARG_MKDIR
},
286 { "rmdir", no_argument
, NULL
, ARG_RMDIR
},
287 { "in-memory", no_argument
, NULL
, ARG_IN_MEMORY
},
288 { "list", no_argument
, NULL
, 'l' },
289 { "mtree", no_argument
, NULL
, ARG_MTREE
},
290 { "copy-from", no_argument
, NULL
, 'x' },
291 { "copy-to", no_argument
, NULL
, 'a' },
292 { "json", required_argument
, NULL
, ARG_JSON
},
293 { "discover", no_argument
, NULL
, ARG_DISCOVER
},
294 { "loop-ref", required_argument
, NULL
, ARG_LOOP_REF
},
295 { "image-policy", required_argument
, NULL
, ARG_IMAGE_POLICY
},
296 { "validate", no_argument
, NULL
, ARG_VALIDATE
},
297 { "mtree-hash", required_argument
, NULL
, ARG_MTREE_HASH
},
301 _cleanup_free_
char **buf
= NULL
; /* we use free(), not strv_free() here, as we don't copy the strings here */
307 r
= patch_argv(&argc
, &argv
, &buf
);
311 while ((c
= getopt_long(argc
, argv
, "hmurMUlxa", options
, NULL
)) >= 0) {
322 arg_pager_flags
|= PAGER_DISABLE
;
330 arg_action
= ACTION_MOUNT
;
334 arg_flags
|= DISSECT_IMAGE_MKDIR
;
338 /* Shortcut combination of the above two */
339 arg_action
= ACTION_MOUNT
;
340 arg_flags
|= DISSECT_IMAGE_MKDIR
;
344 arg_action
= ACTION_UMOUNT
;
352 /* Shortcut combination of the above two */
353 arg_action
= ACTION_UMOUNT
;
358 arg_action
= ACTION_ATTACH
;
362 arg_action
= ACTION_DETACH
;
366 arg_action
= ACTION_LIST
;
367 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
371 arg_action
= ACTION_MTREE
;
372 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
376 arg_action
= ACTION_WITH
;
380 arg_action
= ACTION_COPY_FROM
;
381 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
385 arg_action
= ACTION_COPY_TO
;
389 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
393 DissectImageFlags flags
;
395 if (streq(optarg
, "disabled"))
397 else if (streq(optarg
, "loop"))
398 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
;
399 else if (streq(optarg
, "all"))
400 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
| DISSECT_IMAGE_DISCARD
;
401 else if (streq(optarg
, "crypt"))
402 flags
= DISSECT_IMAGE_DISCARD_ANY
;
403 else if (streq(optarg
, "list")) {
410 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
411 "Unknown --discard= parameter: %s",
413 arg_flags
= (arg_flags
& ~DISSECT_IMAGE_DISCARD_ANY
) | flags
;
419 arg_in_memory
= true;
422 case ARG_ROOT_HASH
: {
423 _cleanup_free_
void *p
= NULL
;
426 r
= unhexmem(optarg
, &p
, &l
);
428 return log_error_errno(r
, "Failed to parse root hash '%s': %m", optarg
);
429 if (l
< sizeof(sd_id128_t
))
430 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
431 "Root hash must be at least 128-bit long: %s", optarg
);
433 free_and_replace(arg_verity_settings
.root_hash
, p
);
434 arg_verity_settings
.root_hash_size
= l
;
438 case ARG_ROOT_HASH_SIG
: {
443 if ((value
= startswith(optarg
, "base64:"))) {
444 r
= unbase64mem(value
, &p
, &l
);
446 return log_error_errno(r
, "Failed to parse root hash signature '%s': %m", optarg
);
448 r
= read_full_file(optarg
, (char**) &p
, &l
);
450 return log_error_errno(r
, "Failed to read root hash signature file '%s': %m", optarg
);
453 free_and_replace(arg_verity_settings
.root_hash_sig
, p
);
454 arg_verity_settings
.root_hash_sig_size
= l
;
458 case ARG_VERITY_DATA
:
459 r
= parse_path_argument(optarg
, false, &arg_verity_settings
.data_path
);
465 r
= parse_boolean(optarg
);
467 return log_error_errno(r
, "Failed to parse --fsck= parameter: %s", optarg
);
469 SET_FLAG(arg_flags
, DISSECT_IMAGE_FSCK
, r
);
473 r
= parse_boolean(optarg
);
475 return log_error_errno(r
, "Failed to parse --growfs= parameter: %s", optarg
);
477 SET_FLAG(arg_flags
, DISSECT_IMAGE_GROWFS
, r
);
481 r
= parse_json_argument(optarg
, &arg_json_format_flags
);
488 arg_action
= ACTION_DISCOVER
;
492 if (isempty(optarg
)) {
493 arg_loop_ref
= mfree(arg_loop_ref
);
497 if (strlen(optarg
) >= sizeof_field(struct loop_info64
, lo_file_name
))
498 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Loop device ref string '%s' is too long.", optarg
);
500 r
= free_and_strdup_warn(&arg_loop_ref
, optarg
);
505 case ARG_IMAGE_POLICY
:
506 r
= parse_image_policy_argument(optarg
, &arg_image_policy
);
512 arg_action
= ACTION_VALIDATE
;
516 r
= parse_boolean_argument("--mtree-hash=", optarg
, &arg_mtree_hash
);
525 assert_not_reached();
529 switch (arg_action
) {
532 if (optind
+ 1 != argc
)
533 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
534 "Expected an image file path as only argument.");
536 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
540 /* when dumping image info be even more liberal than otherwise, do not even require a single valid partition */
541 arg_flags
|= DISSECT_IMAGE_READ_ONLY
|DISSECT_IMAGE_ALLOW_EMPTY
;
545 if (optind
+ 2 != argc
)
546 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
547 "Expected an image file path and mount point path as only arguments.");
549 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
553 r
= parse_path_argument(argv
[optind
+1], /* suppress_root= */ false, &arg_path
);
557 arg_flags
|= DISSECT_IMAGE_REQUIRE_ROOT
;
561 if (optind
+ 1 != argc
)
562 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
563 "Expected a mount point path as only argument.");
565 r
= parse_path_argument(argv
[optind
], /* suppress_root= */ false, &arg_path
);
571 if (optind
+ 1 != argc
)
572 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
573 "Expected an image file path as only argument.");
575 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
581 if (optind
+ 1 != argc
)
582 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
583 "Expected an image file path or loopback device as only argument.");
585 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
592 if (optind
+ 1 != argc
)
593 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
594 "Expected an image file or directory path as only argument.");
596 r
= parse_image_path_argument(argv
[optind
], &arg_root
, &arg_image
);
600 arg_flags
|= DISSECT_IMAGE_READ_ONLY
| DISSECT_IMAGE_REQUIRE_ROOT
;
603 case ACTION_COPY_FROM
:
604 if (argc
< optind
+ 2 || argc
> optind
+ 3)
605 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
606 "Expected an image file or directory path, a source path and an optional destination path as only arguments.");
608 r
= parse_image_path_argument(argv
[optind
], &arg_root
, &arg_image
);
611 arg_source
= argv
[optind
+ 1];
612 arg_target
= argc
> optind
+ 2 ? argv
[optind
+ 2] : "-" /* this means stdout */ ;
614 arg_flags
|= DISSECT_IMAGE_READ_ONLY
| DISSECT_IMAGE_REQUIRE_ROOT
;
618 if (argc
< optind
+ 2 || argc
> optind
+ 3)
619 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
620 "Expected an image file or directory path, an optional source path and a destination path as only arguments.");
622 r
= parse_image_path_argument(argv
[optind
], &arg_root
, &arg_image
);
626 if (argc
> optind
+ 2) {
627 arg_source
= argv
[optind
+ 1];
628 arg_target
= argv
[optind
+ 2];
630 arg_source
= "-"; /* this means stdin */
631 arg_target
= argv
[optind
+ 1];
634 arg_flags
|= DISSECT_IMAGE_REQUIRE_ROOT
;
639 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
640 "Expected an image file path and an optional command line.");
642 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
646 if (argc
> optind
+ 1) {
647 arg_argv
= strv_copy(argv
+ optind
+ 1);
654 case ACTION_DISCOVER
:
656 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
657 "Expected no argument.");
660 case ACTION_VALIDATE
:
661 if (optind
+ 1 != argc
)
662 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
663 "Expected an image file path as only argument.");
665 r
= parse_image_path_argument(argv
[optind
], NULL
, &arg_image
);
669 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
670 arg_flags
&= ~(DISSECT_IMAGE_PIN_PARTITION_DEVICES
|DISSECT_IMAGE_ADD_PARTITION_DEVICES
);
674 assert_not_reached();
680 static int parse_argv_as_mount_helper(int argc
, char *argv
[]) {
681 const char *options
= NULL
;
685 /* Implements util-linux "external helper" command line interface, as per mount(8) man page. */
687 while ((c
= getopt(argc
, argv
, "sfnvN:o:t:")) >= 0) {
699 if (!streq(optarg
, "ddi"))
700 log_debug("Unexpected file system type '%s', ignoring.", optarg
);
703 case 's': /* sloppy mount options */
704 case 'n': /* aka --no-mtab */
705 case 'v': /* aka --verbose */
706 log_debug("Ignoring option -%c, not implemented.", c
);
709 case 'N': /* aka --namespace= */
710 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
), "Option -%c is not implemented, refusing.", c
);
717 if (optind
+ 2 != argc
)
718 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
719 "Expected an image file path and target directory as only argument.");
721 for (const char *p
= options
;;) {
722 _cleanup_free_
char *word
= NULL
;
724 r
= extract_first_word(&p
, &word
, ",", EXTRACT_KEEP_QUOTE
);
726 return log_error_errno(r
, "Failed to extract mount option: %m");
730 if (streq(word
, "ro"))
731 SET_FLAG(arg_flags
, DISSECT_IMAGE_READ_ONLY
, true);
732 else if (streq(word
, "rw"))
733 SET_FLAG(arg_flags
, DISSECT_IMAGE_READ_ONLY
, false);
734 else if (streq(word
, "discard"))
735 SET_FLAG(arg_flags
, DISSECT_IMAGE_DISCARD_ANY
, true);
736 else if (streq(word
, "nodiscard"))
737 SET_FLAG(arg_flags
, DISSECT_IMAGE_DISCARD_ANY
, false);
739 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
740 "Unknown mount option '%s'.", word
);
746 r
= parse_path_argument(argv
[optind
], /* suppress_root= */ false, &arg_image
);
750 r
= parse_path_argument(argv
[optind
+1], /* suppress_root= */ false, &arg_path
);
754 arg_flags
|= DISSECT_IMAGE_REQUIRE_ROOT
;
755 arg_action
= ACTION_MOUNT
;
759 static void strv_pair_print(char **l
, const char *prefix
) {
762 STRV_FOREACH_PAIR(p
, q
, l
)
764 printf("%s %s=%s\n", prefix
, *p
, *q
);
766 printf("%*s %s=%s\n", (int) strlen(prefix
), "", *p
, *q
);
769 static int get_extension_scopes(DissectedImage
*m
, ImageClass
class, char ***ret_scopes
) {
770 _cleanup_strv_free_
char **l
= NULL
;
771 const char *e
, *field_name
;
780 release_data
= m
->sysext_release
;
781 field_name
= "SYSEXT_SCOPE";
785 release_data
= m
->confext_release
;
786 field_name
= "CONFEXT_SCOPE";
793 /* If there's no extension-release file its not a system extension. Otherwise the SYSEXT_SCOPE
794 * field for sysext images and the CONFEXT_SCOPE field for confext images indicates which scope
795 * it is for — and it defaults to "system" + "portable" if unset. */
802 e
= strv_env_pairs_get(release_data
, field_name
);
804 l
= strv_split(e
, WHITESPACE
);
806 l
= strv_new("system", "portable");
810 *ret_scopes
= TAKE_PTR(l
);
814 static int action_dissect(DissectedImage
*m
, LoopDevice
*d
) {
815 _cleanup_(json_variant_unrefp
) JsonVariant
*v
= NULL
;
816 _cleanup_(table_unrefp
) Table
*t
= NULL
;
817 _cleanup_free_
char *bn
= NULL
;
818 uint64_t size
= UINT64_MAX
;
824 r
= path_extract_filename(arg_image
, &bn
);
826 return log_error_errno(r
, "Failed to extract file name from image path '%s': %m", arg_image
);
828 if (arg_json_format_flags
& (JSON_FORMAT_OFF
|JSON_FORMAT_PRETTY
|JSON_FORMAT_PRETTY_AUTO
))
829 pager_open(arg_pager_flags
);
831 if (arg_json_format_flags
& JSON_FORMAT_OFF
) {
832 printf(" Name: %s%s%s\n",
833 ansi_highlight(), bn
, ansi_normal());
835 printf(" Size: %s\n",
836 FORMAT_BYTES(m
->image_size
));
838 printf(" Sec. Size: %" PRIu32
"\n",
841 printf(" Arch.: %s\n",
842 strna(architecture_to_string(dissected_image_architecture(m
))));
848 r
= dissected_image_acquire_metadata(m
, 0);
850 return log_error_errno(r
, "No root partition discovered.");
852 return log_error_errno(r
, "File system check of image failed.");
853 if (r
== -EMEDIUMTYPE
)
854 log_warning_errno(r
, "Not a valid OS image, no os-release file included. Proceeding anyway.");
855 else if (r
== -EUNATCH
)
856 log_warning_errno(r
, "OS image is encrypted, proceeding without showing OS image metadata.");
857 else if (r
== -EBUSY
)
858 log_warning_errno(r
, "OS image is currently in use, proceeding without showing OS image metadata.");
860 return log_error_errno(r
, "Failed to acquire image metadata: %m");
861 else if (arg_json_format_flags
& JSON_FORMAT_OFF
) {
863 if (!sd_id128_is_null(m
->image_uuid
))
864 printf("Image UUID: %s\n", SD_ID128_TO_UUID_STRING(m
->image_uuid
));
867 printf(" Hostname: %s\n", m
->hostname
);
869 if (!sd_id128_is_null(m
->machine_id
))
870 printf("Machine ID: " SD_ID128_FORMAT_STR
"\n", SD_ID128_FORMAT_VAL(m
->machine_id
));
872 strv_pair_print(m
->machine_info
,
874 strv_pair_print(m
->os_release
,
876 strv_pair_print(m
->initrd_release
,
878 strv_pair_print(m
->sysext_release
,
880 strv_pair_print(m
->confext_release
,
884 !sd_id128_is_null(m
->machine_id
) ||
885 !strv_isempty(m
->machine_info
) ||
886 !strv_isempty(m
->os_release
) ||
887 !strv_isempty(m
->initrd_release
) ||
888 !strv_isempty(m
->sysext_release
) ||
889 !strv_isempty(m
->confext_release
))
892 printf(" Use As: %s bootable system for UEFI\n",
893 COLOR_MARK_BOOL(dissected_image_is_bootable_uefi(m
)));
894 printf(" %s bootable system for container\n",
895 COLOR_MARK_BOOL(dissected_image_is_bootable_os(m
)));
896 printf(" %s portable service\n",
897 COLOR_MARK_BOOL(dissected_image_is_portable(m
)));
898 printf(" %s initrd\n",
899 COLOR_MARK_BOOL(dissected_image_is_initrd(m
)));
901 for (ImageClass c
= _IMAGE_CLASS_EXTENSION_FIRST
; c
<= _IMAGE_CLASS_EXTENSION_LAST
; c
++) {
902 const char *string_class
= image_class_to_string(c
);
903 _cleanup_strv_free_
char **extension_scopes
= NULL
;
905 r
= get_extension_scopes(m
, c
, &extension_scopes
);
907 return log_error_errno(r
, "Failed to parse scopes: %m");
909 printf(" %s %s for system\n",
910 COLOR_MARK_BOOL(strv_contains(extension_scopes
, "system")), string_class
);
911 printf(" %s %s for portable service\n",
912 COLOR_MARK_BOOL(strv_contains(extension_scopes
, "portable")), string_class
);
913 printf(" %s %s for initrd\n",
914 COLOR_MARK_BOOL(strv_contains(extension_scopes
, "initrd")), string_class
);
919 _cleanup_strv_free_
char **sysext_scopes
= NULL
, **confext_scopes
= NULL
;
921 r
= get_extension_scopes(m
, IMAGE_SYSEXT
, &sysext_scopes
);
923 return log_error_errno(r
, "Failed to parse sysext scopes: %m");
925 r
= get_extension_scopes(m
, IMAGE_CONFEXT
, &confext_scopes
);
927 return log_error_errno(r
, "Failed to parse confext scopes: %m");
929 Architecture a
= dissected_image_architecture(m
);
931 r
= json_build(&v
, JSON_BUILD_OBJECT(
932 JSON_BUILD_PAIR("name", JSON_BUILD_STRING(bn
)),
933 JSON_BUILD_PAIR_CONDITION(size
!= UINT64_MAX
, "size", JSON_BUILD_INTEGER(size
)),
934 JSON_BUILD_PAIR("sectorSize", JSON_BUILD_INTEGER(m
->sector_size
)),
935 JSON_BUILD_PAIR_CONDITION(a
>= 0, "architecture", JSON_BUILD_STRING(architecture_to_string(a
))),
936 JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m
->image_uuid
), "imageUuid", JSON_BUILD_UUID(m
->image_uuid
)),
937 JSON_BUILD_PAIR_CONDITION(m
->hostname
, "hostname", JSON_BUILD_STRING(m
->hostname
)),
938 JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m
->machine_id
), "machineId", JSON_BUILD_ID128(m
->machine_id
)),
939 JSON_BUILD_PAIR_CONDITION(!strv_isempty(m
->machine_info
), "machineInfo", JSON_BUILD_STRV_ENV_PAIR(m
->machine_info
)),
940 JSON_BUILD_PAIR_CONDITION(!strv_isempty(m
->os_release
), "osRelease", JSON_BUILD_STRV_ENV_PAIR(m
->os_release
)),
941 JSON_BUILD_PAIR_CONDITION(!strv_isempty(m
->initrd_release
), "initrdRelease", JSON_BUILD_STRV_ENV_PAIR(m
->initrd_release
)),
942 JSON_BUILD_PAIR_CONDITION(!strv_isempty(m
->sysext_release
), "sysextRelease", JSON_BUILD_STRV_ENV_PAIR(m
->sysext_release
)),
943 JSON_BUILD_PAIR_CONDITION(!strv_isempty(m
->confext_release
), "confextRelease", JSON_BUILD_STRV_ENV_PAIR(m
->confext_release
)),
944 JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(dissected_image_is_bootable_uefi(m
))),
945 JSON_BUILD_PAIR("useBootableContainer", JSON_BUILD_BOOLEAN(dissected_image_is_bootable_os(m
))),
946 JSON_BUILD_PAIR("useInitrd", JSON_BUILD_BOOLEAN(dissected_image_is_initrd(m
))),
947 JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(dissected_image_is_portable(m
))),
948 JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "system"))),
949 JSON_BUILD_PAIR("useInitRDSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "initrd"))),
950 JSON_BUILD_PAIR("usePortableSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes
, "portable"))),
951 JSON_BUILD_PAIR("useConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes
, "system"))),
952 JSON_BUILD_PAIR("useInitRDConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes
, "initrd"))),
953 JSON_BUILD_PAIR("usePortableConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes
, "portable")))));
958 t
= table_new("rw", "designator", "partition uuid", "partition label", "fstype", "architecture", "verity", "growfs", "node", "partno");
962 table_set_ersatz_string(t
, TABLE_ERSATZ_DASH
);
963 (void) table_set_align_percent(t
, table_get_cell(t
, 0, 9), 100);
965 /* Hide the device path if this is a loopback device that is not relinquished, since that means the
966 * device node is not going to be useful the instant our command exits */
967 if ((!d
|| d
->created
) && (arg_json_format_flags
& JSON_FORMAT_OFF
))
968 table_hide_column_from_display(t
, 8);
970 for (PartitionDesignator i
= 0; i
< _PARTITION_DESIGNATOR_MAX
; i
++) {
971 DissectedPartition
*p
= m
->partitions
+ i
;
978 TABLE_STRING
, p
->rw
? "rw" : "ro",
979 TABLE_STRING
, partition_designator_to_string(i
));
981 return table_log_add_error(r
);
983 if (sd_id128_is_null(p
->uuid
))
984 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
986 r
= table_add_cell(t
, NULL
, TABLE_UUID
, &p
->uuid
);
988 return table_log_add_error(r
);
992 TABLE_STRING
, p
->label
,
993 TABLE_STRING
, p
->fstype
,
994 TABLE_STRING
, architecture_to_string(p
->architecture
));
996 return table_log_add_error(r
);
998 if (arg_verity_settings
.data_path
)
999 r
= table_add_cell(t
, NULL
, TABLE_STRING
, "external");
1000 else if (dissected_image_verity_candidate(m
, i
))
1001 r
= table_add_cell(t
, NULL
, TABLE_STRING
,
1002 dissected_image_verity_sig_ready(m
, i
) ? "signed" :
1003 yes_no(dissected_image_verity_ready(m
, i
)));
1005 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
1007 return table_log_add_error(r
);
1009 r
= table_add_many(t
, TABLE_BOOLEAN
, (int) p
->growfs
);
1011 return table_log_add_error(r
);
1013 if (p
->partno
< 0) /* no partition table, naked file system */ {
1014 r
= table_add_cell(t
, NULL
, TABLE_PATH_BASENAME
, arg_image
);
1016 return table_log_add_error(r
);
1018 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
1020 r
= table_add_cell(t
, NULL
, TABLE_STRING
, p
->node
);
1022 return table_log_add_error(r
);
1024 r
= table_add_cell(t
, NULL
, TABLE_INT
, &p
->partno
);
1027 return table_log_add_error(r
);
1030 if (arg_json_format_flags
& JSON_FORMAT_OFF
) {
1031 (void) table_set_header(t
, arg_legend
);
1033 r
= table_print(t
, NULL
);
1035 return table_log_print_error(r
);
1037 _cleanup_(json_variant_unrefp
) JsonVariant
*jt
= NULL
;
1039 r
= table_to_json(t
, &jt
);
1041 return log_error_errno(r
, "Failed to convert table to JSON: %m");
1043 r
= json_variant_set_field(&v
, "mounts", jt
);
1047 json_variant_dump(v
, arg_json_format_flags
, stdout
, NULL
);
1053 static int action_mount(DissectedImage
*m
, LoopDevice
*d
) {
1058 assert(arg_action
== ACTION_MOUNT
);
1060 r
= dissected_image_mount_and_warn(
1063 /* uid_shift= */ UID_INVALID
,
1064 /* uid_range= */ UID_INVALID
,
1065 /* userns_fd= */ -EBADF
,
1070 r
= loop_device_flock(d
, LOCK_UN
);
1072 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
1074 r
= dissected_image_relinquish(m
);
1076 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1081 static int list_print_item(
1082 RecurseDirEvent event
,
1086 const struct dirent
*de
,
1087 const struct statx
*sx
,
1092 if (event
== RECURSE_DIR_ENTER
)
1093 printf("%s%s/%s\n", path
, ansi_grey(), ansi_normal());
1094 else if (event
== RECURSE_DIR_ENTRY
)
1095 printf("%s\n", path
);
1097 return RECURSE_DIR_CONTINUE
;
1100 static int get_file_sha256(int inode_fd
, uint8_t ret
[static SHA256_DIGEST_SIZE
]) {
1101 _cleanup_close_
int fd
= -EBADF
;
1102 struct sha256_ctx ctx
;
1104 /* convert O_PATH fd into a regular one */
1105 fd
= fd_reopen(inode_fd
, O_RDONLY
|O_CLOEXEC
);
1109 /* Calculating the SHA sum might be slow, hence let's flush STDOUT first, to give user an idea where we are slow. */
1112 sha256_init_ctx(&ctx
);
1115 uint8_t buffer
[64 * 1024];
1118 n
= read(fd
, buffer
, sizeof(buffer
));
1124 sha256_process_bytes(buffer
, n
, &ctx
);
1127 sha256_finish_ctx(&ctx
, ret
);
1131 static const char *pick_color_for_uid_gid(uid_t uid
) {
1132 if (uid
== UID_NOBODY
)
1133 return ansi_highlight_yellow4(); /* files should never be owned by 'nobody' (but might happen due to userns mapping) */
1134 if (uid_is_system(uid
))
1135 return ansi_normal(); /* files in disk images are typically owned by root and other system users, no issue there */
1136 if (uid_is_dynamic(uid
))
1137 return ansi_highlight_red(); /* files should never be owned persistently by dynamic users, and there are just no excuses */
1138 if (uid_is_container(uid
))
1139 return ansi_highlight_cyan();
1141 return ansi_highlight();
1144 static int mtree_print_item(
1145 RecurseDirEvent event
,
1149 const struct dirent
*de
,
1150 const struct statx
*sx
,
1153 _cleanup_free_
char *escaped
= NULL
;
1158 if (!IN_SET(event
, RECURSE_DIR_ENTER
, RECURSE_DIR_ENTRY
))
1159 return RECURSE_DIR_CONTINUE
;
1166 /* BSD mtree uses either C or octal escaping, and covers whitespace, comments and glob characters. We use C style escaping and follow suit */
1167 path
= escaped
= xescape(path
, WHITESPACE COMMENTS GLOB_CHARS
);
1172 printf("%s", isempty(path
) ? "." : path
);
1174 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
)) {
1175 if (S_ISDIR(sx
->stx_mode
))
1176 printf("%s/%s", ansi_grey(), ansi_normal());
1178 printf(" %stype=%s%s%s%s",
1181 S_ISDIR(sx
->stx_mode
) ? ansi_highlight_blue() :
1182 S_ISLNK(sx
->stx_mode
) ? ansi_highlight_cyan() :
1183 (S_ISFIFO(sx
->stx_mode
) || S_ISCHR(sx
->stx_mode
) || S_ISBLK(sx
->stx_mode
)) ? ansi_highlight_yellow4() :
1184 S_ISSOCK(sx
->stx_mode
) ? ansi_highlight_magenta() : "",
1185 ASSERT_PTR(S_ISDIR(sx
->stx_mode
) ? "dir" :
1186 S_ISREG(sx
->stx_mode
) ? "file" :
1187 S_ISLNK(sx
->stx_mode
) ? "link" :
1188 S_ISFIFO(sx
->stx_mode
) ? "fifo" :
1189 S_ISBLK(sx
->stx_mode
) ? "block" :
1190 S_ISCHR(sx
->stx_mode
) ? "char" :
1191 S_ISSOCK(sx
->stx_mode
) ? "socket" : NULL
),
1195 if (FLAGS_SET(sx
->stx_mask
, STATX_MODE
) && (!FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) || !S_ISLNK(sx
->stx_mode
)))
1196 printf(" %smode=%s%04o",
1199 (unsigned) (sx
->stx_mode
& 0777));
1201 if (FLAGS_SET(sx
->stx_mask
, STATX_UID
))
1202 printf(" %suid=%s" UID_FMT
"%s",
1204 pick_color_for_uid_gid(sx
->stx_uid
),
1208 if (FLAGS_SET(sx
->stx_mask
, STATX_GID
))
1209 printf(" %sgid=%s" GID_FMT
"%s",
1211 pick_color_for_uid_gid(sx
->stx_gid
),
1215 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
|STATX_SIZE
) && S_ISREG(sx
->stx_mode
)) {
1216 printf(" %ssize=%s%" PRIu64
,
1219 (uint64_t) sx
->stx_size
);
1221 if (arg_mtree_hash
&& inode_fd
>= 0 && sx
->stx_size
> 0) {
1222 uint8_t hash
[SHA256_DIGEST_SIZE
];
1224 r
= get_file_sha256(inode_fd
, hash
);
1226 log_warning_errno(r
, "Failed to calculate file SHA256 sum for '%s', ignoring: %m", path
);
1228 _cleanup_free_
char *h
= NULL
;
1230 h
= hexmem(hash
, sizeof(hash
));
1234 printf(" %ssha256sum=%s%s",
1242 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) && S_ISLNK(sx
->stx_mode
) && inode_fd
>= 0) {
1243 _cleanup_free_
char *target
= NULL
;
1245 r
= readlinkat_malloc(inode_fd
, "", &target
);
1247 log_warning_errno(r
, "Failed to read symlink '%s', ignoring: %m", path
);
1249 _cleanup_free_
char *target_escaped
= NULL
;
1251 target_escaped
= xescape(target
, WHITESPACE COMMENTS GLOB_CHARS
);
1252 if (!target_escaped
)
1255 printf(" %slink=%s%s",
1262 if (FLAGS_SET(sx
->stx_mask
, STATX_TYPE
) && (S_ISBLK(sx
->stx_mode
) || S_ISCHR(sx
->stx_mode
)))
1263 printf(" %sdevice=%slinux,%" PRIu64
",%" PRIu64
,
1266 (uint64_t) sx
->stx_rdev_major
,
1267 (uint64_t) sx
->stx_rdev_minor
);
1271 return RECURSE_DIR_CONTINUE
;
1274 static int action_list_or_mtree_or_copy(DissectedImage
*m
, LoopDevice
*d
) {
1275 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
1276 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
1277 _cleanup_free_
char *temp
= NULL
;
1281 assert(IN_SET(arg_action
, ACTION_LIST
, ACTION_MTREE
, ACTION_COPY_FROM
, ACTION_COPY_TO
));
1287 r
= detach_mount_namespace();
1289 return log_error_errno(r
, "Failed to detach mount namespace: %m");
1291 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
1293 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
1295 r
= mkdir_p(temp
, 0700);
1297 return log_error_errno(r
, "Failed to create mount point: %m");
1299 created_dir
= TAKE_PTR(temp
);
1301 r
= dissected_image_mount_and_warn(
1304 /* uid_shift= */ UID_INVALID
,
1305 /* uid_range= */ UID_INVALID
,
1306 /* userns_fd= */ -EBADF
,
1311 mounted_dir
= TAKE_PTR(created_dir
);
1313 r
= loop_device_flock(d
, LOCK_UN
);
1315 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
1317 r
= dissected_image_relinquish(m
);
1319 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1322 root
= mounted_dir
?: arg_root
;
1324 switch (arg_action
) {
1326 case ACTION_COPY_FROM
: {
1327 _cleanup_close_
int source_fd
= -EBADF
, target_fd
= -EBADF
;
1329 source_fd
= chase_and_open(arg_source
, root
, CHASE_PREFIX_ROOT
|CHASE_WARN
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
, NULL
);
1331 return log_error_errno(source_fd
, "Failed to open source path '%s' in image '%s': %m", arg_source
, arg_image
);
1333 /* Copying to stdout? */
1334 if (streq(arg_target
, "-")) {
1335 r
= copy_bytes(source_fd
, STDOUT_FILENO
, UINT64_MAX
, COPY_REFLINK
);
1337 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to stdout: %m", arg_source
, arg_image
);
1339 /* When we copy to stdout we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
1343 /* Try to copy as directory? */
1344 r
= copy_directory_at(source_fd
, NULL
, AT_FDCWD
, arg_target
, COPY_REFLINK
|COPY_MERGE_EMPTY
|COPY_SIGINT
|COPY_HARDLINKS
);
1348 return log_error_errno(r
, "Failed to copy %s in image '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
1350 r
= fd_verify_regular(source_fd
);
1352 return log_error_errno(r
, "Target '%s' exists already and is not a directory.", arg_target
);
1354 return log_error_errno(r
, "Source path %s in image '%s' is neither regular file nor directory, refusing: %m", arg_source
, arg_image
);
1356 /* Nah, it's a plain file! */
1357 target_fd
= open(arg_target
, O_WRONLY
|O_CREAT
|O_EXCL
|O_CLOEXEC
|O_NOCTTY
|O_NOFOLLOW
, 0600);
1359 return log_error_errno(errno
, "Failed to create regular file at target path '%s': %m", arg_target
);
1361 r
= copy_bytes(source_fd
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1363 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
1365 (void) copy_xattr(source_fd
, NULL
, target_fd
, NULL
, 0);
1366 (void) copy_access(source_fd
, target_fd
);
1367 (void) copy_times(source_fd
, target_fd
, 0);
1369 /* When this is a regular file we don't copy ownership! */
1373 case ACTION_COPY_TO
: {
1374 _cleanup_close_
int source_fd
= -EBADF
, target_fd
= -EBADF
, dfd
= -EBADF
;
1375 _cleanup_free_
char *dn
= NULL
, *bn
= NULL
;
1378 r
= path_extract_directory(arg_target
, &dn
);
1380 return log_error_errno(r
, "Failed to extract directory from target path '%s': %m", arg_target
);
1381 r
= path_extract_filename(arg_target
, &bn
);
1383 return log_error_errno(r
, "Failed to extract filename from target path '%s': %m", arg_target
);
1384 is_dir
= r
== O_DIRECTORY
;
1386 r
= chase(dn
, root
, CHASE_PREFIX_ROOT
|CHASE_WARN
, NULL
, &dfd
);
1388 return log_error_errno(r
, "Failed to open '%s': %m", dn
);
1390 /* Are we reading from stdin? */
1391 if (streq(arg_source
, "-")) {
1393 return log_error_errno(SYNTHETIC_ERRNO(EISDIR
), "Cannot copy STDIN to a directory, refusing.");
1395 target_fd
= openat(dfd
, bn
, O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0644);
1397 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
1399 r
= copy_bytes(STDIN_FILENO
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1401 return log_error_errno(r
, "Failed to copy bytes from stdin to '%s' in image '%s': %m", arg_target
, arg_image
);
1403 /* When we copy from stdin we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
1407 source_fd
= open(arg_source
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
);
1409 return log_error_errno(source_fd
, "Failed to open source path '%s': %m", arg_source
);
1411 r
= fd_verify_regular(source_fd
);
1414 return log_error_errno(r
, "Source '%s' is neither regular file nor directory: %m", arg_source
);
1416 /* We are looking at a directory. */
1418 target_fd
= openat(dfd
, bn
, O_RDONLY
|O_DIRECTORY
|O_CLOEXEC
);
1419 if (target_fd
< 0) {
1420 if (errno
!= ENOENT
)
1421 return log_error_errno(errno
, "Failed to open destination '%s': %m", arg_target
);
1423 r
= copy_tree_at(source_fd
, ".", dfd
, bn
, UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
, NULL
, NULL
);
1425 r
= copy_tree_at(source_fd
, ".", target_fd
, ".", UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
, NULL
, NULL
);
1427 return log_error_errno(r
, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
1433 return log_error_errno(SYNTHETIC_ERRNO(EISDIR
), "Source is a regular file, but target is not, refusing.");
1435 /* We area looking at a regular file */
1436 target_fd
= openat(dfd
, bn
, O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0600);
1438 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
1440 r
= copy_bytes(source_fd
, target_fd
, UINT64_MAX
, COPY_REFLINK
);
1442 return log_error_errno(r
, "Failed to copy bytes from '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
1444 (void) copy_xattr(source_fd
, NULL
, target_fd
, NULL
, 0);
1445 (void) copy_access(source_fd
, target_fd
);
1446 (void) copy_times(source_fd
, target_fd
, 0);
1448 /* When this is a regular file we don't copy ownership! */
1453 case ACTION_MTREE
: {
1454 _cleanup_close_
int dfd
= -EBADF
;
1456 dfd
= open(root
, O_DIRECTORY
|O_CLOEXEC
|O_RDONLY
);
1458 return log_error_errno(errno
, "Failed to open mount directory: %m");
1460 pager_open(arg_pager_flags
);
1462 if (arg_action
== ACTION_LIST
)
1463 r
= recurse_dir(dfd
, NULL
, 0, UINT_MAX
, RECURSE_DIR_SORT
, list_print_item
, NULL
);
1464 else if (arg_action
== ACTION_MTREE
)
1465 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
);
1467 assert_not_reached();
1469 return log_error_errno(r
, "Failed to list image: %m");
1474 assert_not_reached();
1478 static int action_umount(const char *path
) {
1479 _cleanup_close_
int fd
= -EBADF
;
1480 _cleanup_free_
char *canonical
= NULL
;
1481 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
1482 _cleanup_(sd_device_unrefp
) sd_device
*dev
= NULL
;
1485 fd
= chase_and_open(path
, NULL
, 0, O_DIRECTORY
, &canonical
);
1487 return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR
), "'%s' is not a directory", path
);
1489 return log_error_errno(fd
, "Failed to resolve path '%s': %m", path
);
1491 r
= fd_is_mount_point(fd
, NULL
, 0);
1493 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "'%s' is not a mount point", canonical
);
1495 return log_error_errno(r
, "Failed to determine whether '%s' is a mount point: %m", canonical
);
1497 r
= block_device_new_from_fd(fd
, BLOCK_DEVICE_LOOKUP_WHOLE_DISK
| BLOCK_DEVICE_LOOKUP_BACKING
, &dev
);
1499 _cleanup_close_
int usr_fd
= -EBADF
;
1501 /* The command `systemd-dissect --mount` expects that the image at least has the root or /usr
1502 * partition. If it does not have the root partition, then we mount the /usr partition on a
1503 * tmpfs. Hence, let's try to find the backing block device through the /usr partition. */
1505 usr_fd
= openat(fd
, "usr", O_CLOEXEC
| O_DIRECTORY
| O_NOFOLLOW
);
1507 return log_error_errno(errno
, "Failed to open '%s/usr': %m", canonical
);
1509 r
= block_device_new_from_fd(usr_fd
, BLOCK_DEVICE_LOOKUP_WHOLE_DISK
| BLOCK_DEVICE_LOOKUP_BACKING
, &dev
);
1512 return log_error_errno(r
, "Failed to find backing block device for '%s': %m", canonical
);
1514 r
= loop_device_open(dev
, 0, LOCK_EX
, &d
);
1516 return log_device_error_errno(dev
, r
, "Failed to open loopback block device: %m");
1518 /* We've locked the loop device, now we're ready to unmount. To allow the unmount to succeed, we have
1519 * to close the O_PATH fd we opened earlier. */
1520 fd
= safe_close(fd
);
1522 r
= umount_recursive(canonical
, 0);
1524 return log_error_errno(r
, "Failed to unmount '%s': %m", canonical
);
1526 /* We managed to lock and unmount successfully? That means we can try to remove the loop device. */
1527 loop_device_unrelinquish(d
);
1530 r
= RET_NERRNO(rmdir(canonical
));
1532 return log_error_errno(r
, "Failed to remove mount directory '%s': %m", canonical
);
1538 static int action_with(DissectedImage
*m
, LoopDevice
*d
) {
1539 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
1540 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
1541 _cleanup_free_
char *temp
= NULL
;
1546 assert(arg_action
== ACTION_WITH
);
1548 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
1550 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
1552 r
= mkdir_p(temp
, 0700);
1554 return log_error_errno(r
, "Failed to create mount point: %m");
1556 created_dir
= TAKE_PTR(temp
);
1558 r
= dissected_image_mount_and_warn(
1561 /* uid_shift= */ UID_INVALID
,
1562 /* uid_range= */ UID_INVALID
,
1563 /* userns_fd= */ -EBADF
,
1568 mounted_dir
= TAKE_PTR(created_dir
);
1570 r
= dissected_image_relinquish(m
);
1572 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1574 r
= loop_device_flock(d
, LOCK_UN
);
1576 return log_error_errno(r
, "Failed to unlock loopback block device: %m");
1578 rcode
= safe_fork("(with)", FORK_CLOSE_ALL_FDS
|FORK_LOG
|FORK_WAIT
, NULL
);
1582 if (chdir(mounted_dir
) < 0) {
1583 log_error_errno(errno
, "Failed to change to '%s' directory: %m", mounted_dir
);
1584 _exit(EXIT_FAILURE
);
1587 if (setenv("SYSTEMD_DISSECT_ROOT", mounted_dir
, /* overwrite= */ true) < 0) {
1588 log_error_errno(errno
, "Failed to set $SYSTEMD_DISSECT_ROOT: %m");
1589 _exit(EXIT_FAILURE
);
1592 if (setenv("SYSTEMD_DISSECT_DEVICE", d
->node
, /* overwrite= */ true) < 0) {
1593 log_error_errno(errno
, "Failed to set $SYSTEMD_DISSECT_DEVICE: %m");
1594 _exit(EXIT_FAILURE
);
1597 if (strv_isempty(arg_argv
)) {
1600 sh
= secure_getenv("SHELL");
1602 execvp(sh
, STRV_MAKE(sh
));
1603 log_warning_errno(errno
, "Failed to execute $SHELL, falling back to /bin/sh: %m");
1606 execl("/bin/sh", "sh", NULL
);
1607 log_error_errno(errno
, "Failed to invoke /bin/sh: %m");
1609 execvp(arg_argv
[0], arg_argv
);
1610 log_error_errno(errno
, "Failed to execute '%s': %m", arg_argv
[0]);
1613 _exit(EXIT_FAILURE
);
1616 /* Let's manually detach everything, to make things synchronous */
1617 r
= loop_device_flock(d
, LOCK_SH
);
1619 log_warning_errno(r
, "Failed to lock loopback block device, ignoring: %m");
1621 r
= umount_recursive(mounted_dir
, 0);
1623 log_warning_errno(r
, "Failed to unmount '%s', ignoring: %m", mounted_dir
);
1625 loop_device_unrelinquish(d
); /* Let's try to destroy the loopback device */
1627 created_dir
= TAKE_PTR(mounted_dir
);
1629 if (rmdir(created_dir
) < 0)
1630 log_warning_errno(r
, "Failed to remove directory '%s', ignoring: %m", created_dir
);
1632 temp
= TAKE_PTR(created_dir
);
1637 static int action_discover(void) {
1638 _cleanup_hashmap_free_ Hashmap
*images
= NULL
;
1639 _cleanup_(table_unrefp
) Table
*t
= NULL
;
1643 images
= hashmap_new(&image_hash_ops
);
1647 for (ImageClass cl
= 0; cl
< _IMAGE_CLASS_MAX
; cl
++) {
1648 r
= image_discover(cl
, NULL
, images
);
1650 return log_error_errno(r
, "Failed to discover images: %m");
1653 if ((arg_json_format_flags
& JSON_FORMAT_OFF
) && hashmap_isempty(images
)) {
1654 log_info("No images found.");
1658 t
= table_new("name", "type", "class", "ro", "path", "time", "usage");
1662 table_set_align_percent(t
, table_get_cell(t
, 0, 6), 100);
1663 table_set_ersatz_string(t
, TABLE_ERSATZ_DASH
);
1665 HASHMAP_FOREACH(img
, images
) {
1667 if (!IN_SET(img
->type
, IMAGE_RAW
, IMAGE_BLOCK
))
1672 TABLE_STRING
, img
->name
,
1673 TABLE_STRING
, image_type_to_string(img
->type
),
1674 TABLE_STRING
, image_class_to_string(img
->class),
1675 TABLE_BOOLEAN
, img
->read_only
,
1676 TABLE_PATH
, img
->path
,
1677 TABLE_TIMESTAMP
, img
->mtime
!= 0 ? img
->mtime
: img
->crtime
,
1678 TABLE_SIZE
, img
->usage
);
1680 return table_log_add_error(r
);
1683 (void) table_set_sort(t
, (size_t) 0);
1685 return table_print_with_pager(t
, arg_json_format_flags
, arg_pager_flags
, arg_legend
);
1688 static int action_attach(DissectedImage
*m
, LoopDevice
*d
) {
1694 r
= loop_device_set_autoclear(d
, false);
1696 return log_error_errno(r
, "Failed to disable auto-clear logic on loopback device: %m");
1698 r
= dissected_image_relinquish(m
);
1700 return log_error_errno(r
, "Failed to relinquish DM and loopback block devices: %m");
1706 static int action_detach(const char *path
) {
1707 _cleanup_(loop_device_unrefp
) LoopDevice
*loop
= NULL
;
1708 _cleanup_close_
int fd
= -EBADF
;
1714 fd
= open(path
, O_PATH
|O_CLOEXEC
);
1716 return log_error_errno(errno
, "Failed to open '%s': %m", path
);
1718 if (fstat(fd
, &st
) < 0)
1719 return log_error_errno(errno
, "Failed to stat '%s': %m", path
);
1721 if (S_ISBLK(st
.st_mode
)) {
1722 r
= loop_device_open_from_fd(fd
, O_RDONLY
, LOCK_EX
, &loop
);
1724 return log_error_errno(r
, "Failed to open '%s' as loopback block device: %m", path
);
1726 } else if (S_ISREG(st
.st_mode
)) {
1727 _cleanup_(sd_device_enumerator_unrefp
) sd_device_enumerator
*e
= NULL
;
1729 /* If a regular file is specified, search for a loopback block device that is backed by it */
1731 r
= sd_device_enumerator_new(&e
);
1733 return log_error_errno(r
, "Failed to allocate enumerator: %m");
1735 r
= sd_device_enumerator_add_match_subsystem(e
, "block", true);
1737 return log_error_errno(r
, "Failed to match block devices: %m");
1739 r
= sd_device_enumerator_add_match_sysname(e
, "loop*");
1741 return log_error_errno(r
, "Failed to match loopback block devices: %m");
1743 (void) sd_device_enumerator_allow_uninitialized(e
);
1745 FOREACH_DEVICE(e
, d
) {
1746 _cleanup_(loop_device_unrefp
) LoopDevice
*entry_loop
= NULL
;
1748 if (!device_is_devtype(d
, "disk")) /* Filter out partition block devices */
1751 r
= loop_device_open(d
, O_RDONLY
, LOCK_SH
, &entry_loop
);
1753 log_device_warning_errno(d
, r
, "Failed to open loopback block device, skipping: %m");
1757 if (entry_loop
->backing_devno
== st
.st_dev
&& entry_loop
->backing_inode
== st
.st_ino
) {
1758 /* Found it! The kernel allows attaching a single file to multiple loopback
1759 * devices. Let's destruct them in reverse order, i.e. find the last matching
1760 * loopback device here, rather than the first. */
1762 loop_device_unref(loop
);
1763 loop
= TAKE_PTR(entry_loop
);
1768 return log_error_errno(SYNTHETIC_ERRNO(ENXIO
), "No loopback block device backed by '%s' found.", path
);
1770 r
= loop_device_flock(loop
, LOCK_EX
);
1772 return log_error_errno(r
, "Failed to upgrade device lock: %m");
1775 r
= loop_device_set_autoclear(loop
, true);
1777 log_warning_errno(r
, "Failed to enable autoclear logic on '%s', ignoring: %m", loop
->node
);
1779 loop_device_unrelinquish(loop
);
1783 static int action_validate(void) {
1786 r
= dissect_image_file_and_warn(
1788 &arg_verity_settings
,
1796 if (isatty(STDOUT_FILENO
) && emoji_enabled())
1797 printf("%s ", special_glyph(SPECIAL_GLYPH_SPARKLES
));
1799 printf("%sOK%s", ansi_highlight_green(), ansi_normal());
1801 if (isatty(STDOUT_FILENO
) && emoji_enabled())
1802 printf(" %s", special_glyph(SPECIAL_GLYPH_SPARKLES
));
1808 static int run(int argc
, char *argv
[]) {
1809 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
1810 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
1811 uint32_t loop_flags
;
1816 if (invoked_as(argv
, "mount.ddi"))
1817 r
= parse_argv_as_mount_helper(argc
, argv
);
1819 r
= parse_argv(argc
, argv
);
1824 r
= path_pick_update_warn(
1826 &pick_filter_image_raw
,
1827 PICK_ARCHITECTURE
|PICK_TRIES
,
1828 /* ret_result= */ NULL
);
1833 switch (arg_action
) {
1835 return action_umount(arg_path
);
1838 return action_detach(arg_image
);
1840 case ACTION_DISCOVER
:
1841 return action_discover();
1844 /* All other actions need the image dissected */
1849 r
= verity_settings_load(
1850 &arg_verity_settings
,
1851 arg_image
, NULL
, NULL
);
1853 return log_error_errno(r
, "Failed to read verity artifacts for %s: %m", arg_image
);
1855 if (arg_verity_settings
.data_path
)
1856 arg_flags
|= DISSECT_IMAGE_NO_PARTITION_TABLE
; /* We only support Verity per file system,
1857 * hence if there's external Verity data
1858 * available we turn off partition table
1861 if (arg_action
== ACTION_VALIDATE
)
1862 return action_validate();
1864 open_flags
= FLAGS_SET(arg_flags
, DISSECT_IMAGE_DEVICE_READ_ONLY
) ? O_RDONLY
: O_RDWR
;
1865 loop_flags
= FLAGS_SET(arg_flags
, DISSECT_IMAGE_NO_PARTITION_TABLE
) ? 0 : LO_FLAGS_PARTSCAN
;
1867 r
= loop_device_make_by_path_memory(arg_image
, open_flags
, /* sector_size= */ UINT32_MAX
, loop_flags
, LOCK_SH
, &d
);
1869 r
= loop_device_make_by_path(arg_image
, open_flags
, /* sector_size= */ UINT32_MAX
, loop_flags
, LOCK_SH
, &d
);
1871 return log_error_errno(r
, "Failed to set up loopback device for %s: %m", arg_image
);
1874 r
= loop_device_set_filename(d
, arg_loop_ref
);
1876 log_warning_errno(r
, "Failed to set loop reference string to '%s', ignoring: %m", arg_loop_ref
);
1879 r
= dissect_loop_device_and_warn(
1881 &arg_verity_settings
,
1882 /* mount_options= */ NULL
,
1889 if (arg_action
== ACTION_ATTACH
)
1890 return action_attach(m
, d
);
1892 r
= dissected_image_load_verity_sig_partition(
1895 &arg_verity_settings
);
1897 return log_error_errno(r
, "Failed to load verity signature partition: %m");
1899 if (arg_action
!= ACTION_DISSECT
) {
1900 r
= dissected_image_decrypt_interactively(
1902 &arg_verity_settings
,
1909 switch (arg_action
) {
1911 case ACTION_DISSECT
:
1912 return action_dissect(m
, d
);
1915 return action_mount(m
, d
);
1919 case ACTION_COPY_FROM
:
1920 case ACTION_COPY_TO
:
1921 return action_list_or_mtree_or_copy(m
, d
);
1924 return action_with(m
, d
);
1927 assert_not_reached();
1931 DEFINE_MAIN_FUNCTION(run
);