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

systemd / systemd / 28760256242

04 Jul 2026 01:10PM UTC coverage: 72.813% (-0.09%) from 72.9%
28760256242

push

github

bluca
hwdb: Make Amlogic burn mode work out-of-box

342890 of 470919 relevant lines covered (72.81%)

1341914.37 hits per line

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

69.72
/src/bootctl/bootctl-link.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <fcntl.h>
4
#include <unistd.h>
5

6
#include "sd-json.h"
7
#include "sd-varlink.h"
8

9
#include "boot-entry.h"
10
#include "bootctl.h"
11
#include "bootctl-link.h"
12
#include "bootctl-unlink.h"
13
#include "bootspec.h"
14
#include "bootspec-util.h"
15
#include "chase.h"
16
#include "copy.h"
17
#include "dirent-util.h"
18
#include "efi-loader.h"
19
#include "env-file.h"
20
#include "errno-util.h"
21
#include "fd-util.h"
22
#include "find-esp.h"
23
#include "format-util.h"
24
#include "fs-util.h"
25
#include "hashmap.h"
26
#include "id128-util.h"
27
#include "io-util.h"
28
#include "iovec-util.h"
29
#include "json-util.h"
30
#include "kernel-image.h"
31
#include "log.h"
32
#include "parse-argument.h"
33
#include "path-util.h"
34
#include "recurse-dir.h"
35
#include "set.h"
36
#include "stat-util.h"
37
#include "stdio-util.h"
38
#include "string-util.h"
39
#include "strv.h"
40
#include "tmpfile-util.h"
41
#include "uki.h"
42
#include "utf8.h"
43
#include "varlink-util.h"
44
#include "vpick.h"
45

46
/* Keeps track of an "extra" file to associate with the type 1 entries to generate */
47
typedef struct ExtraFile {
48
        /* The source and the temporary file we copy it into */
49
        int source_fd, temp_fd;
50
        char *filename, *temp_filename;
51
        struct iovec data; /* Alternative to 'source_fd': literal data */
52
} ExtraFile;
53

54
#define EXTRA_FILE_NULL                     \
55
        (const ExtraFile) {                 \
56
                .source_fd = -EBADF,        \
57
                .temp_fd = -EBADF,          \
58
        }
59

60
/* Keeps track of a specific UKI profile we need to generate a type entry for */
61
typedef struct Profile {
62
        /* The final and the temporary file for the .conf entry file, while we write it */
63
        char *entry_filename, *entry_temp_filename;
64
        int entry_temp_fd;
65
} Profile;
66

67
typedef struct LinkContext {
68
        char *root;
69
        int root_fd;
70

71
        sd_id128_t machine_id;
72
        BootEntryTokenType entry_token_type;
73
        char *entry_token;
74

75
        char *entry_title;
76
        char *entry_version;
77
        uint64_t entry_commit;
78

79
        BootEntrySource dollar_boot_source;
80
        char *dollar_boot_path;
81
        int dollar_boot_fd;
82
        int entry_token_dir_fd;
83
        int loader_entries_dir_fd;
84

85
        /* The UKI source and temporary target while we write it. Note that for now we exclusively support
86
         * UKIs, but let's keep things somewhat generic to keep options open for the future. */
87
        char *kernel_filename, *kernel_temp_filename;
88
        int kernel_fd, kernel_temp_fd;
89

90
        ExtraFile *extra;
91
        size_t n_extra;
92

93
        Profile *profiles;
94
        size_t n_profiles;
95

96
        unsigned tries_left;
97

98
        uint64_t keep_free;
99

100
        char **linked_ids;
101
} LinkContext;
102

103
#define LINK_CONTEXT_NULL                                               \
104
        (LinkContext) {                                                 \
105
                .root_fd = -EBADF,                                      \
106
                .entry_token_type = _BOOT_ENTRY_TOKEN_TYPE_INVALID,     \
107
                .dollar_boot_fd = -EBADF,                               \
108
                .loader_entries_dir_fd = -EBADF,                        \
109
                .entry_token_dir_fd = -EBADF,                           \
110
                .kernel_fd = -EBADF,                                    \
111
                .kernel_temp_fd = -EBADF,                               \
112
                .tries_left = UINT_MAX,                                 \
113
                .keep_free = UINT64_MAX,                                \
114
        }
115

116
static void extra_file_done(ExtraFile *x) {
8✔
117
        assert(x);
8✔
118

119
        x->source_fd = safe_close(x->source_fd);
8✔
120
        x->temp_fd = safe_close(x->temp_fd);
8✔
121
        x->filename = mfree(x->filename);
8✔
122
        x->temp_filename = mfree(x->temp_filename);
8✔
123
        iovec_done(&x->data);
8✔
124
}
8✔
125

126
static void profile_done(Profile *p) {
11✔
127
        assert(p);
11✔
128

129
        p->entry_filename = mfree(p->entry_filename);
11✔
130
        p->entry_temp_filename = mfree(p->entry_temp_filename);
11✔
131
        p->entry_temp_fd = safe_close(p->entry_temp_fd);
11✔
132
}
11✔
133

134
static void link_context_unlink_temporary(LinkContext *c) {
26✔
135
        assert(c);
26✔
136

137
        if (c->kernel_temp_filename) {
26✔
138
                if (c->entry_token_dir_fd >= 0)
×
139
                        (void) unlinkat(c->entry_token_dir_fd, c->kernel_temp_filename, /* flags= */ 0);
×
140

141
                c->kernel_temp_fd = safe_close(c->kernel_temp_fd);
×
142
                c->kernel_temp_filename = mfree(c->kernel_temp_filename);
×
143
        }
144

145
        FOREACH_ARRAY(x, c->extra, c->n_extra) {
34✔
146
                if (!x->temp_filename)
8✔
147
                        continue;
8✔
148

149
                if (c->entry_token_dir_fd >= 0)
×
150
                        (void) unlinkat(c->entry_token_dir_fd, x->temp_filename, /* flags= */ 0);
×
151

152
                x->temp_fd = safe_close(x->temp_fd);
×
153
                x->temp_filename = mfree(x->temp_filename);
×
154
        }
155

156
        FOREACH_ARRAY(p, c->profiles, c->n_profiles) {
37✔
157
                if (!p->entry_temp_filename)
11✔
158
                        continue;
11✔
159

160
                if (c->loader_entries_dir_fd >= 0)
×
161
                        (void) unlinkat(c->loader_entries_dir_fd, p->entry_temp_filename, /* flags= */ 0);
×
162

163
                p->entry_temp_fd = safe_close(p->entry_temp_fd);
×
164
                p->entry_temp_filename = mfree(p->entry_temp_filename);
×
165
        }
166
}
26✔
167

168
static void link_context_clear_profiles(LinkContext *c) {
26✔
169
        assert(c);
26✔
170

171
        FOREACH_ARRAY(p, c->profiles, c->n_profiles)
37✔
172
                profile_done(p);
11✔
173

174
        c->profiles = mfree(c->profiles);
26✔
175
        c->n_profiles = 0;
26✔
176
}
26✔
177

178
static void link_context_done(LinkContext *c) {
25✔
179
        assert(c);
25✔
180

181
        link_context_unlink_temporary(c);
25✔
182

183
        FOREACH_ARRAY(x, c->extra, c->n_extra)
33✔
184
                extra_file_done(x);
8✔
185

186
        c->extra = mfree(c->extra);
25✔
187
        c->n_extra = 0;
25✔
188

189
        link_context_clear_profiles(c);
25✔
190

191
        c->kernel_filename = mfree(c->kernel_filename);
25✔
192
        c->kernel_fd = safe_close(c->kernel_fd);
25✔
193
        c->kernel_temp_filename = mfree(c->kernel_temp_filename);
25✔
194
        c->kernel_temp_fd = safe_close(c->kernel_temp_fd);
25✔
195

196
        c->root = mfree(c->root);
25✔
197
        c->root_fd = safe_close(c->root_fd);
25✔
198

199
        c->entry_token = mfree(c->entry_token);
25✔
200
        c->entry_title = mfree(c->entry_title);
25✔
201
        c->entry_version = mfree(c->entry_version);
25✔
202

203
        c->dollar_boot_path = mfree(c->dollar_boot_path);
25✔
204
        c->dollar_boot_fd = safe_close(c->dollar_boot_fd);
25✔
205
        c->entry_token_dir_fd = safe_close(c->entry_token_dir_fd);
25✔
206
        c->loader_entries_dir_fd = safe_close(c->loader_entries_dir_fd);
25✔
207

208
        c->linked_ids = strv_free(c->linked_ids);
25✔
209
}
25✔
210

