• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 25705508282

11 May 2026 11:07PM UTC coverage: 72.65% (+0.1%) from 72.511%
25705508282

push

github

bluca
firstboot,sysinstall,hostnamed: always show FANCY_NAME=

This makes sure that whenever we want to show the OS name we can show
the fancy name. Thus this moves the escaping/validation of the fancy
name out of hostnamed into generic code, and then makes use of it in
sysinstall,firstboot,prompt-util.

17 of 36 new or added lines in 6 files covered. (47.22%)

2673 existing lines in 72 files now uncovered.

327104 of 450245 relevant lines covered (72.65%)

1200575.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.06
/src/basic/os-util.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <stdlib.h>
4

5
#include "alloc-util.h"
6
#include "ansi-color.h"
7
#include "chase.h"
8
#include "dirent-util.h"
9
#include "env-file.h"
10
#include "errno-util.h"
11
#include "escape.h"
12
#include "fd-util.h"
13
#include "fs-util.h"
14
#include "glyph-util.h"
15
#include "log.h"
16
#include "os-util.h"
17
#include "path-util.h"
18
#include "stat-util.h"
19
#include "string-table.h"
20
#include "string-util.h"
21
#include "strv.h"
22
#include "time-util.h"
23
#include "utf8.h"
24
#include "xattr-util.h"
25

26
static const char* const image_class_table[_IMAGE_CLASS_MAX] = {
27
        [IMAGE_MACHINE]  = "machine",
28
        [IMAGE_PORTABLE] = "portable",
29
        [IMAGE_SYSEXT]   = "sysext",
30
        [IMAGE_CONFEXT]  = "confext",
31
};
32

33
DEFINE_STRING_TABLE_LOOKUP(image_class, ImageClass);
901✔
34

35
/* Helper struct for naming simplicity and reusability */
36
static const struct {
37
        const char *release_file_directory;
38
        const char *release_file_path_prefix;
39
} image_class_release_info[_IMAGE_CLASS_MAX] = {
40
        [IMAGE_SYSEXT] = {
41
                .release_file_directory = "/usr/lib/extension-release.d/",
42
                .release_file_path_prefix = "/usr/lib/extension-release.d/extension-release.",
43
        },
44
        [IMAGE_CONFEXT] = {
45
                .release_file_directory = "/etc/extension-release.d/",
46
                .release_file_path_prefix = "/etc/extension-release.d/extension-release.",
47
        }
48
};
49

50
bool image_name_is_valid(const char *s) {
32,568✔
51
        if (!filename_is_valid(s))
32,568✔
52
                return false;
53

54
        if (string_has_cc(s, NULL))
25,702✔
55
                return false;
56

57
        if (!utf8_is_valid(s))
25,702✔
58
                return false;
59

60
        /* Temporary files for atomically creating new files */
61
        if (startswith(s, ".#"))
25,702✔
62
                return false;
3✔
63

64
        return true;
65
}
66

67
int path_extract_image_name(const char *path, char **ret) {
9,172✔
68
        _cleanup_free_ char *fn = NULL;
9,172✔
69
        int r;
9,172✔
70

71
        assert(path);
9,172✔
72
        assert(ret);
9,172✔
73

74
        /* Extract last component from path, without any "/" suffixes. */
75
        r = path_extract_filename(path, &fn);
9,172✔
76
        if (r < 0)
9,172✔
77
                return r;
78
        if (r != O_DIRECTORY) {
9,172✔
79
                /* Chop off any image suffixes we recognize (unless we already know this must refer to some dir) */
80
                char *m = ENDSWITH_SET(fn, ".sysext.raw", ".confext.raw", ".raw");
9,172✔
81
                if (m)
9,172✔
82
                        *m = 0;
4,893✔
83
        }
84

85
        /* Truncate the version/counting suffixes */
86
        fn[strcspn(fn, "_+")] = 0;
9,172✔
87

88
        if (!image_name_is_valid(fn))
9,172✔
89
                return -EINVAL;
90

91
        *ret = TAKE_PTR(fn);
9,172✔
92
        return 0;
9,172✔
93
}
94

95
int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) {
392✔
96
        int r;
392✔
97

98
        assert(path);
392✔
99

100
        /* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
101
         * always results in -ENOENT, and we can properly distinguish the case where the whole root doesn't exist from
102
         * the case where just the os-release file is missing. */
103
        r = access_nofollow(path, F_OK);
392✔
104
        if (r < 0)
392✔
105
                return r;
106

107
        /* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension,
108
         * /etc/extension-release.d/extension-release[.NAME] as flag for something being a system configuration, and finally,
109
         * and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */
110
        r = open_extension_release(path, image_class, extension, relax_extension_release_check, NULL, NULL);
391✔
111
        if (r == -ENOENT) /* We got nothing */
391✔
112
                return 0;
113
        if (r < 0)
334✔
114
                return r;
×
115

116
        return 1;
117
}
118

