]>
Commit | Line | Data |
---|---|---|
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ | |
2 | ||
3 | #include <stdlib.h> | |
4 | ||
5 | #include "alloc-util.h" | |
6 | #include "chase.h" | |
7 | #include "dirent-util.h" | |
8 | #include "env-file.h" | |
9 | #include "errno-util.h" | |
10 | #include "fd-util.h" | |
11 | #include "fs-util.h" | |
12 | #include "glyph-util.h" | |
13 | #include "log.h" | |
14 | #include "os-util.h" | |
15 | #include "path-util.h" | |
16 | #include "stat-util.h" | |
17 | #include "string-table.h" | |
18 | #include "string-util.h" | |
19 | #include "strv.h" | |
20 | #include "time-util.h" | |
21 | #include "utf8.h" | |
22 | #include "xattr-util.h" | |
23 | ||
24 | static const char* const image_class_table[_IMAGE_CLASS_MAX] = { | |
25 | [IMAGE_MACHINE] = "machine", | |
26 | [IMAGE_PORTABLE] = "portable", | |
27 | [IMAGE_SYSEXT] = "sysext", | |
28 | [IMAGE_CONFEXT] = "confext", | |
29 | }; | |
30 | ||
31 | DEFINE_STRING_TABLE_LOOKUP(image_class, ImageClass); | |
32 | ||
33 | /* Helper struct for naming simplicity and reusability */ | |
34 | static const struct { | |
35 | const char *release_file_directory; | |
36 | const char *release_file_path_prefix; | |
37 | } image_class_release_info[_IMAGE_CLASS_MAX] = { | |
38 | [IMAGE_SYSEXT] = { | |
39 | .release_file_directory = "/usr/lib/extension-release.d/", | |
40 | .release_file_path_prefix = "/usr/lib/extension-release.d/extension-release.", | |
41 | }, | |
42 | [IMAGE_CONFEXT] = { | |
43 | .release_file_directory = "/etc/extension-release.d/", | |
44 | .release_file_path_prefix = "/etc/extension-release.d/extension-release.", | |
45 | } | |
46 | }; | |
47 | ||
48 | bool image_name_is_valid(const char *s) { | |
49 | if (!filename_is_valid(s)) | |
50 | return false; | |
51 | ||
52 | if (string_has_cc(s, NULL)) | |
53 | return false; | |
54 | ||
55 | if (!utf8_is_valid(s)) | |
56 | return false; | |
57 | ||
58 | /* Temporary files for atomically creating new files */ | |
59 | if (startswith(s, ".#")) | |
60 | return false; | |
61 | ||
62 | return true; | |
63 | } | |
64 | ||
65 | int path_extract_image_name(const char *path, char **ret) { | |
66 | _cleanup_free_ char *fn = NULL; | |
67 | int r; | |
68 | ||
69 | assert(path); | |
70 | assert(ret); | |
71 | ||
72 | /* Extract last component from path, without any "/" suffixes. */ | |
73 | r = path_extract_filename(path, &fn); | |
74 | if (r < 0) | |
75 | return r; | |
76 | if (r != O_DIRECTORY) { | |
77 | /* Chop off any image suffixes we recognize (unless we already know this must refer to some dir) */ | |
78 | char *m = ENDSWITH_SET(fn, ".sysext.raw", ".confext.raw", ".raw"); | |
79 | if (m) | |
80 | *m = 0; | |
81 | } | |
82 | ||
83 | /* Truncate the version/counting suffixes */ | |
84 | fn[strcspn(fn, "_+")] = 0; | |
85 | ||
86 | if (!image_name_is_valid(fn)) | |
87 | return -EINVAL; | |
88 | ||
89 | *ret = TAKE_PTR(fn); | |
90 | return 0; | |
91 | } | |
92 | ||
93 | int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) { | |
94 | int r; | |
95 | ||
96 | assert(path); | |
97 | ||
98 | /* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir | |
99 | * always results in -ENOENT, and we can properly distinguish the case where the whole root doesn't exist from | |
100 | * the case where just the os-release file is missing. */ | |
101 | r = access_nofollow(path, F_OK); | |
102 | if (r < 0) | |
103 | return r; | |
104 | ||
105 | /* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension, | |
106 | * /etc/extension-release.d/extension-release[.NAME] as flag for something being a system configuration, and finally, | |
107 | * and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */ | |
108 | r = open_extension_release(path, image_class, extension, relax_extension_release_check, NULL, NULL); | |
109 | if (r == -ENOENT) /* We got nothing */ | |
110 | return 0; | |
111 | if (r < 0) | |
112 | return r; | |
113 | ||
114 | return 1; | |
115 | } | |
116 | ||
117 | static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) { | |
118 | int r; | |
119 | ||
120 | assert(extension_release_fd >= 0); | |
121 | assert(extension_release_dir_path); | |
122 | assert(filename); | |
123 | ||
124 | /* No xattr or cannot parse it? Then skip this. */ | |
125 | r = getxattr_at_bool(extension_release_fd, /* path= */ NULL, "user.extension-release.strict", /* at_flags= */ 0); | |
126 | if (ERRNO_IS_NEG_XATTR_ABSENT(r)) | |
127 | return log_debug_errno(r, "%s/%s does not have user.extension-release.strict xattr, ignoring.", | |
128 | extension_release_dir_path, filename); | |
129 | if (r < 0) | |
130 | return log_debug_errno(r, "%s/%s: Failed to read 'user.extension-release.strict' extended attribute from file, ignoring: %m", | |
131 | extension_release_dir_path, filename); | |
132 | ||
133 | /* Explicitly set to request strict matching? Skip it. */ | |
134 | if (r > 0) { | |
135 | log_debug("%s/%s: 'user.extension-release.strict' attribute is true, ignoring file.", | |
136 | extension_release_dir_path, filename); | |
137 | return true; | |
138 | } | |
139 | ||
140 | log_debug("%s/%s: 'user.extension-release.strict' attribute is false%s", | |
141 | extension_release_dir_path, filename, | |
142 | glyph(GLYPH_ELLIPSIS)); | |
143 | ||
144 | return false; | |
145 | } | |
146 | ||
147 | int open_os_release_at(int rfd, char **ret_path, int *ret_fd) { | |
148 | const char *e; | |
149 | int r; | |
150 | ||
151 | assert(rfd >= 0 || rfd == AT_FDCWD); | |
152 | ||
153 | e = secure_getenv("SYSTEMD_OS_RELEASE"); | |
154 | if (e) | |
155 | return chaseat(rfd, e, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); | |
156 | ||
157 | FOREACH_STRING(path, "/etc/os-release", "/usr/lib/os-release") { | |
158 | r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); | |
159 | if (r != -ENOENT) | |
160 | return r; | |
161 | } | |
162 | ||
163 | return -ENOENT; | |
164 | } | |
165 | ||
166 | int open_os_release(const char *root, char **ret_path, int *ret_fd) { | |
167 | _cleanup_close_ int rfd = -EBADF, fd = -EBADF; | |
168 | _cleanup_free_ char *p = NULL; | |
169 | int r; | |
170 | ||
171 | rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH); | |
172 | if (rfd < 0) | |
173 | return -errno; | |
174 | ||
175 | r = open_os_release_at(rfd, ret_path ? &p : NULL, ret_fd ? &fd : NULL); | |
176 | if (r < 0) | |
177 | return r; | |
178 | ||
179 | if (ret_path) { | |
180 | r = chaseat_prefix_root(p, root, ret_path); | |
181 | if (r < 0) | |
182 | return r; | |
183 | } | |
184 | ||
185 | if (ret_fd) | |
186 | *ret_fd = TAKE_FD(fd); | |
187 | ||
188 | return 0; | |
189 | } | |
190 | ||
191 | int open_extension_release_at( | |
192 | int rfd, | |
193 | ImageClass image_class, | |
194 | const char *extension, | |
195 | bool relax_extension_release_check, | |
196 | char **ret_path, | |
197 | int *ret_fd) { | |
198 | ||
199 | _cleanup_free_ char *dir_path = NULL, *path_found = NULL; | |
200 | _cleanup_close_ int fd_found = -EBADF; | |
201 | _cleanup_closedir_ DIR *dir = NULL; | |
202 | bool found = false; | |
203 | const char *p; | |
204 | int r; | |
205 | ||
206 | assert(rfd >= 0 || rfd == AT_FDCWD); | |
207 | assert(!extension || (image_class >= 0 && image_class < _IMAGE_CLASS_MAX)); | |
208 | ||
209 | if (!extension) | |
210 | return open_os_release_at(rfd, ret_path, ret_fd); | |
211 | ||
212 | if (!IN_SET(image_class, IMAGE_SYSEXT, IMAGE_CONFEXT)) | |
213 | return -EINVAL; | |
214 | ||
215 | if (!image_name_is_valid(extension)) | |
216 | return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension); | |
217 | ||
218 | p = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension); | |
219 | r = chaseat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); | |
220 | log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", p); | |
221 | if (r != -ENOENT) | |
222 | return r; | |
223 | ||
224 | /* Cannot find the expected extension-release file? The image filename might have been mangled on | |
225 | * deployment, so fallback to checking for any file in the extension-release.d directory, and return | |
226 | * the first one with a user.extension-release xattr instead. The user.extension-release.strict | |
227 | * xattr is checked to ensure the author of the image considers it OK if names do not match. */ | |
228 | ||
229 | p = image_class_release_info[image_class].release_file_directory; | |
230 | r = chase_and_opendirat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, &dir_path, &dir); | |
231 | if (r < 0) | |
232 | return log_debug_errno(r, "Cannot open %s, ignoring: %m", p); | |
233 | ||
234 | FOREACH_DIRENT(de, dir, return -errno) { | |
235 | _cleanup_close_ int fd = -EBADF; | |
236 | const char *image_name; | |
237 | ||
238 | if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN)) | |
239 | continue; | |
240 | ||
241 | image_name = startswith(de->d_name, "extension-release."); | |
242 | if (!image_name) | |
243 | continue; | |
244 | ||
245 | if (!image_name_is_valid(image_name)) { | |
246 | log_debug("%s/%s is not a valid release file name, ignoring.", dir_path, de->d_name); | |
247 | continue; | |
248 | } | |
249 | ||
250 | /* We already chased the directory, and checked that this is a real file, so we shouldn't | |
251 | * fail to open it. */ | |
252 | fd = openat(dirfd(dir), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW); | |
253 | if (fd < 0) | |
254 | return log_debug_errno(errno, "Failed to open release file %s/%s: %m", dir_path, de->d_name); | |
255 | ||
256 | /* Really ensure it is a regular file after we open it. */ | |
257 | r = fd_verify_regular(fd); | |
258 | if (r < 0) { | |
259 | log_debug_errno(r, "%s/%s is not a regular file, ignoring: %m", dir_path, de->d_name); | |
260 | continue; | |
261 | } | |
262 | ||
263 | if (!relax_extension_release_check) { | |
264 | _cleanup_free_ char *base_extension = NULL; | |
265 | ||
266 | r = path_extract_image_name(extension, &base_extension); | |
267 | if (r < 0) { | |
268 | log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", extension); | |
269 | continue; | |
270 | } | |
271 | ||
272 | if (!streq(image_name, base_extension) && | |
273 | extension_release_strict_xattr_value(fd, dir_path, image_name) != 0) | |
274 | continue; | |
275 | } | |
276 | ||
277 | /* We already found what we were looking for, but there's another candidate? We treat this as | |
278 | * an error, as we want to enforce that there are no ambiguities in case we are in the | |
279 | * fallback path. */ | |
280 | if (found) | |
281 | return -ENOTUNIQ; | |
282 | ||
283 | found = true; | |
284 | ||
285 | if (ret_fd) | |
286 | fd_found = TAKE_FD(fd); | |
287 | ||
288 | if (ret_path) { | |
289 | path_found = path_join(dir_path, de->d_name); | |
290 | if (!path_found) | |
291 | return -ENOMEM; | |
292 | } | |
293 | } | |
294 | if (!found) | |
295 | return -ENOENT; | |
296 | ||
297 | if (ret_fd) | |
298 | *ret_fd = TAKE_FD(fd_found); | |
299 | if (ret_path) | |
300 | *ret_path = TAKE_PTR(path_found); | |
301 | ||
302 | return 0; | |
303 | } | |
304 | ||
305 | int open_extension_release( | |
306 | const char *root, | |
307 | ImageClass image_class, | |
308 | const char *extension, | |
309 | bool relax_extension_release_check, | |
310 | char **ret_path, | |
311 | int *ret_fd) { | |
312 | ||
313 | _cleanup_close_ int rfd = -EBADF, fd = -EBADF; | |
314 | _cleanup_free_ char *p = NULL; | |
315 | int r; | |
316 | ||
317 | rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH); | |
318 | if (rfd < 0) | |
319 | return -errno; | |
320 | ||
321 | r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check, | |
322 | ret_path ? &p : NULL, ret_fd ? &fd : NULL); | |
323 | if (r < 0) | |
324 | return r; | |
325 | ||
326 | if (ret_path) { | |
327 | r = chaseat_prefix_root(p, root, ret_path); | |
328 | if (r < 0) | |
329 | return r; | |
330 | } | |
331 | ||
332 | if (ret_fd) | |
333 | *ret_fd = TAKE_FD(fd); | |
334 | ||
335 | return 0; | |
336 | } | |
337 | ||
338 | static int parse_extension_release_atv( | |
339 | int rfd, | |
340 | ImageClass image_class, | |
341 | const char *extension, | |
342 | bool relax_extension_release_check, | |
343 | va_list ap) { | |
344 | ||
345 | _cleanup_close_ int fd = -EBADF; | |
346 | _cleanup_free_ char *p = NULL; | |
347 | int r; | |
348 | ||
349 | assert(rfd >= 0 || rfd == AT_FDCWD); | |
350 | ||
351 | r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check, &p, &fd); | |
352 | if (r < 0) | |
353 | return r; | |
354 | ||
355 | return parse_env_file_fdv(fd, p, ap); | |
356 | } | |
357 | ||
358 | int parse_extension_release_at_sentinel( | |
359 | int rfd, | |
360 | ImageClass image_class, | |
361 | bool relax_extension_release_check, | |
362 | const char *extension, | |
363 | ...) { | |
364 | ||
365 | va_list ap; | |
366 | int r; | |
367 | ||
368 | assert(rfd >= 0 || rfd == AT_FDCWD); | |
369 | ||
370 | va_start(ap, extension); | |
371 | r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap); | |
372 | va_end(ap); | |
373 | return r; | |
374 | } | |
375 | ||
376 | int parse_extension_release_sentinel( | |
377 | const char *root, | |
378 | ImageClass image_class, | |
379 | bool relax_extension_release_check, | |
380 | const char *extension, | |
381 | ...) { | |
382 | ||
383 | _cleanup_close_ int rfd = -EBADF; | |
384 | va_list ap; | |
385 | int r; | |
386 | ||
387 | rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH); | |
388 | if (rfd < 0) | |
389 | return -errno; | |
390 | ||
391 | va_start(ap, extension); | |
392 | r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap); | |
393 | va_end(ap); | |
394 | return r; | |
395 | } | |
396 | ||
397 | int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char ***ret) { | |
398 | _cleanup_strv_free_ char **os_release_pairs = NULL, **os_release_pairs_prefixed = NULL; | |
399 | int r; | |
400 | ||
401 | r = load_os_release_pairs(root, &os_release_pairs); | |
402 | if (r < 0) | |
403 | return r; | |
404 | ||
405 | STRV_FOREACH_PAIR(p, q, os_release_pairs) { | |
406 | char *line; | |
407 | ||
408 | /* We strictly return only the four main ID fields and ignore the rest */ | |
409 | if (!STR_IN_SET(*p, "ID", "VERSION_ID", "BUILD_ID", "VARIANT_ID")) | |
410 | continue; | |
411 | ||
412 | ascii_strlower(*p); | |
413 | line = strjoin(prefix, *p, "=", *q); | |
414 | if (!line) | |
415 | return -ENOMEM; | |
416 | r = strv_consume(&os_release_pairs_prefixed, line); | |
417 | if (r < 0) | |
418 | return r; | |
419 | } | |
420 | ||
421 | *ret = TAKE_PTR(os_release_pairs_prefixed); | |
422 | ||
423 | return 0; | |
424 | } | |
425 | ||
426 | int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) { | |
427 | _cleanup_close_ int fd = -EBADF; | |
428 | _cleanup_free_ char *p = NULL; | |
429 | int r; | |
430 | ||
431 | r = open_extension_release(root, image_class, extension, relax_extension_release_check, &p, &fd); | |
432 | if (r < 0) | |
433 | return r; | |
434 | ||
435 | return load_env_file_pairs_fd(fd, p, ret); | |
436 | } | |
437 | ||
438 | int os_release_support_ended(const char *support_end, bool quiet, usec_t *ret_eol) { | |
439 | _cleanup_free_ char *_support_end_alloc = NULL; | |
440 | int r; | |
441 | ||
442 | if (!support_end) { | |
443 | /* If the caller has the variably handy, they can pass it in. If not, we'll read it | |
444 | * ourselves. */ | |
445 | ||
446 | r = parse_os_release(NULL, | |
447 | "SUPPORT_END", &_support_end_alloc); | |
448 | if (r < 0 && r != -ENOENT) | |
449 | return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r, | |
450 | "Failed to read os-release file, ignoring: %m"); | |
451 | ||
452 | support_end = _support_end_alloc; | |
453 | } | |
454 | ||
455 | if (isempty(support_end)) { /* An empty string is a explicit way to say "no EOL exists" */ | |
456 | if (ret_eol) | |
457 | *ret_eol = USEC_INFINITY; | |
458 | ||
459 | return false; /* no end date defined */ | |
460 | } | |
461 | ||
462 | struct tm tm = {}; | |
463 | const char *k = strptime(support_end, "%Y-%m-%d", &tm); | |
464 | if (!k || *k) | |
465 | return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, SYNTHETIC_ERRNO(EINVAL), | |
466 | "Failed to parse SUPPORT_END= from os-release file, ignoring: %s", support_end); | |
467 | ||
468 | usec_t eol; | |
469 | r = mktime_or_timegm_usec(&tm, /* utc= */ true, &eol); | |
470 | if (r < 0) | |
471 | return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r, | |
472 | "Failed to convert SUPPORT_END= time from os-release file, ignoring: %m"); | |
473 | ||
474 | if (ret_eol) | |
475 | *ret_eol = eol; | |
476 | ||
477 | return now(CLOCK_REALTIME) > eol; | |
478 | } | |
479 | ||
480 | const char* os_release_pretty_name(const char *pretty_name, const char *name) { | |
481 | /* Distills a "pretty" name to show from os-release data. First argument is supposed to be the | |
482 | * PRETTY_NAME= field, the second one the NAME= field. This function is trivial, of course, and | |
483 | * exists mostly to ensure we use the same logic wherever possible. */ | |
484 | ||
485 | return empty_to_null(pretty_name) ?: | |
486 | empty_to_null(name) ?: "Linux"; | |
487 | } |