211
static int link_context_acquire_dollar_boot(LinkContext *b) {
12✔
212
        int r;
12✔
213

214
        assert(b);
12✔
215

216
        r = acquire_xbootldr(
12✔
217
                        /* unprivileged_mode= */ false,
218
                        &b->dollar_boot_fd,
219
                        /* ret_uuid= */ NULL,
220
                        /* ret_devid= */ NULL);
221
        if (r < 0)
12✔
222
                return r;
223
        if (r > 0) { /* XBOOTLDR has been found */
12✔
224
                assert(arg_xbootldr_path);
×
225

226
                if (arg_root) {
×
227
                        const char *e = path_startswith(arg_xbootldr_path, arg_root);
×
228
                        if (!e)
×
229
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "XBOOTLDR path '%s' not below specified root '%s', refusing.", arg_xbootldr_path, arg_root);
×
230

231
                        r = strdup_to(&b->dollar_boot_path, e);
×
232
                } else
233
                        r = strdup_to(&b->dollar_boot_path, arg_xbootldr_path);
×
234
                if (r < 0)
×
235
                        return log_oom();
×
236

237
                b->dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
×
238
        } else {
239
                /* No XBOOTLDR has been found, look for ESP */
240

241
                r = acquire_esp(/* unprivileged_mode= */ false,
12✔
242
                                /* graceful= */ false,
243
                                &b->dollar_boot_fd,
244
                                /* ret_part= */ NULL,
245
                                /* ret_pstart= */ NULL,
246
                                /* ret_psize= */ NULL,
247
                                /* ret_uuid= */ NULL,
248
                                /* ret_devid= */ NULL);
249
                if (r < 0)
12✔
250
                        return r;
251

252
                assert(arg_esp_path);
12✔
253

254
                if (arg_root) {
12✔
255
                        const char *e = path_startswith(arg_esp_path, arg_root);
×
256
                        if (!e)
×
257
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ESP path '%s' not below specified root '%s', refusing.", arg_esp_path, arg_root);
×
258

259
                        r = strdup_to(&b->dollar_boot_path, e);
×
260
                } else
261
                        r = strdup_to(&b->dollar_boot_path, arg_esp_path);
12✔
262
                if (r < 0)
12✔
263
                        return log_oom();
×
264

265
                b->dollar_boot_source = BOOT_ENTRY_ESP;
12✔
266
        }
267

268
        return 0;
269
}
270

271
/* Fills the bits of a LinkContext that both "bootctl link" and "bootctl link-auto" derive from the global
272
 * command line options (everything except the kernel image and extra files themselves). */
273
static int link_context_from_cmdline_common(LinkContext *b) {
14✔
274
        assert(b);
14✔
275

276
        b->entry_token_type = arg_entry_token_type;
14✔
277
        b->tries_left = arg_tries_left;
14✔
278
        b->entry_commit = arg_entry_commit;
14✔
279
        b->keep_free = arg_keep_free;
14✔
280

281
        if (strdup_to(&b->entry_token, arg_entry_token) < 0 ||
28✔
282
            strdup_to(&b->entry_title, arg_entry_title) < 0 ||
28✔
283
            strdup_to(&b->entry_version, arg_entry_version) < 0)
14✔
284
                return log_oom();
×
285

286
        if (arg_root) {
14✔
287
                b->root_fd = open(arg_root, O_CLOEXEC|O_DIRECTORY|O_PATH);
×
288
                if (b->root_fd < 0)
×
289
                        return log_error_errno(errno, "Failed to open root directory '%s': %m", arg_root);
×
290

291
                if (strdup_to(&b->root, arg_root) < 0)
×
292
                        return log_oom();
×
293
        } else
294
                b->root_fd = XAT_FDROOT;
14✔
295

296
        return 0;
297
}
298

299
/* Appends the files passed via --extra= on the command line as extra resources to the link context. */
300
static int link_context_add_cmdline_extras(LinkContext *b) {
12✔
301
        int r;
12✔
302

303
        assert(b);
12✔
304

305
        STRV_FOREACH(x, arg_extras) {
15✔
306
                _cleanup_free_ char *fn = NULL;
3✔
307
                r = path_extract_filename(*x, &fn);
3✔
308
                if (r < 0)
3✔
309
                        return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x);
×
310
                if (r == O_DIRECTORY)
3✔
311
                        return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x);
×
312

313
                _cleanup_close_ int fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
6✔
314
                if (fd < 0)
3✔
315
                        return log_error_errno(fd, "Failed to open '%s': %m", *x);
×
316

317
                if (!GREEDY_REALLOC(b->extra, b->n_extra+1))
3✔
318
                        return log_oom();
×
319

320
                b->extra[b->n_extra++] = (ExtraFile) {
3✔
321
                        .source_fd = TAKE_FD(fd),
3✔
322
                        .filename = TAKE_PTR(fn),
3✔
323
                        .temp_fd = -EBADF,
324
                };
325
        }
326

327
        return 0;
328
}
329

330
static int validate_kernel(int kernel_fd, const char *filename) {
13✔
331
        int r;
13✔
332

333
        assert(kernel_fd >= 0);
13✔
334

335
        KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
13✔
336
        r = inspect_kernel(kernel_fd, /* filename= */ NULL, &kit);
13✔
337
        if (r == -EBADMSG)
13✔
338
                return log_error_errno(r, "UKI '%s' is not valid.", filename);
×
339
        if (r < 0)
13✔
340
                return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", filename);
×
341
        if (kit != KERNEL_IMAGE_TYPE_UKI)
13✔
342
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image '%s' is not a UKI.", filename);
1✔
343

344
        return 0;
345
}
346

347
static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
10✔
348
        int r;
10✔
349

350
        assert(ret);
10✔
351
        assert(kernel);
10✔
352

353
        _cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL;
10✔
354
        r = link_context_from_cmdline_common(&b);
10✔
355
        if (r < 0)
10✔
356
                return r;
357

358
        r = path_extract_filename(kernel, &b.kernel_filename);
10✔
359
        if (r < 0)
10✔
360
                return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel);
×
361
        if (!efi_loader_entry_resource_filename_valid(b.kernel_filename))
10✔
362
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel);
×
363
        b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
10✔
364
        if (b.kernel_fd < 0)
10✔
365
                return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel);
×
366

367
        r = validate_kernel(b.kernel_fd, kernel);
10✔
368
        if (r < 0)
10✔
369
                return r;
370

371
        r = link_context_add_cmdline_extras(&b);
9✔
372
        if (r < 0)
9✔
373
                return r;
374

375
        r = link_context_acquire_dollar_boot(&b);
9✔
376
        if (r < 0)
9✔
377
                return r;
378

379
        *ret = TAKE_GENERIC(b, LinkContext, LINK_CONTEXT_NULL);
9✔
380
        return 0;
9✔
381
}
382

383
static int link_context_load_etc_machine_id(LinkContext *c) {
13✔
384
        int r;
13✔
385

386
        assert(c);
13✔
387

388
        r = id128_get_machine_at(c->root_fd, &c->machine_id);
13✔
389
        if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) /* Not set or empty */
13✔
390
                return 0;
391
        if (r < 0)
13✔
392
                return log_error_errno(r, "Failed to get machine-id: %m");
×
393

394
        log_debug("Loaded machine ID %s from '%s/etc/machine-id'.", SD_ID128_TO_STRING(c->machine_id), strempty(c->root));
13✔
395
        return 0;
13✔
396
}
397

398
static int link_context_pick_entry_token(LinkContext *c) {
13✔
399
        int r;
13✔
400

401
        assert(c);
13✔
402

403
        r = link_context_load_etc_machine_id(c);
13✔
404
        if (r < 0)
13✔
405
                return r;
406

407
        const char *e = secure_getenv("KERNEL_INSTALL_CONF_ROOT");
13✔
408
        r = boot_entry_token_ensure_at(
13✔
409
                        e ? XAT_FDROOT : c->root_fd,
410
                        e,
411
                        c->machine_id,
412
                        /* machine_id_is_random= */ false,
413
                        &c->entry_token_type,
414
                        &c->entry_token);
415
        if (r < 0)
13✔
416
                return r;
417

418
        log_debug("Using entry token: %s", c->entry_token);
13✔
419
        return 0;
420
}
421

