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

systemd / systemd / 22007273413

13 Feb 2026 09:37PM UTC coverage: 72.743% (+0.3%) from 72.485%
22007273413

push

github

web-flow
test: do not fail when parsing PID that isn't thread-group leader (#40677)

```
TEST-02-UNITTESTS.sh[4382]: [  707.393188] test-cgroup-util[426]: Failed to open pidfd for pid 414: Invalid argument
TEST-02-UNITTESTS.sh[4382]: [  707.393193] test-cgroup-util[426]: src/test/test-cgroup-util.c:249: Assertion failed: Expected "r = proc_dir_read_pidref(d, &pid)" to succeed, but got error: -22/EINVAL
```

The kernel can return EINVAL on pidfd_open() when the selected PID is
not a thread group leader. Don't fail the test, as we are iterating on
everything, so this can seldomly happen.

3 of 4 new or added lines in 1 file covered. (75.0%)

6126 existing lines in 58 files now uncovered.

312809 of 430017 relevant lines covered (72.74%)

1147140.71 hits per line

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

85.46
/src/core/exec-credential.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <sys/mount.h>
4
#include <unistd.h>
5

6
#include "acl-util.h"
7
#include "cgroup.h"
8
#include "creds-util.h"
9
#include "errno-util.h"
10
#include "exec-credential.h"
11
#include "execute.h"
12
#include "fileio.h"
13
#include "fs-util.h"
14
#include "glob-util.h"
15
#include "io-util.h"
16
#include "iovec-util.h"
17
#include "label-util.h"
18
#include "log.h"
19
#include "manager.h"
20
#include "mkdir-label.h"
21
#include "mount-util.h"
22
#include "mountpoint-util.h"
23
#include "namespace-util.h"
24
#include "ordered-set.h"
25
#include "path-lookup.h"
26
#include "path-util.h"
27
#include "pidref.h"
28
#include "process-util.h"
29
#include "random-util.h"
30
#include "recurse-dir.h"
31
#include "rm-rf.h"
32
#include "siphash24.h"
33
#include "socket-util.h"
34
#include "stat-util.h"
35
#include "string-util.h"
36
#include "strv.h"
37
#include "unit.h"
38
#include "user-util.h"
39

40
ExecSetCredential* exec_set_credential_free(ExecSetCredential *sc) {
6✔
41
        if (!sc)
6✔
42
                return NULL;
43

44
        free(sc->id);
6✔
45
        free(sc->data);
6✔
46
        return mfree(sc);
6✔
47
}
48

49
ExecLoadCredential* exec_load_credential_free(ExecLoadCredential *lc) {
31✔
50
        if (!lc)
31✔
51
                return NULL;
52

53
        free(lc->id);
31✔
54
        free(lc->path);
31✔
55
        return mfree(lc);
31✔
56
}
57

58
ExecImportCredential* exec_import_credential_free(ExecImportCredential *ic) {
4,606✔
59
        if (!ic)
4,606✔
60
                return NULL;
61

62
        free(ic->glob);
4,606✔
63
        free(ic->rename);
4,606✔
64
        return mfree(ic);
4,606✔
65
}
66

67
static void exec_import_credential_hash_func(const ExecImportCredential *ic, struct siphash *state) {
19,348✔
68
        assert(ic);
19,348✔
69
        assert(state);
19,348✔
70

71
        siphash24_compress_string(ic->glob, state);
19,348✔
72
        if (ic->rename)
19,348✔
73
                siphash24_compress_string(ic->rename, state);
1,875✔
74
}
19,348✔
75

76
static int exec_import_credential_compare_func(const ExecImportCredential *a, const ExecImportCredential *b) {
6,623✔
77
        int r;
6,623✔
78

79
        assert(a);
6,623✔
80
        assert(b);
6,623✔
81

82
        r = strcmp(a->glob, b->glob);
6,623✔
83
        if (r != 0)
6,623✔
84
                return r;
85

86
        return strcmp_ptr(a->rename, b->rename);
3✔
87
}
88

89
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
6✔
90
        exec_set_credential_hash_ops,
91
        char, string_hash_func, string_compare_func,
92
        ExecSetCredential, exec_set_credential_free);
93

94
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
31✔
95
        exec_load_credential_hash_ops,
96
        char, string_hash_func, string_compare_func,
97
        ExecLoadCredential, exec_load_credential_free);
98

99
DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
4,605✔
100
        exec_import_credential_hash_ops,
101
        ExecImportCredential,
102
        exec_import_credential_hash_func,
103
        exec_import_credential_compare_func,
104
        exec_import_credential_free);
105

106
int exec_context_put_load_credential(ExecContext *c, const char *id, const char *path, bool encrypted) {
65✔
107
        ExecLoadCredential *old;
65✔
108
        int r;
65✔
109

110
        assert(c);
65✔
111
        assert(id);
65✔
112
        assert(path);
65✔
113

114
        old = hashmap_get(c->load_credentials, id);
65✔
115
        if (old) {
65✔
116
                r = free_and_strdup(&old->path, path);
×
UNCOV
117
                if (r < 0)
×
118
                        return r;
119

UNCOV
120
                old->encrypted = encrypted;
×
121
        } else {
UNCOV
122
                _cleanup_(exec_load_credential_freep) ExecLoadCredential *lc = NULL;
×
123

124
                lc = new(ExecLoadCredential, 1);
65✔
125
                if (!lc)
65✔
126
                        return -ENOMEM;
127

128
                *lc = (ExecLoadCredential) {
65✔
129
                        .id = strdup(id),
65✔
130
                        .path = strdup(path),
65✔
131
                        .encrypted = encrypted,
132
                };
133
                if (!lc->id || !lc->path)
65✔
134
                        return -ENOMEM;
135

136
                r = hashmap_ensure_put(&c->load_credentials, &exec_load_credential_hash_ops, lc->id, lc);
65✔
137
                assert(r != -EEXIST);
65✔
138
                if (r < 0)
65✔
139
                        return r;
140

141
                TAKE_PTR(lc);
65✔
142
        }
143

144
        return 0;
145
}
146

147
int exec_context_put_set_credential(
99✔
148
                ExecContext *c,
149
                const char *id,
150
                void *data_consume,
151
                size_t size,
152
                bool encrypted) {
153

154
        _cleanup_free_ void *data = data_consume;
99✔
155
        ExecSetCredential *old;
99✔
156
        int r;
99✔
157

158
        /* Takes the ownership of data both on success and failure */
159

160
        assert(c);
99✔
161
        assert(id);
99✔
162
        assert(data || size == 0);
99✔
163

164
        old = hashmap_get(c->set_credentials, id);
99✔
165
        if (old) {
99✔
UNCOV
166
                free_and_replace(old->data, data);
×
UNCOV
167
                old->size = size;
×
UNCOV
168
                old->encrypted = encrypted;
×
169
        } else {
UNCOV
170
                _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
×
171

172
                sc = new(ExecSetCredential, 1);
99✔
173
                if (!sc)
99✔
174
                        return -ENOMEM;
175

176
                *sc = (ExecSetCredential) {
99✔
177
                        .id = strdup(id),
99✔
178
                        .data = TAKE_PTR(data),
99✔
179
                        .size = size,
180
                        .encrypted = encrypted,
181
                };
182
                if (!sc->id)
99✔
183
                        return -ENOMEM;
184

185
                r = hashmap_ensure_put(&c->set_credentials, &exec_set_credential_hash_ops, sc->id, sc);
99✔
186
                assert(r != -EEXIST);
99✔
187
                if (r < 0)
99✔
188
                        return r;
189

190
                TAKE_PTR(sc);
99✔
191
        }
192

193
        return 0;
194
}
195