119
int fd_is_os_tree(int fd) {
81✔
120
        int r;
81✔
121

122
        assert(fd >= 0);
81✔
123

124
        r = open_extension_release_at(
81✔
125
                        fd,
126
                        IMAGE_MACHINE,
127
                        /* extension= */ NULL,
128
                        /* relax_extension_release_check= */ false,
129
                        /* ret_path= */ NULL,
130
                        /* ret_fd= */ NULL);
131
        if (r == -ENOENT)
81✔
132
                return false;
133
        if (r < 0)
6✔
134
                return r;
×
135

136
        return true;
137
}
138

139
static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) {
685✔
140
        int r;
685✔
141

142
        assert(extension_release_fd >= 0);
685✔
143
        assert(extension_release_dir_path);
685✔
144
        assert(filename);
685✔
145

146
        /* No xattr or cannot parse it? Then skip this. */
147
        r = getxattr_at_bool(extension_release_fd, /* path= */ NULL, "user.extension-release.strict", /* at_flags= */ 0);
685✔
148
        if (ERRNO_IS_NEG_XATTR_ABSENT(r))
685✔
149
                return log_debug_errno(r, "%s/%s does not have user.extension-release.strict xattr, ignoring.",
64✔
150
                                       extension_release_dir_path, filename);
151
        if (r < 0)
621✔
152
                return log_debug_errno(r, "%s/%s: Failed to read 'user.extension-release.strict' extended attribute from file, ignoring: %m",
×
153
                                       extension_release_dir_path, filename);
154

155
        /* Explicitly set to request strict matching? Skip it. */
156
        if (r > 0) {
621✔
157
                log_debug("%s/%s: 'user.extension-release.strict' attribute is true, ignoring file.",
×
158
                          extension_release_dir_path, filename);
159
                return true;
160
        }
161

162
        log_debug("%s/%s: 'user.extension-release.strict' attribute is false%s",
1,242✔
163
                  extension_release_dir_path, filename,
164
                  glyph(GLYPH_ELLIPSIS));
165

166
        return false;
167
}
168

169
int open_os_release_at(int rfd, char **ret_path, int *ret_fd) {
2,845✔
170
        const char *e;
2,845✔
171
        int r;
2,845✔
172

173
        assert(rfd >= 0 || IN_SET(rfd, AT_FDCWD, XAT_FDROOT));
2,845✔
174

175
        e = secure_getenv("SYSTEMD_OS_RELEASE");
2,845✔
176
        if (e)
2,845✔
177
                return chaseat(rfd, rfd, e, /* flags= */ 0, ret_path, ret_fd);
12✔
178

179
        FOREACH_STRING(path, "/etc/os-release", "/usr/lib/os-release") {
3,649✔
180
                r = chaseat(rfd, rfd, path, /* flags= */ 0, ret_path, ret_fd);
3,305✔
181
                if (r != -ENOENT)
3,305✔
182
                        return r;
2,489✔
183
        }
184

185
        return -ENOENT;
344✔
186
}
187

188
int open_os_release(const char *root, char **ret_path, int *ret_fd) {
39✔
189
        _cleanup_close_ int rfd = XAT_FDROOT, fd = -EBADF;
39✔
190
        _cleanup_free_ char *p = NULL;
39✔
191
        int r;
39✔
192

193
        if (!empty_or_root(root)) {
39✔
194
                rfd = open(root, O_CLOEXEC | O_DIRECTORY | O_PATH);
×
195
                if (rfd < 0)
×
196
                        return -errno;
×
197
        }
198

199
        r = open_os_release_at(rfd, ret_path ? &p : NULL, ret_fd ? &fd : NULL);
78✔
200
        if (r < 0)
39✔
201
                return r;
202

203
        if (ret_path) {
35✔
204
                r = chaseat_prefix_root(p, root, ret_path);
×
205
                if (r < 0)
×
206
                        return r;
207
        }
208

209
        if (ret_fd)
35✔
210
                *ret_fd = TAKE_FD(fd);
35✔
211

212
        return 0;
213
}
214