422
static int begin_copy_file(
19✔
423
                int source_fd,               /* Either the source fd is specified, or the 'data' below, not both */
424
                const struct iovec *data,
425
                const char *filename,
426
                int target_dir_fd,
427
                int *ret_tmpfile_fd,
428
                char **ret_tmpfile_filename) {
429

430
        int r;
19✔
431

432
        assert(filename);
19✔
433
        assert(target_dir_fd >= 0);
19✔
434
        assert(ret_tmpfile_fd);
19✔
435
        assert(ret_tmpfile_filename);
19✔
436

437
        if (faccessat(target_dir_fd, filename, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
19✔
438
                if (errno != ENOENT)
17✔
439
                        return log_error_errno(errno, "Failed to check if '%s' exists already: %m", filename);
19✔
440
        } else {
441
                log_info("'%s' already in place, not copying.", filename);
2✔
442

443
                *ret_tmpfile_fd = -EBADF;
2✔
444
                *ret_tmpfile_filename = NULL;
2✔
445
                return 0;
2✔
446
        }
447

448
        _cleanup_free_ char *t = NULL;
17✔
449
        _cleanup_close_ int write_fd = open_tmpfile_linkable_at(target_dir_fd, filename, O_WRONLY|O_CLOEXEC, &t);
34✔
450
        if (write_fd < 0)
17✔
451
                return log_error_errno(write_fd, "Failed to create '%s': %m", filename);
×
452

453
        CLEANUP_TMPFILE_AT(target_dir_fd, t);
17✔
454

455
        if (source_fd >= 0) {
17✔
456
                r = copy_bytes(source_fd, write_fd, UINT64_MAX, COPY_REFLINK|COPY_SEEK0_SOURCE);
17✔
457
                if (r < 0)
17✔
458
                        return log_error_errno(r, "Failed to copy data into '%s': %m", filename);
×
459

460
                (void) copy_times(source_fd, write_fd, /* flags= */ 0);
17✔
461
        } else if (iovec_is_set(data)) {
×
462
                r = loop_write(write_fd, data->iov_base, data->iov_len);
×
463
                if (r < 0)
×
464
                        return log_error_errno(r, "Failed to write data into '%s': %m", filename);
×
465
        }
466

467
        (void) fchmod(write_fd, 0644);
17✔
468

469
        *ret_tmpfile_fd = TAKE_FD(write_fd);
17✔
470
        *ret_tmpfile_filename = TAKE_PTR(t);
17✔
471

472
        return 1;
17✔
473
}
474

475
static int begin_write_entry_file(
12✔
476
                LinkContext *c,
477
                unsigned profile_nr,
478
                const char *osrelease_text,
479
                const char *profile_text,
480
                Profile *ret) {
481

482
        int r;
12✔
483

484
        assert(c);
12✔
485
        assert(osrelease_text);
12✔
486
        assert(ret);
12✔
487

488
        assert(c->entry_token);
12✔
489
        assert(c->kernel_filename);
12✔
490
        assert(c->loader_entries_dir_fd >= 0);
12✔
491

492
        _cleanup_free_ char *good_name = NULL, *good_sort_key = NULL, *os_version_id = NULL, *image_version = NULL;
12✔
493
        r = bootspec_extract_osrelease(
12✔
494
                        osrelease_text,
495
                        /* These three fields are used by systemd-stub for showing entries + sorting them */
496
                        &good_name,     /* human readable */
497
                        /* ret_good_version= */ NULL,
498
                        &good_sort_key,
499
                        /* These four fields are the raw fields provided in os-release */
500
                        /* ret_os_id= */ NULL,
501
                        &os_version_id,
502
                        /* ret_image_id= */ NULL,
503
                        &image_version);
504
        if (r < 0)
12✔
505
                return log_error_errno(r, "Failed to extract name/version/sort-key from os-release data from unified kernel image, refusing.");
×
506

507
        assert(good_name); /* This one is the only field guaranteed to be defined once the above succeeds */
12✔
508

509
        _cleanup_free_ char *profile_id = NULL, *profile_title = NULL;
12✔
510
        if (profile_text) {
12✔
511
                r = parse_env_data(
×
512
                                profile_text, /* size= */ SIZE_MAX,
513
                                ".profile",
514
                                "ID", &profile_id,
515
                                "TITLE", &profile_title);
516
                if (r < 0)
×
517
                        return log_error_errno(r, "Failed to parse profile data from unified kernel image: %m");
×
518
        }
519

520
        const char *version = c->entry_version ?: image_version ?: os_version_id;
12✔
521

522
        _cleanup_free_ char *filename = NULL;
12✔
523
        r = boot_entry_make_commit_filename(
24✔
524
                        c->entry_token,
12✔
525
                        c->entry_commit,
526
                        version,
527
                        profile_nr,
528
                        c->tries_left,
529
                        &filename);
530
        if (r < 0)
12✔
531
                return log_error_errno(r, "Failed to generate filename for entry file: %m");
×
532

533
        if (faccessat(c->loader_entries_dir_fd, filename, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
12✔
534
                if (errno != ENOENT)
11✔
535
                        return log_error_errno(errno, "Failed to check if '%s' exists: %m", filename);
×
536
        } else
537
                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Boot menu entry '%s' exists already, refusing.", filename);
1✔
538

539
        log_info("Writing new boot menu entry '%s/loader/entries/%s' for profile %u.", c->dollar_boot_path, filename, profile_nr);
11✔
540

541
        _cleanup_free_ char *t = NULL;
11✔
542
        _cleanup_close_ int write_fd = open_tmpfile_linkable_at(c->loader_entries_dir_fd, filename, O_WRONLY|O_CLOEXEC, &t);
22✔
543
        if (write_fd < 0)
11✔
544
                return log_error_errno(write_fd, "Failed to create '%s': %m", filename);
×
545

546
        CLEANUP_TMPFILE_AT(c->loader_entries_dir_fd, t);
×
547

548
        _cleanup_free_ char *_title = NULL;
11✔
549
        const char *title;
11✔
550
        if (profile_title || profile_id) {
11✔
551
                _title = strjoin(c->entry_title ?: good_name, " (", profile_title ?: profile_id, ")");
×
552
                if (!_title)
×
553
                        return log_oom();
×
554

555
                title = _title;
556
        } else if (profile_nr > 0) {
11✔
557
                _title = asprintf_safe("%s (Profile #%u)", c->entry_title ?: good_name, profile_nr);
×
558
                if (!_title)
×
559
                        return log_oom();
×
560

561
                title = _title;
562
        } else
563
                title = c->entry_title ?: good_name;
11✔
564

565
        /* Do some validation that this will result in a valid type #1 entry before we write this out */
566
        if (string_has_cc(title, /* ok= */ NULL) || !utf8_is_valid(title))
22✔
567
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to generate valid title for new commit: %s", title);
×
568
        if (string_has_cc(c->kernel_filename, /* ok= */ NULL) || !utf8_is_valid(c->kernel_filename))
11✔
569
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UKI filename is not suitable for inclusion in new commit: %s", c->kernel_filename);
×
570

571
        _cleanup_free_ char *text = NULL;
11✔
572
        if (asprintf(&text,
22✔
573
                     "title %s\n"
574
                     "uki /%s/%s\n"
575
                     "version %" PRIu64 "%s%s\n",
576
                     title,
577
                     c->entry_token, c->kernel_filename,
578
                     c->entry_commit, isempty(version) ? "" : ".", strempty(version)) < 0)
11✔
579
                return log_oom();
×
580

581
        if (good_sort_key && strextendf(&text, "sort-key %s\n", good_sort_key) < 0)
11✔
582
                return log_oom();
×
583

584
        if (profile_nr > 0 && strextendf(&text, "profile %u\n", profile_nr) < 0)
11✔
585
                return log_oom();
×
586

587
        if (!sd_id128_is_null(c->machine_id) && strextendf(&text, "machine-id " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(c->machine_id)) < 0)
11✔
588
                return log_oom();
×
589

590
        FOREACH_ARRAY(x, c->extra, c->n_extra) {
19✔
591
                if (string_has_cc(x->filename, /* ok= */ NULL) || !utf8_is_valid(x->filename))
8✔
592
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra filename is not suitable for inclusion in new commit: %s", x->filename);
×
593

594
                if (strextendf(&text,
8✔
595
                               "extra /%s/%s\n",
596
                               c->entry_token,
597
                               x->filename) < 0)
598
                        return log_oom();
×
599
        }
600

601
        r = loop_write(write_fd, text, /* nbytes= */ SIZE_MAX);
11✔
602
        if (r < 0)
11✔
603
                return log_error_errno(r, "Failed to write entry file: %m");
×
604

605
        *ret = (Profile) {
11✔
606
                .entry_filename = TAKE_PTR(filename),
11✔
607
                .entry_temp_filename = TAKE_PTR(t),
11✔
608
                .entry_temp_fd = TAKE_FD(write_fd),
11✔
609
        };
610

611
        return 0;
11✔
612
}
613

614
static int finalize_file(
30✔
615
                const char *filename,
616
                int target_dir_fd,
617
                int tmpfile_fd,
618
                const char *tmpfile_filename) {
619

620
        int r;
30✔
621

622
        assert(filename);
30✔
623
        assert(target_dir_fd >= 0);
30✔
624

625
        if (tmpfile_fd < 0) /* If the file already existed, we don't move anything into place. */
30✔
626
                return 0;
627

628
        r = link_tmpfile_at(tmpfile_fd, target_dir_fd, tmpfile_filename, filename, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC);
28✔
629
        if (r < 0)
28✔
630
                return log_error_errno(r, "Failed to move from '%s' into place: %m", filename);
×
631

632
        log_info("Installed '%s' into place.", filename);
28✔
633
        return 1;
634
}
635

636
static int link_context_pick_entry_commit(LinkContext *c) {
14✔
637
        int r;
14✔
638

639
        assert(c);
14✔
640
        assert(c->loader_entries_dir_fd >= 0);
14✔
641
        assert(c->entry_token);
14✔
642

643
        /* Already have a commit nr? */
644
        if (c->entry_commit != 0)
14✔
645
                return 0;
14✔
646

647
        _cleanup_close_ int opened_fd = fd_reopen(c->loader_entries_dir_fd, O_DIRECTORY|O_CLOEXEC);
19✔
648
        if (opened_fd < 0)
5✔
649
                return log_error_errno(opened_fd, "Failed to reopen loader entries dir: %m");
×
650

651
        _cleanup_free_ DirectoryEntries *dentries = NULL;
5✔
652
        r = readdir_all(opened_fd, RECURSE_DIR_IGNORE_DOT, &dentries);
5✔
653
        if (r < 0)
5✔
654
                return log_error_errno(r, "Failed to read loader entries directory: %m");
×
655

656
        uint64_t m = 0; /* largest commit number seen */
5✔
657
        FOREACH_ARRAY(i, dentries->entries, dentries->n_entries) {
5✔
658
                const struct dirent *de = *i;
×
659

660
                /* We look for files named <token>-commit_<commit>[.<version>][.p<profile>].conf */
661

662
                if (!dirent_is_file(de))
×
663
                        continue;
×
664

665
                if (!efi_loader_entry_name_valid(de->d_name))
×
666
                        continue;
×
667

668
                _cleanup_free_ char *et = NULL;
×
669
                uint64_t ec;
×
670
                r = boot_entry_parse_commit_filename(de->d_name, &et, &ec);
×
671
                if (r < 0) {
×
672
                        log_debug_errno(r, "Cannot extract entry token/commit number from '%s', ignoring.", de->d_name);
×
673
                        continue;
×
674
                }
675

676
                if (!streq(c->entry_token, et))
×
677
                        continue;
×
678

679
                log_debug("Found existing commit %" PRIu64 ".", ec);
×
680
                if (ec > m)
×
681
                        m = ec;
×
682
        }
683

684
        assert(m < UINT64_MAX);
5✔
685
        uint64_t next = m + 1;
5✔
686

687
        if (!entry_commit_valid(next))
5✔
688
                return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many commits already in place, refusing.");
×
689

690
        log_debug("Picking commit %" PRIu64 " for new commit.", next);
5✔
691
        c->entry_commit = next;
5✔
692
        return 0;
5✔
693
}
694

695
static int clean_temporary_files(int fd) {
28✔
696
        int r;
28✔
697

698
        assert(fd >= 0);
28✔
699

700
        /* Before we create any new files let's clear any possible left-overs from a previous run. We look
701
         * specifically for all temporary files whose name starts with .# because that's what we create, via
702
         * open_tmpfile_linkable_at().
703
         *
704
         * Ideally, this would not be necessary because O_TMPFILE would ensure that files are not
705
         * materialized before they are fully written. However, vfat currently does not support O_TMPFILE,
706
         * hence we need to clean things up manually. */
707

708
        _cleanup_close_ int dfd = fd_reopen(fd, O_CLOEXEC|O_DIRECTORY);
56✔
709
        if (dfd < 0)
28✔
710
                return log_error_errno(dfd, "Failed to open directory: %m");
×
711

712
        _cleanup_free_ DirectoryEntries *de = NULL;
28✔
713
        r = readdir_all(dfd, RECURSE_DIR_ENSURE_TYPE, &de);
28✔
714
        if (r < 0)
28✔
715
                return log_error_errno(r, "Failed to enumerate contents of directory: %m");
×
716

717
        FOREACH_ARRAY(i, de->entries, de->n_entries) {
35✔
718
                struct dirent *e = *i;
7✔
719

720
                if (e->d_type != DT_REG)
7✔
721
                        continue;
×
722

723
                if (!startswith_no_case(e->d_name, ".#"))
7✔
724
                        continue;
7✔
725

726
                if (unlinkat(dfd, e->d_name, /* flags= */ 0) < 0 && errno != ENOENT)
×
727
                        log_warning_errno(errno, "Failed to remove temporary file '%s', ignoring: %m", e->d_name);
7✔
728
        }
729

730
        return 0;
731
}
732

733
static int link_context_unlink_oldest(LinkContext *c) {
1✔
734
        int r;
1✔
735

736
        assert(c);
1✔
737

738
        /* We only load the entries from the partition we want to make space on (!) */
739
        _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
1✔
740
        r = boot_config_load_and_select(
1✔
741
                        &config,
742
                        c->root,
1✔
743
                        c->dollar_boot_source == BOOT_ENTRY_ESP ? c->dollar_boot_path : NULL,
744
                        /* esp_devid= */ 0,
745
                        c->dollar_boot_source == BOOT_ENTRY_XBOOTLDR ? c->dollar_boot_path : NULL,
1✔
746
                        /* xbootldr_devid= */ 0);
747
        if (r < 0)
1✔
748
                return r;
749

750
        _cleanup_(strv_freep) char **ids = NULL;
1✔
751
        r = boot_config_find_oldest_commit(
2✔
752
                        &config,
753
                        c->entry_token,
1✔
754
                        &ids);
755
        if (r == -ENXIO)
1✔
756
                return log_error_errno(r, "No suitable boot menu entry to delete found.");
1✔
757
        if (r == -EBUSY)
×
758
                return log_error_errno(r, "Refusing to remove currently booted boot menu entry.");
×
759
        if (r < 0)
×
760
                return log_error_errno(r, "Failed to find suitable oldest boot menu entry: %m");
×
761

762
        _cleanup_(hashmap_freep) Hashmap *known_files = NULL;
×
763
        r = boot_config_count_known_files(&config, c->dollar_boot_source, &known_files);
×
764
        if (r < 0)
×
765
                return r;
766

767
        int ret = 0;
×
768
        STRV_FOREACH(id, ids) {
×
769
                const BootEntry *entry = boot_config_find_entry(&config, *id);
×
770
                if (!entry)
×
771
                        continue;
×
772

773
                RET_GATHER(ret, boot_entry_unlink(entry, c->dollar_boot_path, c->dollar_boot_fd, known_files, /* dry_run= */ false));
×
774
        }
775

776
        if (ret < 0)
×
777
                return ret;
×
778

779
        return 1;
780
}
781

782
static int verify_keep_free(LinkContext *c) {
26✔
783
        int r;
26✔
784

785
        assert(c);
26✔
786

787
        if (c->keep_free == 0)
26✔
788
                return 0;
26✔
789

790
        uint64_t f;
26✔
791
        r = vfs_free_bytes(ASSERT_FD(c->dollar_boot_fd), &f);
26✔
792
        if (r < 0)
26✔
793
                return log_error_errno(r, "Failed to statvfs() the $BOOT partition: %m");
×
794

795
        if (f < c->keep_free)
26✔
796
                return log_error_errno(
1✔
797
                                SYNTHETIC_ERRNO(EDQUOT),
798
                                "Not installing boot menu entry, free space after installation of %s would be below configured keep free size %s.",
799
                                FORMAT_BYTES(f), FORMAT_BYTES(c->keep_free));
800

801
        return 0;
802
}
803

804
static int run_link_now(LinkContext *c) {
14✔
805
        int r;
14✔
806

807
        assert(c);
14✔
808
        assert(c->dollar_boot_fd >= 0);
14✔
809

810
        _cleanup_free_ char *j = path_join(empty_to_root(c->root), c->dollar_boot_path);
28✔
811
        if (!j)
14✔
812
                return log_oom();
×
813

814
        if (c->loader_entries_dir_fd < 0) {
14✔
815
                r = chaseat(/* root_fd= */ c->dollar_boot_fd,
14✔
816
                            /* dir_fd= */ c->dollar_boot_fd,
817
                            "loader/entries",
818
                            CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
819
                            /* ret_path= */ NULL,
820
                            &c->loader_entries_dir_fd);
821
                if (r < 0)
14✔
822
                        return log_error_errno(r, "Failed to pin '/loader/entries' directory below '%s': %m", j);
×
823
        }
824

825
        /* Remove any left-overs from an earlier run before we write new stuff */
826
        (void) clean_temporary_files(c->loader_entries_dir_fd);
14✔
827

828
        r = link_context_pick_entry_commit(c);
14✔
829
        if (r < 0)
14✔
830
                return r;
831

832
        log_info("Will create commit %" PRIu64 ".", c->entry_commit);
14✔
833

834
        if (c->entry_token_dir_fd < 0) {
14✔
835
                r = chaseat(/* root_fd= */ c->dollar_boot_fd,
28✔
836
                            /* dir_fd= */ c->dollar_boot_fd,
837
                            c->entry_token,
14✔
838
                            CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY,
839
                            /* ret_path= */ NULL,
840
                            &c->entry_token_dir_fd);
841
                if (r < 0)
14✔
842
                        return log_error_errno(r, "Failed to pin '/%s' directory below '%s': %m", c->entry_token, j);
×
843
        }
844

845
        /* As above */
846
        (void) clean_temporary_files(c->entry_token_dir_fd);
14✔
847

848
        /* Synchronize everything to disk before we verify the disk space, to ensure the counters are
849
         * accurate (some file systems delay accurate counters) */
850
        (void) syncfs(c->dollar_boot_fd);
14✔
851

852
        /* Before we start copying things, let's see if there's even a remote chance to get this copied
853
         * in. Note that we do not try to be overly smart here, i.e. we do not try to calculate how much
854
         * extra space we'll need here. Doing that is not trivial since after all the same resources can be
855
         * referenced by multiple entries, which makes copying them multiple times unnecessary. */
856
        r = verify_keep_free(c);
14✔
857
        if (r < 0)
14✔
858
                return r;
859

860
        for (unsigned p = 0; p < UNIFIED_PROFILES_MAX; p++) {
25✔
861
                _cleanup_free_ char *osrelease = NULL, *profile = NULL;
25✔
862
                r = pe_find_uki_sections(c->kernel_fd, j, p, &osrelease, &profile, /* ret_cmdline= */ NULL);
25✔
863
                if (r < 0)
25✔
864
                        return r;
865
                if (r == 0) /* this profile does not exist, we are done */
25✔
866
                        break;
867

868
                if (!GREEDY_REALLOC(c->profiles, c->n_profiles+1))
13✔
869
                        return log_oom();
×
870

871
                r = begin_write_entry_file(
26✔
872
                                c,
873
                                p,
874
                                osrelease,
875
                                profile,
876
                                c->profiles + c->n_profiles);
13✔
877
                if (r < 0)
13✔
878
                        return r;
879

880
                c->n_profiles++;
12✔
881
        }
882

883
        if (c->n_profiles == 0)
12✔
884
                return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "UKI with no valid profile, refusing.");
×
885

886
        r = begin_copy_file(
24✔
887
                        c->kernel_fd,
888
                        /* data= */ NULL,
889
                        c->kernel_filename,
12✔
890
                        c->entry_token_dir_fd,
891
                        &c->kernel_temp_fd,
892
                        &c->kernel_temp_filename);
893
        if (r < 0)
12✔
894
                return r;
895

896
        FOREACH_ARRAY(x, c->extra, c->n_extra) {
20✔
897
                r = begin_copy_file(
16✔
898
                                x->source_fd,
899
                                &x->data,
8✔
900
                                x->filename,
8✔
901
                                c->entry_token_dir_fd,
902
                                &x->temp_fd,
903
                                &x->temp_filename);
904
                if (r < 0)
8✔
905
                        return r;
906
        }
907

908
        /* We copied all files into place, but they are not materialized yet. Let's ensure the data hits the
909
         * disk before we proceed */
910
        (void) syncfs(c->dollar_boot_fd);
12✔
911

912
        /* Before we materialize things, let's ensure the space to keep free is not taken */
913
        r = verify_keep_free(c);
12✔
914
        if (r < 0)
12✔
915
                return r;
916

917
        /* We successfully managed to put all resources we need into the $BOOT partition. Now, let's
918
         * "materialize" them by linking them into the file system. Before this point we'd get rid of every
919
         * file we created on error again. But from now on we switch modes: what we manage to move into place
920
         * we leave in place even on error. These are not lost resources after all, the GC logic implemented
921
         * by "bootctl cleanup" will take care of removing things again if necessary. */
922

923
        r = finalize_file(
24✔
924
                        c->kernel_filename,
12✔
925
                        c->entry_token_dir_fd,
926
                        c->kernel_temp_fd,
927
                        c->kernel_temp_filename);
12✔
928
        if (r < 0)
12✔
929
                return r;
930

931
        c->kernel_temp_fd = safe_close(c->kernel_temp_fd);
12✔
932
        c->kernel_temp_filename = mfree(c->kernel_temp_filename);
12✔
933

934
        FOREACH_ARRAY(x, c->extra, c->n_extra) {
20✔
935
                r = finalize_file(
16✔
936
                                x->filename,
8✔
937
                                c->entry_token_dir_fd,
938
                                x->temp_fd,
939
                                x->temp_filename);
8✔
940
                if (r < 0)
8✔
941
                        return r;
942

943
                x->temp_fd = safe_close(x->temp_fd);
8✔
944
                x->temp_filename = mfree(x->temp_filename);
8✔
945
        }
946

947
        /* Finally, after all our resources are in place, also materialize the menu entry files themselves */
948
        FOREACH_ARRAY(profile, c->profiles, c->n_profiles) {
24✔
949
                r = finalize_file(
24✔
950
                                profile->entry_filename,
12✔
951
                                c->loader_entries_dir_fd,
952
                                profile->entry_temp_fd,
953
                                profile->entry_temp_filename);
12✔
954
                if (r < 0)
12✔
955
                        return r;
×
956

957
                profile->entry_temp_fd = safe_close(profile->entry_temp_fd);
12✔
958
                profile->entry_temp_filename = mfree(profile->entry_temp_filename);
12✔
959

960
                _cleanup_free_ char *stripped = NULL;
12✔
961
                r = boot_filename_extract_tries(
24✔
962
                                profile->entry_filename,
12✔
963
                                &stripped,
964
                                /* ret_tries_left= */ NULL,
965
                                /* ret_tries_done= */ NULL);
966
                if (r < 0)
12✔
967
                        return log_warning_errno(r, "Failed to extract tries counters from id '%s'", profile->entry_filename);
×
968

969
                if (strv_consume(&c->linked_ids, TAKE_PTR(stripped)) < 0)
12✔
970
                        return log_oom();
×
971
        }
972

973
        (void) syncfs(c->dollar_boot_fd);
12✔
974
        return 0;
12✔
975
}
976

977
static int run_link(LinkContext *c) {
14✔
978
        int r;
14✔
979

980
        assert(c);
14✔
981
        assert(c->dollar_boot_path);
14✔
982
        assert(c->dollar_boot_fd >= 0);
14✔
983

984
        if (c->keep_free == UINT64_MAX)
14✔
985
                c->keep_free = KEEP_FREE_BYTES_DEFAULT;
2✔
986

987
        r = link_context_pick_entry_token(c);
14✔
988
        if (r < 0)
14✔
989
                return r;
14✔
990

991
        unsigned n_removals = 0;
992
        for (;;) {
14✔
993
                r = run_link_now(c);
14✔
994
                if (r < 0) {
14✔
995
                        if (!ERRNO_IS_NEG_DISK_SPACE(r))
15✔
996
                                return r;
997
                } else
998
                        break;
999

1000
                log_notice("Attempt to link entry failed due to exhausted disk space, trying to remove oldest boot menu entry.");
1✔
1001

1002
                link_context_unlink_temporary(c);
1✔
1003
                link_context_clear_profiles(c);
1✔
1004

1005
                if (link_context_unlink_oldest(c) <= 0) {
1✔
1006
                        log_warning("Attempted to make space on $BOOT, but this failed, attempt to link entry failed.");
1✔
1007
                        return r; /* propagate original error */
1008
                }
1009

1010
                /* Close entry token dir here, quite possible the unlinking above might have removed it too, in case it was empty */
1011
                c->entry_token_dir_fd = safe_close(c->entry_token_dir_fd);
×
1012

1013
                log_info("Removing oldest boot menu entry succeeded, will retry to create boot loader menu entry.");
×
1014
                n_removals++;
×
1015
        }
1016

1017
        _cleanup_free_ char *j = strv_join(c->linked_ids, "', '");
24✔
1018
        if (!j)
12✔
1019
                return log_oom();
×
1020

1021
        if (n_removals > 0)
12✔
1022
                log_info("Successfully installed boot loader entries '%s', after removing %u old entries.", j, n_removals);
×
1023
        else
1024
                log_info("Successfully installed boot loader entries '%s'.", j);
12✔
1025

1026
        return 0;
1027
}
1028

1029
int verb_link(int argc, char *argv[], uintptr_t data, void *userdata) {
10✔
1030
        int r;
10✔
1031

1032
        assert(argc == 2);
10✔
1033

1034
        _cleanup_free_ char *x = NULL;
10✔
1035
        r = parse_path_argument(argv[1], /* suppress_root= */ false, &x);
10✔
1036
        if (r < 0)
10✔
1037
                return r;
1038

1039
        _cleanup_(link_context_done) LinkContext c = LINK_CONTEXT_NULL;
10✔
1040
        r = link_context_from_cmdline(&c, x);
10✔
1041
        if (r < 0)
10✔
1042
                return r;
1043

1044
        return run_link(&c);
9✔
1045
}
1046

1047
/* Directories (relative to the operative root) below which "bootctl link-auto" looks for a staged UKI and
1048
 * extra resources, in decreasing priority order. /var/lib/systemd/uki/ is where systemd-sysupdate stages
1049
 * downloaded resources; the others follow the usual configuration search path precedence, with
1050
 * /var/lib/ slotted in just below the volatile locations and above the vendor trees. */
1051
static const char* const uki_auto_dirs[] = {
1052
        "etc/systemd/uki",
1053
        "run/systemd/uki",
1054
        "var/lib/systemd/uki",
1055
        "usr/local/lib/systemd/uki",
1056
        "usr/lib/systemd/uki",
1057
};
1058

1059
static const PickFilter pick_filter_uki = {
1060
        .type_mask = UINT32_C(1) << DT_REG,
1061
        .architecture = _ARCHITECTURE_INVALID,
1062
        .suffix = ".efi",
1063
};
1064

1065
/* The recognized suffixes of extra resources we pick up alongside the UKI. Each may appear as a plain file
1066
 * (e.g. foo.sysext.raw) or as a versioned directory (e.g. foo.sysext.raw.v/) resolved via the vpick
1067
 * mechanism. */
1068
static const char* const auto_link_extra_suffixes[] = {
1069
        ".sysext.raw",
1070
        ".confext.raw",
1071
        ".cred",
1072
};
1073

1074
static int link_context_add_extra(LinkContext *c, int dir_fd, const char *path, const char *filename) {
5✔
1075
        assert(c);
5✔
1076
        assert(dir_fd >= 0);
5✔
1077
        assert(path);
5✔
1078
        assert(filename);
5✔
1079

1080
        if (!efi_loader_entry_resource_filename_valid(filename))
5✔
1081
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra file '%s' is not suitable for reference in a boot menu entry, refusing.", filename);
×
1082

1083
        _cleanup_close_ int fd = xopenat_full(dir_fd, path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
10✔
1084
        if (fd < 0)
5✔
1085
                return log_error_errno(fd, "Failed to open extra file '%s': %m", path);
×
1086

1087
        _cleanup_free_ char *fn = strdup(filename);
5✔
1088
        if (!fn)
5✔
1089
                return log_oom();
×
1090

1091
        if (!GREEDY_REALLOC(c->extra, c->n_extra + 1))
5✔
1092
                return log_oom();
×
1093

1094
        c->extra[c->n_extra++] = (ExtraFile) {
5✔
1095
                .source_fd = TAKE_FD(fd),
5✔
1096
                .filename = TAKE_PTR(fn),
5✔
1097
                .temp_fd = -EBADF,
1098
        };
1099

1100
        return 0;
5✔
1101
}
1102

1103
/* Adds the extra resources found in <uki_dir_fd>/extras.d/ to the link context. The 'seen' set tracks the
1104
 * logical resource names already added by a higher-priority directory, so the same resource appearing in
1105
 * multiple directories is taken from the highest-priority one only. */
1106
static int link_context_add_extras(LinkContext *c, int uki_dir_fd, Set **seen) {
4✔
1107
        int r;
4✔
1108

1109
        assert(c);
4✔
1110
        assert(uki_dir_fd >= 0);
4✔
1111
        assert(seen);
4✔
1112

1113
        _cleanup_close_ int extras_dir_fd = chase_and_openat(
8✔
1114
                        c->root_fd,
1115
                        uki_dir_fd,
1116
                        "extras.d",
1117
                        /* chase_flags= */ 0,
1118
                        O_DIRECTORY|O_CLOEXEC,
1119
                        /* ret_path= */ NULL);
1120
        if (extras_dir_fd == -ENOENT)
4✔
1121
                return 0;
1122
        if (extras_dir_fd < 0)
4✔
1123
                return log_error_errno(extras_dir_fd, "Failed to open extras.d/ directory: %m");
×
1124

1125
        _cleanup_free_ DirectoryEntries *des = NULL;
4✔
1126
        r = readdir_all(extras_dir_fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SORT, &des);
4✔
1127
        if (r < 0)
4✔
1128
                return log_error_errno(r, "Failed to enumerate extras.d/ directory: %m");
×
1129

1130
        FOREACH_ARRAY(i, des->entries, des->n_entries) {
10✔
1131
                struct dirent *de = *i;
6✔
1132

1133
                if (!IN_SET(de->d_type, DT_DIR, DT_REG))
6✔
1134
                        continue;
×
1135

1136
                FOREACH_ELEMENT(suffix, auto_link_extra_suffixes) {
13✔
1137
                        _cleanup_free_ char *dot_v = strjoin(*suffix, ".v");
20✔
1138
                        if (!dot_v)
13✔
1139
                                return log_oom();
×
1140

1141
                        bool versioned;
13✔
1142
                        _cleanup_free_ char *key = NULL;
13✔
1143
                        if (endswith(de->d_name, *suffix)) {
13✔
1144
                                if (de->d_type != DT_REG)
5✔
1145
                                        continue;
×
1146

1147
                                versioned = false;
5✔
1148
                                key = strdup(de->d_name);
5✔
1149

1150
                        } else if (endswith(de->d_name, dot_v)) {
8✔
1151
                                if (de->d_type != DT_DIR)
1✔
1152
                                        continue;
×
1153

1154
                                /* The dedup key is the directory entry name with any trailing ".v" stripped, so that
1155
                                 * a plain file and a versioned directory of the same resource collapse, and a
1156
                                 * higher-priority directory wins. */
1157
                                versioned = true;
1✔
1158
                                key = strndup(de->d_name, strlen(de->d_name) - strlen(".v"));
1✔
1159
                        } else
1160
                                continue;
7✔
1161
                        if (!key)
6✔
1162
                                return log_oom();
×
1163

1164
                        if (set_contains(*seen, key))
6✔
1165
                                break;
1166

1167
                        if (versioned) {
5✔
1168
                                /* A versioned directory: resolve the best version via vpick, relative to the
1169
                                 * extras directory fd. We deliberately do not honour boot-counting suffixes
1170
                                 * here. */
1171
                                _cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
×
1172
                                const PickFilter filter = {
1✔
1173
                                        .type_mask = UINT32_C(1) << DT_REG,
1174
                                        .architecture = _ARCHITECTURE_INVALID,
1175
                                        .suffix = *suffix,
1✔
1176
                                };
1177

1178
                                r = path_pick(/* root_path= */ NULL,
1✔
1179
                                              /* root_fd= */ c->root_fd,
1180
                                              /* dir_fd= */ extras_dir_fd,
1181
                                              de->d_name,
1182
                                              &filter,
1183
                                              /* n_filters= */ 1,
1184
                                              /* flags= */ 0,
1185
                                              &pick);
1186
                                if (r == -ENOENT || r == 0) {
1✔
1187
                                        log_debug("No matching entry in versioned directory '%s', skipping.", de->d_name);
×
1188
                                        break;
×
1189
                                }
1190
                                if (r < 0)
1✔
1191
                                        return log_error_errno(r, "Failed to pick version in '%s': %m", de->d_name);
×
1192

1193
                                _cleanup_free_ char *fn = NULL;
1✔
1194
                                r = path_extract_filename(pick.path, &fn);
1✔
1195
                                if (r < 0)
1✔
1196
                                        return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
×
1197

1198
                                r = link_context_add_extra(c, extras_dir_fd, pick.path, fn);
1✔
1199
                        } else
1200
                                /* A plain file. */
1201
                                r = link_context_add_extra(c, extras_dir_fd, de->d_name, de->d_name);
4✔
1202
                        if (r < 0)
5✔
1203
                                return r;
1204

1205
                        r = set_ensure_consume(seen, &string_hash_ops_free, TAKE_PTR(key));
5✔
1206
                        if (r < 0)
5✔
1207
                                return log_oom();
×
1208

1209
                        break;
1210
                }
1211
        }
1212

1213
        return 0;
1214
}
1215

1216
/* Looks for a UKI (kernel.efi, or the best version in kernel.efi.v/) in the directory referenced by
1217
 * uki_dir_fd and, if found, fills c->kernel_fd and c->kernel_filename. Returns 1 if found, 0 if not, and a
1218
 * negative errno on error. We deliberately do not honour boot-counting suffixes here. */
1219
static int link_context_find_kernel(LinkContext *c, int uki_dir_fd) {
3✔
1220
        int r;
3✔
1221

1222
        assert(c);
3✔
1223
        assert(uki_dir_fd >= 0);
3✔
1224

1225
        _cleanup_free_ char *filename = NULL;
3✔
1226
        _cleanup_close_ int kernel_fd = xopenat_full(
6✔
1227
                        uki_dir_fd, "kernel.efi",
1228
                        O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW,
1229
                        XO_REGULAR,
1230
                        /* mode= */ MODE_INVALID);
1231
        if (kernel_fd == -ENOENT) {
3✔
1232
                _cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
1✔
1233
                r = path_pick(/* root_path= */ NULL,
1✔
1234
                              /* root_fd= */ c->root_fd,
1235
                              /* dir_fd= */ uki_dir_fd,
1236
                              "kernel.efi.v",
1237
                              &pick_filter_uki,
1238
                              /* n_filters= */ 1,
1239
                              /* flags= */ 0,
1240
                              &pick);
1241
                if (r == -ENOENT || r == 0)
1✔
1242
                        return 0;
1243
                if (r < 0)
1✔
1244
                        return log_error_errno(r, "Failed to pick UKI version in 'kernel.efi.v': %m");
×
1245

1246
                r = path_extract_filename(pick.path, &filename);
1✔
1247
                if (r < 0)
1✔
1248
                        return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
×
1249

1250
                kernel_fd = fd_reopen(pick.fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
1✔
1251
                if (kernel_fd < 0)
1✔
1252
                        return log_error_errno(kernel_fd, "Failed to open UKI '%s': %m", pick.path);
×
1253

1254
                r = fd_verify_regular(kernel_fd);
1✔
1255
                if (r < 0)
1✔
1256
                        return log_error_errno(r, "UKI '%s' is not a regular file: %m", pick.path);
×
1257

1258
        } else if (kernel_fd < 0)
2✔
1259
                return log_error_errno(kernel_fd, "Failed to open 'kernel.efi': %m");
×
1260
        else {
1261
                filename = strdup("kernel.efi");
2✔
1262
                if (!filename)
2✔
1263
                        return log_oom();
×
1264
        }
1265

1266
        if (!efi_loader_entry_resource_filename_valid(filename))
3✔
1267
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UKI '%s' is not suitable for reference in a boot menu entry.", filename);
×
1268

1269
        r = validate_kernel(kernel_fd, filename);
3✔
1270
        if (r < 0)
3✔
1271
                return r;
1272

1273
        c->kernel_fd = TAKE_FD(kernel_fd);
3✔
1274
        c->kernel_filename = TAKE_PTR(filename);
3✔
1275
        return 1;
3✔
1276
}
1277

1278
/* Discovers the staged UKI and extra resources below the uki_auto_dirs[] (relative to the already-pinned
1279
 * c->root_fd) and fills c->kernel_fd, c->kernel_filename and c->extra[] accordingly. The directories are
1280
 * searched in priority order: the UKI is taken from the first directory that has one, and extra resources
1281
 * are combined across all directories with higher-priority directories winning on conflicts. Returns 1 if a
1282
 * UKI was found, 0 if there is nothing to link, and a negative errno on error. */
1283
static int link_context_discover_resources(LinkContext *c) {
4✔
1284
        _cleanup_set_free_ Set *seen = NULL;
×
1285
        int r;
4✔
1286

1287
        assert(c);
4✔
1288

1289
        FOREACH_ELEMENT(dir, uki_auto_dirs) {
24✔
1290
                _cleanup_close_ int uki_dir_fd = chase_and_openat(
44✔
1291
                                /* root_fd= */ c->root_fd,
1292
                                /* dir_fd= */ c->root_fd,
1293
                                *dir,
1294
                                CHASE_MUST_BE_DIRECTORY,
1295
                                O_RDONLY|O_DIRECTORY|O_CLOEXEC,
1296
                                /* ret_path= */ NULL);
1297
                if (uki_dir_fd == -ENOENT)
20✔
1298
                        continue;
16✔
1299
                if (uki_dir_fd < 0)
4✔
1300
                        return log_error_errno(uki_dir_fd, "Failed to open '%s': %m", *dir);
×
1301

1302
                /* Take the UKI from the first (highest-priority) directory that has one. */
1303
                if (c->kernel_fd < 0) {
4✔
1304
                        r = link_context_find_kernel(c, uki_dir_fd);
3✔
1305
                        if (r < 0)
3✔
1306
                                return r;
1307
                }
1308

1309
                r = link_context_add_extras(c, uki_dir_fd, &seen);
4✔
1310
                if (r < 0)
4✔
1311
                        return r;
1312
        }
1313

1314
        if (c->kernel_fd < 0) {
4✔
1315
                log_debug("No UKI found in any of the search directories, nothing to link.");
1✔
1316
                return 0;
1317
        }
1318

1319
        return 1;
1320
}
1321

1322
int verb_link_auto(int argc, char *argv[], uintptr_t data, void *userdata) {
4✔
1323
        int r;
4✔
1324

1325
        assert(argc == 1);
4✔
1326

1327
        _cleanup_(link_context_done) LinkContext c = LINK_CONTEXT_NULL;
4✔
1328
        r = link_context_from_cmdline_common(&c);
4✔
1329
        if (r < 0)
4✔
1330
                return r;
1331

1332
        r = link_context_discover_resources(&c);
4✔
1333
        if (r < 0)
4✔
1334
                return r;
1335
        if (r == 0) {
4✔
1336
                log_info("No UKI staged for linking, nothing to do.");
1✔
1337
                return 0;
1338
        }
1339

1340
        /* In addition to the auto-discovered resources, also honour any files passed via --extra=. */
1341
        r = link_context_add_cmdline_extras(&c);
3✔
1342
        if (r < 0)
3✔
1343
                return r;
1344

1345
        r = link_context_acquire_dollar_boot(&c);
3✔
1346
        if (r < 0)
3✔
1347
                return r;
1348

1349
        return run_link(&c);
3✔
1350
}
1351

1352
static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string);
×
1353

1354
typedef struct LinkParameters {
1355
        LinkContext context;
1356
        unsigned root_fd_index;
1357
        unsigned kernel_fd_index;
1358
        char *esp_path;
1359
        char *xbootldr_path;
1360
        sd_varlink *link;
1361
} LinkParameters;
1362

1363
static void link_parameters_done(LinkParameters *p) {
2✔
1364
        assert(p);
2✔
1365

1366
        link_context_done(&p->context);
2✔
1367
        free(p->esp_path);
2✔
1368
        free(p->xbootldr_path);
2✔
1369
}
2✔
1370

1371
typedef struct ExtraParameters {
1372
        ExtraFile extra_file;
1373
        unsigned fd_index;
1374
} ExtraParameters;
1375

1376
static void extra_parameters_done(ExtraParameters *p) {
×
1377
        assert(p);
×
1378

1379
        extra_file_done(&p->extra_file);
×
1380
}
×
1381

1382
static int json_dispatch_loader_entry_resource_filename(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
2✔
1383
        char **n = ASSERT_PTR(userdata);
2✔
1384
        const char *filename;
2✔
1385
        int r;
2✔
1386

1387
        assert(variant);
2✔
1388

1389
        r = json_dispatch_const_filename(name, variant, flags, &filename);
2✔
1390
        if (r < 0)
2✔
1391
                return r;
2✔
1392

1393
        if (filename && !efi_loader_entry_resource_filename_valid(filename))
2✔
1394
                return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid boot entry resource filename.", strna(name));
×
1395

1396
        if (free_and_strdup(n, filename) < 0)
2✔
1397
                return json_log_oom(variant, flags);
×
1398

1399
        return 0;
1400
}
1401

1402
static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) {
×
1403
        LinkParameters *c = ASSERT_PTR(userdata);
×
1404
        int r;
×
1405

1406
        if (!sd_json_variant_is_array(v))
×
1407
                return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
×
1408

1409
        sd_json_variant *i;
×
1410
        JSON_VARIANT_ARRAY_FOREACH(i, v) {
×
1411
                _cleanup_(extra_parameters_done) ExtraParameters xp = {
×
1412
                        .extra_file = EXTRA_FILE_NULL,
1413
                        .fd_index = UINT_MAX,
1414
                };
1415

1416
                static const sd_json_dispatch_field dispatch_table[] = {
×
1417
                        { "filename",       SD_JSON_VARIANT_STRING,        json_dispatch_loader_entry_resource_filename, offsetof(ExtraParameters, extra_file.filename),  SD_JSON_MANDATORY },
1418
                        { "fileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,                        offsetof(ExtraParameters, fd_index),             0                 },
1419
                        { "data",           SD_JSON_VARIANT_STRING,        json_dispatch_unbase64_iovec,                 offsetof(ExtraParameters, extra_file.data),      0                 },
1420
                        {},
1421
                };
1422

1423
                r = sd_json_dispatch(i, dispatch_table, /* flags= */ 0, &xp);
×
1424
                if (r < 0)
×
1425
                        return r;
1426

1427
                if (iovec_is_set(&xp.extra_file.data) == (xp.fd_index != UINT_MAX))
×
1428
                        return sd_varlink_error_invalid_parameter_name(c->link, name);
×
1429
                if (xp.fd_index != UINT_MAX) {
×
1430
                        xp.extra_file.source_fd = sd_varlink_peek_dup_fd(c->link, xp.fd_index);
×
1431
                        if (xp.extra_file.source_fd < 0)
×
1432
                                return log_debug_errno(xp.extra_file.source_fd, "Failed to acquire extra fd from Varlink: %m");
×
1433

1434
                        r = fd_verify_safe_flags(xp.extra_file.source_fd);
×
1435
                        if (r < 0)
×
1436
                                return sd_varlink_error_invalid_parameter_name(c->link, name);
×
1437

1438
                        r = fd_verify_regular(xp.extra_file.source_fd);
×
1439
                        if (r < 0)
×
1440
                                return log_debug_errno(r, "Failed to validate that the extra file is a regular file descriptor: %m");
×
1441
                }
1442

1443
                if (!GREEDY_REALLOC(c->context.extra, c->context.n_extra+1))
×
1444
                        return log_oom();
×
1445

1446
                c->context.extra[c->context.n_extra++] = TAKE_GENERIC(xp.extra_file, ExtraFile, EXTRA_FILE_NULL);
×
1447
        }
1448

1449
        return 0;
×
1450
}
1451

1452
/* Resolves the root_fd of the LinkContext from the dispatched Link/LinkAuto parameters, fills in the
1453
 * boot entry token type default, and validates the entry title/version/commit fields. Returns 0 on success,
1454
 * non-zero (Varlink error or negative errno) otherwise. */
1455
static int vl_link_prepare(sd_varlink *link, LinkParameters *p) {
2✔
1456
        int r;
2✔
1457

1458
        assert(link);
2✔
1459
        assert(p);
2✔
1460

1461
        if (p->root_fd_index != UINT_MAX) {
2✔
1462
                p->context.root_fd = sd_varlink_peek_dup_fd(link, p->root_fd_index);
×
1463
                if (p->context.root_fd < 0)
×
1464
                        return log_debug_errno(p->context.root_fd, "Failed to acquire root fd from Varlink: %m");
×
1465

1466
                r = fd_verify_safe_flags_full(p->context.root_fd, O_DIRECTORY);
×
1467
                if (r < 0)
×
1468
                        return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor");
×
1469

1470
                r = fd_verify_directory(p->context.root_fd);
×
1471
                if (r < 0)
×
1472
                        return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m");
×
1473

1474
                if (!p->context.root) {
×
1475
                        r = fd_get_path(p->context.root_fd, &p->context.root);
×
1476
                        if (r < 0)
×
1477
                                return log_debug_errno(r, "Failed to get path of file descriptor: %m");
×
1478

1479
                        if (empty_or_root(p->context.root))
×
1480
                                p->context.root = mfree(p->context.root);
×
1481
                }
1482
        } else if (p->context.root) {
2✔
1483
                p->context.root_fd = open(p->context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
×
1484
                if (p->context.root_fd < 0)
×
1485
                        return log_debug_errno(errno, "Failed to open '%s': %m", p->context.root);
×
1486
        } else
1487
                p->context.root_fd = XAT_FDROOT;
2✔
1488

1489
        if (p->context.entry_token_type < 0)
2✔
1490
                p->context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO;
2✔
1491

1492
        if (p->context.entry_title && !efi_loader_entry_title_valid(p->context.entry_title))
2✔
1493
                return sd_varlink_error_invalid_parameter_name(link, "entryTitle");
×
1494

1495
        if (p->context.entry_version && !version_is_valid_versionspec(p->context.entry_version))
2✔
1496
                return sd_varlink_error_invalid_parameter_name(link, "entryVersion");
×
1497

1498
        if (p->context.entry_commit != 0 && !entry_commit_valid(p->context.entry_commit))
2✔
1499
                return sd_varlink_error_invalid_parameter_name(link, "entryCommit");
×
1500

1501
        return 0;
1502
}
1503

1504
/* Resolves $BOOT, runs the link operation and sends the reply. Expects the kernel and extra files to already
1505
 * be set up in the context. If with_ids is true the reply carries the list of created entry IDs (as expected
1506
 * by Link()/LinkAuto()), otherwise an empty reply is sent (as expected by
1507
 * io.systemd.SysUpdate.Notify.OnCompletedUpdate(), which declares no output fields). */
1508
static int vl_link_finish(sd_varlink *link, LinkParameters *p, bool with_ids) {
2✔
1509
        int r;
2✔
1510

1511
        assert(link);
2✔
1512
        assert(p);
2✔
1513

1514
        r = find_xbootldr_and_warn_at(
4✔
1515
                        p->context.root_fd,
1516
                        p->xbootldr_path,
2✔
1517
                        /* unprivileged_mode= */ false,
1518
                        &p->context.dollar_boot_path,
1519
                        &p->context.dollar_boot_fd);
1520
        if (r < 0) {
2✔
1521
                if (r != -ENOKEY)
2✔
1522
                        return r;
1523

1524
                /* No XBOOTLDR found, let's look for ESP then. */
1525

1526
                r = find_esp_and_warn_at(
4✔
1527
                                p->context.root_fd,
1528
                                p->esp_path,
2✔
1529
                                /* unprivileged_mode= */ false,
1530
                                &p->context.dollar_boot_path,
1531
                                &p->context.dollar_boot_fd);
1532
                if (r == -ENOKEY)
2✔
1533
                        return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL);
×
1534
                if (r < 0)
2✔
1535
                        return r;
1536

1537
                p->context.dollar_boot_source = BOOT_ENTRY_ESP;
2✔
1538
        } else
1539
                p->context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
×
1540

1541
        r = run_link(&p->context);
2✔
1542
        if (r == -EUNATCH) /* no boot entry token is set */
2✔
1543
                return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL);
×
1544
        if (r < 0)
2✔
1545
                return r;
1546

1547
        if (with_ids)
2✔
1548
                return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p->context.linked_ids));
