1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <fcntl.h>
4 #include <getopt.h>
5 #include <linux/loop.h>
6 #include <sys/mount.h>
7 #include <unistd.h>
8 
9 #include "capability-util.h"
10 #include "chase-symlinks.h"
11 #include "devnum-util.h"
12 #include "discover-image.h"
13 #include "dissect-image.h"
14 #include "env-util.h"
15 #include "escape.h"
16 #include "extension-release.h"
17 #include "fd-util.h"
18 #include "fileio.h"
19 #include "format-table.h"
20 #include "fs-util.h"
21 #include "hashmap.h"
22 #include "log.h"
23 #include "main-func.h"
24 #include "missing_magic.h"
25 #include "mkdir.h"
26 #include "mount-util.h"
27 #include "mountpoint-util.h"
28 #include "os-util.h"
29 #include "pager.h"
30 #include "parse-argument.h"
31 #include "parse-util.h"
32 #include "pretty-print.h"
33 #include "process-util.h"
34 #include "sort-util.h"
35 #include "terminal-util.h"
36 #include "user-util.h"
37 #include "verbs.h"
38 
39 static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default */
40 static char *arg_root = NULL;
41 static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
42 static PagerFlags arg_pager_flags = 0;
43 static bool arg_legend = true;
44 static bool arg_force = false;
45 
46 STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep);
47 STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
48 
is_our_mount_point(const char * p)49 static int is_our_mount_point(const char *p) {
50         _cleanup_free_ char *buf = NULL, *f = NULL;
51         struct stat st;
52         dev_t dev;
53         int r;
54 
55         r = path_is_mount_point(p, NULL, 0);
56         if (r == -ENOENT) {
57                 log_debug_errno(r, "Hierarchy '%s' doesn't exist.", p);
58                 return false;
59         }
60         if (r < 0)
61                 return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", p);
62         if (r == 0) {
63                 log_debug("Hierarchy '%s' is not a mount point, skipping.", p);
64                 return false;
65         }
66 
67         /* 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
68          * accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this
69          * check by looking into the metadata directory we place in merged mounts: if the file
70          * .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to
71          * believe this is one of our mounts. This thorough check has the benefit that we aren't easily
72          * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake
73          * them for a live sysext tree. */
74 
75         f = path_join(p, ".systemd-sysext/dev");
76         if (!f)
77                 return log_oom();
78 
79         r = read_one_line_file(f, &buf);
80         if (r == -ENOENT) {
81                 log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p);
82                 return false;
83         }
84         if (r < 0)
85                 return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p);
86 
87         r = parse_devnum(buf, &dev);
88         if (r < 0)
89                 return log_error_errno(r, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p);
90 
91         if (lstat(p, &st) < 0)
92                 return log_error_errno(r, "Failed to stat %s: %m", p);
93 
94         if (st.st_dev != dev) {
95                 log_debug("Hierarchy '%s' reports a different device major/minor than what we are seeing, assuming offline copy.", p);
96                 return false;
97         }
98 
99         return true;
100 }
101 
unmerge_hierarchy(const char * p)102 static int unmerge_hierarchy(const char *p) {
103         int r;
104 
105         for (;;) {
106                 /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
107                  * systems where /usr/ is a mount point of its own already. */
108 
109                 r = is_our_mount_point(p);
110                 if (r < 0)
111                         return r;
112                 if (r == 0)
113                         break;
114 
115                 r = umount_verbose(LOG_ERR, p, MNT_DETACH|UMOUNT_NOFOLLOW);
116                 if (r < 0)
117                         return log_error_errno(r, "Failed to unmount file system '%s': %m", p);
118 
119                 log_info("Unmerged '%s'.", p);
120         }
121 
122         return 0;
123 }
124 
unmerge(void)125 static int unmerge(void) {
126         int r, ret = 0;
127 
128         STRV_FOREACH(p, arg_hierarchies) {
129                 _cleanup_free_ char *resolved = NULL;
130 
131                 r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
132                 if (r == -ENOENT) {
133                         log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
134                         continue;
135                 }
136                 if (r < 0) {
137                         log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
138                         if (ret == 0)
139                                 ret = r;
140 
141                         continue;
142                 }
143 
144                 r = unmerge_hierarchy(resolved);
145                 if (r < 0 && ret == 0)
146                         ret = r;
147         }
148 
149         return ret;
150 }
151 
verb_unmerge(int argc,char ** argv,void * userdata)152 static int verb_unmerge(int argc, char **argv, void *userdata) {
153 
154         if (!have_effective_cap(CAP_SYS_ADMIN))
155                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
156 
157         return unmerge();
158 }
159 
verb_status(int argc,char ** argv,void * userdata)160 static int verb_status(int argc, char **argv, void *userdata) {
161         _cleanup_(table_unrefp) Table *t = NULL;
162         int r, ret = 0;
163 
164         t = table_new("hierarchy", "extensions", "since");
165         if (!t)
166                 return log_oom();
167 
168         (void) table_set_empty_string(t, "-");
169 
170         STRV_FOREACH(p, arg_hierarchies) {
171                 _cleanup_free_ char *resolved = NULL, *f = NULL, *buf = NULL;
172                 _cleanup_strv_free_ char **l = NULL;
173                 struct stat st;
174 
175                 r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
176                 if (r == -ENOENT) {
177                         log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
178                         continue;
179                 }
180                 if (r < 0) {
181                         log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
182                         goto inner_fail;
183                 }
184 
185                 r = is_our_mount_point(resolved);
186                 if (r < 0)
187                         goto inner_fail;
188                 if (r == 0) {
189                         r = table_add_many(
190                                         t,
191                                         TABLE_PATH, *p,
192                                         TABLE_STRING, "none",
193                                         TABLE_SET_COLOR, ansi_grey(),
194                                         TABLE_EMPTY);
195                         if (r < 0)
196                                 return table_log_add_error(r);
197 
198                         continue;
199                 }
200 
201                 f = path_join(*p, ".systemd-sysext/extensions");
202                 if (!f)
203                         return log_oom();
204 
205                 r = read_full_file(f, &buf, NULL);
206                 if (r < 0)
207                         return log_error_errno(r, "Failed to open '%s': %m", f);
208 
209                 l = strv_split_newlines(buf);
210                 if (!l)
211                         return log_oom();
212 
213                 if (stat(*p, &st) < 0)
214                         return log_error_errno(r, "Failed to stat() '%s': %m", *p);
215 
216                 r = table_add_many(
217                                 t,
218                                 TABLE_PATH, *p,
219                                 TABLE_STRV, l,
220                                 TABLE_TIMESTAMP, timespec_load(&st.st_mtim));
221                 if (r < 0)
222                         return table_log_add_error(r);
223 
224                 continue;
225 
226         inner_fail:
227                 if (ret == 0)
228                         ret = r;
229         }
230 
231         (void) table_set_sort(t, (size_t) 0);
232 
233         r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
234         if (r < 0)
235                 return r;
236 
237         return ret;
238 }
239 
mount_overlayfs(const char * where,char ** layers)240 static int mount_overlayfs(
241                 const char *where,
242                 char **layers) {
243 
244         _cleanup_free_ char *options = NULL;
245         bool separator = false;
246         int r;
247 
248         assert(where);
249 
250         options = strdup("lowerdir=");
251         if (!options)
252                 return log_oom();
253 
254         STRV_FOREACH(l, layers) {
255                 _cleanup_free_ char *escaped = NULL;
256 
257                 escaped = shell_escape(*l, ",:");
258                 if (!escaped)
259                         return log_oom();
260 
261                 if (!strextend(&options, separator ? ":" : "", escaped))
262                         return log_oom();
263 
264                 separator = true;
265         }
266 
267         /* Now mount the actual overlayfs */
268         r = mount_nofollow_verbose(LOG_ERR, "sysext", where, "overlay", MS_RDONLY, options);
269         if (r < 0)
270                 return r;
271 
272         return 0;
273 }
274 
merge_hierarchy(const char * hierarchy,char ** extensions,char ** paths,const char * meta_path,const char * overlay_path)275 static int merge_hierarchy(
276                 const char *hierarchy,
277                 char **extensions,
278                 char **paths,
279                 const char *meta_path,
280                 const char *overlay_path) {
281 
282         _cleanup_free_ char *resolved_hierarchy = NULL, *f = NULL, *buf = NULL;
283         _cleanup_strv_free_ char **layers = NULL;
284         struct stat st;
285         int r;
286 
287         assert(hierarchy);
288         assert(meta_path);
289         assert(overlay_path);
290 
291         /* Resolve the path of the host's version of the hierarchy, i.e. what we want to use as lowest layer
292          * in the overlayfs stack. */
293         r = chase_symlinks(hierarchy, arg_root, CHASE_PREFIX_ROOT, &resolved_hierarchy, NULL);
294         if (r == -ENOENT)
295                 log_debug_errno(r, "Hierarchy '%s' on host doesn't exist, not merging.", hierarchy);
296         else if (r < 0)
297                 return log_error_errno(r, "Failed to resolve host hierarchy '%s': %m", hierarchy);
298         else {
299                 r = dir_is_empty(resolved_hierarchy, /* ignore_hidden_or_backup= */ false);
300                 if (r < 0)
301                         return log_error_errno(r, "Failed to check if host hierarchy '%s' is empty: %m", resolved_hierarchy);
302                 if (r > 0) {
303                         log_debug("Host hierarchy '%s' is empty, not merging.", resolved_hierarchy);
304                         resolved_hierarchy = mfree(resolved_hierarchy);
305                 }
306         }
307 
308         /* Let's generate a metadata file that lists all extensions we took into account for this
309          * hierarchy. We include this in the final fs, to make things nicely discoverable and
310          * recognizable. */
311         f = path_join(meta_path, ".systemd-sysext/extensions");
312         if (!f)
313                 return log_oom();
314 
315         buf = strv_join(extensions, "\n");
316         if (!buf)
317                 return log_oom();
318 
319         r = write_string_file(f, buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755);
320         if (r < 0)
321                 return log_error_errno(r, "Failed to write extension meta file '%s': %m", f);
322 
323         /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */
324         layers = strv_new(meta_path);
325         if (!layers)
326                 return log_oom();
327 
328         /* Put the extensions in the middle */
329         STRV_FOREACH(p, paths) {
330                 _cleanup_free_ char *resolved = NULL;
331 
332                 r = chase_symlinks(hierarchy, *p, CHASE_PREFIX_ROOT, &resolved, NULL);
333                 if (r == -ENOENT) {
334                         log_debug_errno(r, "Hierarchy '%s' in extension '%s' doesn't exist, not merging.", hierarchy, *p);
335                         continue;
336                 }
337                 if (r < 0)
338                         return log_error_errno(r, "Failed to resolve hierarchy '%s' in extension '%s': %m", hierarchy, *p);
339 
340                 r = dir_is_empty(resolved, /* ignore_hidden_or_backup= */ false);
341                 if (r < 0)
342                         return log_error_errno(r, "Failed to check if hierarchy '%s' in extension '%s' is empty: %m", resolved, *p);
343                 if (r > 0) {
344                         log_debug("Hierarchy '%s' in extension '%s' is empty, not merging.", hierarchy, *p);
345                         continue;
346                 }
347 
348                 r = strv_consume(&layers, TAKE_PTR(resolved));
349                 if (r < 0)
350                         return log_oom();
351         }
352 
353         if (!layers[1]) /* No extension with files in this hierarchy? Then don't do anything. */
354                 return 0;
355 
356         if (resolved_hierarchy) {
357                 /* Add the host hierarchy as last (lowest) layer in the stack */
358                 r = strv_consume(&layers, TAKE_PTR(resolved_hierarchy));
359                 if (r < 0)
360                         return log_oom();
361         }
362 
363         r = mkdir_p(overlay_path, 0700);
364         if (r < 0)
365                 return log_error_errno(r, "Failed to make directory '%s': %m", overlay_path);
366 
367         r = mount_overlayfs(overlay_path, layers);
368         if (r < 0)
369                 return r;
370 
371         /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra turbo safety �� */
372         r = bind_remount_recursive(overlay_path, MS_RDONLY, MS_RDONLY, NULL);
373         if (r < 0)
374                 return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", overlay_path);
375 
376         /* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that
377          * available in the metadata directory. This is useful to detect whether the metadata dir actually
378          * belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely
379          * we are looking at a live sysext tree, and not an unpacked tar or so of one. */
380         if (stat(overlay_path, &st) < 0)
381                 return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path);
382 
383         free(f);
384         f = path_join(meta_path, ".systemd-sysext/dev");
385         if (!f)
386                 return log_oom();
387 
388         r = write_string_filef(f, WRITE_STRING_FILE_CREATE, "%u:%u", major(st.st_dev), minor(st.st_dev));
389         if (r < 0)
390                 return log_error_errno(r, "Failed to write '%s': %m", f);
391 
392         /* Make sure the top-level dir has an mtime marking the point we established the merge */
393         if (utimensat(AT_FDCWD, meta_path, NULL, AT_SYMLINK_NOFOLLOW) < 0)
394                 return log_error_errno(r, "Failed fix mtime of '%s': %m", meta_path);
395 
396         return 1;
397 }
398 
strverscmp_improvedp(char * const * a,char * const * b)399 static int strverscmp_improvedp(char *const* a, char *const* b) {
400         /* usable in qsort() for sorting a string array with strverscmp_improved() */
401         return strverscmp_improved(*a, *b);
402 }
403 
validate_version(const char * root,const Image * img,const char * host_os_release_id,const char * host_os_release_version_id,const char * host_os_release_sysext_level)404 static int validate_version(
405                 const char *root,
406                 const Image *img,
407                 const char *host_os_release_id,
408                 const char *host_os_release_version_id,
409                 const char *host_os_release_sysext_level) {
410 
411         int r;
412 
413         assert(root);
414         assert(img);
415 
416         if (arg_force) {
417                 log_debug("Force mode enabled, skipping version validation.");
418                 return 1;
419         }
420 
421         /* Insist that extension images do not overwrite the underlying OS release file (it's fine if
422          * they place one in /etc/os-release, i.e. where things don't matter, as they aren't
423          * merged.) */
424         r = chase_symlinks("/usr/lib/os-release", root, CHASE_PREFIX_ROOT, NULL, NULL);
425         if (r < 0) {
426                 if (r != -ENOENT)
427                         return log_error_errno(r, "Failed to determine whether /usr/lib/os-release exists in the extension image: %m");
428         } else
429                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
430                                        "Extension image contains /usr/lib/os-release file, which is not allowed (it may carry /etc/os-release), refusing.");
431 
432         r = extension_release_validate(
433                         img->name,
434                         host_os_release_id,
435                         host_os_release_version_id,
436                         host_os_release_sysext_level,
437                         in_initrd() ? "initrd" : "system",
438                         img->extension_release);
439         if (r < 0)
440                 return log_error_errno(r, "Failed to validate extension release information: %m");
441 
442         return r;
443 }
444 
merge_subprocess(Hashmap * images,const char * workspace)445 static int merge_subprocess(Hashmap *images, const char *workspace) {
446         _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_sysext_level = NULL,
447                 *buf = NULL;
448         _cleanup_strv_free_ char **extensions = NULL, **paths = NULL;
449         size_t n_extensions = 0;
450         unsigned n_ignored = 0;
451         Image *img;
452         int r;
453 
454         /* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on
455          * the host otherwise. */
456         r = mount_nofollow_verbose(LOG_ERR, NULL, "/run", NULL, MS_SLAVE|MS_REC, NULL);
457         if (r < 0)
458                 return log_error_errno(r, "Failed to remount /run/ MS_SLAVE: %m");
459 
460         /* Let's create the workspace if it's missing */
461         r = mkdir_p(workspace, 0700);
462         if (r < 0)
463                 return log_error_errno(r, "Failed to create /run/systemd/sysext: %m");
464 
465         /* Let's mount a tmpfs to our workspace. This way we don't need to clean up the inodes we mount over,
466          * but let the kernel do that entirely automatically, once our namespace dies. Note that this file
467          * system won't be visible to anyone but us, since we opened our own namespace and then made the
468          * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */
469         r = mount_nofollow_verbose(LOG_ERR, "sysext", workspace, "tmpfs", 0, "mode=0700");
470         if (r < 0)
471                 return r;
472 
473         /* Acquire host OS release info, so that we can compare it with the extension's data */
474         r = parse_os_release(
475                         arg_root,
476                         "ID", &host_os_release_id,
477                         "VERSION_ID", &host_os_release_version_id,
478                         "SYSEXT_LEVEL", &host_os_release_sysext_level);
479         if (r < 0)
480                 return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root));
481         if (isempty(host_os_release_id))
482                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
483                                        "'ID' field not found or empty in 'os-release' data of OS tree '%s': %m",
484                                        empty_to_root(arg_root));
485 
486         /* Let's now mount all images */
487         HASHMAP_FOREACH(img, images) {
488                 _cleanup_free_ char *p = NULL;
489 
490                 p = path_join(workspace, "extensions", img->name);
491                 if (!p)
492                         return log_oom();
493 
494                 r = mkdir_p(p, 0700);
495                 if (r < 0)
496                         return log_error_errno(r, "Failed to create %s: %m", p);
497 
498                 switch (img->type) {
499                 case IMAGE_DIRECTORY:
500                 case IMAGE_SUBVOLUME:
501                         r = mount_nofollow_verbose(LOG_ERR, img->path, p, NULL, MS_BIND, NULL);
502                         if (r < 0)
503                                 return r;
504 
505                         /* Make this a read-only bind mount */
506                         r = bind_remount_recursive(p, MS_RDONLY, MS_RDONLY, NULL);
507                         if (r < 0)
508                                 return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", p);
509 
510                         break;
511 
512                 case IMAGE_RAW:
513                 case IMAGE_BLOCK: {
514                         _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
515                         _cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
516                         _cleanup_(decrypted_image_unrefp) DecryptedImage *di = NULL;
517                         _cleanup_(verity_settings_done) VeritySettings verity_settings = VERITY_SETTINGS_DEFAULT;
518                         DissectImageFlags flags =
519                                 DISSECT_IMAGE_READ_ONLY |
520                                 DISSECT_IMAGE_GENERIC_ROOT |
521                                 DISSECT_IMAGE_REQUIRE_ROOT |
522                                 DISSECT_IMAGE_MOUNT_ROOT_ONLY |
523                                 DISSECT_IMAGE_USR_NO_ROOT;
524 
525                         r = verity_settings_load(&verity_settings, img->path, NULL, NULL);
526                         if (r < 0)
527                                 return log_error_errno(r, "Failed to read verity artifacts for %s: %m", img->path);
528 
529                         if (verity_settings.data_path)
530                                 flags |= DISSECT_IMAGE_NO_PARTITION_TABLE;
531 
532                         r = loop_device_make_by_path(
533                                         img->path,
534                                         O_RDONLY,
535                                         FLAGS_SET(flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN,
536                                         &d);
537                         if (r < 0)
538                                 return log_error_errno(r, "Failed to set up loopback device for %s: %m", img->path);
539 
540                         r = loop_device_flock(d, LOCK_SH);
541                         if (r < 0)
542                                 return log_error_errno(r, "Failed to lock loopback device: %m");
543 
544                         r = dissect_image_and_warn(
545                                         d->fd,
546                                         img->path,
547                                         &verity_settings,
548                                         NULL,
549                                         d->diskseq,
550                                         d->uevent_seqnum_not_before,
551                                         d->timestamp_not_before,
552                                         flags,
553                                         &m);
554                         if (r < 0)
555                                 return r;
556 
557                         r = dissected_image_load_verity_sig_partition(
558                                         m,
559                                         d->fd,
560                                         &verity_settings);
561                         if (r < 0)
562                                 return r;
563 
564                         r = dissected_image_decrypt_interactively(
565                                         m, NULL,
566                                         &verity_settings,
567                                         flags,
568                                         &di);
569                         if (r < 0)
570                                 return r;
571 
572                         r = dissected_image_mount_and_warn(
573                                         m,
574                                         p,
575                                         UID_INVALID,
576                                         UID_INVALID,
577                                         flags);
578                         if (r < 0)
579                                 return r;
580 
581                         if (di) {
582                                 r = decrypted_image_relinquish(di);
583                                 if (r < 0)
584                                         return log_error_errno(r, "Failed to relinquish DM devices: %m");
585                         }
586 
587                         loop_device_relinquish(d);
588                         break;
589                 }
590                 default:
591                         assert_not_reached();
592                 }
593 
594                 r = validate_version(
595                                 p,
596                                 img,
597                                 host_os_release_id,
598                                 host_os_release_version_id,
599                                 host_os_release_sysext_level);
600                 if (r < 0)
601                         return r;
602                 if (r == 0) {
603                         n_ignored++;
604                         continue;
605                 }
606 
607                 /* Nice! This one is an extension we want. */
608                 r = strv_extend(&extensions, img->name);
609                 if (r < 0)
610                         return log_oom();
611 
612                 n_extensions ++;
613         }
614 
615         /* Nothing left? Then shortcut things */
616         if (n_extensions == 0) {
617                 if (n_ignored > 0)
618                         log_info("No suitable extensions found (%u ignored due to incompatible version).", n_ignored);
619                 else
620                         log_info("No extensions found.");
621                 return 0;
622         }
623 
624         /* Order by version sort with strverscmp_improved() */
625         typesafe_qsort(extensions, n_extensions, strverscmp_improvedp);
626 
627         buf = strv_join(extensions, "', '");
628         if (!buf)
629                 return log_oom();
630 
631         log_info("Using extensions '%s'.", buf);
632 
633         /* Build table of extension paths (in reverse order) */
634         paths = new0(char*, n_extensions + 1);
635         if (!paths)
636                 return log_oom();
637 
638         for (size_t k = 0; k < n_extensions; k++) {
639                 _cleanup_free_ char *p = NULL;
640 
641                 assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k]));
642 
643                 p = path_join(workspace, "extensions", img->name);
644                 if (!p)
645                         return log_oom();
646 
647                 paths[k] = TAKE_PTR(p);
648         }
649 
650         /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
651          * underlying fs. */
652         STRV_FOREACH(h, arg_hierarchies) {
653                 _cleanup_free_ char *resolved = NULL;
654 
655                 r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
656                 if (r < 0)
657                         return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
658 
659                 r = unmerge_hierarchy(resolved);
660                 if (r < 0)
661                         return r;
662         }
663 
664         /* Create overlayfs mounts for all hierarchies */
665         STRV_FOREACH(h, arg_hierarchies) {
666                 _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL;
667 
668                 meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */
669                 if (!meta_path)
670                         return log_oom();
671 
672                 overlay_path = path_join(workspace, "overlay", *h); /* The resulting overlayfs instance */
673                 if (!overlay_path)
674                         return log_oom();
675 
676                 r = merge_hierarchy(*h, extensions, paths, meta_path, overlay_path);
677                 if (r < 0)
678                         return r;
679         }
680 
681         /* And move them all into place. This is where things appear in the host namespace */
682         STRV_FOREACH(h, arg_hierarchies) {
683                 _cleanup_free_ char *p = NULL, *resolved = NULL;
684 
685                 p = path_join(workspace, "overlay", *h);
686                 if (!p)
687                         return log_oom();
688 
689                 if (laccess(p, F_OK) < 0) {
690                         if (errno != ENOENT)
691                                 return log_error_errno(errno, "Failed to check if '%s' exists: %m", p);
692 
693                         /* Hierarchy apparently was empty in all extensions, and wasn't mounted, ignoring. */
694                         continue;
695                 }
696 
697                 r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
698                 if (r < 0)
699                         return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
700 
701                 r = mkdir_p(resolved, 0755);
702                 if (r < 0)
703                         return log_error_errno(r, "Failed to create hierarchy mount point '%s': %m", resolved);
704 
705                 r = mount_nofollow_verbose(LOG_ERR, p, resolved, NULL, MS_BIND, NULL);
706                 if (r < 0)
707                         return r;
708 
709                 log_info("Merged extensions into '%s'.", resolved);
710         }
711 
712         return 1;
713 }
714 
merge(Hashmap * images)715 static int merge(Hashmap *images) {
716         pid_t pid;
717         int r;
718 
719         r = safe_fork("(sd-sysext)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid);
720         if (r < 0)
721                 return log_error_errno(r, "Failed to fork off child: %m");
722         if (r == 0) {
723                 /* Child with its own mount namespace */
724 
725                 r = merge_subprocess(images, "/run/systemd/sysext");
726                 if (r < 0)
727                         _exit(EXIT_FAILURE);
728 
729                 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
730                  * created below /run. Nice! */
731 
732                 _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */
733         }
734 
735         r = wait_for_terminate_and_check("(sd-sysext)", pid, WAIT_LOG_ABNORMAL);
736         if (r < 0)
737                 return r;
738 
739         return r != 123; /* exit code 123 means: didn't do anything */
740 }
741 
image_discover_and_read_metadata(Hashmap ** ret_images)742 static int image_discover_and_read_metadata(Hashmap **ret_images) {
743         _cleanup_(hashmap_freep) Hashmap *images = NULL;
744         Image *img;
745         int r;
746 
747         assert(ret_images);
748 
749         images = hashmap_new(&image_hash_ops);
750         if (!images)
751                 return log_oom();
752 
753         r = image_discover(IMAGE_EXTENSION, arg_root, images);
754         if (r < 0)
755                 return log_error_errno(r, "Failed to discover extension images: %m");
756 
757         HASHMAP_FOREACH(img, images) {
758                 r = image_read_metadata(img);
759                 if (r < 0)
760                         return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
761         }
762 
763         *ret_images = TAKE_PTR(images);
764 
765         return 0;
766 }
767 
verb_merge(int argc,char ** argv,void * userdata)768 static int verb_merge(int argc, char **argv, void *userdata) {
769         _cleanup_(hashmap_freep) Hashmap *images = NULL;
770         int r;
771 
772         if (!have_effective_cap(CAP_SYS_ADMIN))
773                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
774 
775         r = image_discover_and_read_metadata(&images);
776         if (r < 0)
777                 return r;
778 
779         /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find
780          * things are already merged...) */
781         STRV_FOREACH(p, arg_hierarchies) {
782                 _cleanup_free_ char *resolved = NULL;
783 
784                 r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
785                 if (r == -ENOENT) {
786                         log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
787                         continue;
788                 }
789                 if (r < 0)
790                         return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
791 
792                 r = is_our_mount_point(resolved);
793                 if (r < 0)
794                         return r;
795                 if (r > 0)
796                         return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
797                                                "Hierarchy '%s' is already merged.", *p);
798         }
799 
800         return merge(images);
801 }
802 
verb_refresh(int argc,char ** argv,void * userdata)803 static int verb_refresh(int argc, char **argv, void *userdata) {
804         _cleanup_(hashmap_freep) Hashmap *images = NULL;
805         int r;
806 
807         if (!have_effective_cap(CAP_SYS_ADMIN))
808                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
809 
810         r = image_discover_and_read_metadata(&images);
811         if (r < 0)
812                 return r;
813 
814         r = merge(images); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it
815                             * does so it implicitly unmounts any overlayfs placed there before. Returns == 0
816                             * if it did nothing, i.e. no extension images found. In this case the old
817                             * overlayfs remains in place if there was one. */
818         if (r < 0)
819                 return r;
820         if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
821                      * called there's a guarantee that the merge status matches the installed extensions. */
822                 r = unmerge();
823 
824         /* Net result here is that:
825          *
826          * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things.
827          *
828          * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have
829          *    unmerged and then merged things again.
830          *
831          * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it
832          *    mounted now.
833          *
834          * 4. If there was no overlayfs mount so far, and no extensions installed, we implement a NOP.
835          */
836 
837         return 0;
838 }
839 
verb_list(int argc,char ** argv,void * userdata)840 static int verb_list(int argc, char **argv, void *userdata) {
841         _cleanup_(hashmap_freep) Hashmap *images = NULL;
842         _cleanup_(table_unrefp) Table *t = NULL;
843         Image *img;
844         int r;
845 
846         images = hashmap_new(&image_hash_ops);
847         if (!images)
848                 return log_oom();
849 
850         r = image_discover(IMAGE_EXTENSION, arg_root, images);
851         if (r < 0)
852                 return log_error_errno(r, "Failed to discover extension images: %m");
853 
854         if ((arg_json_format_flags & JSON_FORMAT_OFF) && hashmap_isempty(images)) {
855                 log_info("No OS extensions found.");
856                 return 0;
857         }
858 
859         t = table_new("name", "type", "path", "time");
860         if (!t)
861                 return log_oom();
862 
863         HASHMAP_FOREACH(img, images) {
864                 r = table_add_many(
865                                 t,
866                                 TABLE_STRING, img->name,
867                                 TABLE_STRING, image_type_to_string(img->type),
868                                 TABLE_PATH, img->path,
869                                 TABLE_TIMESTAMP, img->mtime != 0 ? img->mtime : img->crtime);
870                 if (r < 0)
871                         return table_log_add_error(r);
872         }
873 
874         (void) table_set_sort(t, (size_t) 0);
875 
876         return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
877 }
878 
verb_help(int argc,char ** argv,void * userdata)879 static int verb_help(int argc, char **argv, void *userdata) {
880         _cleanup_free_ char *link = NULL;
881         int r;
882 
883         r = terminal_urlify_man("systemd-sysext", "1", &link);
884         if (r < 0)
885                 return log_oom();
886 
887         printf("%1$s [OPTIONS...] [DEVICE]\n"
888                "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n"
889                "\n%3$sCommands:%4$s\n"
890                "  status                  Show current merge status (default)\n"
891                "  merge                   Merge extensions into /usr/ and /opt/\n"
892                "  unmerge                 Unmerge extensions from /usr/ and /opt/\n"
893                "  refresh                 Unmerge/merge extensions again\n"
894                "  list                    List installed extensions\n"
895                "  -h --help               Show this help\n"
896                "     --version            Show package version\n"
897                "\n%3$sOptions:%4$s\n"
898                "     --no-pager           Do not pipe output into a pager\n"
899                "     --no-legend          Do not show the headers and footers\n"
900                "     --root=PATH          Operate relative to root path\n"
901                "     --json=pretty|short|off\n"
902                "                          Generate JSON output\n"
903                "     --force              Ignore version incompatibilities\n"
904                "\nSee the %2$s for details.\n",
905                program_invocation_short_name,
906                link,
907                ansi_underline(),
908                ansi_normal(),
909                ansi_highlight(),
910                ansi_normal());
911 
912         return 0;
913 }
914 
parse_argv(int argc,char * argv[])915 static int parse_argv(int argc, char *argv[]) {
916 
917         enum {
918                 ARG_VERSION = 0x100,
919                 ARG_NO_PAGER,
920                 ARG_NO_LEGEND,
921                 ARG_ROOT,
922                 ARG_JSON,
923                 ARG_FORCE,
924         };
925 
926         static const struct option options[] = {
927                 { "help",      no_argument,       NULL, 'h'           },
928                 { "version",   no_argument,       NULL, ARG_VERSION   },
929                 { "no-pager",  no_argument,       NULL, ARG_NO_PAGER  },
930                 { "no-legend", no_argument,       NULL, ARG_NO_LEGEND },
931                 { "root",      required_argument, NULL, ARG_ROOT      },
932                 { "json",      required_argument, NULL, ARG_JSON      },
933                 { "force",     no_argument,       NULL, ARG_FORCE     },
934                 {}
935         };
936 
937         int c, r;
938 
939         assert(argc >= 0);
940         assert(argv);
941 
942         while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
943 
944                 switch (c) {
945 
946                 case 'h':
947                         return verb_help(argc, argv, NULL);
948 
949                 case ARG_VERSION:
950                         return version();
951 
952                 case ARG_NO_PAGER:
953                         arg_pager_flags |= PAGER_DISABLE;
954                         break;
955 
956                 case ARG_NO_LEGEND:
957                         arg_legend = false;
958                         break;
959 
960                 case ARG_ROOT:
961                         r = parse_path_argument(optarg, false, &arg_root);
962                         if (r < 0)
963                                 return r;
964                         break;
965 
966                 case ARG_JSON:
967                         r = parse_json_argument(optarg, &arg_json_format_flags);
968                         if (r <= 0)
969                                 return r;
970 
971                         break;
972 
973                 case ARG_FORCE:
974                         arg_force = true;
975                         break;
976 
977                 case '?':
978                         return -EINVAL;
979 
980                 default:
981                         assert_not_reached();
982                 }
983 
984         return 1;
985 }
986 
sysext_main(int argc,char * argv[])987 static int sysext_main(int argc, char *argv[]) {
988 
989         static const Verb verbs[] = {
990                 { "status",   VERB_ANY, 1, VERB_DEFAULT, verb_status  },
991                 { "merge",    VERB_ANY, 1, 0,            verb_merge   },
992                 { "unmerge",  VERB_ANY, 1, 0,            verb_unmerge },
993                 { "refresh",  VERB_ANY, 1, 0,            verb_refresh },
994                 { "list",     VERB_ANY, 1, 0,            verb_list    },
995                 { "help",     VERB_ANY, 1, 0,            verb_help    },
996                 {}
997         };
998 
999         return dispatch_verb(argc, argv, verbs, NULL);
1000 }
1001 
run(int argc,char * argv[])1002 static int run(int argc, char *argv[]) {
1003         int r;
1004 
1005         log_setup();
1006 
1007         r = parse_argv(argc, argv);
1008         if (r <= 0)
1009                 return r;
1010 
1011         /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
1012          * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
1013          * switch. */
1014         r = parse_env_extension_hierarchies(&arg_hierarchies);
1015         if (r < 0)
1016                 return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m");
1017 
1018         return sysext_main(argc, argv);
1019 }
1020 
1021 DEFINE_MAIN_FUNCTION(run);
1022