215
int open_extension_release_at(
3,740✔
216
                int rfd,
217
                ImageClass image_class,
218
                const char *extension,
219
                bool relax_extension_release_check,
220
                char **ret_path,
221
                int *ret_fd) {
222

223
        _cleanup_free_ char *dir_path = NULL, *path_found = NULL;
3,740✔
224
        _cleanup_close_ int fd_found = -EBADF;
3,740✔
225
        _cleanup_closedir_ DIR *dir = NULL;
3,740✔
226
        bool found = false;
3,740✔
227
        const char *p;
3,740✔
228
        int r;
3,740✔
229

230
        assert(rfd >= 0 || IN_SET(rfd, AT_FDCWD, XAT_FDROOT));
3,740✔
231
        assert(!extension || (image_class >= 0 && image_class < _IMAGE_CLASS_MAX));
3,740✔
232

233
        if (!extension)
3,740✔
234
                return open_os_release_at(rfd, ret_path, ret_fd);
1,267✔
235

236
        if (!IN_SET(image_class, IMAGE_SYSEXT, IMAGE_CONFEXT))
2,473✔
237
                return -EINVAL;
238

239
        if (!image_name_is_valid(extension))
2,473✔
240
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension);
×
241

242
        p = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension);
12,365✔
243
        r = chaseat(rfd, rfd, p, /* flags= */ 0, ret_path, ret_fd);
2,473✔
244
        log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", p);
2,473✔
245
        if (r != -ENOENT)
2,473✔
246
                return r;
247

248
        /* Cannot find the expected extension-release file? The image filename might have been mangled on
249
         * deployment, so fallback to checking for any file in the extension-release.d directory, and return
250
         * the first one with a user.extension-release xattr instead. The user.extension-release.strict
251
         * xattr is checked to ensure the author of the image considers it OK if names do not match. */
252

253
        p = image_class_release_info[image_class].release_file_directory;
1,075✔
254
        r = chase_and_opendirat(rfd, rfd, p, /* chase_flags= */ 0, &dir_path, &dir);
1,075✔
255
        if (r < 0)
1,075✔
256
                return log_debug_errno(r, "Cannot open %s, ignoring: %m", p);
434✔
257

258
        FOREACH_DIRENT(de, dir, return -errno) {
2,628✔
259
                _cleanup_close_ int fd = -EBADF;
5,727✔
260
                const char *image_name;
705✔
261

262
                if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN))
705✔
263
                        continue;
×
264

265
                image_name = startswith(de->d_name, "extension-release.");
705✔
266
                if (!image_name)
705✔
267
                        continue;
×
268

269
                if (!image_name_is_valid(image_name)) {
705✔
270
                        log_debug("%s/%s is not a valid release file name, ignoring.", dir_path, de->d_name);
×
271
                        continue;
×
272
                }
273

274
                /* We already chased the directory, and checked that this is a real file, so we shouldn't
275
                 * fail to open it. */
276
                fd = openat(dirfd(dir), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW);
705✔
277
                if (fd < 0)
705✔
278
                        return log_debug_errno(errno, "Failed to open release file %s/%s: %m", dir_path, de->d_name);
×
279

280
                /* Really ensure it is a regular file after we open it. */
281
                r = fd_verify_regular(fd);
705✔
282
                if (r < 0) {
705✔
283
                        log_debug_errno(r, "%s/%s is not a regular file, ignoring: %m", dir_path, de->d_name);
×
284
                        continue;
×
285
                }
286

287
                if (!relax_extension_release_check) {
705✔
288
                        _cleanup_free_ char *base_extension = NULL;
693✔
289

290
                        r = path_extract_image_name(extension, &base_extension);
693✔
291
                        if (r < 0) {
693✔
292
                                log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", extension);
×
293
                                continue;
×
294
                        }
295

296
                        if (!streq(image_name, base_extension) &&
1,378✔
297
                            extension_release_strict_xattr_value(fd, dir_path, image_name) != 0)
685✔
298
                                continue;
64✔
299
                }
300

301
                /* We already found what we were looking for, but there's another candidate? We treat this as
302
                 * an error, as we want to enforce that there are no ambiguities in case we are in the
303
                 * fallback path. */
304
                if (found)
641✔
305
                        return -ENOTUNIQ;
306

307
                found = true;
641✔
308

309
                if (ret_fd)
641✔
310
                        fd_found = TAKE_FD(fd);
613✔
311

312
                if (ret_path) {
641✔
313
                        path_found = path_join(dir_path, de->d_name);
612✔
314
                        if (!path_found)
612✔
315
                                return -ENOMEM;
316
                }
317
        }
318
        if (!found)
641✔
319
                return -ENOENT;
320

321
        if (ret_fd)