2✔
1549

1550
        return sd_varlink_reply(link, NULL);
×
1551
}
1552

1553
int vl_method_link(
2✔
1554
                sd_varlink *link,
1555
                sd_json_variant *parameters,
1556
                sd_varlink_method_flags_t flags,
1557
                void *userdata) {
1558

1559
        int r;
2✔
1560

1561
        assert(link);
2✔
1562

1563
        _cleanup_(link_parameters_done) LinkParameters p = {
2✔
1564
                .context = LINK_CONTEXT_NULL,
1565
                .root_fd_index = UINT_MAX,
1566
                .kernel_fd_index = UINT_MAX,
1567
                .link = link,
1568
        };
1569

1570
        static const sd_json_dispatch_field dispatch_table[] = {
2✔
1571
                { "rootFileDescriptor",   _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,                        voffsetof(p, root_fd_index),            0                 },
1572
                { "rootDirectory",        SD_JSON_VARIANT_STRING,        json_dispatch_path,                           voffsetof(p, context.root),             0                 },
1573
                { "espPath",              SD_JSON_VARIANT_STRING,        json_dispatch_path,                           voffsetof(p, esp_path),                 0                 },
1574
                { "xbootldrPath",         SD_JSON_VARIANT_STRING,        json_dispatch_path,                           voffsetof(p, xbootldr_path),            0                 },
1575
                { "bootEntryTokenType",   SD_JSON_VARIANT_STRING,        json_dispatch_boot_entry_token_type,          voffsetof(p, context.entry_token_type), 0                 },
1576
                { "entryTitle",           SD_JSON_VARIANT_STRING,        sd_json_dispatch_string,                      voffsetof(p, context.entry_title),      0                 },
1577
                { "entryVersion",         SD_JSON_VARIANT_STRING,        sd_json_dispatch_string,                      voffsetof(p, context.entry_version),    0                 },
1578
                { "entryCommit",          SD_JSON_VARIANT_INTEGER,       sd_json_dispatch_uint64,                      voffsetof(p, context.entry_commit),     0                 },
1579
                { "kernelFilename",       SD_JSON_VARIANT_STRING,        json_dispatch_loader_entry_resource_filename, voffsetof(p, context.kernel_filename),  SD_JSON_MANDATORY },
1580
                { "kernelFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,                        voffsetof(p, kernel_fd_index),          SD_JSON_MANDATORY },
1581
                { "extraFiles",           SD_JSON_VARIANT_ARRAY,         dispatch_extras,                              0,                                      0                 },
1582
                { "triesLeft",            _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,                        voffsetof(p, context.tries_left),       0                 },
1583
                { "keepFree",             _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64,                      voffsetof(p, context.keep_free),        0                 },
1584
                {},
1585
        };
1586

1587
        r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
2✔
1588
        if (r != 0)
2✔
1589
                return r;
1590

1591
        r = varlink_check_privileged_peer(link);
2✔
1592
        if (r < 0)
2✔
1593
                return r;
1594

1595
        r = vl_link_prepare(link, &p);
2✔
1596
        if (r != 0)
2✔
1597
                return r;
1598

1599
        p.context.kernel_fd = sd_varlink_peek_dup_fd(link, p.kernel_fd_index);
2✔
1600
        if (p.context.kernel_fd < 0)
2✔
1601
                return log_debug_errno(p.context.kernel_fd, "Failed to acquire kernel fd from Varlink: %m");
×
1602

1603
        r = fd_verify_safe_flags(p.context.kernel_fd);
2✔
1604
        if (r < 0)
2✔
1605
                return sd_varlink_error_invalid_parameter_name(link, "kernelFileDescriptor");
×
1606
        r = fd_verify_regular(p.context.kernel_fd);
2✔
1607
        if (r < 0)
2✔
1608
                return log_debug_errno(r, "Failed to validate that kernel image file is a regular file descriptor: %m");
×
1609

1610
        /* Refuse non-UKIs for now. */
1611
        KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
2✔
1612
        r = inspect_kernel(p.context.kernel_fd, /* filename= */ NULL, &kit);
2✔
1613
        if (r == -EBADMSG)
2✔
1614
                return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL);
×
1615
        if (r < 0)
2✔
1616
                return r;
1617
        if (kit != KERNEL_IMAGE_TYPE_UKI)
2✔
1618
                return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL);
