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

systemd / systemd / 19349812769

13 Nov 2025 10:50PM UTC coverage: 72.393% (+0.1%) from 72.251%
19349812769

push

github

yuwata
sd-dhcp-server: Add Hostname= option to static leases

This adds a new `Hostname=` option to the [DHCPServerStaticLease]
section in .network files, allowing an administrator to assign a
specific hostname to a client receiving a static lease.

We automatically select the correct DHCP option to use based on the
format of the provided string:

- Single DNS labels are sent as Option 12.
- Names with multiple DNS labels are sent as Option 81 in wire format.

Fixes: #39634

90 of 109 new or added lines in 8 files covered. (82.57%)

1896 existing lines in 46 files now uncovered.

307159 of 424291 relevant lines covered (72.39%)

1105072.08 hits per line

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

90.39
/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 "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);
229✔
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) {
24,760✔
49
        if (!filename_is_valid(s))
24,760✔
50
                return false;
51

52
        if (string_has_cc(s, NULL))
19,575✔
53
                return false;
54

55
        if (!utf8_is_valid(s))
19,575✔
56
                return false;
57

58
        /* Temporary files for atomically creating new files */
59
        if (startswith(s, ".#"))
19,575✔
60
                return false;
1✔
61

62
        return true;
63
}
64

65
int path_extract_image_name(const char *path, char **ret) {
6,778✔
66
        _cleanup_free_ char *fn = NULL;
6,778✔
67
        int r;
6,778✔
68

69
        assert(path);
6,778✔
70
        assert(ret);
6,778✔
71

72
        /* Extract last component from path, without any "/" suffixes. */
73
        r = path_extract_filename(path, &fn);
6,778✔
74
        if (r < 0)
6,778✔
75
                return r;
76
        if (r != O_DIRECTORY) {
6,778✔
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");
6,778✔
79
                if (m)
6,778✔
80
                        *m = 0;
3,339✔
81
        }
82

83
        /* Truncate the version/counting suffixes */
84
        fn[strcspn(fn, "_+")] = 0;
6,778✔
85

86
        if (!image_name_is_valid(fn))
6,778✔
87
                return -EINVAL;
88

89
        *ret = TAKE_PTR(fn);
6,778✔
90
        return 0;
6,778✔
91
}
92

93
int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) {
245✔
94
        int r;
245✔
95

96
        assert(path);
245✔
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);
245✔
102
        if (r < 0)
245✔
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);
244✔
109
        if (r == -ENOENT) /* We got nothing */
244✔
110
                return 0;
111
        if (r < 0)
219✔
112
                return r;
×
113

114
        return 1;
115
}
116

117
int fd_is_os_tree(int fd) {
31✔
118
        int r;
31✔
119

120
        assert(fd >= 0);
31✔
121

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

134
        return true;
135
}
136

137
static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) {
579✔
138
        int r;
579✔
139

140
        assert(extension_release_fd >= 0);
579✔
141
        assert(extension_release_dir_path);
579✔
142
        assert(filename);
579✔
143

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

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

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

164
        return false;
165
}
166

167
int open_os_release_at(int rfd, char **ret_path, int *ret_fd) {
2,259✔
168
        const char *e;
2,259✔
169
        int r;
2,259✔
170

171
        assert(rfd >= 0 || rfd == AT_FDCWD);
2,259✔
172

173
        e = secure_getenv("SYSTEMD_OS_RELEASE");
2,259✔
174
        if (e)
2,259✔
175
                return chaseat(rfd, e, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
12✔
176

177
        FOREACH_STRING(path, "/etc/os-release", "/usr/lib/os-release") {
2,792✔
178
                r = chaseat(rfd, path, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
2,567✔
179
                if (r != -ENOENT)
2,567✔
180
                        return r;
2,022✔
181
        }
182

183
        return -ENOENT;
225✔
184
}
185

186
int open_os_release(const char *root, char **ret_path, int *ret_fd) {
1,240✔
187
        _cleanup_close_ int rfd = -EBADF, fd = -EBADF;
1,240✔
188
        _cleanup_free_ char *p = NULL;
1,240✔
189
        int r;
1,240✔
190

191
        rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH);
1,240✔
192
        if (rfd < 0)
1,240✔
193
                return -errno;
×
194

195
        r = open_os_release_at(rfd, ret_path ? &p : NULL, ret_fd ? &fd : NULL);
1,273✔
196
        if (r < 0)
1,240✔
197
                return r;
198

199
        if (ret_path) {
1,236✔
200
                r = chaseat_prefix_root(p, root, ret_path);
1,207✔
201
                if (r < 0)
1,207✔
202
                        return r;
203
        }
204

205
        if (ret_fd)
1,236✔
206
                *ret_fd = TAKE_FD(fd);
1,236✔
207

208
        return 0;
209
}
210

211
int open_extension_release_at(
2,815✔
212
                int rfd,
213
                ImageClass image_class,
214
                const char *extension,
215
                bool relax_extension_release_check,
216
                char **ret_path,
217
                int *ret_fd) {
218

219
        _cleanup_free_ char *dir_path = NULL, *path_found = NULL;
2,815✔
220
        _cleanup_close_ int fd_found = -EBADF;
2,815✔
221
        _cleanup_closedir_ DIR *dir = NULL;
2,815✔
222
        bool found = false;
2,815✔
223
        const char *p;
2,815✔
224
        int r;
2,815✔
225

226
        assert(rfd >= 0 || rfd == AT_FDCWD);
2,815✔
227
        assert(!extension || (image_class >= 0 && image_class < _IMAGE_CLASS_MAX));
2,815✔
228

229
        if (!extension)
2,815✔
230
                return open_os_release_at(rfd, ret_path, ret_fd);
1,019✔
231

232
        if (!IN_SET(image_class, IMAGE_SYSEXT, IMAGE_CONFEXT))
1,796✔
233
                return -EINVAL;
234

235
        if (!image_name_is_valid(extension))
1,796✔
236
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "The extension name %s is invalid.", extension);
×
237

238
        p = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension);