641✔
322
                *ret_fd = TAKE_FD(fd_found);
613✔
323
        if (ret_path)
641✔
324
                *ret_path = TAKE_PTR(path_found);
612✔
325

326
        return 0;
327
}
328

329
int open_extension_release(
1,954✔
330
                const char *root,
331
                ImageClass image_class,
332
                const char *extension,
333
                bool relax_extension_release_check,
334
                char **ret_path,
335
                int *ret_fd) {
336

337
        _cleanup_close_ int rfd = XAT_FDROOT, fd = -EBADF;
1,954✔
338
        _cleanup_free_ char *p = NULL;
1,954✔
339
        int r;
1,954✔
340

341
        if (!empty_or_root(root)) {
1,954✔
342
                rfd = open(root, O_CLOEXEC | O_DIRECTORY | O_PATH);
1,348✔
343
                if (rfd < 0)
1,348✔
344
                        return -errno;
×
345
        }
346

347
        r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check,
2,890✔
348
                                      ret_path ? &p : NULL, ret_fd ? &fd : NULL);
349
        if (r < 0)
1,954✔
350
                return r;
351

352
        if (ret_path) {
1,267✔
353
                r = chaseat_prefix_root(p, root, ret_path);
886✔
354
                if (r < 0)
886✔
355
                        return r;
356
        }
357

358
        if (ret_fd)
1,267✔
359
                *ret_fd = TAKE_FD(fd);
933✔
360

361
        return 0;
362
}
363

364
static int parse_extension_release_atv(
424✔
365
                int rfd,
366
                ImageClass image_class,
367
                const char *extension,
368
                bool relax_extension_release_check,
369
                va_list ap) {
370

371
        _cleanup_close_ int fd = -EBADF;
424✔
372
        _cleanup_free_ char *p = NULL;
424✔
373
        int r;
424✔
374

375
        assert(rfd >= 0 || IN_SET(rfd, AT_FDCWD, XAT_FDROOT));
424✔
376

377
        r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check, &p, &fd);
424✔
378
        if (r < 0)
424✔
379
                return r;
380

381
        return parse_env_file_fdv(fd, p, ap);
417✔
382
}
383

384
int parse_extension_release_at_sentinel(
8✔
385
                int rfd,
386
                ImageClass image_class,
387
                bool relax_extension_release_check,
388
                const char *extension,
389
                ...) {
390

391
        va_list ap;
8✔
392
        int r;
8✔
393

394
        assert(rfd >= 0 || IN_SET(rfd, AT_FDCWD, XAT_FDROOT));
8✔
395

396
        va_start(ap, extension);
8✔
397
        r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap);
8✔
398
        va_end(ap);
8✔
399
        return r;
8✔
400
}
401

402
int parse_extension_release_sentinel(
416✔
403
                const char *root,
404
                ImageClass image_class,
405
                bool relax_extension_release_check,
406
                const char *extension,
407
                ...) {
408

409
        _cleanup_close_ int rfd = XAT_FDROOT;
416✔
410
        va_list ap;
416✔
411
        int r;
416✔
412

413
        if (!empty_or_root(root)) {
416✔
414
                rfd = open(root, O_CLOEXEC | O_DIRECTORY | O_PATH);
90✔
415
                if (rfd < 0)
90✔
416
                        return -errno;
×
417
        }
418

419
        va_start(ap, extension);
416✔
420
        r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap);
416✔
421
        va_end(ap);
416✔
422

423
        return r;
416✔
424
}
425

426
int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char ***ret) {
259✔
427
        _cleanup_strv_free_ char **os_release_pairs = NULL, **os_release_pairs_prefixed = NULL;
259✔
428
        int r;
259✔
429

430
        r = load_os_release_pairs(root, &os_release_pairs);
259✔
431
        if (r < 0)
259✔
432
                return r;
433

434
        STRV_FOREACH_PAIR(p, q, os_release_pairs) {
3,108✔
435
                char *line;
2,849✔
436

437
                /* We strictly return only the four main ID fields and ignore the rest */
438
                if (!STR_IN_SET(*p, "ID", "VERSION_ID", "BUILD_ID", "VARIANT_ID"))
2,849✔
439
                        continue;
2,331✔
440

441
                ascii_strlower(*p);
518✔
442
                line = strjoin(prefix, *p, "=", *q);
518✔
443
                if (!line)
518✔
444
                        return -ENOMEM;
445
                r = strv_consume(&os_release_pairs_prefixed, line);
518✔
446
                if (r < 0)
518✔
447
                        return r;
448
        }
449

450
        *ret = TAKE_PTR(os_release_pairs_prefixed);
259✔
451

452
        return 0;
259✔
453
}
454