196
int exec_context_put_import_credential(ExecContext *c, const char *glob, const char *rename) {
8,643✔
197
        _cleanup_(exec_import_credential_freep) ExecImportCredential *ic = NULL;
8,643✔
198
        int r;
8,643✔
199

200
        assert(c);
8,643✔
201
        assert(glob);
8,643✔
202

203
        rename = empty_to_null(rename);
8,643✔
204

205
        ic = new(ExecImportCredential, 1);
8,643✔
206
        if (!ic)
8,643✔
207
                return -ENOMEM;
208

209
        *ic = (ExecImportCredential) {
8,643✔
210
                .glob = strdup(glob),
8,643✔
211
        };
212
        if (!ic->glob)
8,643✔
213
                return -ENOMEM;
214
        if (rename) {
8,643✔
215
                ic->rename = strdup(rename);
539✔
216
                if (!ic->rename)
539✔
217
                        return -ENOMEM;
218
        }
219

220
        if (ordered_set_contains(c->import_credentials, ic))
8,643✔
221
                return 0;
222

223
        r = ordered_set_ensure_put(&c->import_credentials, &exec_import_credential_hash_ops, ic);
8,642✔
224
        assert(r != -EEXIST);
8,642✔
225
        if (r < 0)
8,642✔
226
                return r;
227

228
        TAKE_PTR(ic);
8,642✔
229

230
        return 0;
8,642✔
231
}
232

233
bool exec_params_need_credentials(const ExecParameters *p) {
22,475✔
234
        assert(p);
22,475✔
235

236
        return p->flags & (EXEC_SETUP_CREDENTIALS|EXEC_SETUP_CREDENTIALS_FRESH);
22,475✔
237
}
238

239
bool exec_context_has_credentials(const ExecContext *c) {
21,324✔
240
        assert(c);
21,324✔
241

242
        return !hashmap_isempty(c->set_credentials) ||
21,324✔
243
                !hashmap_isempty(c->load_credentials) ||
21,324✔
244
                !ordered_set_isempty(c->import_credentials);
21,073✔
245
}
246

247
bool mount_point_is_credentials(const char *runtime_prefix, const char *path) {
101,565✔
248
        const char *e;
101,565✔
249

250
        assert(runtime_prefix);
101,565✔
251
        assert(path);
101,565✔
252

253
        e = path_startswith(path, runtime_prefix);
101,565✔
254
        if (!e)
101,565✔
255
                return false;
256

257
        return path_startswith(e, "credentials");
7,278✔
258
}
259

260
static int get_credential_directory(
15,801✔
261
                const char *runtime_prefix,
262
                const char *unit,
263
                char **ret) {
264

265
        char *p;
15,801✔
266

267
        assert(ret);
15,801✔
268

269
        if (!runtime_prefix || !unit) {
15,801✔
UNCOV
270
                *ret = NULL;
×
UNCOV
271
                return 0;
×
272
        }
273

274
        p = path_join(runtime_prefix, "credentials", unit);
15,801✔
275
        if (!p)
15,801✔
276
                return -ENOMEM;
277

278
        *ret = p;
15,801✔
279
        return 1;
15,801✔
280
}
281

282
int exec_context_get_credential_directory(
12,265✔
283
                const ExecContext *context,
284
                const ExecParameters *params,
285
                const char *unit,
286
                char **ret) {
287

288
        assert(context);
12,265✔
289
        assert(params);
12,265✔
290
        assert(unit);
12,265✔
291
        assert(ret);
12,265✔
292

293
        if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context)) {
12,265✔
294
                *ret = NULL;
9,532✔
295
                return 0;
9,532✔
296
        }
297

298
        return get_credential_directory(params->prefix[EXEC_DIRECTORY_RUNTIME], unit, ret);
2,733✔
299
}
300

301
int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) {
13,063✔
302
        _cleanup_free_ char *p = NULL;
13,063✔
303
        int r;
13,063✔
304

305
        assert(c);
13,063✔
306

307
        r = get_credential_directory(runtime_prefix, unit, &p);
13,063✔
308
        if (r <= 0)
13,063✔
309
                return r;
310

311
        /* This is either a tmpfs/ramfs of its own, or a plain directory. Either way, let's first try to
312
         * unmount it, and afterwards remove the mount point */
313
        (void) umount2(p, MNT_DETACH|UMOUNT_NOFOLLOW);
13,063✔
314
        (void) rm_rf(p, REMOVE_ROOT|REMOVE_CHMOD);
13,063✔
315

316
        return 0;
317
}
318

319
typedef struct SetupCredentialsContext {
320
        RuntimeScope scope;
321

322
        const ExecContext *exec_context;
323
        const char *unit;
324

325
        const char *runtime_prefix;
326

327
        const char *received_credentials_directory;
328
        const char *received_encrypted_credentials_directory;
329

330
        bool always_ipc;
331

332
        uid_t uid;
333
        gid_t gid;
334
} SetupCredentialsContext;
335

336
typedef struct LoadCredentialArguments {
337
        const SetupCredentialsContext *context;
338

339
        bool encrypted;
340

341
        int write_dfd;
342
        bool ownership_ok;
343

344
        uint64_t left;
345
} LoadCredentialArguments;
346

347
typedef enum CredentialSearchPath {
348
        CREDENTIAL_SEARCH_PATH_TRUSTED,
349
        CREDENTIAL_SEARCH_PATH_ENCRYPTED,
350
        CREDENTIAL_SEARCH_PATH_ALL,
351
        _CREDENTIAL_SEARCH_PATH_MAX,
352
        _CREDENTIAL_SEARCH_PATH_INVALID = -EINVAL,
353
} CredentialSearchPath;
354

355
static int credential_search_path(
8,010✔
356
                const SetupCredentialsContext *context,
357
                CredentialSearchPath path,
358
                char ***ret) {
359

360
        _cleanup_strv_free_ char **l = NULL;
8,010✔
361
        int r;
8,010✔
362

363
        assert(context);
8,010✔
364
        assert(path >= 0 && path < _CREDENTIAL_SEARCH_PATH_MAX);
8,010✔
365
        assert(ret);
8,010✔
366

367
        /* Assemble a search path to find credentials in. For non-encrypted credentials, We'll look in
368
         * /etc/credstore/ (and similar directories in /usr/lib/ + /run/). If we're looking for encrypted
369
         * credentials, we'll look in /etc/credstore.encrypted/ (and similar dirs). */
370

371
        if (IN_SET(path, CREDENTIAL_SEARCH_PATH_ENCRYPTED, CREDENTIAL_SEARCH_PATH_ALL)) {
8,010✔
372
                r = strv_extend(&l, context->received_encrypted_credentials_directory);
4,009✔
373
                if (r < 0)
4,009✔
UNCOV
374
                        return r;
×
375

376
                _cleanup_strv_free_ char **add = NULL;
4,009✔
377
                r = credential_store_path_encrypted(context->scope, &add);
4,009✔
378
                if (r < 0)
4,009✔
379
                        return r;
380

381
                r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ false);
4,009✔
382
                if (r < 0)
4,009✔
383
                        return r;
384
        }