×
1619

1620
        return vl_link_finish(link, &p, /* with_ids= */ true);
2✔
1621
}
1622

1623
int vl_method_link_auto(
×
1624
                sd_varlink *link,
1625
                sd_json_variant *parameters,
1626
                sd_varlink_method_flags_t flags,
1627
                void *userdata) {
1628

1629
        int r;
×
1630

1631
        assert(link);
×
1632

1633
        _cleanup_(link_parameters_done) LinkParameters p = {
×
1634
                .context = LINK_CONTEXT_NULL,
1635
                .root_fd_index = UINT_MAX,
1636
                .kernel_fd_index = UINT_MAX,
1637
                .link = link,
1638
        };
1639

1640
        static const sd_json_dispatch_field dispatch_table[] = {
×
1641
                { "rootFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,               voffsetof(p, root_fd_index),            0 },
1642
                { "rootDirectory",      SD_JSON_VARIANT_STRING,        json_dispatch_path,                  voffsetof(p, context.root),             0 },
1643
                { "espPath",            SD_JSON_VARIANT_STRING,        json_dispatch_path,                  voffsetof(p, esp_path),                 0 },
1644
                { "xbootldrPath",       SD_JSON_VARIANT_STRING,        json_dispatch_path,                  voffsetof(p, xbootldr_path),            0 },
1645
                { "bootEntryTokenType", SD_JSON_VARIANT_STRING,        json_dispatch_boot_entry_token_type, voffsetof(p, context.entry_token_type), 0 },
1646
                { "entryTitle",         SD_JSON_VARIANT_STRING,        sd_json_dispatch_string,             voffsetof(p, context.entry_title),      0 },
1647
                { "entryVersion",       SD_JSON_VARIANT_STRING,        sd_json_dispatch_string,             voffsetof(p, context.entry_version),    0 },
1648
                { "entryCommit",        SD_JSON_VARIANT_INTEGER,       sd_json_dispatch_uint64,             voffsetof(p, context.entry_commit),     0 },
1649
                { "triesLeft",          _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,               voffsetof(p, context.tries_left),       0 },
1650
                { "keepFree",           _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64,             voffsetof(p, context.keep_free),        0 },
1651
                {},
1652
        };
1653

1654
        r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
×
1655
        if (r != 0)
×
1656
                return r;
1657

1658
        r = varlink_check_privileged_peer(link);
×
1659
        if (r < 0)
×
1660
                return r;
1661

1662
        r = vl_link_prepare(link, &p);
×
1663
        if (r != 0)
×
1664
                return r;
1665

1666
        r = link_context_discover_resources(&p.context);
×
1667
        if (r < 0)
×
1668
                return r;
1669
        if (r == 0) /* Nothing staged for linking. */
×
1670
                return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", STRV_EMPTY));
×
1671

1672
        return vl_link_finish(link, &p, /* with_ids= */ true);
×
1673
}
1674

1675
int vl_method_on_completed_update(
×
1676
                sd_varlink *link,
1677
                sd_json_variant *parameters,
1678
                sd_varlink_method_flags_t flags,
1679
                void *userdata) {
1680

1681
        int r;
×
1682

1683
        assert(link);
×
1684

1685
        /* Only honour update notifications if they come from root */
1686
        r = varlink_check_privileged_peer(link);
×
1687
        if (r < 0)
×
1688
                return r;
×
1689

1690
        /* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
1691
         * (we don't even dispatch them) and behave like LinkAuto() with default parameters: discover the
1692
         * staged UKI and extra resources and link them in. Contrary to LinkAuto() we reply without any
1693
         * parameters, matching the io.systemd.SysUpdate.Notify.OnCompletedUpdate() signature. */
1694

1695
        _cleanup_(link_parameters_done) LinkParameters p = {
×
1696
                .context = LINK_CONTEXT_NULL,
1697
                .root_fd_index = UINT_MAX,
1698
                .kernel_fd_index = UINT_MAX,
1699
                .link = link,
1700
        };
1701

1702
        r = vl_link_prepare(link, &p);
×
1703
        if (r != 0)
×
1704
                return r;
1705

1706
        r = link_context_discover_resources(&p.context);
×
1707
        if (r < 0)
×
1708
                return r;
1709
        if (r == 0) /* Nothing staged for linking. */
×
1710
                return sd_varlink_reply(link, NULL);
×
1711

1712
        return vl_link_finish(link, &p, /* with_ids= */ false);
×
1713
}
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