1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
8 #include "capability-util.h"
9 #include "dissect-image.h"
13 #include "format-table.h"
17 #include "machine-image.h"
18 #include "main-func.h"
19 #include "missing_magic.h"
21 #include "mount-util.h"
22 #include "mountpoint-util.h"
25 #include "parse-util.h"
26 #include "pretty-print.h"
27 #include "process-util.h"
28 #include "sort-util.h"
29 #include "stat-util.h"
30 #include "terminal-util.h"
31 #include "user-util.h"
39 } arg_action
= ACTION_STATUS
;
40 static char **arg_hierarchies
= NULL
; /* "/usr" + "/opt" by default */
41 static char *arg_root
= NULL
;
42 static JsonFormatFlags arg_json_format_flags
= JSON_FORMAT_OFF
;
43 static PagerFlags arg_pager_flags
= 0;
45 STATIC_DESTRUCTOR_REGISTER(arg_hierarchies
, strv_freep
);
46 STATIC_DESTRUCTOR_REGISTER(arg_root
, freep
);
48 static int is_our_mount_point(const char *p
) {
49 _cleanup_free_
char *buf
= NULL
, *f
= NULL
;
54 r
= path_is_mount_point(p
, NULL
, 0);
56 log_debug_errno(r
, "Hierarchy '%s' doesn't exist.", p
);
60 return log_error_errno(r
, "Failed to determine whether '%s' is a mount point: %m", p
);
62 log_debug("Hierarchy '%s' is not a mount point, skipping.", p
);
66 /* So we know now that it's a mount point. Now let's check if it's one of ours, so that we don't
67 * accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this
68 * check by looking into the metadata directory we place in merged mounts: if the file
69 * .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to
70 * believe this is one of our mounts. This thorough check has the benefit that we aren't easily
71 * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake
72 * them for a live sysext tree. */
74 f
= path_join(p
, ".systemd-sysext/dev");
78 r
= read_one_line_file(f
, &buf
);
80 log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p
);
84 return log_error_errno(r
, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p
);
86 r
= parse_dev(buf
, &dev
);
88 return log_error_errno(r
, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p
);
90 if (lstat(p
, &st
) < 0)
91 return log_error_errno(r
, "Failed to stat %s: %m", p
);
93 if (st
.st_dev
!= dev
) {
94 log_debug("Hierarchy '%s' reports a different device major/minor than what we are seeing, assuming offline copy.", p
);
101 static int unmerge_hierarchy(const char *p
) {
105 /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
106 * systems where /usr/ is a mount point of its own already. */
108 r
= is_our_mount_point(p
);
114 r
= umount_verbose(LOG_ERR
, p
, MNT_DETACH
|UMOUNT_NOFOLLOW
);
116 return log_error_errno(r
, "Failed to unmount file system '%s': %m", p
);
118 log_info("Unmerged '%s'.", p
);
124 static int unmerge(void) {
128 STRV_FOREACH(p
, arg_hierarchies
) {
129 _cleanup_free_
char *resolved
= NULL
;
131 r
= chase_symlinks(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
133 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
137 log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
144 r
= unmerge_hierarchy(resolved
);
145 if (r
< 0 && ret
== 0)
152 static int status(void) {
153 _cleanup_(table_unrefp
) Table
*t
= NULL
;
157 t
= table_new("hierarchy", "extensions", "since");
161 (void) table_set_empty_string(t
, "-");
163 STRV_FOREACH(p
, arg_hierarchies
) {
164 _cleanup_free_
char *resolved
= NULL
, *f
= NULL
, *buf
= NULL
;
165 _cleanup_strv_free_
char **l
= NULL
;
168 r
= chase_symlinks(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
170 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
174 log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
178 r
= is_our_mount_point(resolved
);
185 TABLE_STRING
, "none",
186 TABLE_SET_COLOR
, ansi_grey(),
189 return table_log_add_error(r
);
194 f
= path_join(*p
, ".systemd-sysext/extensions");
198 r
= read_full_file(f
, &buf
, NULL
);
200 return log_error_errno(r
, "Failed to open '%s': %m", f
);
202 l
= strv_split_newlines(buf
);
206 if (stat(*p
, &st
) < 0)
207 return log_error_errno(r
, "Failed to stat() '%s': %m", *p
);
213 TABLE_TIMESTAMP
, timespec_load(&st
.st_mtim
));
215 return table_log_add_error(r
);
224 (void) table_set_sort(t
, (size_t) 0, (size_t) -1);
226 if (arg_json_format_flags
& (JSON_FORMAT_OFF
|JSON_FORMAT_PRETTY
|JSON_FORMAT_PRETTY_AUTO
))
227 (void) pager_open(arg_pager_flags
);
229 r
= table_print_json(t
, stdout
, arg_json_format_flags
);
231 return table_log_add_error(r
);
236 static int mount_overlayfs(
240 _cleanup_free_
char *options
= NULL
;
241 bool separator
= false;
247 options
= strdup("lowerdir=");
251 STRV_FOREACH(l
, layers
) {
252 _cleanup_free_
char *escaped
= NULL
;
254 escaped
= shell_escape(*l
, ",:");
258 if (!strextend(&options
, separator
? ":" : "", escaped
))
264 /* Now mount the actual overlayfs */
265 r
= mount_nofollow_verbose(LOG_ERR
, "sysext", where
, "overlay", MS_RDONLY
, options
);
272 static int merge_hierarchy(
273 const char *hierarchy
,
276 const char *meta_path
,
277 const char *overlay_path
) {
279 _cleanup_free_
char *resolved_hierarchy
= NULL
, *f
= NULL
, *buf
= NULL
;
280 _cleanup_strv_free_
char **layers
= NULL
;
287 assert(overlay_path
);
289 /* Resolve the path of the host's version of the hierarchy, i.e. what we want to use as lowest layer
290 * in the overlayfs stack. */
291 r
= chase_symlinks(hierarchy
, arg_root
, CHASE_PREFIX_ROOT
, &resolved_hierarchy
, NULL
);
293 log_debug_errno(r
, "Hierarchy '%s' on host doesn't exist, not merging.", hierarchy
);
295 return log_error_errno(r
, "Failed to resolve host hierarchy '%s': %m", hierarchy
);
297 r
= dir_is_empty(resolved_hierarchy
);
299 return log_error_errno(r
, "Failed to check if host hierarchy '%s' is empty: %m", resolved_hierarchy
);
301 log_debug("Host hierarchy '%s' is empty, not merging.", resolved_hierarchy
);
302 resolved_hierarchy
= mfree(resolved_hierarchy
);
306 /* Let's generate a metadata file that lists all extensions we took into account for this
307 * hierarchy. We include this in the final fs, to make things nicely discoverable and
309 f
= path_join(meta_path
, ".systemd-sysext/extensions");
313 buf
= strv_join(extensions
, "\n");
317 r
= write_string_file(f
, buf
, WRITE_STRING_FILE_CREATE
|WRITE_STRING_FILE_MKDIR_0755
);
319 return log_error_errno(r
, "Failed to write extension meta file '%s': %m", f
);
321 /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */
322 layers
= strv_new(meta_path
);
326 /* Put the extensions in the middle */
327 STRV_FOREACH(p
, paths
) {
328 _cleanup_free_
char *resolved
= NULL
;
330 r
= chase_symlinks(hierarchy
, *p
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
332 log_debug_errno(r
, "Hierarchy '%s' in extension '%s' doesn't exist, not merging.", hierarchy
, *p
);
336 return log_error_errno(r
, "Failed to resolve hierarchy '%s' in extension '%s': %m", hierarchy
, *p
);
338 r
= dir_is_empty(resolved
);
340 return log_error_errno(r
, "Failed to check if hierarchy '%s' in extension '%s' is empty: %m", resolved
, *p
);
342 log_debug("Hierarchy '%s' in extension '%s' is empty, not merging.", hierarchy
, *p
);
346 r
= strv_consume(&layers
, TAKE_PTR(resolved
));
351 if (!layers
[1]) /* No extension with files in this hierarchy? Then don't do anything. */
354 if (resolved_hierarchy
) {
355 /* Add the host hierarchy as last (lowest) layer in the stack */
356 r
= strv_consume(&layers
, TAKE_PTR(resolved_hierarchy
));
361 r
= mkdir_p(overlay_path
, 0700);
363 return log_error_errno(r
, "Failed to make directory '%s': %m", overlay_path
);
365 r
= mount_overlayfs(overlay_path
, layers
);
369 /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra turbo safety 😎 */
370 r
= bind_remount_recursive(overlay_path
, MS_RDONLY
, MS_RDONLY
, NULL
);
372 return log_error_errno(r
, "Failed to make bind mount '%s' read-only: %m", overlay_path
);
374 /* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that
375 * available in the metadata directory. This is useful to detect whether the metadata dir actually
376 * belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely
377 * we are looking at a live sysext tree, and not an unpacked tar or so of one. */
378 if (stat(overlay_path
, &st
) < 0)
379 return log_error_errno(r
, "Failed to stat mount '%s': %m", overlay_path
);
382 f
= path_join(meta_path
, ".systemd-sysext/dev");
386 r
= write_string_filef(f
, WRITE_STRING_FILE_CREATE
, "%u:%u", major(st
.st_dev
), minor(st
.st_dev
));
388 return log_error_errno(r
, "Failed to write '%s': %m", f
);
390 /* Make sure the top-level dir has an mtime marking the point we established the merge */
391 if (utimensat(AT_FDCWD
, meta_path
, NULL
, AT_SYMLINK_NOFOLLOW
) < 0)
392 return log_error_errno(r
, "Failed fix mtime of '%s': %m", meta_path
);
397 static int strverscmpp(char *const* a
, char *const* b
) {
398 /* usable in qsort() for sorting a string array with strverscmp() */
399 return strverscmp(*a
, *b
);
402 static int merge_subprocess(Hashmap
*images
, const char *workspace
) {
403 _cleanup_free_
char *host_os_release_id
= NULL
, *host_os_release_version_id
= NULL
, *host_os_release_sysext_level
= NULL
,
405 _cleanup_strv_free_
char **extensions
= NULL
, **paths
= NULL
;
406 size_t n_extensions
= 0;
407 unsigned n_ignored
= 0;
412 /* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on
413 * the host otherwise. */
414 r
= mount_nofollow_verbose(LOG_ERR
, NULL
, "/run", NULL
, MS_SLAVE
|MS_REC
, NULL
);
416 return log_error_errno(r
, "Failed to remount /run/ MS_SLAVE: %m");
418 /* Let's create the workspace if it's missing */
419 r
= mkdir_p(workspace
, 0700);
421 return log_error_errno(r
, "Failed to create /run/systemd/sysext: %m");
423 /* Let's mount a tmpfs to our workspace. This way we don't need to clean up the inodes we mount over,
424 * but let the kernel do that entirely automatically, once our namespace dies. Note that this file
425 * system won't be visible to anyone but us, since we opened our own namespace and then made the
426 * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */
427 r
= mount_nofollow_verbose(LOG_ERR
, "sysexit", workspace
, "tmpfs", 0, "mode=0700");
431 /* Acquire host OS release info, so that we can compare it with the extension's data */
432 r
= parse_os_release(
434 "ID", &host_os_release_id
,
435 "VERSION_ID", &host_os_release_version_id
,
436 "SYSEXT_LEVEL", &host_os_release_sysext_level
,
439 return log_error_errno(r
, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root
));
441 /* Let's now mount all images */
442 HASHMAP_FOREACH(img
, images
) {
443 _cleanup_free_
char *p
= NULL
,
444 *extension_os_release_id
= NULL
, *extension_os_release_version_id
= NULL
, *extension_os_release_sysext_level
= NULL
;
446 p
= path_join(workspace
, "extensions", img
->name
);
450 r
= mkdir_p(p
, 0700);
452 return log_error_errno(r
, "Failed to create %s: %m", p
);
455 case IMAGE_DIRECTORY
:
456 case IMAGE_SUBVOLUME
:
457 r
= mount_nofollow_verbose(LOG_ERR
, img
->path
, p
, NULL
, MS_BIND
, NULL
);
461 /* Make this a read-only bind mount */
462 r
= bind_remount_recursive(p
, MS_RDONLY
, MS_RDONLY
, NULL
);
464 return log_error_errno(r
, "Failed to make bind mount '%s' read-only: %m", p
);
470 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
471 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
472 _cleanup_(decrypted_image_unrefp
) DecryptedImage
*di
= NULL
;
473 DissectImageFlags flags
= DISSECT_IMAGE_READ_ONLY
|DISSECT_IMAGE_REQUIRE_ROOT
|DISSECT_IMAGE_MOUNT_ROOT_ONLY
;
475 r
= loop_device_make_by_path(img
->path
, O_RDONLY
, 0, &d
);
477 return log_error_errno(r
, "Failed to set up loopback device: %m");
479 r
= dissect_image_and_warn(
489 r
= dissected_image_decrypt_interactively(
497 r
= dissected_image_mount_and_warn(
506 r
= decrypted_image_relinquish(di
);
508 return log_error_errno(r
, "Failed to relinquish DM devices: %m");
511 loop_device_relinquish(d
);
515 assert_not_reached("Unsupported image type");
518 /* Insist that extension images do not overwrite the underlying OS release file (it's fine if
519 * they place one in /etc/os-release, i.e. where things don't matter, as they aren't
521 r
= chase_symlinks("/usr/lib/os-release", p
, CHASE_PREFIX_ROOT
, NULL
, NULL
);
524 return log_error_errno(r
, "Failed to determine whether /usr/lib/os-release exists in the extension image: %m");
526 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
527 "Extension image contains /usr/lib/os-release file, which is not allowed (it may carry /etc/os-release), refusing.");
529 /* Now that we can look into the extension image, let's see if the OS version is compatible */
530 r
= parse_os_release(
532 "ID", &extension_os_release_id
,
533 "VERSION_ID", &extension_os_release_version_id
,
534 "SYSEXT_LEVEL", &extension_os_release_sysext_level
,
537 log_notice_errno(r
, "Extension '%s' carries no os-release data, not checking for version compatibility.", img
->name
);
539 return log_error_errno(r
, "Failed to acquire 'os-release' data of extension '%s': %m", img
->name
);
541 if (!streq_ptr(host_os_release_id
, extension_os_release_id
)) {
542 log_notice("Extension '%s' is for OS '%s', but running on '%s', ignoring extension.",
543 img
->name
, strna(extension_os_release_id
), strna(host_os_release_id
));
548 /* If the extension has a sysext API level declared, then it must match the host API level. Otherwise, compare OS version as a whole */
549 if (extension_os_release_sysext_level
) {
550 if (!streq_ptr(host_os_release_sysext_level
, extension_os_release_sysext_level
)) {
551 log_notice("Extension '%s' is for sysext API level '%s', but running on sysext API level '%s', ignoring extension.",
552 img
->name
, extension_os_release_sysext_level
, strna(host_os_release_sysext_level
));
557 if (!streq_ptr(host_os_release_version_id
, extension_os_release_version_id
)) {
558 log_notice("Extension '%s' is for OS version '%s', but running on OS version '%s', ignoring extension.",
559 img
->name
, extension_os_release_version_id
, strna(host_os_release_version_id
));
565 log_debug("Version info of extension '%s' matches host.", img
->name
);
568 /* Noice! This one is an extension we want. */
569 r
= strv_extend(&extensions
, img
->name
);
576 /* Nothing left? Then shortcut things */
577 if (n_extensions
== 0) {
579 log_info("No suitable extensions found (%u ignored due to incompatible version).", n_ignored
);
581 log_info("No extensions found.");
585 /* Order by version sort (i.e. libc strverscmp()) */
586 typesafe_qsort(extensions
, n_extensions
, strverscmpp
);
588 buf
= strv_join(extensions
, "', '");
592 log_info("Using extensions '%s'.", buf
);
594 /* Build table of extension paths (in reverse order) */
595 paths
= new0(char*, n_extensions
+ 1);
599 for (size_t k
= 0; k
< n_extensions
; k
++) {
600 _cleanup_free_
char *p
= NULL
;
602 assert_se(img
= hashmap_get(images
, extensions
[n_extensions
- 1 - k
]));
604 p
= path_join(workspace
, "extensions", img
->name
);
608 paths
[k
] = TAKE_PTR(p
);
611 /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
613 STRV_FOREACH(h
, arg_hierarchies
) {
614 _cleanup_free_
char *resolved
= NULL
;
616 r
= chase_symlinks(*h
, arg_root
, CHASE_PREFIX_ROOT
|CHASE_NONEXISTENT
, &resolved
, NULL
);
618 return log_error_errno(r
, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root
), *h
);
620 r
= unmerge_hierarchy(resolved
);
625 /* Create overlayfs mounts for all hierarchies */
626 STRV_FOREACH(h
, arg_hierarchies
) {
627 _cleanup_free_
char *meta_path
= NULL
, *overlay_path
= NULL
;
629 meta_path
= path_join(workspace
, "meta", *h
); /* The place where to store metadata about this instance */
633 overlay_path
= path_join(workspace
, "overlay", *h
); /* The resulting overlayfs instance */
637 r
= merge_hierarchy(*h
, extensions
, paths
, meta_path
, overlay_path
);
642 /* And move them all into place. This is where things appear in the host namespace */
643 STRV_FOREACH(h
, arg_hierarchies
) {
644 _cleanup_free_
char *p
= NULL
, *resolved
= NULL
;
646 p
= path_join(workspace
, "overlay", *h
);
650 if (laccess(p
, F_OK
) < 0) {
652 return log_error_errno(errno
, "Failed to check if '%s' exists: %m", p
);
654 /* Hierarchy apparently was empty in all extensions, and wasn't mounted, ignoring. */
658 r
= chase_symlinks(*h
, arg_root
, CHASE_PREFIX_ROOT
|CHASE_NONEXISTENT
, &resolved
, NULL
);
660 return log_error_errno(r
, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root
), *h
);
662 r
= mkdir_p(resolved
, 0755);
664 return log_error_errno(r
, "Failed to create hierarchy mount point '%s': %m", resolved
);
666 r
= mount_nofollow_verbose(LOG_ERR
, p
, resolved
, NULL
, MS_BIND
, NULL
);
670 log_info("Merged extensions into '%s'.", resolved
);
676 static int merge(Hashmap
*images
) {
680 r
= safe_fork("(sd-sysext)", FORK_DEATHSIG
|FORK_LOG
|FORK_NEW_MOUNTNS
, &pid
);
682 return log_error_errno(r
, "Failed to fork off child: %m");
684 /* Child with its own mount namespace */
686 r
= merge_subprocess(images
, "/run/systemd/sysext");
690 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
691 * created below /run. Nice! */
693 _exit(r
> 0 ? EXIT_SUCCESS
: 123); /* 123 means: didn't find any extensions */
696 r
= wait_for_terminate_and_check("(sd-sysext)", pid
, WAIT_LOG_ABNORMAL
);
700 return r
!= 123; /* exit code 123 means: didn't do anything */
703 static int help(void) {
704 _cleanup_free_
char *link
= NULL
;
707 r
= terminal_urlify_man("systemd-sysext", "1", &link
);
711 printf("%1$s [OPTIONS...] [DEVICE]\n"
712 "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n"
713 "\n%3$sCommands:%4$s\n"
714 " -h --help Show this help\n"
715 " --version Show package version\n"
716 " -m --merge Merge extensions into /usr/ and /opt/\n"
717 " -u --unmerge Unmerge extensions from /usr/ and /opt/\n"
718 " -R --refresh Unmerge/merge extensions again\n"
719 " -l --list List all OS images\n"
720 "\n%3$sOptions:%4$s\n"
721 " --no-pager Do not pipe output into a pager\n"
722 " --root=PATH Operate relative to root path\n"
723 " --json=pretty|short|off\n"
724 " Generate JSON output\n"
725 "\nSee the %2$s for details.\n"
726 , program_invocation_short_name
728 , ansi_underline(), ansi_normal()
729 , ansi_highlight(), ansi_normal()
735 static int parse_argv(int argc
, char *argv
[]) {
748 static const struct option options
[] = {
749 { "help", no_argument
, NULL
, 'h' },
750 { "version", no_argument
, NULL
, ARG_VERSION
},
751 { "no-pager", no_argument
, NULL
, ARG_NO_PAGER
},
752 { "root", required_argument
, NULL
, ARG_ROOT
},
753 { "merge", no_argument
, NULL
, 'm' },
754 { "unmerge", no_argument
, NULL
, 'u' },
755 { "refresh", no_argument
, NULL
, 'R' },
756 { "list", no_argument
, NULL
, 'l' },
757 { "json", required_argument
, NULL
, ARG_JSON
},
766 while ((c
= getopt_long(argc
, argv
, "hmuRl", options
, NULL
)) >= 0)
777 arg_pager_flags
|= PAGER_DISABLE
;
781 arg_action
= ACTION_MERGE
;
785 arg_action
= ACTION_UNMERGE
;
789 arg_action
= ACTION_REFRESH
;
793 arg_action
= ACTION_LIST
;
797 r
= parse_path_argument_and_warn(optarg
, false, &arg_root
);
803 r
= json_parse_cmdline_parameter_and_warn(optarg
, &arg_json_format_flags
);
813 assert_not_reached("Unhandled option");
816 if (argc
- optind
> 0)
817 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
818 "Unexpected argument.");
823 static int parse_env(void) {
824 _cleanup_strv_free_
char **l
= NULL
;
829 e
= secure_getenv("SYSTEMD_SYSEXT_HIERARCHIES");
833 /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
834 * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
837 r
= strv_split_full(&l
, e
, ":", EXTRACT_DONT_COALESCE_SEPARATORS
);
839 return log_error_errno(r
, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES: %m");
842 if (!path_is_absolute(*p
))
843 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
844 "Hierarchy path '%s' is not absolute, refusing.", *p
);
846 if (!path_is_normalized(*p
))
847 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
848 "Hierarchy path '%s' is not normalized, refusing.", *p
);
850 if (path_equal(*p
, "/"))
851 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
852 "Hierarchy path '%s' is the root fs, refusing.", *p
);
856 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
857 "No hierarchies specified, refusing.");
859 strv_free_and_replace(arg_hierarchies
, l
);
863 static int run(int argc
, char *argv
[]) {
864 _cleanup_(hashmap_freep
) Hashmap
*images
= NULL
;
867 log_show_color(true);
868 log_parse_environment();
871 r
= parse_argv(argc
, argv
);
879 if (!arg_hierarchies
) {
880 arg_hierarchies
= strv_new("/usr", "/opt");
881 if (!arg_hierarchies
)
885 /* Given that things deep down in the child process will fail, let's catch the no-privilege issue
887 if (!IN_SET(arg_action
, ACTION_STATUS
, ACTION_LIST
) && !have_effective_cap(CAP_SYS_ADMIN
))
888 return log_error_errno(SYNTHETIC_ERRNO(EPERM
), "Need to be privileged.");
890 if (arg_action
== ACTION_STATUS
)
893 if (arg_action
== ACTION_UNMERGE
)
896 images
= hashmap_new(&image_hash_ops
);
900 r
= image_discover(IMAGE_EXTENSION
, arg_root
, images
);
902 return log_error_errno(r
, "Failed to discover extension images: %m");
904 switch (arg_action
) {
907 _cleanup_(table_unrefp
) Table
*t
= NULL
;
910 if ((arg_json_format_flags
& JSON_FORMAT_OFF
) && hashmap_isempty(images
)) {
911 log_info("No OS extensions found.");
915 t
= table_new("name", "type", "path", "time");
919 HASHMAP_FOREACH(img
, images
) {
922 TABLE_STRING
, img
->name
,
923 TABLE_STRING
, image_type_to_string(img
->type
),
924 TABLE_PATH
, img
->path
,
925 TABLE_TIMESTAMP
, img
->mtime
!= 0 ? img
->mtime
: img
->crtime
);
927 return table_log_add_error(r
);
930 (void) table_set_sort(t
, (size_t) 0, (size_t) -1);
932 if (arg_json_format_flags
& (JSON_FORMAT_OFF
|JSON_FORMAT_PRETTY
|JSON_FORMAT_PRETTY_AUTO
))
933 (void) pager_open(arg_pager_flags
);
935 r
= table_print_json(t
, stdout
, arg_json_format_flags
);
937 return table_log_print_error(r
);
946 /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if
947 * we find things are already merged...) */
948 STRV_FOREACH(p
, arg_hierarchies
) {
949 _cleanup_free_
char *resolved
= NULL
;
951 r
= chase_symlinks(*p
, arg_root
, CHASE_PREFIX_ROOT
, &resolved
, NULL
);
953 log_debug_errno(r
, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root
), *p
);
957 return log_error_errno(r
, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root
), *p
);
959 r
= is_our_mount_point(resolved
);
963 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
964 "Hierarchy '%s' is already merged.", *p
);
972 r
= merge(images
); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted
973 * now. When it does so it implicitly unmounts any overlayfs placed there
974 * before. Returns == 0 if it did nothing, i.e. no extension images
975 * found. In this case the old overlayfs remains in place if there was
979 if (r
== 0) /* No images found? Then unmerge. The goal of --refresh is after all that after
980 * having called there's a guarantee that the merge status matches the installed
984 /* Net result here is that:
986 * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged
989 * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll
990 * have unmerged and then merged things again.
992 * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have
995 * 4. If there was no overlayfs mount so far, and no extensions installed, we implement a
1001 assert_not_reached("Uneexpected action");
1007 DEFINE_MAIN_FUNCTION(run
);