385

386
        if (IN_SET(path, CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ALL)) {
8,010✔
387
                r = strv_extend(&l, context->received_credentials_directory);
4,009✔
388
                if (r < 0)
4,009✔
UNCOV
389
                        return r;
×
390

391
                _cleanup_strv_free_ char **add = NULL;
4,009✔
392
                r = credential_store_path(context->scope, &add);
4,009✔
393
                if (r < 0)
4,009✔
394
                        return r;
395

396
                r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ false);
4,009✔
397
                if (r < 0)
4,009✔
398
                        return r;
399
        }
400

401
        if (DEBUG_LOGGING) {
8,010✔
402
                _cleanup_free_ char *t = strv_join(l, ":");
16,020✔
403
                log_debug("Credential search path is: %s", strempty(t));
8,010✔
404
        }
405

406
        *ret = TAKE_PTR(l);
8,010✔
407
        return 0;
8,010✔
408
}
409

410
static int write_credential(
1,186✔
411
                int dfd,
412
                const char *id,
413
                const void *data,
414
                size_t size,
415
                uid_t uid,
416
                gid_t gid,
417
                bool ownership_ok) {
418

419
        _cleanup_close_ int fd = -EBADF;
1,186✔
420
        int r;
1,186✔
421

422
        assert(dfd >= 0);
1,186✔
423
        assert(id);
1,186✔
424
        assert(data || size == 0);
1,186✔
425

426
        fd = openat(dfd, id, O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0600);
1,186✔
427
        if (fd < 0)
1,186✔
UNCOV
428
                return -errno;
×
429

430
        r = loop_write(fd, data, size);
1,186✔
431
        if (r < 0)
1,186✔
432
                return r;
433

434
        r = RET_NERRNO(fchmod(fd, 0400)); /* Take away "w" bit */
1,186✔
UNCOV
435
        if (r < 0)
×
436
                return r;
437

438
        if (uid_is_valid(uid) && uid != getuid()) {
1,186✔
439
                r = fd_add_uid_acl_permission(fd, uid, ACL_READ);
92✔
440
                /* Ideally we use ACLs, since we can neatly express what we want to express:
441
                 * the user gets read access and nothing else. But if the backing fs can't
442
                 * support that (e.g. ramfs), then we can use file ownership instead. But that's
443
                 * only safe if we can then re-mount the whole thing read-only, so that the user
444
                 * can no longer chmod() the file to gain write access. */
445
                if ((ERRNO_IS_NEG_NOT_SUPPORTED(r) || ERRNO_IS_NEG_PRIVILEGE(r)) && ownership_ok)
92✔
446
                        r = RET_NERRNO(fchown(fd, uid, gid));
1,186✔
447
                if (r < 0)
92✔
UNCOV
448
                        return r;
×
449
        }
450

451
        return 0;
452
}
453

454
static int maybe_decrypt_and_write_credential(
1,186✔
455
                LoadCredentialArguments *args,
456
                const char *id,
457
                const char *data,
458
                size_t size,
459
                bool graceful) {
460

461
        _cleanup_(iovec_done_erase) struct iovec plaintext = {};
1,186✔
462
        size_t add;
1,186✔
463
        int r;
1,186✔
464

465
        assert(args);
1,186✔
466
        assert(args->write_dfd >= 0);
1,186✔
467
        assert(id);
1,186✔
468
        assert(data || size == 0);
1,186✔
469

470
        if (args->encrypted) {
1,186✔
471
                CredentialFlags flags = 0; /* only allow user creds in user scope */
12✔
472

473
                switch (args->context->scope) {
12✔
474

475
                case RUNTIME_SCOPE_SYSTEM:
5✔
476
                        /* In system mode talk directly to the TPM – unless we live in a device sandbox
477
                         * which might block TPM device access. */
478

479
                        flags |= CREDENTIAL_ANY_SCOPE;
5✔
480

481
                        if (!args->context->always_ipc) {
5✔
482
                                r = decrypt_credential_and_warn(
3✔
483
                                                id,
484
                                                now(CLOCK_REALTIME),
485
                                                /* tpm2_device= */ NULL,
486
                                                /* tpm2_signature_path= */ NULL,
487
                                                getuid(),
3✔
488
                                                &IOVEC_MAKE(data, size),
3✔
489
                                                flags,
490
                                                &plaintext);
491
                                break;
3✔
492
                        }
493

494
                        _fallthrough_;
9✔
495

496
                case RUNTIME_SCOPE_USER:
497
                        /* In per user mode we'll not have access to the machine secret, nor to the TPM (most
498
                         * likely), hence go via the IPC service instead. Do this if we are run in root's
499
                         * per-user invocation too, to minimize differences and because isolating this logic
500
                         * into a separate process is generally a good thing anyway. */
501
                        r = ipc_decrypt_credential(
9✔
502
                                        id,
503
                                        now(CLOCK_REALTIME),
504
                                        getuid(),
9✔
505
                                        &IOVEC_MAKE(data, size),
9✔
506
                                        flags,
507
                                        &plaintext);
508
                        break;
509

UNCOV
510
                default:
×
511
                        assert_not_reached();
×
512
                }
513
                if (r < 0) {
12✔
UNCOV
514
                        if (graceful) {
×
UNCOV
515
                                log_warning_errno(r, "Unable to decrypt credential '%s', skipping: %m", id);
×
UNCOV
516
                                return 0;
×
517
                        }
518

519
                        return r;
520
                }
521

522
                data = plaintext.iov_base;
12✔
523
                size = plaintext.iov_len;
12✔
524
        }
525

526
        add = strlen(id) + size;
1,186✔
527
        if (add > args->left)
1,186✔
528
                return -E2BIG;
529

530
        r = write_credential(args->write_dfd, id, data, size, args->context->uid, args->context->gid, args->ownership_ok);
1,186✔
531
        if (r < 0)
1,186✔
532
                return log_debug_errno(r, "Failed to write credential '%s': %m", id);
×
533

534
        args->left -= add;
1,186✔
535

536
        return 0;
1,186✔
537
}
538