8,980✔
239
        r = chaseat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, ret_path, ret_fd);
1,796✔
240
        log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", p);
1,796✔
241
        if (r != -ENOENT)
1,796✔
242
                return r;
243

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

249
        p = image_class_release_info[image_class].release_file_directory;
884✔
250
        r = chase_and_opendirat(rfd, p, CHASE_AT_RESOLVE_IN_ROOT, &dir_path, &dir);
884✔
251
        if (r < 0)
884✔
252
                return log_debug_errno(r, "Cannot open %s, ignoring: %m", p);
351✔
253

254
        FOREACH_DIRENT(de, dir, return -errno) {
2,188✔
255
                _cleanup_close_ int fd = -EBADF;
4,470✔
256
                const char *image_name;
589✔
257

258
                if (!IN_SET(de->d_type, DT_REG, DT_UNKNOWN))
589✔
259
                        continue;
×
260

261
                image_name = startswith(de->d_name, "extension-release.");
589✔
262
                if (!image_name)
589✔
263
                        continue;
×
264

265
                if (!image_name_is_valid(image_name)) {
589✔
266
                        log_debug("%s/%s is not a valid release file name, ignoring.", dir_path, de->d_name);
×
267
                        continue;
×
268
                }
269

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

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

283
                if (!relax_extension_release_check) {
589✔
284
                        _cleanup_free_ char *base_extension = NULL;
583✔
285

286
                        r = path_extract_image_name(extension, &base_extension);
583✔
287
                        if (r < 0) {
583✔
288
                                log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", extension);
×
289
                                continue;
×
290
                        }
291

292
                        if (!streq(image_name, base_extension) &&
1,162✔
293
                            extension_release_strict_xattr_value(fd, dir_path, image_name) != 0)
579✔
294
                                continue;
56✔
295
                }
296

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

303
                found = true;
533✔
304

305
                if (ret_fd)
533✔
306
                        fd_found = TAKE_FD(fd);
520✔
307

308
                if (ret_path) {
533✔
309
                        path_found = path_join(dir_path, de->d_name);
519✔
310
                        if (!path_found)
519✔
311
                                return -ENOMEM;
312
                }
313
        }
314
        if (!found)
533✔
315
                return -ENOENT;
316

317
        if (ret_fd)
533✔
318
                *ret_fd = TAKE_FD(fd_found);
520✔
319
        if (ret_path)
533✔
320
                *ret_path = TAKE_PTR(path_found);
519✔
321

322
        return 0;
323
}
324

325
int open_extension_release(
2,458✔
326
                const char *root,
327
                ImageClass image_class,
328
                const char *extension,
329
                bool relax_extension_release_check,
330
                char **ret_path,
331
                int *ret_fd) {
332

333
        _cleanup_close_ int rfd = -EBADF, fd = -EBADF;
2,458✔
334
        _cleanup_free_ char *p = NULL;
2,458✔
335
        int r;
2,458✔
336

337
        rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH);
2,458✔
338
        if (rfd < 0)
2,458✔
339
                return -errno;
×
340

341
        r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check,
3,032✔
342
                                      ret_path ? &p : NULL, ret_fd ? &fd : NULL);
343
        if (r < 0)
2,458✔
344
                return r;
345

346
        if (ret_path) {
1,917✔
347
                r = chaseat_prefix_root(p, root, ret_path);
1,680✔
348
                if (r < 0)
1,680✔
349
                        return r;
350
        }
351

352
        if (ret_fd)
1,917✔
353
                *ret_fd = TAKE_FD(fd);
1,698✔
354

355
        return 0;
356
}
357

358
static int parse_extension_release_atv(
326✔
359
                int rfd,
360
                ImageClass image_class,
361
                const char *extension,
362
                bool relax_extension_release_check,
363
                va_list ap) {
364

365
        _cleanup_close_ int fd = -EBADF;
326✔
366
        _cleanup_free_ char *p = NULL;
326✔
367
        int r;
326✔
368

369
        assert(rfd >= 0 || rfd == AT_FDCWD);
326✔
370

371
        r = open_extension_release_at(rfd, image_class, extension, relax_extension_release_check, &p, &fd);
326✔
372
        if (r < 0)
326✔
373
                return r;
374

375
        return parse_env_file_fdv(fd, p, ap);
319✔
376
}
377

