]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
61d0578b | 2 | |
e08f94ac LP |
3 | #include <linux/loop.h> |
4 | ||
61d0578b LP |
5 | #include "bus-common-errors.h" |
6 | #include "bus-error.h" | |
f4351959 | 7 | #include "chase-symlinks.h" |
61d0578b LP |
8 | #include "conf-files.h" |
9 | #include "copy.h" | |
6a818c3c | 10 | #include "data-fd-util.h" |
28db6fbf | 11 | #include "constants.h" |
61d0578b | 12 | #include "dirent-util.h" |
57f1b61b | 13 | #include "discover-image.h" |
61d0578b | 14 | #include "dissect-image.h" |
239ac0c7 | 15 | #include "env-file.h" |
8a129c80 | 16 | #include "env-util.h" |
ba5b6c59 | 17 | #include "errno-list.h" |
907952bb | 18 | #include "escape.h" |
239ac0c7 | 19 | #include "extension-release.h" |
61d0578b LP |
20 | #include "fd-util.h" |
21 | #include "fileio.h" | |
22 | #include "fs-util.h" | |
5cfa33e0 | 23 | #include "install.h" |
61d0578b LP |
24 | #include "io-util.h" |
25 | #include "locale-util.h" | |
26 | #include "loop-util.h" | |
61d0578b | 27 | #include "mkdir.h" |
d8b4d14d | 28 | #include "nulstr-util.h" |
61d0578b LP |
29 | #include "os-util.h" |
30 | #include "path-lookup.h" | |
31 | #include "portable.h" | |
32 | #include "process-util.h" | |
409f4cf1 | 33 | #include "selinux-util.h" |
61d0578b LP |
34 | #include "set.h" |
35 | #include "signal-util.h" | |
36 | #include "socket-util.h" | |
760877e9 | 37 | #include "sort-util.h" |
61d0578b LP |
38 | #include "string-table.h" |
39 | #include "strv.h" | |
e4de7287 | 40 | #include "tmpfile-util.h" |
61d0578b LP |
41 | #include "user-util.h" |
42 | ||
61d0578b LP |
43 | /* Markers used in the first line of our 20-portable.conf unit file drop-in to determine, that a) the unit file was |
44 | * dropped there by the portable service logic and b) for which image it was dropped there. */ | |
45 | #define PORTABLE_DROPIN_MARKER_BEGIN "# Drop-in created for image '" | |
46 | #define PORTABLE_DROPIN_MARKER_END "', do not edit." | |
47 | ||
48 | static bool prefix_match(const char *unit, const char *prefix) { | |
49 | const char *p; | |
50 | ||
51 | p = startswith(unit, prefix); | |
52 | if (!p) | |
53 | return false; | |
54 | ||
55 | /* Only respect prefixes followed by dash or dot or when there's a complete match */ | |
56 | return IN_SET(*p, '-', '.', '@', 0); | |
57 | } | |
58 | ||
59 | static bool unit_match(const char *unit, char **matches) { | |
60 | const char *dot; | |
61d0578b LP |
61 | |
62 | dot = strrchr(unit, '.'); | |
63 | if (!dot) | |
64 | return false; | |
65 | ||
66 | if (!STR_IN_SET(dot, ".service", ".socket", ".target", ".timer", ".path")) | |
67 | return false; | |
68 | ||
69 | /* Empty match expression means: everything */ | |
70 | if (strv_isempty(matches)) | |
71 | return true; | |
72 | ||
73 | /* Otherwise, at least one needs to match */ | |
74 | STRV_FOREACH(i, matches) | |
75 | if (prefix_match(unit, *i)) | |
76 | return true; | |
77 | ||
78 | return false; | |
79 | } | |
80 | ||
409f4cf1 | 81 | static PortableMetadata *portable_metadata_new(const char *name, const char *path, const char *selinux_label, int fd) { |
61d0578b LP |
82 | PortableMetadata *m; |
83 | ||
84 | m = malloc0(offsetof(PortableMetadata, name) + strlen(name) + 1); | |
85 | if (!m) | |
86 | return NULL; | |
87 | ||
907952bb LB |
88 | /* In case of a layered attach, we want to remember which image the unit came from */ |
89 | if (path) { | |
90 | m->image_path = strdup(path); | |
2f092762 FS |
91 | if (!m->image_path) |
92 | return mfree(m); | |
907952bb LB |
93 | } |
94 | ||
409f4cf1 LB |
95 | /* The metadata file might have SELinux labels, we need to carry them and reapply them */ |
96 | if (!isempty(selinux_label)) { | |
97 | m->selinux_label = strdup(selinux_label); | |
98 | if (!m->selinux_label) { | |
99 | free(m->image_path); | |
100 | return mfree(m); | |
101 | } | |
102 | } | |
103 | ||
61d0578b LP |
104 | strcpy(m->name, name); |
105 | m->fd = fd; | |
106 | ||
907952bb | 107 | return TAKE_PTR(m); |
61d0578b LP |
108 | } |
109 | ||
110 | PortableMetadata *portable_metadata_unref(PortableMetadata *i) { | |
111 | if (!i) | |
112 | return NULL; | |
113 | ||
114 | safe_close(i->fd); | |
115 | free(i->source); | |
907952bb | 116 | free(i->image_path); |
409f4cf1 | 117 | free(i->selinux_label); |
61d0578b LP |
118 | |
119 | return mfree(i); | |
120 | } | |
121 | ||
61d0578b LP |
122 | static int compare_metadata(PortableMetadata *const *x, PortableMetadata *const *y) { |
123 | return strcmp((*x)->name, (*y)->name); | |
124 | } | |
125 | ||
126 | int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret) { | |
127 | ||
128 | _cleanup_free_ PortableMetadata **sorted = NULL; | |
61d0578b LP |
129 | PortableMetadata *item; |
130 | size_t k = 0; | |
131 | ||
132 | sorted = new(PortableMetadata*, hashmap_size(unit_files)); | |
133 | if (!sorted) | |
134 | return -ENOMEM; | |
135 | ||
90e74a66 | 136 | HASHMAP_FOREACH(item, unit_files) |
61d0578b LP |
137 | sorted[k++] = item; |
138 | ||
139 | assert(k == hashmap_size(unit_files)); | |
140 | ||
141 | typesafe_qsort(sorted, k, compare_metadata); | |
142 | ||
143 | *ret = TAKE_PTR(sorted); | |
144 | return 0; | |
145 | } | |
146 | ||
447d4687 | 147 | static int send_one_fd_iov_with_data_fd( |
61d0578b | 148 | int socket_fd, |
447d4687 LB |
149 | const struct iovec *iov, |
150 | size_t iovlen, | |
61d0578b LP |
151 | int fd) { |
152 | ||
254d1313 | 153 | _cleanup_close_ int data_fd = -EBADF; |
61d0578b | 154 | |
447d4687 | 155 | assert(iov || iovlen == 0); |
61d0578b | 156 | assert(socket_fd >= 0); |
61d0578b LP |
157 | assert(fd >= 0); |
158 | ||
6a818c3c | 159 | data_fd = copy_data_fd(fd); |
61d0578b LP |
160 | if (data_fd < 0) |
161 | return data_fd; | |
162 | ||
447d4687 | 163 | return send_one_fd_iov(socket_fd, data_fd, iov, iovlen, 0); |
61d0578b LP |
164 | } |
165 | ||
67818055 YW |
166 | DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(portable_metadata_hash_ops, char, string_hash_func, string_compare_func, |
167 | PortableMetadata, portable_metadata_unref); | |
168 | ||
61d0578b LP |
169 | static int extract_now( |
170 | const char *where, | |
171 | char **matches, | |
239ac0c7 LB |
172 | const char *image_name, |
173 | bool path_is_extension, | |
06768b90 | 174 | bool relax_extension_release_check, |
61d0578b LP |
175 | int socket_fd, |
176 | PortableMetadata **ret_os_release, | |
177 | Hashmap **ret_unit_files) { | |
178 | ||
67818055 | 179 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b LP |
180 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; |
181 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
254d1313 | 182 | _cleanup_close_ int os_release_fd = -EBADF; |
61d0578b | 183 | _cleanup_free_ char *os_release_path = NULL; |
239ac0c7 | 184 | const char *os_release_id; |
61d0578b LP |
185 | int r; |
186 | ||
187 | /* Extracts the metadata from a directory tree 'where'. Extracts two kinds of information: the /etc/os-release | |
188 | * data, and all unit files matching the specified expression. Note that this function is called in two very | |
189 | * different but also similar contexts. When the tool gets invoked on a directory tree, we'll process it | |
190 | * directly, and in-process, and thus can return the requested data directly, via 'ret_os_release' and | |
191 | * 'ret_unit_files'. However, if the tool is invoked on a raw disk image — which needs to be mounted first — we | |
192 | * are invoked in a child process with private mounts and then need to send the collected data to our | |
193 | * parent. To handle both cases in one call this function also gets a 'socket_fd' parameter, which when >= 0 is | |
194 | * used to send the data to the parent. */ | |
195 | ||
196 | assert(where); | |
197 | ||
239ac0c7 LB |
198 | /* First, find os-release/extension-release and send it upstream (or just save it). */ |
199 | if (path_is_extension) { | |
200 | os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name); | |
06768b90 | 201 | r = open_extension_release(where, image_name, relax_extension_release_check, &os_release_path, &os_release_fd); |
239ac0c7 LB |
202 | } else { |
203 | os_release_id = "/etc/os-release"; | |
204 | r = open_os_release(where, &os_release_path, &os_release_fd); | |
205 | } | |
61d0578b | 206 | if (r < 0) |
239ac0c7 LB |
207 | log_debug_errno(r, |
208 | "Couldn't acquire %s file, ignoring: %m", | |
209 | path_is_extension ? "extension-release " : "os-release"); | |
61d0578b LP |
210 | else { |
211 | if (socket_fd >= 0) { | |
447d4687 LB |
212 | struct iovec iov[] = { |
213 | IOVEC_MAKE_STRING(os_release_id), | |
409f4cf1 | 214 | IOVEC_MAKE((char *)"\0", sizeof(char)), |
447d4687 LB |
215 | }; |
216 | ||
217 | r = send_one_fd_iov_with_data_fd(socket_fd, iov, ELEMENTSOF(iov), os_release_fd); | |
61d0578b LP |
218 | if (r < 0) |
219 | return log_debug_errno(r, "Failed to send os-release file: %m"); | |
220 | } | |
221 | ||
222 | if (ret_os_release) { | |
409f4cf1 | 223 | os_release = portable_metadata_new(os_release_id, NULL, NULL, os_release_fd); |
61d0578b LP |
224 | if (!os_release) |
225 | return -ENOMEM; | |
226 | ||
254d1313 | 227 | os_release_fd = -EBADF; |
61d0578b LP |
228 | os_release->source = TAKE_PTR(os_release_path); |
229 | } | |
230 | } | |
231 | ||
232 | /* Then, send unit file data to the parent (or/and add it to the hashmap). For that we use our usual unit | |
233 | * discovery logic. Note that we force looking inside of /lib/systemd/system/ for units too, as we mightbe | |
234 | * compiled for a split-usr system but the image might be a legacy-usr one. */ | |
b380b643 | 235 | r = lookup_paths_init(&paths, LOOKUP_SCOPE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, where); |
61d0578b LP |
236 | if (r < 0) |
237 | return log_debug_errno(r, "Failed to acquire lookup paths: %m"); | |
238 | ||
67818055 | 239 | unit_files = hashmap_new(&portable_metadata_hash_ops); |
61d0578b LP |
240 | if (!unit_files) |
241 | return -ENOMEM; | |
242 | ||
243 | STRV_FOREACH(i, paths.search_path) { | |
244 | _cleanup_free_ char *resolved = NULL; | |
245 | _cleanup_closedir_ DIR *d = NULL; | |
61d0578b LP |
246 | |
247 | r = chase_symlinks_and_opendir(*i, where, 0, &resolved, &d); | |
248 | if (r < 0) { | |
249 | log_debug_errno(r, "Failed to open unit path '%s', ignoring: %m", *i); | |
250 | continue; | |
251 | } | |
252 | ||
253 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to read directory: %m")) { | |
254 | _cleanup_(portable_metadata_unrefp) PortableMetadata *m = NULL; | |
2ef20244 | 255 | _cleanup_(mac_selinux_freep) char *con = NULL; |
254d1313 | 256 | _cleanup_close_ int fd = -EBADF; |
61d0578b LP |
257 | |
258 | if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) | |
259 | continue; | |
260 | ||
261 | if (!unit_match(de->d_name, matches)) | |
262 | continue; | |
263 | ||
264 | /* Filter out duplicates */ | |
265 | if (hashmap_get(unit_files, de->d_name)) | |
266 | continue; | |
267 | ||
61d0578b LP |
268 | if (!IN_SET(de->d_type, DT_LNK, DT_REG)) |
269 | continue; | |
270 | ||
271 | fd = openat(dirfd(d), de->d_name, O_CLOEXEC|O_RDONLY); | |
272 | if (fd < 0) { | |
273 | log_debug_errno(errno, "Failed to open unit file '%s', ignoring: %m", de->d_name); | |
274 | continue; | |
275 | } | |
276 | ||
409f4cf1 | 277 | #if HAVE_SELINUX |
2ef20244 LB |
278 | /* The units will be copied on the host's filesystem, so if they had a SELinux label |
279 | * we have to preserve it. Copy it out so that it can be applied later. */ | |
409f4cf1 | 280 | |
2ef20244 | 281 | r = fgetfilecon_raw(fd, &con); |
00675c36 | 282 | if (r < 0 && !ERRNO_IS_XATTR_ABSENT(errno)) |
2ef20244 | 283 | log_debug_errno(errno, "Failed to get SELinux file context from '%s', ignoring: %m", de->d_name); |
409f4cf1 | 284 | #endif |
2ef20244 LB |
285 | |
286 | if (socket_fd >= 0) { | |
447d4687 LB |
287 | struct iovec iov[] = { |
288 | IOVEC_MAKE_STRING(de->d_name), | |
409f4cf1 LB |
289 | IOVEC_MAKE((char *)"\0", sizeof(char)), |
290 | IOVEC_MAKE_STRING(strempty(con)), | |
447d4687 LB |
291 | }; |
292 | ||
293 | r = send_one_fd_iov_with_data_fd(socket_fd, iov, ELEMENTSOF(iov), fd); | |
61d0578b LP |
294 | if (r < 0) |
295 | return log_debug_errno(r, "Failed to send unit metadata to parent: %m"); | |
296 | } | |
297 | ||
2ef20244 | 298 | m = portable_metadata_new(de->d_name, where, con, fd); |
61d0578b LP |
299 | if (!m) |
300 | return -ENOMEM; | |
254d1313 | 301 | fd = -EBADF; |
61d0578b | 302 | |
657ee2d8 | 303 | m->source = path_join(resolved, de->d_name); |
61d0578b LP |
304 | if (!m->source) |
305 | return -ENOMEM; | |
306 | ||
307 | r = hashmap_put(unit_files, m->name, m); | |
308 | if (r < 0) | |
309 | return log_debug_errno(r, "Failed to add unit to hashmap: %m"); | |
310 | m = NULL; | |
311 | } | |
312 | } | |
313 | ||
314 | if (ret_os_release) | |
315 | *ret_os_release = TAKE_PTR(os_release); | |
316 | if (ret_unit_files) | |
317 | *ret_unit_files = TAKE_PTR(unit_files); | |
318 | ||
319 | return 0; | |
320 | } | |
321 | ||
322 | static int portable_extract_by_path( | |
323 | const char *path, | |
239ac0c7 | 324 | bool path_is_extension, |
06768b90 | 325 | bool relax_extension_release_check, |
61d0578b LP |
326 | char **matches, |
327 | PortableMetadata **ret_os_release, | |
328 | Hashmap **ret_unit_files, | |
329 | sd_bus_error *error) { | |
330 | ||
67818055 | 331 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b LP |
332 | _cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL; |
333 | _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; | |
334 | int r; | |
335 | ||
336 | assert(path); | |
337 | ||
7f52206a | 338 | r = loop_device_make_by_path(path, O_RDONLY, LO_FLAGS_PARTSCAN, LOCK_SH, &d); |
61d0578b | 339 | if (r == -EISDIR) { |
2ef20244 LB |
340 | _cleanup_free_ char *image_name = NULL; |
341 | ||
61d0578b LP |
342 | /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory |
343 | * tree and not a raw device. It's easy then. */ | |
344 | ||
2ef20244 LB |
345 | r = path_extract_filename(path, &image_name); |
346 | if (r < 0) | |
347 | return log_error_errno(r, "Failed to extract image name from path '%s': %m", path); | |
348 | ||
06768b90 | 349 | r = extract_now(path, matches, image_name, path_is_extension, /* relax_extension_release_check= */ false, -1, &os_release, &unit_files); |
61d0578b LP |
350 | if (r < 0) |
351 | return r; | |
352 | ||
353 | } else if (r < 0) | |
7b87fe4c | 354 | return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path); |
61d0578b LP |
355 | else { |
356 | _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; | |
357 | _cleanup_(rmdir_and_freep) char *tmpdir = NULL; | |
19ee48a6 | 358 | _cleanup_(close_pairp) int seq[2] = PIPE_EBADF; |
61d0578b LP |
359 | _cleanup_(sigkill_waitp) pid_t child = 0; |
360 | ||
361 | /* We now have a loopback block device, let's fork off a child in its own mount namespace, mount it | |
362 | * there, and extract the metadata we need. The metadata is sent from the child back to us. */ | |
363 | ||
364 | BLOCK_SIGNALS(SIGCHLD); | |
365 | ||
366 | r = mkdtemp_malloc("/tmp/inspect-XXXXXX", &tmpdir); | |
367 | if (r < 0) | |
368 | return log_debug_errno(r, "Failed to create temporary directory: %m"); | |
369 | ||
bad31660 YW |
370 | r = dissect_loop_device( |
371 | d, | |
4b5de5dd LP |
372 | NULL, NULL, |
373 | DISSECT_IMAGE_READ_ONLY | | |
374 | DISSECT_IMAGE_GENERIC_ROOT | | |
375 | DISSECT_IMAGE_REQUIRE_ROOT | | |
376 | DISSECT_IMAGE_DISCARD_ON_LOOP | | |
377 | DISSECT_IMAGE_RELAX_VAR_CHECK | | |
73d88b80 LP |
378 | DISSECT_IMAGE_USR_NO_ROOT | |
379 | DISSECT_IMAGE_ADD_PARTITION_DEVICES | | |
380 | DISSECT_IMAGE_PIN_PARTITION_DEVICES, | |
4b5de5dd | 381 | &m); |
61d0578b LP |
382 | if (r == -ENOPKG) |
383 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Couldn't identify a suitable partition table or file system in '%s'.", path); | |
384 | else if (r == -EADDRNOTAVAIL) | |
385 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No root partition for specified root hash found in '%s'.", path); | |
386 | else if (r == -ENOTUNIQ) | |
387 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Multiple suitable root partitions found in image '%s'.", path); | |
388 | else if (r == -ENXIO) | |
389 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No suitable root partition found in image '%s'.", path); | |
390 | else if (r == -EPROTONOSUPPORT) | |
391 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Device '%s' is loopback block device with partition scanning turned off, please turn it on.", path); | |
392 | if (r < 0) | |
393 | return r; | |
394 | ||
395 | if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0) | |
396 | return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m"); | |
397 | ||
398 | r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE|FORK_LOG, &child); | |
399 | if (r < 0) | |
400 | return r; | |
401 | if (r == 0) { | |
9ccb531a LB |
402 | DissectImageFlags flags = DISSECT_IMAGE_READ_ONLY; |
403 | ||
61d0578b LP |
404 | seq[0] = safe_close(seq[0]); |
405 | ||
239ac0c7 | 406 | if (path_is_extension) |
06768b90 | 407 | flags |= DISSECT_IMAGE_VALIDATE_OS_EXT | (relax_extension_release_check ? DISSECT_IMAGE_RELAX_SYSEXT_CHECK : 0); |
9ccb531a LB |
408 | else |
409 | flags |= DISSECT_IMAGE_VALIDATE_OS; | |
410 | ||
411 | r = dissected_image_mount(m, tmpdir, UID_INVALID, UID_INVALID, flags); | |
61d0578b LP |
412 | if (r < 0) { |
413 | log_debug_errno(r, "Failed to mount dissected image: %m"); | |
414 | goto child_finish; | |
415 | } | |
416 | ||
06768b90 | 417 | r = extract_now(tmpdir, matches, m->image_name, path_is_extension, relax_extension_release_check, seq[1], NULL, NULL); |
61d0578b LP |
418 | |
419 | child_finish: | |
420 | _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); | |
421 | } | |
422 | ||
423 | seq[1] = safe_close(seq[1]); | |
424 | ||
67818055 | 425 | unit_files = hashmap_new(&portable_metadata_hash_ops); |
61d0578b LP |
426 | if (!unit_files) |
427 | return -ENOMEM; | |
428 | ||
429 | for (;;) { | |
430 | _cleanup_(portable_metadata_unrefp) PortableMetadata *add = NULL; | |
254d1313 | 431 | _cleanup_close_ int fd = -EBADF; |
409f4cf1 LB |
432 | /* We use NAME_MAX space for the SELinux label here. The kernel currently enforces no limit, but |
433 | * according to suggestions from the SELinux people this will change and it will probably be | |
434 | * identical to NAME_MAX. For now we use that, but this should be updated one day when the final | |
435 | * limit is known. */ | |
436 | char iov_buffer[PATH_MAX + NAME_MAX + 2]; | |
447d4687 | 437 | struct iovec iov = IOVEC_INIT(iov_buffer, sizeof(iov_buffer)); |
61d0578b | 438 | |
447d4687 LB |
439 | ssize_t n = receive_one_fd_iov(seq[0], &iov, 1, 0, &fd); |
440 | if (n == -EIO) | |
441 | break; | |
442 | if (n < 0) | |
443 | return log_debug_errno(n, "Failed to receive item: %m"); | |
444 | iov_buffer[n] = 0; | |
61d0578b | 445 | |
5238e957 | 446 | /* We can't really distinguish a zero-length datagram without any fds from EOF (both are signalled the |
61d0578b | 447 | * same way by recvmsg()). Hence, accept either as end notification. */ |
447d4687 | 448 | if (isempty(iov_buffer) && fd < 0) |
61d0578b LP |
449 | break; |
450 | ||
447d4687 | 451 | if (isempty(iov_buffer) || fd < 0) |
d7a0f1f4 FS |
452 | return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), |
453 | "Invalid item sent from child."); | |
61d0578b | 454 | |
409f4cf1 LB |
455 | /* Given recvmsg cannot be used with multiple io vectors if you don't know the size in advance, |
456 | * use a marker to separate the name and the optional SELinux context. */ | |
457 | char *selinux_label = memchr(iov_buffer, 0, n); | |
458 | assert(selinux_label); | |
459 | selinux_label++; | |
460 | ||
461 | add = portable_metadata_new(iov_buffer, path, selinux_label, fd); | |
61d0578b LP |
462 | if (!add) |
463 | return -ENOMEM; | |
254d1313 | 464 | fd = -EBADF; |
61d0578b LP |
465 | |
466 | /* Note that we do not initialize 'add->source' here, as the source path is not usable here as | |
467 | * it refers to a path only valid in the short-living namespaced child process we forked | |
468 | * here. */ | |
469 | ||
470 | if (PORTABLE_METADATA_IS_UNIT(add)) { | |
471 | r = hashmap_put(unit_files, add->name, add); | |
472 | if (r < 0) | |
473 | return log_debug_errno(r, "Failed to add item to unit file list: %m"); | |
474 | ||
475 | add = NULL; | |
476 | ||
239ac0c7 | 477 | } else if (PORTABLE_METADATA_IS_OS_RELEASE(add) || PORTABLE_METADATA_IS_EXTENSION_RELEASE(add)) { |
61d0578b LP |
478 | |
479 | assert(!os_release); | |
480 | os_release = TAKE_PTR(add); | |
481 | } else | |
04499a70 | 482 | assert_not_reached(); |
61d0578b LP |
483 | } |
484 | ||
485 | r = wait_for_terminate_and_check("(sd-dissect)", child, 0); | |
486 | if (r < 0) | |
487 | return r; | |
488 | child = 0; | |
489 | } | |
490 | ||
239ac0c7 LB |
491 | if (!os_release) |
492 | return sd_bus_error_setf(error, | |
493 | SD_BUS_ERROR_INVALID_ARGS, | |
494 | "Image '%s' lacks %s data, refusing.", | |
495 | path, | |
496 | path_is_extension ? "extension-release" : "os-release"); | |
61d0578b | 497 | |
61d0578b LP |
498 | if (ret_unit_files) |
499 | *ret_unit_files = TAKE_PTR(unit_files); | |
500 | ||
501 | if (ret_os_release) | |
502 | *ret_os_release = TAKE_PTR(os_release); | |
503 | ||
504 | return 0; | |
505 | } | |
506 | ||
9ff61565 | 507 | static int extract_image_and_extensions( |
61d0578b LP |
508 | const char *name_or_path, |
509 | char **matches, | |
907952bb | 510 | char **extension_image_paths, |
9ff61565 | 511 | bool validate_sysext, |
06768b90 | 512 | bool relax_extension_release_check, |
9ff61565 LB |
513 | Image **ret_image, |
514 | OrderedHashmap **ret_extension_images, | |
e3f7ed94 | 515 | OrderedHashmap **ret_extension_releases, |
61d0578b LP |
516 | PortableMetadata **ret_os_release, |
517 | Hashmap **ret_unit_files, | |
8a129c80 | 518 | char ***ret_valid_prefixes, |
61d0578b LP |
519 | sd_bus_error *error) { |
520 | ||
9ff61565 | 521 | _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL; |
907952bb | 522 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; |
e3f7ed94 | 523 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL; |
907952bb | 524 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
8a129c80 | 525 | _cleanup_strv_free_ char **valid_prefixes = NULL; |
61d0578b | 526 | _cleanup_(image_unrefp) Image *image = NULL; |
907952bb | 527 | Image *ext; |
61d0578b LP |
528 | int r; |
529 | ||
530 | assert(name_or_path); | |
531 | ||
d577d4a4 | 532 | r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image); |
61d0578b LP |
533 | if (r < 0) |
534 | return r; | |
535 | ||
907952bb | 536 | if (!strv_isempty(extension_image_paths)) { |
907952bb LB |
537 | extension_images = ordered_hashmap_new(&image_hash_ops); |
538 | if (!extension_images) | |
539 | return -ENOMEM; | |
540 | ||
e3f7ed94 LB |
541 | if (ret_extension_releases) { |
542 | extension_releases = ordered_hashmap_new(&portable_metadata_hash_ops); | |
543 | if (!extension_releases) | |
544 | return -ENOMEM; | |
545 | } | |
546 | ||
907952bb LB |
547 | STRV_FOREACH(p, extension_image_paths) { |
548 | _cleanup_(image_unrefp) Image *new = NULL; | |
549 | ||
550 | r = image_find_harder(IMAGE_PORTABLE, *p, NULL, &new); | |
551 | if (r < 0) | |
552 | return r; | |
553 | ||
554 | r = ordered_hashmap_put(extension_images, new->name, new); | |
555 | if (r < 0) | |
556 | return r; | |
557 | TAKE_PTR(new); | |
558 | } | |
559 | } | |
560 | ||
06768b90 | 561 | r = portable_extract_by_path(image->path, /* path_is_extension= */ false, /* relax_extension_release_check= */ false, matches, &os_release, &unit_files, error); |
907952bb LB |
562 | if (r < 0) |
563 | return r; | |
564 | ||
8a129c80 LP |
565 | /* If we are layering extension images on top of a runtime image, check that the os-release and |
566 | * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when | |
567 | * the units are started. Also, collect valid portable prefixes if caller requested that. */ | |
568 | if (validate_sysext || ret_valid_prefixes) { | |
9ff61565 | 569 | _cleanup_fclose_ FILE *f = NULL; |
8a129c80 | 570 | _cleanup_free_ char *prefixes = NULL; |
9ff61565 LB |
571 | |
572 | r = take_fdopen_unlocked(&os_release->fd, "r", &f); | |
573 | if (r < 0) | |
574 | return r; | |
575 | ||
576 | r = parse_env_file(f, os_release->name, | |
577 | "ID", &id, | |
578 | "VERSION_ID", &version_id, | |
8a129c80 LP |
579 | "SYSEXT_LEVEL", &sysext_level, |
580 | "PORTABLE_PREFIXES", &prefixes); | |
9ff61565 LB |
581 | if (r < 0) |
582 | return r; | |
7b2e7632 LB |
583 | if (isempty(id)) |
584 | return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s os-release metadata lacks the ID field", name_or_path); | |
8a129c80 LP |
585 | |
586 | if (prefixes) { | |
587 | valid_prefixes = strv_split(prefixes, WHITESPACE); | |
588 | if (!valid_prefixes) | |
589 | return -ENOMEM; | |
590 | } | |
9ff61565 LB |
591 | } |
592 | ||
907952bb | 593 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { |
9ff61565 | 594 | _cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL; |
907952bb | 595 | _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; |
9ff61565 | 596 | _cleanup_strv_free_ char **extension_release = NULL; |
254d1313 | 597 | _cleanup_close_ int extension_release_fd = -EBADF; |
9ff61565 | 598 | _cleanup_fclose_ FILE *f = NULL; |
8a129c80 | 599 | const char *e; |
907952bb | 600 | |
06768b90 | 601 | r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, relax_extension_release_check, matches, &extension_release_meta, &extra_unit_files, error); |
907952bb LB |
602 | if (r < 0) |
603 | return r; | |
9ff61565 | 604 | |
907952bb LB |
605 | r = hashmap_move(unit_files, extra_unit_files); |
606 | if (r < 0) | |
607 | return r; | |
9ff61565 | 608 | |
e3f7ed94 | 609 | if (!validate_sysext && !ret_valid_prefixes && !ret_extension_releases) |
9ff61565 LB |
610 | continue; |
611 | ||
e3f7ed94 | 612 | /* We need to keep the fd valid, to return the PortableMetadata to the caller. */ |
1d2bf1f0 | 613 | extension_release_fd = fd_reopen(extension_release_meta->fd, O_CLOEXEC|O_RDONLY); |
e3f7ed94 LB |
614 | if (extension_release_fd < 0) |
615 | return extension_release_fd; | |
616 | ||
617 | r = take_fdopen_unlocked(&extension_release_fd, "r", &f); | |
9ff61565 LB |
618 | if (r < 0) |
619 | return r; | |
620 | ||
621 | r = load_env_file_pairs(f, extension_release_meta->name, &extension_release); | |
622 | if (r < 0) | |
623 | return r; | |
624 | ||
8a129c80 LP |
625 | if (validate_sysext) { |
626 | r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); | |
627 | if (r == 0) | |
628 | return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path); | |
629 | if (r < 0) | |
630 | return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path); | |
631 | } | |
632 | ||
633 | e = strv_env_pairs_get(extension_release, "PORTABLE_PREFIXES"); | |
634 | if (e) { | |
635 | _cleanup_strv_free_ char **l = NULL; | |
636 | ||
637 | l = strv_split(e, WHITESPACE); | |
638 | if (!l) | |
639 | return -ENOMEM; | |
640 | ||
641 | r = strv_extend_strv(&valid_prefixes, l, true); | |
642 | if (r < 0) | |
643 | return r; | |
644 | } | |
e3f7ed94 LB |
645 | |
646 | if (ret_extension_releases) { | |
647 | r = ordered_hashmap_put(extension_releases, ext->name, extension_release_meta); | |
648 | if (r < 0) | |
649 | return r; | |
650 | TAKE_PTR(extension_release_meta); | |
651 | } | |
907952bb LB |
652 | } |
653 | ||
8a129c80 LP |
654 | strv_sort(valid_prefixes); |
655 | ||
656 | if (ret_image) | |
657 | *ret_image = TAKE_PTR(image); | |
658 | if (ret_extension_images) | |
659 | *ret_extension_images = TAKE_PTR(extension_images); | |
e3f7ed94 LB |
660 | if (ret_extension_releases) |
661 | *ret_extension_releases = TAKE_PTR(extension_releases); | |
9ff61565 LB |
662 | if (ret_os_release) |
663 | *ret_os_release = TAKE_PTR(os_release); | |
664 | if (ret_unit_files) | |
665 | *ret_unit_files = TAKE_PTR(unit_files); | |
8a129c80 LP |
666 | if (ret_valid_prefixes) |
667 | *ret_valid_prefixes = TAKE_PTR(valid_prefixes); | |
9ff61565 LB |
668 | |
669 | return 0; | |
670 | } | |
671 | ||
672 | int portable_extract( | |
673 | const char *name_or_path, | |
674 | char **matches, | |
675 | char **extension_image_paths, | |
06768b90 | 676 | PortableFlags flags, |
9ff61565 | 677 | PortableMetadata **ret_os_release, |
e3f7ed94 | 678 | OrderedHashmap **ret_extension_releases, |
9ff61565 | 679 | Hashmap **ret_unit_files, |
8a129c80 | 680 | char ***ret_valid_prefixes, |
9ff61565 LB |
681 | sd_bus_error *error) { |
682 | ||
683 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; | |
e3f7ed94 | 684 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL; |
9ff61565 | 685 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
8a129c80 | 686 | _cleanup_(strv_freep) char **valid_prefixes = NULL; |
9ff61565 LB |
687 | _cleanup_(image_unrefp) Image *image = NULL; |
688 | int r; | |
689 | ||
8a129c80 LP |
690 | assert(name_or_path); |
691 | ||
692 | r = extract_image_and_extensions( | |
693 | name_or_path, | |
694 | matches, | |
695 | extension_image_paths, | |
696 | /* validate_sysext= */ false, | |
06768b90 | 697 | /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT), |
8a129c80 LP |
698 | &image, |
699 | &extension_images, | |
e3f7ed94 | 700 | &extension_releases, |
8a129c80 LP |
701 | &os_release, |
702 | &unit_files, | |
703 | ret_valid_prefixes ? &valid_prefixes : NULL, | |
704 | error); | |
9ff61565 LB |
705 | if (r < 0) |
706 | return r; | |
707 | ||
7bf5ec45 LB |
708 | if (hashmap_isempty(unit_files)) { |
709 | _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); | |
710 | if (!extensions) | |
711 | return -ENOMEM; | |
712 | ||
713 | return sd_bus_error_setf(error, | |
714 | SD_BUS_ERROR_INVALID_ARGS, | |
715 | "Couldn't find any matching unit files in image '%s%s%s', refusing.", | |
716 | image->path, | |
717 | isempty(extensions) ? "" : "' or any of its extensions '", | |
718 | isempty(extensions) ? "" : extensions); | |
719 | } | |
720 | ||
8a129c80 LP |
721 | if (ret_os_release) |
722 | *ret_os_release = TAKE_PTR(os_release); | |
e3f7ed94 LB |
723 | if (ret_extension_releases) |
724 | *ret_extension_releases = TAKE_PTR(extension_releases); | |
8a129c80 LP |
725 | if (ret_unit_files) |
726 | *ret_unit_files = TAKE_PTR(unit_files); | |
727 | if (ret_valid_prefixes) | |
728 | *ret_valid_prefixes = TAKE_PTR(valid_prefixes); | |
907952bb LB |
729 | |
730 | return 0; | |
61d0578b LP |
731 | } |
732 | ||
733 | static int unit_file_is_active( | |
734 | sd_bus *bus, | |
735 | const char *name, | |
736 | sd_bus_error *error) { | |
737 | ||
738 | static const char *const active_states[] = { | |
739 | "activating", | |
740 | "active", | |
741 | "reloading", | |
742 | "deactivating", | |
743 | NULL, | |
744 | }; | |
745 | int r; | |
746 | ||
747 | if (!bus) | |
748 | return false; | |
749 | ||
750 | /* If we are looking at a plain or instance things are easy, we can just query the state */ | |
751 | if (unit_name_is_valid(name, UNIT_NAME_PLAIN|UNIT_NAME_INSTANCE)) { | |
752 | _cleanup_free_ char *path = NULL, *buf = NULL; | |
753 | ||
754 | path = unit_dbus_path_from_name(name); | |
755 | if (!path) | |
756 | return -ENOMEM; | |
757 | ||
758 | r = sd_bus_get_property_string( | |
759 | bus, | |
760 | "org.freedesktop.systemd1", | |
761 | path, | |
762 | "org.freedesktop.systemd1.Unit", | |
763 | "ActiveState", | |
764 | error, | |
765 | &buf); | |
766 | if (r < 0) | |
767 | return log_debug_errno(r, "Failed to retrieve unit state: %s", bus_error_message(error, r)); | |
768 | ||
769 | return strv_contains((char**) active_states, buf); | |
770 | } | |
771 | ||
772 | /* Otherwise we need to enumerate. But let's build the most restricted query we can */ | |
773 | if (unit_name_is_valid(name, UNIT_NAME_TEMPLATE)) { | |
774 | _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; | |
775 | const char *at, *prefix, *joined; | |
776 | ||
777 | r = sd_bus_message_new_method_call( | |
778 | bus, | |
779 | &m, | |
780 | "org.freedesktop.systemd1", | |
781 | "/org/freedesktop/systemd1", | |
782 | "org.freedesktop.systemd1.Manager", | |
783 | "ListUnitsByPatterns"); | |
784 | if (r < 0) | |
785 | return r; | |
786 | ||
787 | r = sd_bus_message_append_strv(m, (char**) active_states); | |
788 | if (r < 0) | |
789 | return r; | |
790 | ||
791 | at = strchr(name, '@'); | |
792 | assert(at); | |
793 | ||
2f82562b | 794 | prefix = strndupa_safe(name, at + 1 - name); |
61d0578b LP |
795 | joined = strjoina(prefix, "*", at + 1); |
796 | ||
797 | r = sd_bus_message_append_strv(m, STRV_MAKE(joined)); | |
798 | if (r < 0) | |
799 | return r; | |
800 | ||
801 | r = sd_bus_call(bus, m, 0, error, &reply); | |
802 | if (r < 0) | |
803 | return log_debug_errno(r, "Failed to list units: %s", bus_error_message(error, r)); | |
804 | ||
805 | r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); | |
806 | if (r < 0) | |
807 | return r; | |
808 | ||
809 | r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_STRUCT, "ssssssouso"); | |
810 | if (r < 0) | |
811 | return r; | |
812 | ||
813 | return r > 0; | |
814 | } | |
815 | ||
816 | return -EINVAL; | |
817 | } | |
818 | ||
819 | static int portable_changes_add( | |
820 | PortableChange **changes, | |
821 | size_t *n_changes, | |
ba5b6c59 | 822 | int type_or_errno, /* PORTABLE_COPY, PORTABLE_SYMLINK, … if positive, or errno if negative */ |
61d0578b LP |
823 | const char *path, |
824 | const char *source) { | |
825 | ||
826 | _cleanup_free_ char *p = NULL, *s = NULL; | |
827 | PortableChange *c; | |
828 | ||
829 | assert(path); | |
830 | assert(!changes == !n_changes); | |
831 | ||
ba5b6c59 LP |
832 | if (type_or_errno >= 0) |
833 | assert(type_or_errno < _PORTABLE_CHANGE_TYPE_MAX); | |
834 | else | |
835 | assert(type_or_errno >= -ERRNO_MAX); | |
836 | ||
61d0578b LP |
837 | if (!changes) |
838 | return 0; | |
839 | ||
840 | c = reallocarray(*changes, *n_changes + 1, sizeof(PortableChange)); | |
841 | if (!c) | |
842 | return -ENOMEM; | |
843 | *changes = c; | |
844 | ||
845 | p = strdup(path); | |
846 | if (!p) | |
847 | return -ENOMEM; | |
848 | ||
4ff361cc | 849 | path_simplify(p); |
61d0578b LP |
850 | |
851 | if (source) { | |
852 | s = strdup(source); | |
853 | if (!s) | |
854 | return -ENOMEM; | |
855 | ||
4ff361cc | 856 | path_simplify(s); |
61d0578b LP |
857 | } |
858 | ||
859 | c[(*n_changes)++] = (PortableChange) { | |
ba5b6c59 | 860 | .type_or_errno = type_or_errno, |
61d0578b LP |
861 | .path = TAKE_PTR(p), |
862 | .source = TAKE_PTR(s), | |
863 | }; | |
864 | ||
865 | return 0; | |
866 | } | |
867 | ||
868 | static int portable_changes_add_with_prefix( | |
869 | PortableChange **changes, | |
870 | size_t *n_changes, | |
ba5b6c59 | 871 | int type_or_errno, |
61d0578b LP |
872 | const char *prefix, |
873 | const char *path, | |
874 | const char *source) { | |
875 | ||
8e7e4a73 LP |
876 | _cleanup_free_ char *path_buf = NULL, *source_buf = NULL; |
877 | ||
61d0578b LP |
878 | assert(path); |
879 | assert(!changes == !n_changes); | |
880 | ||
881 | if (!changes) | |
882 | return 0; | |
883 | ||
884 | if (prefix) { | |
8e7e4a73 LP |
885 | path_buf = path_join(prefix, path); |
886 | if (!path_buf) | |
887 | return -ENOMEM; | |
888 | ||
889 | path = path_buf; | |
890 | ||
891 | if (source) { | |
892 | source_buf = path_join(prefix, source); | |
893 | if (!source_buf) | |
894 | return -ENOMEM; | |
61d0578b | 895 | |
8e7e4a73 LP |
896 | source = source_buf; |
897 | } | |
61d0578b LP |
898 | } |
899 | ||
ba5b6c59 | 900 | return portable_changes_add(changes, n_changes, type_or_errno, path, source); |
61d0578b LP |
901 | } |
902 | ||
903 | void portable_changes_free(PortableChange *changes, size_t n_changes) { | |
904 | size_t i; | |
905 | ||
906 | assert(changes || n_changes == 0); | |
907 | ||
908 | for (i = 0; i < n_changes; i++) { | |
909 | free(changes[i].path); | |
910 | free(changes[i].source); | |
911 | } | |
912 | ||
913 | free(changes); | |
914 | } | |
915 | ||
907952bb LB |
916 | static const char *root_setting_from_image(ImageType type) { |
917 | return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "RootDirectory=" : "RootImage="; | |
918 | } | |
919 | ||
2ef20244 LB |
920 | static const char *extension_setting_from_image(ImageType type) { |
921 | return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "ExtensionDirectories=" : "ExtensionImages="; | |
922 | } | |
923 | ||
907952bb LB |
924 | static int make_marker_text(const char *image_path, OrderedHashmap *extension_images, char **ret_text) { |
925 | _cleanup_free_ char *text = NULL, *escaped_image_path = NULL; | |
926 | Image *ext; | |
927 | ||
928 | assert(image_path); | |
929 | assert(ret_text); | |
930 | ||
931 | escaped_image_path = xescape(image_path, ":"); | |
932 | if (!escaped_image_path) | |
933 | return -ENOMEM; | |
934 | ||
935 | /* If the image is layered, include all layers in the marker as a colon-separated | |
936 | * list of paths, so that we can do exact matches on removal. */ | |
937 | text = strjoin(PORTABLE_DROPIN_MARKER_BEGIN, escaped_image_path); | |
938 | if (!text) | |
939 | return -ENOMEM; | |
940 | ||
941 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { | |
942 | _cleanup_free_ char *escaped = NULL; | |
943 | ||
944 | escaped = xescape(ext->path, ":"); | |
945 | if (!escaped) | |
946 | return -ENOMEM; | |
947 | ||
948 | if (!strextend(&text, ":", escaped)) | |
949 | return -ENOMEM; | |
950 | } | |
951 | ||
952 | if (!strextend(&text, PORTABLE_DROPIN_MARKER_END "\n")) | |
953 | return -ENOMEM; | |
954 | ||
955 | *ret_text = TAKE_PTR(text); | |
956 | return 0; | |
957 | } | |
958 | ||
61d0578b LP |
959 | static int install_chroot_dropin( |
960 | const char *image_path, | |
961 | ImageType type, | |
907952bb | 962 | OrderedHashmap *extension_images, |
61d0578b LP |
963 | const PortableMetadata *m, |
964 | const char *dropin_dir, | |
06768b90 | 965 | PortableFlags flags, |
61d0578b LP |
966 | char **ret_dropin, |
967 | PortableChange **changes, | |
968 | size_t *n_changes) { | |
969 | ||
970 | _cleanup_free_ char *text = NULL, *dropin = NULL; | |
971 | int r; | |
972 | ||
973 | assert(image_path); | |
974 | assert(m); | |
975 | assert(dropin_dir); | |
976 | ||
657ee2d8 | 977 | dropin = path_join(dropin_dir, "20-portable.conf"); |
61d0578b LP |
978 | if (!dropin) |
979 | return -ENOMEM; | |
980 | ||
907952bb LB |
981 | r = make_marker_text(image_path, extension_images, &text); |
982 | if (r < 0) | |
983 | return log_debug_errno(r, "Failed to generate marker string for portable drop-in: %m"); | |
61d0578b | 984 | |
62b0ee9e | 985 | if (endswith(m->name, ".service")) { |
907952bb LB |
986 | const char *os_release_source, *root_type; |
987 | _cleanup_free_ char *base_name = NULL; | |
2ef20244 | 988 | Image *ext; |
907952bb LB |
989 | |
990 | root_type = root_setting_from_image(type); | |
62b0ee9e LB |
991 | |
992 | if (access("/etc/os-release", F_OK) < 0) { | |
993 | if (errno != ENOENT) | |
994 | return log_debug_errno(errno, "Failed to check if /etc/os-release exists: %m"); | |
995 | ||
996 | os_release_source = "/usr/lib/os-release"; | |
997 | } else | |
998 | os_release_source = "/etc/os-release"; | |
999 | ||
907952bb LB |
1000 | r = path_extract_filename(m->image_path ?: image_path, &base_name); |
1001 | if (r < 0) | |
1002 | return log_debug_errno(r, "Failed to extract basename from '%s': %m", m->image_path ?: image_path); | |
1003 | ||
61d0578b LP |
1004 | if (!strextend(&text, |
1005 | "\n" | |
1006 | "[Service]\n", | |
907952bb LB |
1007 | root_type, image_path, "\n" |
1008 | "Environment=PORTABLE=", base_name, "\n" | |
62b0ee9e | 1009 | "BindReadOnlyPaths=", os_release_source, ":/run/host/os-release\n" |
907952bb | 1010 | "LogExtraFields=PORTABLE=", base_name, "\n")) |
61d0578b | 1011 | return -ENOMEM; |
907952bb LB |
1012 | |
1013 | if (m->image_path && !path_equal(m->image_path, image_path)) | |
1014 | ORDERED_HASHMAP_FOREACH(ext, extension_images) | |
06768b90 LB |
1015 | if (!strextend(&text, |
1016 | extension_setting_from_image(ext->type), | |
1017 | ext->path, | |
1018 | /* With --force tell PID1 to avoid enforcing that the image <name> and | |
1019 | * extension-release.<name> have to match. */ | |
1020 | !IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) && | |
1021 | FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT) ? | |
1022 | ":x-systemd.relax-extension-release-check" : | |
1023 | "", | |
1024 | "\n")) | |
907952bb | 1025 | return -ENOMEM; |
62b0ee9e | 1026 | } |
61d0578b LP |
1027 | |
1028 | r = write_string_file(dropin, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); | |
1029 | if (r < 0) | |
1030 | return log_debug_errno(r, "Failed to write '%s': %m", dropin); | |
1031 | ||
1032 | (void) portable_changes_add(changes, n_changes, PORTABLE_WRITE, dropin, NULL); | |
1033 | ||
1034 | if (ret_dropin) | |
1035 | *ret_dropin = TAKE_PTR(dropin); | |
1036 | ||
1037 | return 0; | |
1038 | } | |
1039 | ||
61d0578b LP |
1040 | static int install_profile_dropin( |
1041 | const char *image_path, | |
1042 | const PortableMetadata *m, | |
1043 | const char *dropin_dir, | |
1044 | const char *profile, | |
1045 | PortableFlags flags, | |
1046 | char **ret_dropin, | |
1047 | PortableChange **changes, | |
1048 | size_t *n_changes) { | |
1049 | ||
1050 | _cleanup_free_ char *dropin = NULL, *from = NULL; | |
1051 | int r; | |
1052 | ||
1053 | assert(image_path); | |
1054 | assert(m); | |
1055 | assert(dropin_dir); | |
1056 | ||
1057 | if (!profile) | |
1058 | return 0; | |
1059 | ||
13c02e7b | 1060 | r = find_portable_profile(profile, m->name, &from); |
61d0578b | 1061 | if (r < 0) { |
99c89da0 | 1062 | if (r != -ENOENT) |
61d0578b LP |
1063 | return log_debug_errno(errno, "Profile '%s' is not accessible: %m", profile); |
1064 | ||
1065 | log_debug_errno(errno, "Skipping link to profile '%s', as it does not exist: %m", profile); | |
1066 | return 0; | |
1067 | } | |
1068 | ||
657ee2d8 | 1069 | dropin = path_join(dropin_dir, "10-profile.conf"); |
61d0578b LP |
1070 | if (!dropin) |
1071 | return -ENOMEM; | |
1072 | ||
1073 | if (flags & PORTABLE_PREFER_COPY) { | |
1074 | ||
8a016c74 | 1075 | r = copy_file_atomic(from, dropin, 0644, 0, 0, COPY_REFLINK); |
61d0578b | 1076 | if (r < 0) |
fc03e80c | 1077 | return log_debug_errno(r, "Failed to copy %s %s %s: %m", from, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), dropin); |
61d0578b LP |
1078 | |
1079 | (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, dropin, from); | |
1080 | ||
1081 | } else { | |
1082 | ||
1083 | if (symlink(from, dropin) < 0) | |
fc03e80c | 1084 | return log_debug_errno(errno, "Failed to link %s %s %s: %m", from, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), dropin); |
61d0578b LP |
1085 | |
1086 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, dropin, from); | |
1087 | } | |
1088 | ||
1089 | if (ret_dropin) | |
1090 | *ret_dropin = TAKE_PTR(dropin); | |
1091 | ||
1092 | return 0; | |
1093 | } | |
1094 | ||
40a7b232 | 1095 | static const char *attached_path(const LookupPaths *paths, PortableFlags flags) { |
61d0578b LP |
1096 | const char *where; |
1097 | ||
1098 | assert(paths); | |
1099 | ||
1100 | if (flags & PORTABLE_RUNTIME) | |
40a7b232 | 1101 | where = paths->runtime_attached; |
61d0578b | 1102 | else |
40a7b232 | 1103 | where = paths->persistent_attached; |
61d0578b LP |
1104 | |
1105 | assert(where); | |
1106 | return where; | |
1107 | } | |
1108 | ||
1109 | static int attach_unit_file( | |
1110 | const LookupPaths *paths, | |
1111 | const char *image_path, | |
1112 | ImageType type, | |
907952bb | 1113 | OrderedHashmap *extension_images, |
61d0578b LP |
1114 | const PortableMetadata *m, |
1115 | const char *profile, | |
1116 | PortableFlags flags, | |
1117 | PortableChange **changes, | |
1118 | size_t *n_changes) { | |
1119 | ||
1120 | _cleanup_(unlink_and_freep) char *chroot_dropin = NULL, *profile_dropin = NULL; | |
1121 | _cleanup_(rmdir_and_freep) char *dropin_dir = NULL; | |
8e7e4a73 LP |
1122 | _cleanup_free_ char *path = NULL; |
1123 | const char *where; | |
61d0578b LP |
1124 | int r; |
1125 | ||
1126 | assert(paths); | |
1127 | assert(image_path); | |
1128 | assert(m); | |
1129 | assert(PORTABLE_METADATA_IS_UNIT(m)); | |
1130 | ||
40a7b232 | 1131 | where = attached_path(paths, flags); |
61d0578b | 1132 | |
d09d85a2 LP |
1133 | (void) mkdir_parents(where, 0755); |
1134 | if (mkdir(where, 0755) < 0) { | |
1135 | if (errno != EEXIST) | |
80d95fcd | 1136 | return log_debug_errno(errno, "Failed to create attach directory %s: %m", where); |
d09d85a2 LP |
1137 | } else |
1138 | (void) portable_changes_add(changes, n_changes, PORTABLE_MKDIR, where, NULL); | |
1139 | ||
8e7e4a73 LP |
1140 | path = path_join(where, m->name); |
1141 | if (!path) | |
1142 | return -ENOMEM; | |
1143 | ||
61d0578b LP |
1144 | dropin_dir = strjoin(path, ".d"); |
1145 | if (!dropin_dir) | |
1146 | return -ENOMEM; | |
1147 | ||
d09d85a2 LP |
1148 | if (mkdir(dropin_dir, 0755) < 0) { |
1149 | if (errno != EEXIST) | |
80d95fcd | 1150 | return log_debug_errno(errno, "Failed to create drop-in directory %s: %m", dropin_dir); |
d09d85a2 LP |
1151 | } else |
1152 | (void) portable_changes_add(changes, n_changes, PORTABLE_MKDIR, dropin_dir, NULL); | |
61d0578b LP |
1153 | |
1154 | /* We install the drop-ins first, and the actual unit file last to achieve somewhat atomic behaviour if PID 1 | |
1155 | * is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at | |
1156 | * all for PID 1. */ | |
1157 | ||
06768b90 | 1158 | r = install_chroot_dropin(image_path, type, extension_images, m, dropin_dir, flags, &chroot_dropin, changes, n_changes); |
61d0578b LP |
1159 | if (r < 0) |
1160 | return r; | |
1161 | ||
1162 | r = install_profile_dropin(image_path, m, dropin_dir, profile, flags, &profile_dropin, changes, n_changes); | |
1163 | if (r < 0) | |
1164 | return r; | |
1165 | ||
1166 | if ((flags & PORTABLE_PREFER_SYMLINK) && m->source) { | |
1167 | ||
1168 | if (symlink(m->source, path) < 0) | |
1169 | return log_debug_errno(errno, "Failed to symlink unit file '%s': %m", path); | |
1170 | ||
1171 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, path, m->source); | |
1172 | ||
1173 | } else { | |
1174 | _cleanup_(unlink_and_freep) char *tmp = NULL; | |
254d1313 | 1175 | _cleanup_close_ int fd = -EBADF; |
61d0578b | 1176 | |
409f4cf1 LB |
1177 | (void) mac_selinux_create_file_prepare_label(path, m->selinux_label); |
1178 | ||
6d88513e | 1179 | fd = open_tmpfile_linkable(path, O_WRONLY|O_CLOEXEC, &tmp); |
409f4cf1 | 1180 | mac_selinux_create_file_clear(); /* Clear immediately in case of errors */ |
61d0578b LP |
1181 | if (fd < 0) |
1182 | return log_debug_errno(fd, "Failed to create unit file '%s': %m", path); | |
1183 | ||
1184 | r = copy_bytes(m->fd, fd, UINT64_MAX, COPY_REFLINK); | |
1185 | if (r < 0) | |
1186 | return log_debug_errno(r, "Failed to copy unit file '%s': %m", path); | |
1187 | ||
1188 | if (fchmod(fd, 0644) < 0) | |
1189 | return log_debug_errno(errno, "Failed to change unit file access mode for '%s': %m", path); | |
1190 | ||
1191 | r = link_tmpfile(fd, tmp, path); | |
1192 | if (r < 0) | |
1193 | return log_debug_errno(r, "Failed to install unit file '%s': %m", path); | |
1194 | ||
1195 | tmp = mfree(tmp); | |
1196 | ||
1197 | (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, path, m->source); | |
1198 | } | |
1199 | ||
1200 | /* All is established now, now let's disable any rollbacks */ | |
1201 | chroot_dropin = mfree(chroot_dropin); | |
1202 | profile_dropin = mfree(profile_dropin); | |
1203 | dropin_dir = mfree(dropin_dir); | |
1204 | ||
1205 | return 0; | |
1206 | } | |
1207 | ||
1208 | static int image_symlink( | |
1209 | const char *image_path, | |
1210 | PortableFlags flags, | |
1211 | char **ret) { | |
1212 | ||
1213 | const char *fn, *where; | |
1214 | char *joined = NULL; | |
1215 | ||
1216 | assert(image_path); | |
1217 | assert(ret); | |
1218 | ||
1219 | fn = last_path_component(image_path); | |
1220 | ||
1221 | if (flags & PORTABLE_RUNTIME) | |
1222 | where = "/run/portables/"; | |
1223 | else | |
1224 | where = "/etc/portables/"; | |
1225 | ||
1226 | joined = strjoin(where, fn); | |
1227 | if (!joined) | |
1228 | return -ENOMEM; | |
1229 | ||
1230 | *ret = joined; | |
1231 | return 0; | |
1232 | } | |
1233 | ||
1234 | static int install_image_symlink( | |
1235 | const char *image_path, | |
1236 | PortableFlags flags, | |
1237 | PortableChange **changes, | |
1238 | size_t *n_changes) { | |
1239 | ||
1240 | _cleanup_free_ char *sl = NULL; | |
1241 | int r; | |
1242 | ||
1243 | assert(image_path); | |
1244 | ||
1245 | /* If the image is outside of the image search also link it into it, so that it can be found with short image | |
1246 | * names and is listed among the images. */ | |
1247 | ||
d577d4a4 | 1248 | if (image_in_search_path(IMAGE_PORTABLE, NULL, image_path)) |
61d0578b LP |
1249 | return 0; |
1250 | ||
1251 | r = image_symlink(image_path, flags, &sl); | |
1252 | if (r < 0) | |
1253 | return log_debug_errno(r, "Failed to generate image symlink path: %m"); | |
1254 | ||
1255 | (void) mkdir_parents(sl, 0755); | |
1256 | ||
1257 | if (symlink(image_path, sl) < 0) | |
fc03e80c | 1258 | return log_debug_errno(errno, "Failed to link %s %s %s: %m", image_path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), sl); |
61d0578b LP |
1259 | |
1260 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, sl, image_path); | |
1261 | return 0; | |
1262 | } | |
1263 | ||
907952bb LB |
1264 | static int install_image_and_extensions_symlinks( |
1265 | const Image *image, | |
1266 | OrderedHashmap *extension_images, | |
1267 | PortableFlags flags, | |
1268 | PortableChange **changes, | |
1269 | size_t *n_changes) { | |
1270 | ||
1271 | Image *ext; | |
1272 | int r; | |
1273 | ||
1274 | assert(image); | |
1275 | ||
1276 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { | |
1277 | r = install_image_symlink(ext->path, flags, changes, n_changes); | |
1278 | if (r < 0) | |
1279 | return r; | |
1280 | } | |
1281 | ||
1282 | r = install_image_symlink(image->path, flags, changes, n_changes); | |
1283 | if (r < 0) | |
1284 | return r; | |
1285 | ||
1286 | return 0; | |
1287 | } | |
1288 | ||
8a129c80 | 1289 | static bool prefix_matches_compatible(char **matches, char **valid_prefixes) { |
8a129c80 LP |
1290 | /* Checks if all 'matches' are included in the list of 'valid_prefixes' */ |
1291 | ||
1292 | STRV_FOREACH(m, matches) | |
1293 | if (!strv_contains(valid_prefixes, *m)) | |
1294 | return false; | |
1295 | ||
1296 | return true; | |
1297 | } | |
1298 | ||
61d0578b LP |
1299 | int portable_attach( |
1300 | sd_bus *bus, | |
1301 | const char *name_or_path, | |
1302 | char **matches, | |
1303 | const char *profile, | |
907952bb | 1304 | char **extension_image_paths, |
61d0578b LP |
1305 | PortableFlags flags, |
1306 | PortableChange **changes, | |
1307 | size_t *n_changes, | |
1308 | sd_bus_error *error) { | |
1309 | ||
907952bb | 1310 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL; |
67818055 | 1311 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b | 1312 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; |
8a129c80 | 1313 | _cleanup_strv_free_ char **valid_prefixes = NULL; |
61d0578b LP |
1314 | _cleanup_(image_unrefp) Image *image = NULL; |
1315 | PortableMetadata *item; | |
61d0578b LP |
1316 | int r; |
1317 | ||
8a129c80 LP |
1318 | r = extract_image_and_extensions( |
1319 | name_or_path, | |
1320 | matches, | |
1321 | extension_image_paths, | |
1322 | /* validate_sysext= */ true, | |
06768b90 | 1323 | /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT), |
8a129c80 LP |
1324 | &image, |
1325 | &extension_images, | |
e3f7ed94 | 1326 | /* extension_releases= */ NULL, |
8a129c80 LP |
1327 | /* os_release= */ NULL, |
1328 | &unit_files, | |
1329 | &valid_prefixes, | |
1330 | error); | |
61d0578b LP |
1331 | if (r < 0) |
1332 | return r; | |
1333 | ||
8a129c80 LP |
1334 | if (valid_prefixes && !prefix_matches_compatible(matches, valid_prefixes)) { |
1335 | _cleanup_free_ char *matches_joined = NULL, *extensions_joined = NULL, *valid_prefixes_joined = NULL; | |
1336 | ||
1337 | matches_joined = strv_join(matches, "', '"); | |
1338 | if (!matches_joined) | |
1339 | return -ENOMEM; | |
1340 | ||
1341 | extensions_joined = strv_join(extension_image_paths, ", "); | |
1342 | if (!extensions_joined) | |
1343 | return -ENOMEM; | |
1344 | ||
1345 | valid_prefixes_joined = strv_join(valid_prefixes, ", "); | |
1346 | if (!valid_prefixes_joined) | |
1347 | return -ENOMEM; | |
1348 | ||
1349 | return sd_bus_error_setf( | |
1350 | error, | |
1351 | SD_BUS_ERROR_INVALID_ARGS, | |
1352 | "Selected matches '%s' are not compatible with portable service image '%s%s%s', refusing. (Acceptable prefix matches are: %s)", | |
1353 | matches_joined, | |
1354 | image->path, | |
1355 | isempty(extensions_joined) ? "" : "' or any of its extensions '", | |
1356 | strempty(extensions_joined), | |
1357 | valid_prefixes_joined); | |
1358 | } | |
1359 | ||
7bf5ec45 | 1360 | if (hashmap_isempty(unit_files)) { |
8a129c80 LP |
1361 | _cleanup_free_ char *extensions_joined = strv_join(extension_image_paths, ", "); |
1362 | if (!extensions_joined) | |
7bf5ec45 LB |
1363 | return -ENOMEM; |
1364 | ||
8a129c80 LP |
1365 | return sd_bus_error_setf( |
1366 | error, | |
1367 | SD_BUS_ERROR_INVALID_ARGS, | |
1368 | "Couldn't find any matching unit files in image '%s%s%s', refusing.", | |
1369 | image->path, | |
1370 | isempty(extensions_joined) ? "" : "' or any of its extensions '", | |
1371 | strempty(extensions_joined)); | |
7bf5ec45 LB |
1372 | } |
1373 | ||
b380b643 | 1374 | r = lookup_paths_init(&paths, LOOKUP_SCOPE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); |
61d0578b LP |
1375 | if (r < 0) |
1376 | return r; | |
1377 | ||
aad813bf | 1378 | if (!FLAGS_SET(flags, PORTABLE_REATTACH) && !FLAGS_SET(flags, PORTABLE_FORCE_ATTACH)) |
ace212f5 LB |
1379 | HASHMAP_FOREACH(item, unit_files) { |
1380 | r = unit_file_exists(LOOKUP_SCOPE_SYSTEM, &paths, item->name); | |
1381 | if (r < 0) | |
1382 | return sd_bus_error_set_errnof(error, r, "Failed to determine whether unit '%s' exists on the host: %m", item->name); | |
1383 | if (r > 0) | |
1384 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' exists on the host already, refusing.", item->name); | |
61d0578b | 1385 | |
ace212f5 LB |
1386 | r = unit_file_is_active(bus, item->name, error); |
1387 | if (r < 0) | |
1388 | return r; | |
1389 | if (r > 0) | |
1390 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active already, refusing.", item->name); | |
1391 | } | |
61d0578b | 1392 | |
90e74a66 | 1393 | HASHMAP_FOREACH(item, unit_files) { |
907952bb LB |
1394 | r = attach_unit_file(&paths, image->path, image->type, extension_images, |
1395 | item, profile, flags, changes, n_changes); | |
61d0578b | 1396 | if (r < 0) |
80d95fcd | 1397 | return sd_bus_error_set_errnof(error, r, "Failed to attach unit '%s': %m", item->name); |
61d0578b LP |
1398 | } |
1399 | ||
1400 | /* We don't care too much for the image symlink, it's just a convenience thing, it's not necessary for proper | |
1401 | * operation otherwise. */ | |
907952bb | 1402 | (void) install_image_and_extensions_symlinks(image, extension_images, flags, changes, n_changes); |
61d0578b LP |
1403 | |
1404 | return 0; | |
1405 | } | |
1406 | ||
907952bb LB |
1407 | static bool marker_matches_images(const char *marker, const char *name_or_path, char **extension_image_paths) { |
1408 | _cleanup_strv_free_ char **root_and_extensions = NULL; | |
61d0578b | 1409 | const char *a; |
907952bb | 1410 | int r; |
61d0578b LP |
1411 | |
1412 | assert(marker); | |
1413 | assert(name_or_path); | |
1414 | ||
907952bb LB |
1415 | /* If extensions were used when attaching, the marker will be a colon-separated |
1416 | * list of images/paths. We enforce strict 1:1 matching, so that we are sure | |
1417 | * we are detaching exactly what was attached. | |
1418 | * For each image, starting with the root, we look for a token in the marker, | |
1419 | * and return a negative answer on any non-matching combination. */ | |
61d0578b | 1420 | |
907952bb LB |
1421 | root_and_extensions = strv_new(name_or_path); |
1422 | if (!root_and_extensions) | |
1423 | return -ENOMEM; | |
61d0578b | 1424 | |
907952bb LB |
1425 | r = strv_extend_strv(&root_and_extensions, extension_image_paths, false); |
1426 | if (r < 0) | |
1427 | return r; | |
61d0578b | 1428 | |
907952bb LB |
1429 | STRV_FOREACH(image_name_or_path, root_and_extensions) { |
1430 | _cleanup_free_ char *image = NULL; | |
1431 | ||
1432 | r = extract_first_word(&marker, &image, ":", EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE); | |
1433 | if (r < 0) | |
1434 | return log_debug_errno(r, "Failed to parse marker: %s", marker); | |
1435 | if (r == 0) | |
61d0578b LP |
1436 | return false; |
1437 | ||
907952bb LB |
1438 | a = last_path_component(image); |
1439 | ||
1440 | if (image_name_is_valid(*image_name_or_path)) { | |
1441 | const char *e, *underscore; | |
61d0578b | 1442 | |
907952bb LB |
1443 | /* We shall match against an image name. In that case let's compare the last component, and optionally |
1444 | * allow either a suffix of ".raw" or a series of "/". | |
1445 | * But allow matching on a different version of the same image, when a "_" is used as a separator. */ | |
1446 | underscore = strchr(*image_name_or_path, '_'); | |
1447 | if (underscore) { | |
1448 | if (strneq(a, *image_name_or_path, underscore - *image_name_or_path)) | |
1449 | continue; | |
1450 | return false; | |
1451 | } | |
61d0578b | 1452 | |
907952bb LB |
1453 | e = startswith(a, *image_name_or_path); |
1454 | if (!e) | |
1455 | return false; | |
61d0578b | 1456 | |
907952bb LB |
1457 | if(!(e[strspn(e, "/")] == 0 || streq(e, ".raw"))) |
1458 | return false; | |
1459 | } else { | |
1460 | const char *b, *underscore; | |
1461 | size_t l; | |
1462 | ||
1463 | /* We shall match against a path. Let's ignore any prefix here though, as often there are many ways to | |
2350712e LB |
1464 | * reach the same file. However, in this mode, let's validate any file suffix. |
1465 | * But also ensure that we don't fail if both components don't have a '/' at all | |
1466 | * (strcspn returns the full length of the string in that case, which might not | |
1467 | * match as the versions might differ). */ | |
907952bb LB |
1468 | |
1469 | l = strcspn(a, "/"); | |
1470 | b = last_path_component(*image_name_or_path); | |
1471 | ||
2350712e LB |
1472 | if ((a[l] != '/') != !strchr(b, '/')) /* One is a directory, the other is not */ |
1473 | return false; | |
1474 | ||
1475 | if (a[l] != 0 && strcspn(b, "/") != l) | |
907952bb | 1476 | return false; |
61d0578b | 1477 | |
907952bb LB |
1478 | underscore = strchr(b, '_'); |
1479 | if (underscore) | |
1480 | l = underscore - b; | |
2350712e LB |
1481 | else { /* Either component could be versioned */ |
1482 | underscore = strchr(a, '_'); | |
1483 | if (underscore) | |
1484 | l = underscore - a; | |
1485 | } | |
9e4079d4 | 1486 | |
907952bb LB |
1487 | if (!strneq(a, b, l)) |
1488 | return false; | |
1489 | } | |
61d0578b | 1490 | } |
907952bb LB |
1491 | |
1492 | return true; | |
61d0578b LP |
1493 | } |
1494 | ||
1495 | static int test_chroot_dropin( | |
1496 | DIR *d, | |
1497 | const char *where, | |
1498 | const char *fname, | |
1499 | const char *name_or_path, | |
907952bb | 1500 | char **extension_image_paths, |
61d0578b LP |
1501 | char **ret_marker) { |
1502 | ||
1503 | _cleanup_free_ char *line = NULL, *marker = NULL; | |
1504 | _cleanup_fclose_ FILE *f = NULL; | |
254d1313 | 1505 | _cleanup_close_ int fd = -EBADF; |
61d0578b LP |
1506 | const char *p, *e, *k; |
1507 | int r; | |
1508 | ||
1509 | assert(d); | |
1510 | assert(where); | |
1511 | assert(fname); | |
1512 | ||
1513 | /* We recognize unis created from portable images via the drop-in we created for them */ | |
1514 | ||
1515 | p = strjoina(fname, ".d/20-portable.conf"); | |
1516 | fd = openat(dirfd(d), p, O_RDONLY|O_CLOEXEC); | |
1517 | if (fd < 0) { | |
1518 | if (errno == ENOENT) | |
1519 | return 0; | |
1520 | ||
1521 | return log_debug_errno(errno, "Failed to open %s/%s: %m", where, p); | |
1522 | } | |
1523 | ||
4fa744a3 | 1524 | r = take_fdopen_unlocked(&fd, "r", &f); |
02e23d1a ZJS |
1525 | if (r < 0) |
1526 | return log_debug_errno(r, "Failed to convert file handle: %m"); | |
61d0578b LP |
1527 | |
1528 | r = read_line(f, LONG_LINE_MAX, &line); | |
1529 | if (r < 0) | |
1530 | return log_debug_errno(r, "Failed to read from %s/%s: %m", where, p); | |
1531 | ||
1532 | e = startswith(line, PORTABLE_DROPIN_MARKER_BEGIN); | |
1533 | if (!e) | |
1534 | return 0; | |
1535 | ||
1536 | k = endswith(e, PORTABLE_DROPIN_MARKER_END); | |
1537 | if (!k) | |
1538 | return 0; | |
1539 | ||
1540 | marker = strndup(e, k - e); | |
1541 | if (!marker) | |
1542 | return -ENOMEM; | |
1543 | ||
1544 | if (!name_or_path) | |
1545 | r = true; | |
1546 | else | |
907952bb | 1547 | r = marker_matches_images(marker, name_or_path, extension_image_paths); |
61d0578b LP |
1548 | |
1549 | if (ret_marker) | |
1550 | *ret_marker = TAKE_PTR(marker); | |
1551 | ||
1552 | return r; | |
1553 | } | |
1554 | ||
1555 | int portable_detach( | |
1556 | sd_bus *bus, | |
1557 | const char *name_or_path, | |
907952bb | 1558 | char **extension_image_paths, |
61d0578b LP |
1559 | PortableFlags flags, |
1560 | PortableChange **changes, | |
1561 | size_t *n_changes, | |
1562 | sd_bus_error *error) { | |
1563 | ||
1564 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
d378ef70 | 1565 | _cleanup_set_free_ Set *unit_files = NULL, *markers = NULL; |
6b72105a | 1566 | _cleanup_free_ char *extensions = NULL; |
61d0578b LP |
1567 | _cleanup_closedir_ DIR *d = NULL; |
1568 | const char *where, *item; | |
61d0578b LP |
1569 | int ret = 0; |
1570 | int r; | |
1571 | ||
1572 | assert(name_or_path); | |
1573 | ||
b380b643 | 1574 | r = lookup_paths_init(&paths, LOOKUP_SCOPE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); |
61d0578b LP |
1575 | if (r < 0) |
1576 | return r; | |
1577 | ||
40a7b232 | 1578 | where = attached_path(&paths, flags); |
61d0578b LP |
1579 | |
1580 | d = opendir(where); | |
339731db LP |
1581 | if (!d) { |
1582 | if (errno == ENOENT) | |
1583 | goto not_found; | |
1584 | ||
61d0578b | 1585 | return log_debug_errno(errno, "Failed to open '%s' directory: %m", where); |
339731db | 1586 | } |
61d0578b | 1587 | |
61d0578b | 1588 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { |
edea3702 YW |
1589 | _cleanup_free_ char *marker = NULL, *unit_name = NULL; |
1590 | const char *dot; | |
1591 | ||
a0f0cf0a YW |
1592 | /* When a portable service is enabled with "portablectl --copy=symlink --enable --now attach", |
1593 | * and is disabled with "portablectl --enable --now detach", which calls DisableUnitFilesWithFlags | |
1594 | * DBus method, the main unit file is removed, but its drop-ins are not. Hence, here we need | |
1595 | * to list both main unit files and drop-in directories (without the main unit files). */ | |
edea3702 YW |
1596 | |
1597 | dot = endswith(de->d_name, ".d"); | |
1598 | if (dot) | |
1599 | unit_name = strndup(de->d_name, dot - de->d_name); | |
1600 | else | |
1601 | unit_name = strdup(de->d_name); | |
1602 | if (!unit_name) | |
1603 | return -ENOMEM; | |
61d0578b | 1604 | |
edea3702 | 1605 | if (!unit_name_is_valid(unit_name, UNIT_NAME_ANY)) |
61d0578b LP |
1606 | continue; |
1607 | ||
1608 | /* Filter out duplicates */ | |
edea3702 | 1609 | if (set_contains(unit_files, unit_name)) |
61d0578b LP |
1610 | continue; |
1611 | ||
edea3702 | 1612 | if (dot ? !IN_SET(de->d_type, DT_LNK, DT_DIR) : !IN_SET(de->d_type, DT_LNK, DT_REG)) |
61d0578b LP |
1613 | continue; |
1614 | ||
edea3702 | 1615 | r = test_chroot_dropin(d, where, unit_name, name_or_path, extension_image_paths, &marker); |
61d0578b LP |
1616 | if (r < 0) |
1617 | return r; | |
1618 | if (r == 0) | |
1619 | continue; | |
1620 | ||
aad813bf | 1621 | if (!FLAGS_SET(flags, PORTABLE_REATTACH) && !FLAGS_SET(flags, PORTABLE_FORCE_ATTACH)) { |
ace212f5 LB |
1622 | r = unit_file_is_active(bus, unit_name, error); |
1623 | if (r < 0) | |
1624 | return r; | |
1625 | if (r > 0) | |
1626 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active, can't detach.", unit_name); | |
1627 | } | |
61d0578b | 1628 | |
edea3702 | 1629 | r = set_ensure_consume(&unit_files, &string_hash_ops_free, TAKE_PTR(unit_name)); |
61d0578b | 1630 | if (r < 0) |
edea3702 | 1631 | return log_oom_debug(); |
61d0578b | 1632 | |
907952bb LB |
1633 | for (const char *p = marker;;) { |
1634 | _cleanup_free_ char *image = NULL; | |
61d0578b | 1635 | |
907952bb | 1636 | r = extract_first_word(&p, &image, ":", EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_RETAIN_ESCAPE); |
d378ef70 | 1637 | if (r < 0) |
907952bb LB |
1638 | return log_debug_errno(r, "Failed to parse marker: %s", p); |
1639 | if (r == 0) | |
1640 | break; | |
1641 | ||
1642 | if (path_is_absolute(image) && !image_in_search_path(IMAGE_PORTABLE, NULL, image)) { | |
1643 | r = set_ensure_consume(&markers, &path_hash_ops_free, TAKE_PTR(image)); | |
1644 | if (r < 0) | |
1645 | return r; | |
1646 | } | |
61d0578b LP |
1647 | } |
1648 | } | |
1649 | ||
339731db LP |
1650 | if (set_isempty(unit_files)) |
1651 | goto not_found; | |
61d0578b | 1652 | |
90e74a66 | 1653 | SET_FOREACH(item, unit_files) { |
61d0578b | 1654 | _cleanup_free_ char *md = NULL; |
61d0578b LP |
1655 | |
1656 | if (unlinkat(dirfd(d), item, 0) < 0) { | |
1657 | log_debug_errno(errno, "Can't remove unit file %s/%s: %m", where, item); | |
1658 | ||
1659 | if (errno != ENOENT && ret >= 0) | |
1660 | ret = -errno; | |
1661 | } else | |
1662 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, item, NULL); | |
1663 | ||
1664 | FOREACH_STRING(suffix, ".d/10-profile.conf", ".d/20-portable.conf") { | |
1665 | _cleanup_free_ char *dropin = NULL; | |
1666 | ||
1667 | dropin = strjoin(item, suffix); | |
1668 | if (!dropin) | |
1669 | return -ENOMEM; | |
1670 | ||
1671 | if (unlinkat(dirfd(d), dropin, 0) < 0) { | |
1672 | log_debug_errno(errno, "Can't remove drop-in %s/%s: %m", where, dropin); | |
1673 | ||
1674 | if (errno != ENOENT && ret >= 0) | |
1675 | ret = -errno; | |
1676 | } else | |
1677 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, dropin, NULL); | |
1678 | } | |
1679 | ||
1680 | md = strjoin(item, ".d"); | |
1681 | if (!md) | |
1682 | return -ENOMEM; | |
1683 | ||
1684 | if (unlinkat(dirfd(d), md, AT_REMOVEDIR) < 0) { | |
1685 | log_debug_errno(errno, "Can't remove drop-in directory %s/%s: %m", where, md); | |
1686 | ||
1687 | if (errno != ENOENT && ret >= 0) | |
1688 | ret = -errno; | |
1689 | } else | |
1690 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, md, NULL); | |
1691 | } | |
1692 | ||
1693 | /* Now, also drop any image symlink, for images outside of the sarch path */ | |
90e74a66 | 1694 | SET_FOREACH(item, markers) { |
61d0578b LP |
1695 | _cleanup_free_ char *sl = NULL; |
1696 | struct stat st; | |
1697 | ||
1698 | r = image_symlink(item, flags, &sl); | |
1699 | if (r < 0) { | |
1700 | log_debug_errno(r, "Failed to determine image symlink for '%s', ignoring: %m", item); | |
1701 | continue; | |
1702 | } | |
1703 | ||
1704 | if (lstat(sl, &st) < 0) { | |
1705 | log_debug_errno(errno, "Failed to stat '%s', ignoring: %m", sl); | |
1706 | continue; | |
1707 | } | |
1708 | ||
1709 | if (!S_ISLNK(st.st_mode)) { | |
1710 | log_debug("Image '%s' is not a symlink, ignoring.", sl); | |
1711 | continue; | |
1712 | } | |
1713 | ||
1714 | if (unlink(sl) < 0) { | |
1715 | log_debug_errno(errno, "Can't remove image symlink '%s': %m", sl); | |
1716 | ||
1717 | if (errno != ENOENT && ret >= 0) | |
1718 | ret = -errno; | |
1719 | } else | |
1720 | portable_changes_add(changes, n_changes, PORTABLE_UNLINK, sl, NULL); | |
1721 | } | |
1722 | ||
d09d85a2 LP |
1723 | /* Try to remove the unit file directory, if we can */ |
1724 | if (rmdir(where) >= 0) | |
1725 | portable_changes_add(changes, n_changes, PORTABLE_UNLINK, where, NULL); | |
1726 | ||
61d0578b | 1727 | return ret; |
339731db LP |
1728 | |
1729 | not_found: | |
6b72105a LB |
1730 | extensions = strv_join(extension_image_paths, ", "); |
1731 | if (!extensions) | |
1732 | return -ENOMEM; | |
1733 | ||
1734 | r = sd_bus_error_setf(error, | |
1735 | BUS_ERROR_NO_SUCH_UNIT, | |
1736 | "No unit files associated with '%s%s%s' found attached to the system. Image not attached?", | |
1737 | name_or_path, | |
1738 | isempty(extensions) ? "" : "' or any of its extensions '", | |
1739 | isempty(extensions) ? "" : extensions); | |
1740 | return log_debug_errno(r, "%s", error->message); | |
61d0578b LP |
1741 | } |
1742 | ||
1743 | static int portable_get_state_internal( | |
1744 | sd_bus *bus, | |
1745 | const char *name_or_path, | |
0017415c | 1746 | char **extension_image_paths, |
61d0578b LP |
1747 | PortableFlags flags, |
1748 | PortableState *ret, | |
1749 | sd_bus_error *error) { | |
1750 | ||
1751 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
1752 | bool found_enabled = false, found_running = false; | |
be327321 | 1753 | _cleanup_set_free_ Set *unit_files = NULL; |
61d0578b LP |
1754 | _cleanup_closedir_ DIR *d = NULL; |
1755 | const char *where; | |
61d0578b LP |
1756 | int r; |
1757 | ||
1758 | assert(name_or_path); | |
1759 | assert(ret); | |
1760 | ||
b380b643 | 1761 | r = lookup_paths_init(&paths, LOOKUP_SCOPE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); |
61d0578b LP |
1762 | if (r < 0) |
1763 | return r; | |
1764 | ||
40a7b232 | 1765 | where = attached_path(&paths, flags); |
61d0578b LP |
1766 | |
1767 | d = opendir(where); | |
339731db LP |
1768 | if (!d) { |
1769 | if (errno == ENOENT) { | |
1770 | /* If the 'attached' directory doesn't exist at all, then we know for sure this image isn't attached. */ | |
1771 | *ret = PORTABLE_DETACHED; | |
1772 | return 0; | |
1773 | } | |
1774 | ||
61d0578b | 1775 | return log_debug_errno(errno, "Failed to open '%s' directory: %m", where); |
339731db | 1776 | } |
61d0578b | 1777 | |
61d0578b LP |
1778 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { |
1779 | UnitFileState state; | |
1780 | ||
1781 | if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) | |
1782 | continue; | |
1783 | ||
1784 | /* Filter out duplicates */ | |
be327321 | 1785 | if (set_contains(unit_files, de->d_name)) |
61d0578b LP |
1786 | continue; |
1787 | ||
61d0578b LP |
1788 | if (!IN_SET(de->d_type, DT_LNK, DT_REG)) |
1789 | continue; | |
1790 | ||
0017415c | 1791 | r = test_chroot_dropin(d, where, de->d_name, name_or_path, extension_image_paths, NULL); |
61d0578b LP |
1792 | if (r < 0) |
1793 | return r; | |
1794 | if (r == 0) | |
1795 | continue; | |
1796 | ||
b380b643 | 1797 | r = unit_file_lookup_state(LOOKUP_SCOPE_SYSTEM, &paths, de->d_name, &state); |
61d0578b LP |
1798 | if (r < 0) |
1799 | return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name); | |
1800 | if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME)) | |
1801 | found_enabled = true; | |
1802 | ||
1803 | r = unit_file_is_active(bus, de->d_name, error); | |
1804 | if (r < 0) | |
1805 | return r; | |
1806 | if (r > 0) | |
1807 | found_running = true; | |
1808 | ||
be327321 | 1809 | r = set_put_strdup(&unit_files, de->d_name); |
61d0578b LP |
1810 | if (r < 0) |
1811 | return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name); | |
1812 | } | |
1813 | ||
1814 | *ret = found_running ? (!set_isempty(unit_files) && (flags & PORTABLE_RUNTIME) ? PORTABLE_RUNNING_RUNTIME : PORTABLE_RUNNING) : | |
1815 | found_enabled ? (flags & PORTABLE_RUNTIME ? PORTABLE_ENABLED_RUNTIME : PORTABLE_ENABLED) : | |
1816 | !set_isempty(unit_files) ? (flags & PORTABLE_RUNTIME ? PORTABLE_ATTACHED_RUNTIME : PORTABLE_ATTACHED) : PORTABLE_DETACHED; | |
1817 | ||
1818 | return 0; | |
1819 | } | |
1820 | ||
1821 | int portable_get_state( | |
1822 | sd_bus *bus, | |
1823 | const char *name_or_path, | |
0017415c | 1824 | char **extension_image_paths, |
61d0578b LP |
1825 | PortableFlags flags, |
1826 | PortableState *ret, | |
1827 | sd_bus_error *error) { | |
1828 | ||
1829 | PortableState state; | |
1830 | int r; | |
1831 | ||
1832 | assert(name_or_path); | |
1833 | assert(ret); | |
1834 | ||
1835 | /* We look for matching units twice: once in the regular directories, and once in the runtime directories — but | |
1836 | * the latter only if we didn't find anything in the former. */ | |
1837 | ||
0017415c | 1838 | r = portable_get_state_internal(bus, name_or_path, extension_image_paths, flags & ~PORTABLE_RUNTIME, &state, error); |
61d0578b LP |
1839 | if (r < 0) |
1840 | return r; | |
1841 | ||
1842 | if (state == PORTABLE_DETACHED) { | |
0017415c | 1843 | r = portable_get_state_internal(bus, name_or_path, extension_image_paths, flags | PORTABLE_RUNTIME, &state, error); |
61d0578b LP |
1844 | if (r < 0) |
1845 | return r; | |
1846 | } | |
1847 | ||
1848 | *ret = state; | |
1849 | return 0; | |
1850 | } | |
1851 | ||
1852 | int portable_get_profiles(char ***ret) { | |
1853 | assert(ret); | |
1854 | ||
13c02e7b | 1855 | return conf_files_list_nulstr(ret, NULL, NULL, CONF_FILES_DIRECTORY|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED, PORTABLE_PROFILE_DIRS); |
61d0578b LP |
1856 | } |
1857 | ||
1858 | static const char* const portable_change_type_table[_PORTABLE_CHANGE_TYPE_MAX] = { | |
1859 | [PORTABLE_COPY] = "copy", | |
1860 | [PORTABLE_MKDIR] = "mkdir", | |
1861 | [PORTABLE_SYMLINK] = "symlink", | |
1862 | [PORTABLE_UNLINK] = "unlink", | |
1863 | [PORTABLE_WRITE] = "write", | |
1864 | }; | |
1865 | ||
ba5b6c59 | 1866 | DEFINE_STRING_TABLE_LOOKUP(portable_change_type, int); |
61d0578b LP |
1867 | |
1868 | static const char* const portable_state_table[_PORTABLE_STATE_MAX] = { | |
1869 | [PORTABLE_DETACHED] = "detached", | |
1870 | [PORTABLE_ATTACHED] = "attached", | |
1871 | [PORTABLE_ATTACHED_RUNTIME] = "attached-runtime", | |
1872 | [PORTABLE_ENABLED] = "enabled", | |
1873 | [PORTABLE_ENABLED_RUNTIME] = "enabled-runtime", | |
1874 | [PORTABLE_RUNNING] = "running", | |
1875 | [PORTABLE_RUNNING_RUNTIME] = "running-runtime", | |
1876 | }; | |
1877 | ||
1878 | DEFINE_STRING_TABLE_LOOKUP(portable_state, PortableState); |