539
static int load_credential_glob(
8,002✔
540
                LoadCredentialArguments *args,
541
                const ExecImportCredential *ic,
542
                char * const *search_path,
543
                ReadFullFileFlags flags) {
544

545
        int r;
8,002✔
546

547
        assert(args);
8,002✔
548
        assert(args->write_dfd >= 0);
8,002✔
549
        assert(ic);
8,002✔
550
        assert(search_path);
8,002✔
551

552
        STRV_FOREACH(d, search_path) {
43,993✔
UNCOV
553
                _cleanup_strv_free_ char **paths = NULL;
×
554
                _cleanup_free_ char *j = NULL;
35,991✔
555

556
                j = path_join(*d, ic->glob);
35,991✔
557
                if (!j)
35,991✔
558
                        return -ENOMEM;
559

560
                r = safe_glob(j, /* flags= */ 0, &paths);
35,991✔
561
                if (r == -ENOENT)
35,991✔
562
                        continue;
35,184✔
563
                if (r < 0)
807✔
564
                        return r;
565

566
                STRV_FOREACH(p, paths) {
1,896✔
567
                        _cleanup_free_ char *fn = NULL;
1,089✔
568
                        _cleanup_(erase_and_freep) char *data = NULL;
1,089✔
569
                        size_t size;
1,089✔
570

571
                        r = path_extract_filename(*p, &fn);
1,089✔
572
                        if (r < 0)
1,089✔
UNCOV
573
                                return log_debug_errno(r, "Failed to extract filename from '%s': %m", *p);
×
574

575
                        if (ic->rename) {
1,089✔
UNCOV
576
                                _cleanup_free_ char *renamed = NULL;
×
577

578
                                renamed = strjoin(ic->rename, fn + strlen(ic->glob) - !!endswith(ic->glob, "*"));
182✔
579
                                if (!renamed)
182✔
UNCOV
580
                                        return log_oom_debug();
×
581

582
                                free_and_replace(fn, renamed);
182✔
583
                        }
584

585
                        if (!credential_name_valid(fn)) {
1,089✔
586
                                log_debug("Skipping credential with invalid name: %s", fn);
11✔
587
                                continue;
11✔
588
                        }
589

590
                        if (faccessat(args->write_dfd, fn, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
1,078✔
591
                                log_debug("Skipping credential with duplicated ID %s at %s", fn, *p);
16✔
592
                                continue;
16✔
593
                        }
594
                        if (errno != ENOENT)
1,062✔
UNCOV
595
                                return log_debug_errno(errno, "Failed to test if credential %s exists: %m", fn);
×
596

597
                        /* path is absolute, hence pass AT_FDCWD as nop dir fd here */
598
                        r = read_full_file_full(
1,062✔
599
                                        AT_FDCWD,
600
                                        *p,
601
                                        UINT64_MAX,
602
                                        args->encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
1,062✔
603
                                        flags,
604
                                        NULL,
605
                                        &data, &size);
606
                        if (r < 0)
1,062✔
UNCOV
607
                                return log_debug_errno(r, "Failed to read credential '%s': %m", *p);
×
608

609
                        r = maybe_decrypt_and_write_credential(args, fn, data, size, /* graceful= */ true);
1,062✔
610
                        if (r < 0)
1,062✔
611
                                return r;
612
                }
613
        }
614

615
        return 0;
616
}
617

618
static int load_credential(
34✔
619
                LoadCredentialArguments *args,
620
                const char *id,
621
                int read_dfd,
622
                const char *path) {
623

624
        ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
34✔
UNCOV
625
        _cleanup_strv_free_ char **search_path = NULL;
×
626
        _cleanup_free_ char *bindname = NULL;
34✔
627
        const char *source = NULL;
34✔
628
        bool missing_ok;
34✔
629
        _cleanup_(erase_and_freep) char *data = NULL;
34✔
630
        size_t size, maxsz;
34✔
631
        int r;
34✔
632

633
        assert(args);
34✔
634
        assert(args->context);
34✔
635
        assert(args->context->exec_context);
34✔
636
        assert(args->context->unit);
34✔
637
        assert(id);
34✔
638
        assert(read_dfd >= 0 || read_dfd == AT_FDCWD);
34✔
639
        assert(path);
34✔
640

641
        if (read_dfd >= 0) {
34✔
642
                /* If a directory fd is specified, then read the file directly from that dir. In this case we
643
                 * won't do AF_UNIX stuff (we simply don't want to recursively iterate down a tree of AF_UNIX
644
                 * IPC sockets). It's OK if a file vanishes here in the time we enumerate it and intend to
645
                 * open it. */
646

647
                if (!filename_is_valid(path)) /* safety check */
4✔
648
                        return -EINVAL;
649

650
                missing_ok = true;
651
                source = path;
652

653
        } else if (path_is_absolute(path)) {
30✔
654
                /* If this is an absolute path, read the data directly from it, and support AF_UNIX
655
                 * sockets */
656

657
                if (!path_is_valid(path)) /* safety check */
22✔
658
                        return -EINVAL;
659

660
                flags |= READ_FULL_FILE_CONNECT_SOCKET;
22✔
661

662
                /* Pass some minimal info about the unit and the credential name we are looking to acquire
663
                 * via the source socket address in case we read off an AF_UNIX socket. */
664
                if (asprintf(&bindname, "@%" PRIx64 "/unit/%s/%s", random_u64(), args->context->unit, id) < 0)
22✔
665
                        return -ENOMEM;
666

667
                missing_ok = false;
668
                source = path;
669

670
        } else if (credential_name_valid(path)) {
8✔
671
                /* If this is a relative path, take it as credential name relative to the credentials
672
                 * directory we received ourselves. We don't support the AF_UNIX stuff in this mode, since we
673
                 * are operating on a credential store, i.e. this is guaranteed to be regular files. */
674

675
                r = credential_search_path(args->context, CREDENTIAL_SEARCH_PATH_ALL, &search_path);
8✔
676
                if (r < 0)
8✔
677
                        return r;
678

679
                missing_ok = true;
680
        } else
681
                return -EINVAL;
682

683
        if (args->encrypted) {
34✔
684
                flags |= READ_FULL_FILE_UNBASE64;
3✔
685
                maxsz = CREDENTIAL_ENCRYPTED_SIZE_MAX;
3✔
686
        } else
687
                maxsz = CREDENTIAL_SIZE_MAX;
688

689
        if (search_path)
34✔
690
                STRV_FOREACH(d, search_path) {
44✔
691
                        _cleanup_free_ char *j = NULL;
44✔
692

693
                        j = path_join(*d, path);
44✔
694
                        if (!j)
44✔
UNCOV
695
                                return -ENOMEM;
×
696

697
                        r = read_full_file_full(
44✔
698
                                        AT_FDCWD, j, /* path is absolute, hence pass AT_FDCWD as nop dir fd here */
699
                                        UINT64_MAX,
700
                                        maxsz,
701
                                        flags,
702
                                        NULL,
703
                                        &data, &size);
704
                        if (r != -ENOENT)
44✔
705
                                break;
706
                }
707
        else if (source)
26✔
708
                r = read_full_file_full(
26✔
709
                                read_dfd, source,
710
                                UINT64_MAX,
711
                                maxsz,
712
                                flags,
713
                                bindname,
714
                                &data, &size);
715
        else
UNCOV
716
                assert_not_reached();
×
717

718
        if (r == -ENOENT) {
34✔
719
                bool in_set_credentials = hashmap_contains(args->context->exec_context->set_credentials, id);
1✔
720
                if (missing_ok || in_set_credentials) {
1✔
721
                        /* Make a missing inherited credential non-fatal, let's just continue. After all apps
722
                         * will get clear errors if we don't pass such a missing credential on as they
723
                         * themselves will get ENOENT when trying to read them, which should not be much
724
                         * worse than when we handle the error here and make it fatal.
725
                         *
726
                         * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
727
                         * we are fine, too. */
728
                        log_full_errno(in_set_credentials ? LOG_DEBUG : LOG_INFO,
1✔
729
                                       r, "Couldn't read inherited credential '%s', skipping: %m", path);
730
                        return 0;
1✔
731
                }
732
        }
733
        if (r < 0)
33✔
UNCOV
734
                return log_debug_errno(r, "Failed to read credential '%s': %m", path);
×
735

736
        return maybe_decrypt_and_write_credential(args, id, data, size, /* graceful= */ false);
33✔
737
}
738

739
static int load_cred_recurse_dir_cb(
6✔
740
                RecurseDirEvent event,
741
                const char *path,
742
                int dir_fd,
743
                int inode_fd,
744
                const struct dirent *de,
745
                const struct statx *sx,
746
                void *userdata) {
747

748
        LoadCredentialArguments *args = ASSERT_PTR(userdata);
6✔
749
        _cleanup_free_ char *sub_id = NULL;
6✔
750
        int r;
6✔
751

752
        assert(path);
6✔
753
        assert(de);
6✔
754

755
        if (event != RECURSE_DIR_ENTRY)
6✔
756
                return RECURSE_DIR_CONTINUE;
757

758
        if (!IN_SET(de->d_type, DT_REG, DT_SOCK))
4✔
759
                return RECURSE_DIR_CONTINUE;
760

761
        sub_id = strreplace(path, "/", "_");
4✔
762
        if (!sub_id)
4✔
763
                return -ENOMEM;
764

765
        if (!credential_name_valid(sub_id))
4✔
UNCOV
766
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Credential would get ID '%s', which is not valid, refusing.", sub_id);
×
767

768
        if (faccessat(args->write_dfd, sub_id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
4✔
UNCOV
769
                log_debug("Skipping credential with duplicated ID %s at %s", sub_id, path);
×
UNCOV
770
                return RECURSE_DIR_CONTINUE;
×
771
        }
772
        if (errno != ENOENT)
4✔
UNCOV
773
                return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sub_id);
×
774

775
        r = load_credential(args,
8✔
776
                            sub_id,
777
                            dir_fd, de->d_name);
4✔
778
        if (r < 0)
4✔
UNCOV
779
                return r;
×
780

781
        return RECURSE_DIR_CONTINUE;
782
}
783

784
static bool device_nodes_restricted(
2,079✔
785
                const ExecContext *c,
786
                const CGroupContext *cgroup_context) {
787

788
        assert(c);
2,079✔
789
        assert(cgroup_context);
2,079✔
790

791
        /* Returns true if we have any reason to believe we might not be able to access the TPM device
792
         * directly, even if we run as root/PID 1. This could be because /dev/ is replaced by a private
793
         * version, or because a device node access list is configured. */
794

795
        if (c->private_devices)
2,079✔
796
                return true;
797

798
        if (cgroup_context_has_device_policy(cgroup_context))
1,902✔
799
                return true;
576✔
800

801
        return false;
802
}
803

804
static int acquire_credentials(
2,080✔
805
                const SetupCredentialsContext *context,
806
                int dfd,
807
                bool ownership_ok) {
808

809
        int r;
2,080✔
810

811
        assert(context);
2,080✔
812
        assert(context->exec_context);
2,080✔
813
        assert(dfd >= 0);
2,080✔
814

815
        LoadCredentialArguments args = {
2,080✔
816
                .context = context,
817

818
                .write_dfd = dfd,
819
                .ownership_ok = ownership_ok,
820
                .left = CREDENTIALS_TOTAL_SIZE_MAX,
821
        };
822

823
        /* First, load credentials off disk (or acquire via AF_UNIX socket) */
824
        ExecLoadCredential *lc;
2,080✔
825
        HASHMAP_FOREACH(lc, context->exec_context->load_credentials) {
4,191✔
826
                _cleanup_close_ int sub_fd = -EBADF;
2,111✔
827

828
                args.encrypted = lc->encrypted;
31✔
829

830
                /* If this is an absolute path, then try to open it as a directory. If that works, then we'll
831
                 * recurse into it. If it is an absolute path but it isn't a directory, then we'll open it as
832
                 * a regular file. Finally, if it's a relative path we will use it as a credential name to
833
                 * propagate a credential passed to us from further up. */
834

835
                if (path_is_absolute(lc->path)) {
31✔
836
                        sub_fd = open(lc->path, O_DIRECTORY|O_CLOEXEC);
23✔
837
                        if (sub_fd < 0 && !IN_SET(errno,
23✔
838
                                                  ENOTDIR,  /* Not a directory */
839
                                                  ENOENT))  /* Doesn't exist? */
UNCOV
840
                                return log_debug_errno(errno, "Failed to open credential source '%s': %m", lc->path);
×
841
                }
842

843
                if (sub_fd < 0)
22✔
844
                        /* Regular file (incl. a credential passed in from higher up) */
845
                        r = load_credential(&args,
30✔
846
                                            lc->id,
30✔
847
                                            AT_FDCWD, lc->path);
30✔
848
                else
849
                        /* Directory */
850
                        r = recurse_dir(sub_fd,
1✔
851
                                        /* path= */ lc->id, /* recurse_dir() will suffix the subdir paths from here to the top-level id */
1✔
852
                                        /* statx_mask= */ 0,
853
                                        /* n_depth_max= */ UINT_MAX,
854
                                        RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE,
855
                                        load_cred_recurse_dir_cb,
856
                                        &args);
857
                if (r < 0)
31✔
858
                        return r;
859
        }
860

861
        /* Next, look for system credentials and credentials in the credentials store. Note that these do not
862
         * override any credentials found earlier. */
863
        ExecImportCredential *ic;
2,080✔
864
        ORDERED_SET_FOREACH(ic, context->exec_context->import_credentials) {
6,081✔
865
                _cleanup_free_ char **search_path = NULL;
4,001✔
866

867
                r = credential_search_path(context, CREDENTIAL_SEARCH_PATH_TRUSTED, &search_path);
4,001✔
868
                if (r < 0)
4,001✔
869
                        return r;
870

871
                args.encrypted = false;
4,001✔
872

873
                r = load_credential_glob(
4,001✔
874
                                &args,
875
                                ic,
876
                                search_path,
877
                                READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER);
878
                if (r < 0)
4,001✔
879
                        return r;
880

881
                search_path = strv_free(search_path);
4,001✔
882

883
                r = credential_search_path(context, CREDENTIAL_SEARCH_PATH_ENCRYPTED, &search_path);
4,001✔
884
                if (r < 0)
4,001✔
885
                        return r;
886

887
                args.encrypted = true;
4,001✔
888

889
                r = load_credential_glob(
4,001✔
890
                                &args,
891
                                ic,
892
                                search_path,
893
                                READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_UNBASE64);
894
                if (r < 0)
4,001✔
895
                        return r;
896
        }
897

898
        /* Finally, we add in literally specified credentials. If the credentials already exist, we'll not
899
         * add them, so that they can act as a "default" if the same credential is specified multiple times. */
900
        ExecSetCredential *sc;
2,080✔
901
        HASHMAP_FOREACH(sc, context->exec_context->set_credentials) {
2,173✔
902
                args.encrypted = sc->encrypted;
93✔
903

904
                if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
93✔
905
                        log_debug("Skipping credential with duplicated ID %s", sc->id);
2✔
906
                        continue;
2✔
907
                }
908
                if (errno != ENOENT)
91✔
UNCOV
909
                        return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sc->id);
×
910

911
                r = maybe_decrypt_and_write_credential(&args, sc->id, sc->data, sc->size, /* graceful= */ false);
91✔
912
                if (r < 0)
91✔
913
                        return r;
914
        }