378
int parse_extension_release_at_sentinel(
6✔
379
                int rfd,
380
                ImageClass image_class,
381
                bool relax_extension_release_check,
382
                const char *extension,
383
                ...) {
384

385
        va_list ap;
6✔
386
        int r;
6✔
387

388
        assert(rfd >= 0 || rfd == AT_FDCWD);
6✔
389

390
        va_start(ap, extension);
6✔
391
        r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap);
6✔
392
        va_end(ap);
6✔
393
        return r;
6✔
394
}
395

396
int parse_extension_release_sentinel(
320✔
397
                const char *root,
398
                ImageClass image_class,
399
                bool relax_extension_release_check,
400
                const char *extension,
401
                ...) {
402

403
        _cleanup_close_ int rfd = -EBADF;
320✔
404
        va_list ap;
320✔
405
        int r;
320✔
406

407
        rfd = open(empty_to_root(root), O_CLOEXEC | O_DIRECTORY | O_PATH);
320✔
408
        if (rfd < 0)
320✔
409
                return -errno;
×
410

411
        va_start(ap, extension);
320✔
412
        r = parse_extension_release_atv(rfd, image_class, extension, relax_extension_release_check, ap);
320✔
413
        va_end(ap);
320✔
414
        return r;
320✔
415
}
416

417
int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char ***ret) {
254✔
418
        _cleanup_strv_free_ char **os_release_pairs = NULL, **os_release_pairs_prefixed = NULL;
254✔
419
        int r;
254✔
420

421
        r = load_os_release_pairs(root, &os_release_pairs);
254✔
422
        if (r < 0)
254✔
423
                return r;
424

425
        STRV_FOREACH_PAIR(p, q, os_release_pairs) {
3,048✔
426
                char *line;
2,794✔
427

428
                /* We strictly return only the four main ID fields and ignore the rest */
429
                if (!STR_IN_SET(*p, "ID", "VERSION_ID", "BUILD_ID", "VARIANT_ID"))
2,794✔
430
                        continue;
2,286✔
431

432
                ascii_strlower(*p);
508✔
433
                line = strjoin(prefix, *p, "=", *q);
508✔
434
                if (!line)
508✔
435
                        return -ENOMEM;
436
                r = strv_consume(&os_release_pairs_prefixed, line);
508✔
437
                if (r < 0)
508✔
438
                        return r;
439
        }
440

441
        *ret = TAKE_PTR(os_release_pairs_prefixed);
254✔
442

443
        return 0;
254✔
444
}
445

446
int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) {
1,149✔
447
        _cleanup_close_ int fd = -EBADF;
1,149✔
448
        _cleanup_free_ char *p = NULL;
1,149✔
449
        int r;
1,149✔
450

451
        r = open_extension_release(root, image_class, extension, relax_extension_release_check, &p, &fd);
1,149✔
452
        if (r < 0)
1,149✔
453
                return r;
454

455
        return load_env_file_pairs_fd(fd, p, ret);
704✔
456
}
457

458
int os_release_support_ended(const char *support_end, bool quiet, usec_t *ret_eol) {
54✔
459
        _cleanup_free_ char *_support_end_alloc = NULL;
54✔
460
        int r;
54✔
461

462
        if (!support_end) {
54✔
463
                /* If the caller has the variably handy, they can pass it in. If not, we'll read it
464
                 * ourselves. */
465

466
                r = parse_os_release(NULL,
51✔
467
                                     "SUPPORT_END", &_support_end_alloc);
468
                if (r < 0 && r != -ENOENT)
51✔
469
                        return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r,
×
470
                                              "Failed to read os-release file, ignoring: %m");
471

472
                support_end = _support_end_alloc;
51✔
473
        }
474

475
        if (isempty(support_end)) { /* An empty string is a explicit way to say "no EOL exists" */
54✔
476
                if (ret_eol)
51✔
477
                        *ret_eol = USEC_INFINITY;
×
478

479
                return false;  /* no end date defined */
51✔
480
        }
481

482
        struct tm tm = {};
3✔
483
        const char *k = strptime(support_end, "%Y-%m-%d", &tm);
3✔
484
        if (!k || *k)
3✔
UNCOV
485
                return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, SYNTHETIC_ERRNO(EINVAL),
×
486
                                      "Failed to parse SUPPORT_END= from os-release file, ignoring: %s", support_end);
487

488
        usec_t eol;
3✔
489
        r = mktime_or_timegm_usec(&tm, /* utc= */ true, &eol);
3✔
490
        if (r < 0)
3✔
491
                return log_full_errno(quiet ? LOG_DEBUG : LOG_WARNING, r,
1✔
492
                                      "Failed to convert SUPPORT_END= time from os-release file, ignoring: %m");
493

494
        if (ret_eol)
2✔
495
                *ret_eol = eol;
×
496

497
        return now(CLOCK_REALTIME) > eol;
2✔
498
}
499

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

505
        return empty_to_null(pretty_name) ?:
51✔
506
                empty_to_null(name) ?: "Linux";
6✔
507
}
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