]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
d58ad743 | 2 | |
0c15577a DDM |
3 | #include <stdlib.h> |
4 | ||
d58ad743 | 5 | #include "alloc-util.h" |
f461a28d | 6 | #include "chase.h" |
9a4b883b | 7 | #include "dirent-util.h" |
686d13b9 | 8 | #include "env-file.h" |
fda65211 | 9 | #include "errno-util.h" |
d58ad743 LP |
10 | #include "fd-util.h" |
11 | #include "fs-util.h" | |
28e5e1e9 | 12 | #include "glyph-util.h" |
93a1f792 | 13 | #include "log.h" |
d58ad743 | 14 | #include "os-util.h" |
6ef06723 | 15 | #include "path-util.h" |
9a4b883b | 16 | #include "stat-util.h" |
c2cd9508 | 17 | #include "string-table.h" |
d58ad743 | 18 | #include "string-util.h" |
686d13b9 | 19 | #include "strv.h" |
0c15577a | 20 | #include "time-util.h" |
6ef06723 | 21 | #include "utf8.h" |
9a4b883b | 22 | #include "xattr-util.h" |
6ef06723 | 23 | |
c2cd9508 YW |
24 | static const char* const image_class_table[_IMAGE_CLASS_MAX] = { |
25 | [IMAGE_MACHINE] = "machine", | |
26 | [IMAGE_PORTABLE] = "portable", | |
8a324d16 | 27 | [IMAGE_SYSEXT] = "sysext", |
c2cd9508 YW |
28 | [IMAGE_CONFEXT] = "confext", |
29 | }; | |
30 | ||
31 | DEFINE_STRING_TABLE_LOOKUP(image_class, ImageClass); | |
32 | ||
b60e0f57 | 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 | ||
6ef06723 ZJS |
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 | } | |
d58ad743 | 64 | |
37543971 LB |
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; | |
37543971 | 76 | if (r != O_DIRECTORY) { |
99f3b67f MY |
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; | |
37543971 LB |
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 | ||
b60e0f57 | 93 | int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) { |
d58ad743 LP |
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 | |
5238e957 | 99 | * always results in -ENOENT, and we can properly distinguish the case where the whole root doesn't exist from |
d58ad743 | 100 | * the case where just the os-release file is missing. */ |
3f8999a7 | 101 | r = access_nofollow(path, F_OK); |
7c1dd9e2 MY |
102 | if (r < 0) |
103 | return r; | |
d58ad743 | 104 | |
9a4b883b | 105 | /* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension, |
b60e0f57 | 106 | * /etc/extension-release.d/extension-release[.NAME] as flag for something being a system configuration, and finally, |
9a4b883b | 107 | * and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */ |
b60e0f57 | 108 | r = open_extension_release(path, image_class, extension, relax_extension_release_check, NULL, NULL); |
d58ad743 LP |
109 | if (r == -ENOENT) /* We got nothing */ |
110 | return 0; | |
111 | if (r < 0) | |
112 | return r; | |
113 | ||
114 | return 1; | |
115 | } | |
116 | ||
192a9b70 LB |
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. */ | |
09755ba5 | 125 | r = getxattr_at_bool(extension_release_fd, /* path= */ NULL, "user.extension-release.strict", /* at_flags= */ 0); |
bb44fd07 ZJS |
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); | |
192a9b70 LB |
132 | |
133 | /* Explicitly set to request strict matching? Skip it. */ | |
192a9b70 LB |
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, | |
1ae9b0cf | 142 | glyph(GLYPH_ELLIPSIS)); |
192a9b70 LB |
143 | |
144 | return false; | |
145 | } | |
146 | ||
538d878d | 147 | int open_os_release_at(int rfd, char **ret_path, int *ret_fd) { |
a84677e0 YW |
148 | const char *e; |
149 | int r; | |
150 | ||
538d878d YW |
151 | assert(rfd >= 0 || rfd == AT_FDCWD); |
152 | ||
a84677e0 YW |
153 | e = secure_getenv("SYSTEMD_OS_RELEASE"); |
154 | if (e) | |
538d878d | 155 | return chaseat(rfd, e, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); |
a84677e0 YW |
156 | |
157 | FOREACH_STRING(path, "/etc/os-release", "/usr/lib/os-release") { | |
538d878d | 158 | r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); |
a84677e0 YW |
159 | if (r != -ENOENT) |
160 | return r; | |
161 | } | |
162 | ||
163 | return -ENOENT; | |
164 | } | |
165 | ||
538d878d YW |
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) { | |
60e761d8 | 180 | r = chaseat_prefix_root(p, root, ret_path); |
538d878d YW |
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, | |
a84677e0 YW |
193 | ImageClass image_class, |
194 | const char *extension, | |
195 | bool relax_extension_release_check, | |
196 | char **ret_path, | |
197 | int *ret_fd) { | |
198 | ||
61acfd83 YW |
199 | _cleanup_free_ char *dir_path = NULL, *path_found = NULL; |
200 | _cleanup_close_ int fd_found = -EBADF; | |
201 | _cleanup_closedir_ DIR *dir = NULL; | |
c9d64f8a | 202 | bool found = false; |
61acfd83 | 203 | const char *p; |
6f0f4d14 | 204 | int r; |
d58ad743 | 205 | |
538d878d | 206 | assert(rfd >= 0 || rfd == AT_FDCWD); |
a84677e0 | 207 | assert(!extension || (image_class >= 0 && image_class < _IMAGE_CLASS_MAX)); |
b60e0f57 | 208 | |
a84677e0 | 209 | if (!extension) |
538d878d | 210 | return open_os_release_at(rfd, ret_path, ret_fd); |
6ddd0511 | 211 | |
7421f20c YW |
212 | if (!IN_SET(image_class, IMAGE_SYSEXT, IMAGE_CONFEXT)) |
213 | return -EINVAL; | |
214 | ||
a84677e0 YW |
215 | if (!image_name_is_valid(extension)) |
216 | return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension); | |
ed15f8bc | 217 | |
61acfd83 | 218 | p = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension); |
538d878d | 219 | r = chaseat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd); |
61acfd83 | 220 | log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", p); |
7213c750 YW |
221 | if (r != -ENOENT) |
222 | return r; | |
9a4b883b | 223 | |
a84677e0 YW |
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. */ | |
9a4b883b | 228 | |
61acfd83 | 229 | p = image_class_release_info[image_class].release_file_directory; |
538d878d | 230 | r = chase_and_opendirat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, &dir_path, &dir); |
7213c750 | 231 | if (r < 0) |
538d878d | 232 | return log_debug_errno(r, "Cannot open %s, ignoring: %m", p); |
9a4b883b | 233 | |
61acfd83 YW |
234 | FOREACH_DIRENT(de, dir, return -errno) { |
235 | _cleanup_close_ int fd = -EBADF; | |
236 | const char *image_name; | |
9a4b883b | 237 | |
7213c750 YW |
238 | if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN)) |
239 | continue; | |
a84677e0 | 240 | |
61acfd83 | 241 | image_name = startswith(de->d_name, "extension-release."); |
7213c750 YW |
242 | if (!image_name) |
243 | continue; | |
244 | ||
245 | if (!image_name_is_valid(image_name)) { | |
61acfd83 | 246 | log_debug("%s/%s is not a valid release file name, ignoring.", dir_path, de->d_name); |
7213c750 YW |
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. */ | |
61acfd83 YW |
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); | |
7213c750 YW |
255 | |
256 | /* Really ensure it is a regular file after we open it. */ | |
61acfd83 | 257 | r = fd_verify_regular(fd); |
59c47075 | 258 | if (r < 0) { |
61acfd83 | 259 | log_debug_errno(r, "%s/%s is not a regular file, ignoring: %m", dir_path, de->d_name); |
7213c750 YW |
260 | continue; |
261 | } | |
262 | ||
37543971 | 263 | if (!relax_extension_release_check) { |
92d1fe3e | 264 | _cleanup_free_ char *base_extension = NULL; |
37543971 LB |
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 | ||
92d1fe3e | 272 | if (!streq(image_name, base_extension) && |
37543971 LB |
273 | extension_release_strict_xattr_value(fd, dir_path, image_name) != 0) |
274 | continue; | |
275 | } | |
7213c750 YW |
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. */ | |
c9d64f8a YW |
280 | if (found) |
281 | return -ENOTUNIQ; | |
7213c750 | 282 | |
c9d64f8a | 283 | found = true; |
7213c750 YW |
284 | |
285 | if (ret_fd) | |
61acfd83 | 286 | fd_found = TAKE_FD(fd); |
7213c750 YW |
287 | |
288 | if (ret_path) { | |
61acfd83 YW |
289 | path_found = path_join(dir_path, de->d_name); |
290 | if (!path_found) | |
7213c750 | 291 | return -ENOMEM; |
a84677e0 | 292 | } |
d58ad743 | 293 | } |
c9d64f8a YW |
294 | if (!found) |
295 | return -ENOENT; | |
d58ad743 | 296 | |
396ec958 | 297 | if (ret_fd) |
61acfd83 | 298 | *ret_fd = TAKE_FD(fd_found); |
d58ad743 | 299 | if (ret_path) |
61acfd83 | 300 | *ret_path = TAKE_PTR(path_found); |
d58ad743 LP |
301 | |
302 | return 0; | |
303 | } | |
304 | ||
538d878d | 305 | int open_extension_release( |
f4a1d32c YW |
306 | const char *root, |
307 | ImageClass image_class, | |
538d878d | 308 | const char *extension, |
f4a1d32c | 309 | bool relax_extension_release_check, |
538d878d YW |
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) { | |
60e761d8 | 327 | r = chaseat_prefix_root(p, root, ret_path); |
538d878d YW |
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, | |
f4a1d32c | 341 | const char *extension, |
538d878d YW |
342 | bool relax_extension_release_check, |
343 | va_list ap) { | |
f4a1d32c | 344 | |
53cbf5f9 | 345 | _cleanup_close_ int fd = -EBADF; |
d58ad743 | 346 | _cleanup_free_ char *p = NULL; |
d58ad743 LP |
347 | int r; |
348 | ||
538d878d YW |
349 | assert(rfd >= 0 || rfd == AT_FDCWD); |
350 | ||
351 | r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check, &p, &fd); | |
d58ad743 LP |
352 | if (r < 0) |
353 | return r; | |
354 | ||
538d878d YW |
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 | ||
6ddd0511 | 391 | va_start(ap, extension); |
538d878d | 392 | r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap); |
d58ad743 | 393 | va_end(ap); |
d58ad743 LP |
394 | return r; |
395 | } | |
396 | ||
e1bb4b0d LB |
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; | |
e1bb4b0d LB |
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 | ||
2094cd49 | 408 | /* We strictly return only the four main ID fields and ignore the rest */ |
e1bb4b0d LB |
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 | } | |
eb590035 | 425 | |
b60e0f57 | 426 | int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) { |
53cbf5f9 | 427 | _cleanup_close_ int fd = -EBADF; |
eb590035 LB |
428 | _cleanup_free_ char *p = NULL; |
429 | int r; | |
430 | ||
53cbf5f9 | 431 | r = open_extension_release(root, image_class, extension, relax_extension_release_check, &p, &fd); |
eb590035 LB |
432 | if (r < 0) |
433 | return r; | |
434 | ||
53cbf5f9 | 435 | return load_env_file_pairs_fd(fd, p, ret); |
eb590035 | 436 | } |
4bd03515 | 437 | |
6bfe9b3b | 438 | int os_release_support_ended(const char *support_end, bool quiet, usec_t *ret_eol) { |
4bd03515 ZJS |
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); | |
469af08f LP |
448 | if (r < 0 && r != -ENOENT) |
449 | return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r, | |
4bd03515 | 450 | "Failed to read os-release file, ignoring: %m"); |
4bd03515 ZJS |
451 | |
452 | support_end = _support_end_alloc; | |
453 | } | |
454 | ||
6f5cf415 LP |
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 | ||
a9bd4b4e | 459 | return false; /* no end date defined */ |
6f5cf415 | 460 | } |
4bd03515 | 461 | |
a9bd4b4e | 462 | struct tm tm = {}; |
4bd03515 ZJS |
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), | |
6f5cf415 | 466 | "Failed to parse SUPPORT_END= from os-release file, ignoring: %s", support_end); |
4bd03515 | 467 | |
6f5cf415 LP |
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"); | |
4bd03515 | 473 | |
6bfe9b3b | 474 | if (ret_eol) |
6f5cf415 | 475 | *ret_eol = eol; |
6bfe9b3b | 476 | |
6f5cf415 | 477 | return now(CLOCK_REALTIME) > eol; |
4bd03515 | 478 | } |
02b7005e | 479 | |
bfd5a068 | 480 | const char* os_release_pretty_name(const char *pretty_name, const char *name) { |
02b7005e LP |
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 | } |