915

916
        return 0;
2,080✔
917
}
918

919
static int credentials_dir_finalize_permissions(int dfd, uid_t uid, gid_t gid, bool ownership_ok) {
2,080✔
920
        int r;
2,080✔
921

922
        assert(dfd >= 0);
2,080✔
923

924
        r = fd_acl_make_read_only(dfd); /* Take away the "w" bit */
2,080✔
925
        if (r < 0)
2,080✔
926
                return r;
927

928
        /* After we created all keys with the right perms, also make sure the credential store as a whole is
929
         * accessible */
930

931
        if (uid_is_valid(uid) && uid != getuid()) {
2,080✔
932
                r = fd_add_uid_acl_permission(dfd, uid, ACL_READ | ACL_EXECUTE);
687✔
933
                if (r < 0) {
687✔
UNCOV
934
                        if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
×
935
                                return r;
936

UNCOV
937
                        if (!ownership_ok)
×
938
                                return r;
939

UNCOV
940
                        if (fchown(dfd, uid, gid) < 0)
×
UNCOV
941
                                return -errno;
×
942
                }
943
        }
944

945
        return 0;
946
}
947

948
static int setup_credentials_plain_dir(
7✔
949
                const SetupCredentialsContext *context,
950
                const char *cred_dir) {
951

952
        _cleanup_free_ char *t = NULL, *workspace = NULL;
7✔
UNCOV
953
        _cleanup_(rm_rf_safep) const char *workspace_rm = NULL;
×
954
        _cleanup_close_ int dfd = -EBADF;
7✔
955
        int r;
7✔
956

957
        assert(context);
7✔
958
        assert(context->unit);
7✔
959
        assert(context->runtime_prefix);
7✔
960
        assert(cred_dir);
7✔
961

962
        /* Temporary workspace, that remains inaccessible all the time. We prepare stuff there before moving
963
         * it into place, so that users can't access half-initialized credential stores. */
964
        t = path_join(context->runtime_prefix, "systemd/temporary-credentials");
7✔
965
        if (!t)
7✔
966
                return -ENOMEM;
967

968
        r = mkdir_label(t, 0700);
7✔
969
        if (r < 0 && r != -EEXIST)
7✔
970
                return r;
971

972
        workspace = path_join(t, context->unit);
7✔
973
        if (!workspace)
7✔
974
                return -ENOMEM;
975

976
        dfd = open_mkdir(workspace, O_CLOEXEC|O_EXCL, 0700);
7✔
977
        if (dfd < 0)
7✔
UNCOV
978
                return log_debug_errno(dfd, "Failed to create workspace for credentials: %m");
×
979
        workspace_rm = workspace;
7✔
980

981
        (void) label_fix_full(dfd, /* inode_path= */ NULL, cred_dir, /* flags= */ 0);
7✔
982

983
        r = acquire_credentials(context, dfd, /* ownership_ok= */ false);
7✔
984
        if (r < 0)
7✔
985
                return r;
986

987
        r = RET_NERRNO(rename(workspace, cred_dir));
7✔
988
        if (r >= 0)
2✔
989
                workspace_rm = NULL;
5✔
990
        if (IN_SET(r, -ENOTEMPTY, -EEXIST)) {
7✔
991
                _cleanup_close_ int old_dfd = open(cred_dir, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
9✔
992
                if (old_dfd < 0)
2✔
UNCOV
993
                        return log_debug_errno(errno, "Failed to open credentials dir '%s': %m", cred_dir);
×
994

995
                (void) fd_acl_make_writable(old_dfd);
2✔
996

997
                log_debug_errno(r, "Credential dir '%s' already populated, exchanging with workspace.", cred_dir);
2✔
998
                r = RET_NERRNO(renameat2(AT_FDCWD, workspace, AT_FDCWD, cred_dir, RENAME_EXCHANGE));
2✔
999
        }
1000
        if (r < 0)
7✔
UNCOV
1001
                return log_debug_errno(r, "Failed to move credentials workspace into place: %m");
×
1002

1003
        /* rename() requires both the source and target to be writable, hence lock down write permission
1004
         * as last step. */
1005
        r = credentials_dir_finalize_permissions(dfd, context->uid, context->gid, /* ownership_ok= */ false);
7✔
1006
        if (r < 0)
7✔
UNCOV
1007
                return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m");
×
1008

1009
        return 0;
1010
}
1011

1012
static int setup_credentials_internal(
2,084✔
1013
                const SetupCredentialsContext *context,
1014
                bool may_reuse,
1015
                const char *cred_dir) {
1016

1017
        _cleanup_close_ int fs_fd = -EBADF, mfd = -EBADF, dfd = -EBADF;
4,168✔
1018
        bool dir_mounted;
2,084✔
1019
        int r;
2,084✔
1020

1021
        assert(context);
2,084✔
1022
        assert(cred_dir);
2,084✔
1023

1024
        r = path_is_mount_point(cred_dir);
2,084✔
1025
        if (r < 0)
2,084✔
UNCOV
1026
                return log_debug_errno(r, "Failed to determine if '%s' is a mountpoint: %m", cred_dir);
×
1027
        dir_mounted = r > 0;
2,084✔
1028

1029
        if (may_reuse) {
2,084✔
1030
                bool populated;
7✔
1031

1032
                /* If the cred dir is a mount, let's treat it as populated, and only look at the contents
1033
                 * if it's a plain dir, where we can't reasonably differentiate populated yet empty vs
1034
                 * not set up. */
1035

1036
                if (dir_mounted)
7✔
1037
                        populated = true;
1038
                else {
1039
                        r = dir_is_empty(cred_dir, /* ignore_hidden_or_backup= */ false);
3✔
1040
                        if (r < 0)
3✔
1041
                                return r;
1042
                        populated = r == 0;
3✔
1043
                }
1044
                if (populated) {
3✔
1045
                        log_debug("Credential dir for unit '%s' already set up, skipping.", context->unit);
4✔
1046
                        return 0;
4✔
1047
                }
1048
        }
1049

1050
        mfd = fsmount_credentials_fs(&fs_fd);
2,080✔
1051
        if (ERRNO_IS_NEG_PRIVILEGE(mfd) && !dir_mounted) {
2,080✔
1052
                log_debug_errno(mfd, "Lacking privilege to mount credentials fs, falling back to plain directory.");
7✔
1053
                return setup_credentials_plain_dir(context, cred_dir);
7✔
1054
        }
1055
        if (mfd < 0)
2,073✔
UNCOV
1056
                return log_debug_errno(mfd, "Failed to mount credentials fs: %m");
×
1057

1058
        dfd = fd_reopen(mfd, O_DIRECTORY|O_CLOEXEC);
2,073✔
1059
        if (dfd < 0)
2,073✔
1060
                return dfd;
1061

1062
        (void) label_fix_full(dfd, /* inode_path= */ NULL, cred_dir, /* flags= */ 0);
2,073✔
1063

1064
        r = acquire_credentials(context, dfd, /* ownership_ok= */ true);
2,073✔
1065
        if (r < 0)
2,073✔
1066
                return r;
1067

1068
        r = credentials_dir_finalize_permissions(dfd, context->uid, context->gid, /* ownership_ok= */ true);
2,073✔
1069
        if (r < 0)
2,073✔
1070
                return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m");
×
1071

1072
        // Work around a kernel bug that results in tmpfs reconfiguration failure.
1073
        // FIXME: drop this once https://lore.kernel.org/linux-fsdevel/20251108190930.440685-1-me@yhndnzj.com/
1074
        // is merged and hits the distro kernels.
1075
        (void) fsconfig(fs_fd, FSCONFIG_SET_FLAG, "noswap", NULL, 0);
2,073✔
1076

1077
        if (fsconfig(fs_fd, FSCONFIG_SET_FLAG, "ro", NULL, 0) < 0)
2,073✔
UNCOV
1078
                return -errno;
×
1079

1080
        if (fsconfig(fs_fd, FSCONFIG_CMD_RECONFIGURE, NULL, NULL, 0) < 0)
2,073✔
1081
                return -errno;
×
1082

1083
        log_debug("Successfully reconfigured credentials fs to be read only.");
2,073✔
1084

1085
        if (dir_mounted) {
2,073✔
1086
                /* Firstly, try to move beneath the existing mount, which guarantees strictly atomic replacement
1087
                 * (needs kernel >= 6.5) */
1088
                r = move_mount(mfd, "", AT_FDCWD, cred_dir, MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_BENEATH);
9✔
1089
                if (r >= 0)
9✔
1090
                        return umount_verbose(LOG_DEBUG, cred_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
9✔
UNCOV
1091
                if (errno != EINVAL)
×
UNCOV
1092
                        return log_debug_errno(errno, "Failed to move credentials fs into place: %m");
×
1093

UNCOV
1094
                log_debug_errno(errno, "Unable to move credentials fs beneath existing mount '%s', unmounting instead: %m",
×
1095
                                cred_dir);
1096

UNCOV
1097
                r = umount_verbose(LOG_DEBUG, cred_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
×
UNCOV
1098
                if (r < 0)
×
1099
                        return r;
1100
        }
1101

1102
        r = move_mount(mfd, "", AT_FDCWD, cred_dir, MOVE_MOUNT_F_EMPTY_PATH);
2,064✔
1103
        if (r < 0)
2,064✔
UNCOV
1104
                return log_debug_errno(errno, "Failed to move credentials fs into place: %m");
×
1105

1106
        return 0;
1107
}
1108

1109
int exec_setup_credentials(
10,210✔
1110
                const ExecContext *context,
1111
                const CGroupContext *cgroup_context,
1112
                const ExecParameters *params,
1113
                uid_t uid,
1114
                gid_t gid) {
1115

1116
        _cleanup_free_ char *p = NULL, *q = NULL;
10,210✔
1117
        int r;
10,210✔
1118

1119
        assert(context);
10,210✔
1120
        assert(params);
10,210✔
1121

1122
        if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context))
10,210✔
1123
                return 0;
8,131✔
1124

1125
        if (!params->prefix[EXEC_DIRECTORY_RUNTIME])
2,079✔
1126
                return -EINVAL;
1127

1128
        /* This is where we'll place stuff when we are done; the main credentials directory is world-readable,
1129
         * and the subdir we mount over with a read-only file system readable by the service's user. */
1130
        q = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials");
2,079✔
1131
        if (!q)
2,079✔
1132
                return -ENOMEM;
1133

1134
        r = mkdir_label(q, 0755); /* top-level dir: world readable/searchable */
2,079✔
1135
        if (r < 0 && r != -EEXIST)
2,079✔
1136
                return r;
1137

1138
        p = path_join(q, params->unit_id);
2,079✔
1139
        if (!p)
2,079✔
1140
                return -ENOMEM;
1141

1142
        r = mkdir_label(p, 0700); /* per-unit dir: private to user */
2,079✔
1143
        if (r < 0 && r != -EEXIST)
2,079✔
1144
                return r;
1145

1146
        SetupCredentialsContext ctx = {
4,158✔
1147
                .scope = params->runtime_scope,
2,079✔
1148
                .exec_context = context,
1149
                .unit = params->unit_id,
2,079✔
1150

1151
                .runtime_prefix = params->prefix[EXEC_DIRECTORY_RUNTIME],
2,079✔
1152
                .received_credentials_directory = params->received_credentials_directory,
2,079✔
1153
                .received_encrypted_credentials_directory = params->received_encrypted_credentials_directory,
2,079✔
1154

1155
                .always_ipc = device_nodes_restricted(context, cgroup_context),
2,079✔
1156

1157
                .uid = uid,
1158
                .gid = gid,
1159
        };
1160

1161
        r = setup_credentials_internal(&ctx, /* may_reuse = */ !FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH), p);
2,079✔
1162
        if (r < 0)
2,079✔
UNCOV
1163
                (void) rmdir(p);
×
1164

1165
        return r;
1166
}
1167

1168
static int refresh_credentials_in_namespace_child(int cfd, const char *cred_dir) {
3✔
1169
        int r;
3✔
1170

1171
        assert(cfd >= 0);
3✔
1172
        assert(cred_dir);
3✔
1173

1174
        /* Paranoia: before doing anything, check if the credentials tree inside the mountns is available.
1175
         *
1176
         * Note that setup_namespace() always installs a mount for cred dir, hence path_is_mount_point()
1177
         * is the appropriate check here. */
1178
        r = path_is_mount_point(cred_dir);
3✔
1179
        if (IN_SET(r, 0, -ENOENT)) {
3✔
UNCOV
1180
                log_full_errno_zerook(LOG_WARNING, r,
×
1181
                                      "Credentials tree in the unit mount namespace is masked, skipping refresh.");
UNCOV
1182
                return 0;
×
1183
        }
1184
        if (r < 0)
3✔
UNCOV
1185
                return log_error_errno(r, "Failed to check whether '%s' is a mountpoint in unit mount namespace: %m",
×
1186
                                       cred_dir);
1187

1188
        /* Inform the parent that we're good to go */
1189
        ssize_t n = write(cfd, &r, sizeof(r));
3✔
1190
        if (n < 0)
3✔
UNCOV
1191
                return log_error_errno(errno, "Failed to write to socket: %m");
×
1192

1193
        _cleanup_close_ int mfd = receive_one_fd(cfd, /* flags = */ 0);
6✔
1194
        if (mfd < 0)
3✔
UNCOV
1195
                return log_error_errno(mfd, "Failed to receive credentials tree fd from socket: %m");
×
1196

1197
        r = mount_exchange_graceful(mfd, cred_dir, /* mount_beneath = */ true);
3✔
1198
        if (r < 0)
3✔
UNCOV
1199
                return log_error_errno(r, "Failed to update credentials mount in namespace: %m");
×
1200

1201
        return 1;
1202
}
1203

1204
int unit_refresh_credentials(Unit *u) {
5✔
1205
        _cleanup_free_ char *cred_dir = NULL;
5✔
1206
        int r;
5✔
1207

1208
        /* Refresh the credentials for a unit, potentially forking off a second process to join the mountns
1209
         * if needed. Returns > 0 on successful refresh, == 0 if the credentials tree is masked and the operation
1210
         * is skipped. */
1211

1212
        assert(u);
5✔
1213
        assert(u->manager);
5✔
1214

1215
        r = get_credential_directory(u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id, &cred_dir);
5✔
1216
        if (r < 0)
5✔
UNCOV
1217
                return log_oom();
×
1218
        assert(r > 0);
5✔
1219

1220
        if (access(cred_dir, F_OK) < 0) {
5✔
UNCOV
1221
                if (errno == ENOENT) {
×
UNCOV
1222
                        log_warning_errno(errno, "Requested to refresh credentials, but credentials aren't populated, skipping.");
×
UNCOV
1223
                        return 0;
×
1224
                }
1225

UNCOV
1226
                return log_error_errno(errno, "Failed to check if credentials dir '%s' exists: %m", cred_dir);
×
1227
        }
1228

1229
        _cleanup_close_pair_ int tunnel_fds[2] = EBADF_PAIR;
5✔
UNCOV
1230
        _cleanup_(pidref_done) PidRef child = PIDREF_NULL;
×
1231
        _cleanup_close_ int userns_fd = -EBADF;
5✔
1232

1233
        PidRef *main_pid = unit_main_pid(u);
5✔
1234
        if (pidref_is_set(main_pid)) {
5✔
1235
                _cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF;
15✔
1236

1237
                r = pidref_namespace_open(main_pid,
5✔
1238
                                          &pidns_fd,
1239
                                          &mntns_fd,
1240
                                          /* ret_netns_fd = */ NULL,
1241
                                          MANAGER_IS_USER(u->manager) ? &userns_fd : NULL,
5✔
1242
                                          &root_fd);
1243
                if (r < 0)
5✔
UNCOV
1244
                        return log_error_errno(r, "Failed to open namespace of unit main process '" PID_FMT "': %m",
×
1245
                                               main_pid->pid);
1246

1247
                r = is_our_namespace(mntns_fd, NAMESPACE_MOUNT);
5✔
1248
                if (r < 0)
5✔
UNCOV
1249
                        return log_error_errno(r, "Failed to check if main process resides in a separate mount namespace: %m");
×
1250
                if (r == 0) {
5✔
1251
                        if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, tunnel_fds) < 0)
4✔
UNCOV
1252
                                return log_error_errno(errno, "Failed to allocate socket pair: %m");
×
1253

1254
                        r = namespace_fork_full("(sd-creds-ns)", "(sd-creds-ns-inner)",
11✔
1255
                                                (int[]) { tunnel_fds[1] }, 1,
4✔
1256
                                                FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG,
1257
                                                pidns_fd, mntns_fd, /* netns_fd = */ -EBADF, userns_fd, root_fd,
1258
                                                /* delegated = */ MANAGER_IS_USER(u->manager),
4✔
1259
                                                &child);
1260
                        if (r < 0)
7✔
UNCOV
1261
                                return log_full_errno(ERRNO_IS_NEG_PRIVILEGE(r) ? LOG_WARNING : LOG_ERR, r,
×
1262
                                                      "Failed to fork off process into unit namespace to refresh credentials: %m");
1263
                        if (r == 0) {
7✔
1264
                                r = refresh_credentials_in_namespace_child(tunnel_fds[1], cred_dir);
3✔
1265
                                report_errno_and_exit(tunnel_fds[1], r);
3✔
1266
                        }
1267

1268
                        tunnel_fds[1] = safe_close(tunnel_fds[1]);
4✔
1269

1270
                        /* Wait for the child to validate the creds tree in the unit namespace is populated. */
1271
                        ssize_t n = read(tunnel_fds[0], &r, sizeof(r));
4✔
1272
                        if (n < 0)
4✔
UNCOV
1273
                                return log_error_errno(errno, "Failed to read from socket: %m");
×
1274
                        if (!IN_SET(n, 0, sizeof(r)))
4✔
UNCOV
1275
                                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Received unexpected amount of bytes (%zi) while reading errno.", n);
×
1276
                        if (n == 0 || r == 0) {
4✔
1277
                                /* The child exited without sending anything or 0 is received signifying
1278
                                 * the credentials are masked? Check the exit status to be sure. */
UNCOV
1279
                                r = pidref_wait_for_terminate_and_check("(sd-creds-ns)", &child, WAIT_LOG);
×
UNCOV
1280
                                if (r < 0)
×
1281
                                        return r;
UNCOV
1282
                                if (r != EXIT_SUCCESS)
×
1283
                                        return -EPROTO;
1284

UNCOV
1285
                                return 0; /* skipped */
×
1286
                        }
1287
                        if (r < 0)
4✔
1288
                                return r;
1289

1290
                        /* Yay! Got > 0 from child indicating all good, proceed with refreshing. */
1291
                }
1292
        }
1293

1294
        SetupCredentialsContext ctx = {
10✔
1295
                .scope = u->manager->runtime_scope,
5✔
1296
                .exec_context = ASSERT_PTR(unit_get_exec_context(u)),
5✔
1297
                .unit = u->id,
5✔
1298

1299
                .runtime_prefix = u->manager->prefix[EXEC_DIRECTORY_RUNTIME],
5✔
1300
                .received_credentials_directory = u->manager->received_credentials_directory,
5✔
1301
                .received_encrypted_credentials_directory = u->manager->received_encrypted_credentials_directory,
5✔
1302

1303
                .always_ipc = false, /* we don't migrate to unit cgroup, hence cannot be restricted by cgroup bpf */
1304

1305
                .uid = u->ref_uid,
5✔
1306
                .gid = u->ref_gid,
5✔
1307
        };
1308

1309
        r = setup_credentials_internal(&ctx, /* may_reuse = */ false, cred_dir);
5✔
1310
        if (r < 0)
5✔
UNCOV
1311
                return log_error_errno(r, "Failed to set up credentials: %m");
×
1312

1313
        /* The main process doesn't run in a mountns hence nothing got forked off? Then we're all set. */
1314
        if (!pidref_is_set(&child))
5✔
1315
                return 1;
1316

1317
        if (userns_fd >= 0) {
4✔
1318
                assert(MANAGER_IS_USER(u->manager));
1✔
1319

1320
                /* Enter the unit userns now and unshare mountns, so that we have permissions to clone
1321
                 * the mount tree using open_tree() */
1322

1323
                if (setns(userns_fd, CLONE_NEWUSER) < 0)
1✔
UNCOV
1324
                        return log_error_errno(errno, "Failed to enter user namespace: %m");
×
1325

1326
                if (unshare(CLONE_NEWNS) < 0)
1✔
UNCOV
1327
                        return log_error_errno(errno, "Failed to unshare mount namespace: %m");
×
1328
        }
1329

1330
        _cleanup_close_ int tfd = open_tree(AT_FDCWD, cred_dir, OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW);
9✔
1331
        if (tfd < 0)
4✔
UNCOV
1332
                return log_error_errno(errno, "Failed to clone mount tree at '%s': %m", cred_dir);
×
1333

1334
        r = send_one_fd(tunnel_fds[0], tfd, /* flags = */ 0);
4✔
1335
        if (r < 0)
4✔
UNCOV
1336
                return log_error_errno(r, "Failed to send mount fd to child: %m");
×
1337

1338
        r = pidref_wait_for_terminate_and_check("(sd-creds-ns)", &child, WAIT_LOG_ABNORMAL);
4✔
1339
        if (r < 0)
4✔
1340
                return r;
1341
        if (r != EXIT_SUCCESS) {
4✔
UNCOV
1342
                r = read_errno(tunnel_fds[0]);
×
UNCOV
1343
                if (r < 0)
×
1344
                        return r;
1345

UNCOV
1346
                return -EPROTO;
×
1347
        }
1348

1349
        return 1;
1350
}
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