455
int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) {
1,409✔
456
        _cleanup_close_ int fd = -EBADF;
1,409✔
457
        _cleanup_free_ char *p = NULL;
1,409✔
458
        int r;
1,409✔
459

460
        r = open_extension_release(root, image_class, extension, relax_extension_release_check, &p, &fd);
1,409✔
461
        if (r < 0)
1,409✔
462
                return r;
463

464
        return load_env_file_pairs_fd(fd, p, ret);
886✔
465
}
466

467
int os_release_support_ended(const char *support_end, bool quiet, usec_t *ret_eol) {
62✔
468
        _cleanup_free_ char *_support_end_alloc = NULL;
62✔
469
        int r;
62✔
470

471
        if (!support_end) {
62✔
472
                /* If the caller has the variably handy, they can pass it in. If not, we'll read it
473
                 * ourselves. */
474

475
                r = parse_os_release(NULL,
59✔
476
                                     "SUPPORT_END", &_support_end_alloc);
477
                if (r < 0 && r != -ENOENT)
59✔
478
                        return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r,
×
479
                                              "Failed to read os-release file, ignoring: %m");
480

481
                support_end = _support_end_alloc;
59✔
482
        }
483

484
        if (isempty(support_end)) { /* An empty string is a explicit way to say "no EOL exists" */
62✔
485
                if (ret_eol)
59✔
486
                        *ret_eol = USEC_INFINITY;
×
487

488
                return false;  /* no end date defined */
489
        }
490

491
        struct tm tm = {};
3✔
492
        const char *k = strptime(support_end, "%Y-%m-%d", &tm);
3✔
493
        if (!k || *k)
3✔
494
                return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, SYNTHETIC_ERRNO(EINVAL),
×
495
                                      "Failed to parse SUPPORT_END= from os-release file, ignoring: %s", support_end);
496

497
        usec_t eol;
3✔
498
        r = mktime_or_timegm_usec(&tm, /* utc= */ true, &eol);
3✔
499
        if (r < 0)
3✔
500
                return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r,
1✔
501
                                      "Failed to convert SUPPORT_END= time from os-release file, ignoring: %m");
502

503
        if (ret_eol)
2✔
504
                *ret_eol = eol;
×
505

506
        return now(CLOCK_REALTIME) > eol;
2✔
507
}
508

509
const char* os_release_pretty_name(const char *pretty_name, const char *name) {
53✔
510
        /* Distills a "pretty" name to show from os-release data. First argument is supposed to be the
511
         * PRETTY_NAME= field, the second one the NAME= field. This function is trivial, of course, and
512
         * exists mostly to ensure we use the same logic wherever possible. */
513

514
        return empty_to_null(pretty_name) ?:
53✔
515
                empty_to_null(name) ?: "Linux";
6✔
516
}
517

518
char *unescape_fancy_name(char **fancy_name) {
14✔
519
        assert(fancy_name);
14✔
520

521
        /* Checks if the fancy name is valid, unescapes if it is, nullifies it if not */
522

523
        _cleanup_free_ char *unescaped_fancy_name = NULL;
14✔
524

525
        if (isempty(*fancy_name))
14✔
526
                goto clear;
14✔
527

528
        /* We undo one level of C escapes on this */
NEW
529
        ssize_t n = cunescape(*fancy_name, /* flags= */ 0, &unescaped_fancy_name);
×
NEW
530
        if (n < 0) {
×
NEW
531
                log_debug_errno((int) n, "Failed to unescape FANCY_NAME= string, suppressing: %m");
×
NEW
532
                goto clear;
×
533
        }
534

NEW
535
        if (!utf8_is_valid(unescaped_fancy_name)) {
×
NEW
536
                log_debug("Unescaped FANCY_NAME= string is not valid UTF-8, suppressing.");
×
NEW
537
                goto clear;
×
538
        }
539

NEW
540
        free_and_replace(*fancy_name, unescaped_fancy_name);
×
NEW
541
        return *fancy_name;
×
542

543
clear:
14✔
544
        *fancy_name = mfree(*fancy_name);
14✔
545
        return NULL;
14✔
546
}
547

548
bool use_fancy_name(const char *fancy_name) {
12✔
549

550
        /* Decides whether to show the specified fancy name */
551

552
        if (isempty(fancy_name))
12✔
553
                return false;
554

555
        if (!colors_enabled())
6✔
556
                return false;
557

NEW
558
        return emoji_enabled() || ascii_is_valid(fancy_name);
×
559
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc