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

systemd / systemd / 19282013399

12 Nov 2025 12:00AM UTC coverage: 72.412% (+0.01%) from 72.402%
19282013399

push

github

web-flow
core/exec-credentials: port to new mount API, ensure atomicity for creds installation (#39637)

103 of 137 new or added lines in 4 files covered. (75.18%)

850 existing lines in 45 files now uncovered.

307170 of 424195 relevant lines covered (72.41%)

1105108.57 hits per line

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

86.88
/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 "mkdir-label.h"
20
#include "mount-util.h"
21
#include "mountpoint-util.h"
22
#include "ordered-set.h"
23
#include "path-lookup.h"
24
#include "path-util.h"
25
#include "process-util.h"
26
#include "random-util.h"
27
#include "recurse-dir.h"
28
#include "rm-rf.h"
29
#include "siphash24.h"
30
#include "stat-util.h"
31
#include "strv.h"
32
#include "user-util.h"
33

34
ExecSetCredential* exec_set_credential_free(ExecSetCredential *sc) {
×
35
        if (!sc)
×
36
                return NULL;
37

38
        free(sc->id);
×
39
        free(sc->data);
×
40
        return mfree(sc);
×
41
}
42

43
ExecLoadCredential* exec_load_credential_free(ExecLoadCredential *lc) {
25✔
44
        if (!lc)
25✔
45
                return NULL;
46

47
        free(lc->id);
25✔
48
        free(lc->path);
25✔
49
        return mfree(lc);
25✔
50
}
51

52
ExecImportCredential* exec_import_credential_free(ExecImportCredential *ic) {
4,103✔
53
        if (!ic)
4,103✔
54
                return NULL;
55

56
        free(ic->glob);
4,103✔
57
        free(ic->rename);
4,103✔
58
        return mfree(ic);
4,103✔
59
}
60

61
static void exec_import_credential_hash_func(const ExecImportCredential *ic, struct siphash *state) {
17,863✔
62
        assert(ic);
17,863✔
63
        assert(state);
17,863✔
64

65
        siphash24_compress_string(ic->glob, state);
17,863✔
66
        if (ic->rename)
17,863✔
67
                siphash24_compress_string(ic->rename, state);
1,553✔
68
}
17,863✔
69

70
static int exec_import_credential_compare_func(const ExecImportCredential *a, const ExecImportCredential *b) {
6,117✔
71
        int r;
6,117✔
72

73
        assert(a);
6,117✔
74
        assert(b);
6,117✔
75

76
        r = strcmp(a->glob, b->glob);
6,117✔
77
        if (r != 0)
6,117✔
78
                return r;
79

80
        return strcmp_ptr(a->rename, b->rename);
3✔
81
}
82

83
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
×
84
        exec_set_credential_hash_ops,
85
        char, string_hash_func, string_compare_func,
86
        ExecSetCredential, exec_set_credential_free);
87

88
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
25✔
89
        exec_load_credential_hash_ops,
90
        char, string_hash_func, string_compare_func,
91
        ExecLoadCredential, exec_load_credential_free);
92

93
DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
4,102✔
94
        exec_import_credential_hash_ops,
95
        ExecImportCredential,
96
        exec_import_credential_hash_func,
97
        exec_import_credential_compare_func,
98
        exec_import_credential_free);
99

100
int exec_context_put_load_credential(ExecContext *c, const char *id, const char *path, bool encrypted) {
59✔
101
        ExecLoadCredential *old;
59✔
102
        int r;
59✔
103

104
        assert(c);
59✔
105
        assert(id);
59✔
106
        assert(path);
59✔
107

108
        old = hashmap_get(c->load_credentials, id);
59✔
109
        if (old) {
59✔
110
                r = free_and_strdup(&old->path, path);
×
111
                if (r < 0)
×
112
                        return r;
113

114
                old->encrypted = encrypted;
×
115
        } else {
116
                _cleanup_(exec_load_credential_freep) ExecLoadCredential *lc = NULL;
×
117

118
                lc = new(ExecLoadCredential, 1);
59✔
119
                if (!lc)
59✔
120
                        return -ENOMEM;
121

122
                *lc = (ExecLoadCredential) {
59✔
123
                        .id = strdup(id),
59✔
124
                        .path = strdup(path),
59✔
125
                        .encrypted = encrypted,
126
                };
127
                if (!lc->id || !lc->path)
59✔
128
                        return -ENOMEM;
129

130
                r = hashmap_ensure_put(&c->load_credentials, &exec_load_credential_hash_ops, lc->id, lc);
59✔
131
                assert(r != -EEXIST);
59✔
132
                if (r < 0)
59✔
133
                        return r;
134

135
                TAKE_PTR(lc);
59✔
136
        }
137

138
        return 0;
139
}
140

141
int exec_context_put_set_credential(
93✔
142
                ExecContext *c,
143
                const char *id,
144
                void *data_consume,
145
                size_t size,
146
                bool encrypted) {
147

148
        _cleanup_free_ void *data = data_consume;
93✔
149
        ExecSetCredential *old;
93✔
150
        int r;
93✔
151

152
        /* Takes the ownership of data both on success and failure */
153

154
        assert(c);
93✔
155
        assert(id);
93✔
156
        assert(data || size == 0);
93✔
157

158
        old = hashmap_get(c->set_credentials, id);
93✔
159
        if (old) {
93✔
160
                free_and_replace(old->data, data);
×
161
                old->size = size;
×
162
                old->encrypted = encrypted;
×
163
        } else {
164
                _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
×
165

166
                sc = new(ExecSetCredential, 1);
93✔
167
                if (!sc)
93✔
168
                        return -ENOMEM;
169

170
                *sc = (ExecSetCredential) {
93✔
171
                        .id = strdup(id),
93✔
172
                        .data = TAKE_PTR(data),
93✔
173
                        .size = size,
174
                        .encrypted = encrypted,
175
                };
176
                if (!sc->id)
93✔
177
                        return -ENOMEM;
178

179
                r = hashmap_ensure_put(&c->set_credentials, &exec_set_credential_hash_ops, sc->id, sc);
93✔
180
                assert(r != -EEXIST);
93✔
181
                if (r < 0)
93✔
182
                        return r;
183

184
                TAKE_PTR(sc);
93✔
185
        }
186

187
        return 0;
188
}
189

190
int exec_context_put_import_credential(ExecContext *c, const char *glob, const char *rename) {
8,046✔
191
        _cleanup_(exec_import_credential_freep) ExecImportCredential *ic = NULL;
8,046✔
192
        int r;
8,046✔
193

194
        assert(c);
8,046✔
195
        assert(glob);
8,046✔
196

197
        rename = empty_to_null(rename);
8,046✔
198

199
        ic = new(ExecImportCredential, 1);
8,046✔
200
        if (!ic)
8,046✔
201
                return -ENOMEM;
202

203
        *ic = (ExecImportCredential) {
8,046✔
204
                .glob = strdup(glob),
8,046✔
205
        };
206
        if (!ic->glob)
8,046✔
207
                return -ENOMEM;
208
        if (rename) {
8,046✔
209
                ic->rename = strdup(rename);
447✔
210
                if (!ic->rename)
447✔
211
                        return -ENOMEM;
212
        }
213

214
        if (ordered_set_contains(c->import_credentials, ic))
8,046✔
215
                return 0;
216

217
        r = ordered_set_ensure_put(&c->import_credentials, &exec_import_credential_hash_ops, ic);
8,045✔
218
        assert(r != -EEXIST);
8,045✔
219
        if (r < 0)
8,045✔
220
                return r;
221

222
        TAKE_PTR(ic);
8,045✔
223

224
        return 0;
8,045✔
225
}
226

227
bool exec_params_need_credentials(const ExecParameters *p) {
21,929✔
228
        assert(p);
21,929✔
229

230
        return p->flags & (EXEC_SETUP_CREDENTIALS|EXEC_SETUP_CREDENTIALS_FRESH);
21,929✔
231
}
232

233
bool exec_context_has_credentials(const ExecContext *c) {
20,516✔
234
        assert(c);
20,516✔
235

236
        return !hashmap_isempty(c->set_credentials) ||
20,516✔
237
                !hashmap_isempty(c->load_credentials) ||
20,516✔
238
                !ordered_set_isempty(c->import_credentials);
20,265✔
239
}
240

241
bool mount_point_is_credentials(const char *runtime_prefix, const char *path) {
91,787✔
242
        const char *e;
91,787✔
243

244
        assert(runtime_prefix);
91,787✔
245
        assert(path);
91,787✔
246

247
        e = path_startswith(path, runtime_prefix);
91,787✔
248
        if (!e)
91,787✔
249
                return false;
250

251
        return path_startswith(e, "credentials");
6,578✔
252
}
253

254
static int get_credential_directory(
14,830✔
255
                const char *runtime_prefix,
256
                const char *unit,
257
                char **ret) {
258

259
        char *p;
14,830✔
260

261
        assert(ret);
14,830✔
262

263
        if (!runtime_prefix || !unit) {
14,830✔
264
                *ret = NULL;
×
265
                return 0;
×
266
        }
267

268
        p = path_join(runtime_prefix, "credentials", unit);
14,830✔
269
        if (!p)
14,830✔
270
                return -ENOMEM;
271

272
        *ret = p;
14,830✔
273
        return 1;
14,830✔
274
}
275

276
int exec_context_get_credential_directory(
11,969✔
277
                const ExecContext *context,
278
                const ExecParameters *params,
279
                const char *unit,
280
                char **ret) {
281

282
        assert(context);
11,969✔
283
        assert(params);
11,969✔
284
        assert(unit);
11,969✔
285
        assert(ret);
11,969✔
286

287
        if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context)) {
11,969✔
288
                *ret = NULL;
9,305✔
289
                return 0;
9,305✔
290
        }
291

292
        return get_credential_directory(params->prefix[EXEC_DIRECTORY_RUNTIME], unit, ret);
2,664✔
293
}
294

295
int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) {
12,166✔
296
        _cleanup_free_ char *p = NULL;
12,166✔
297
        int r;
12,166✔
298

299
        assert(c);
12,166✔
300

301
        r = get_credential_directory(runtime_prefix, unit, &p);
12,166✔
302
        if (r <= 0)
12,166✔
303
                return r;
304

305
        /* This is either a tmpfs/ramfs of its own, or a plain directory. Either way, let's first try to
306
         * unmount it, and afterwards remove the mount point */
307
        (void) umount2(p, MNT_DETACH|UMOUNT_NOFOLLOW);
12,166✔
308
        (void) rm_rf(p, REMOVE_ROOT|REMOVE_CHMOD);
12,166✔
309

310
        return 0;
311
}
312

313
static int write_credential(
1,137✔
314
                int dfd,
315
                const char *id,
316
                const void *data,
317
                size_t size,
318
                uid_t uid,
319
                gid_t gid,
320
                bool ownership_ok) {
321

322
        _cleanup_close_ int fd = -EBADF;
1,137✔
323
        int r;
1,137✔
324

325
        assert(dfd >= 0);
1,137✔
326
        assert(id);
1,137✔
327
        assert(data || size == 0);
1,137✔
328

329
        fd = openat(dfd, id, O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0600);
1,137✔
330
        if (fd < 0)
1,137✔
331
                return -errno;
×
332

333
        r = loop_write(fd, data, size);
1,137✔
334
        if (r < 0)
1,137✔
335
                return r;
336

337
        r = RET_NERRNO(fchmod(fd, 0400)); /* Take away "w" bit */
1,137✔
338
        if (r < 0)
×
339
                return r;
340

341
        if (uid_is_valid(uid) && uid != getuid()) {
1,137✔
342
                r = fd_add_uid_acl_permission(fd, uid, ACL_READ);
92✔
343
                /* Ideally we use ACLs, since we can neatly express what we want to express:
344
                 * the user gets read access and nothing else. But if the backing fs can't
345
                 * support that (e.g. ramfs), then we can use file ownership instead. But that's
346
                 * only safe if we can then re-mount the whole thing read-only, so that the user
347
                 * can no longer chmod() the file to gain write access. */
348
                if ((ERRNO_IS_NEG_NOT_SUPPORTED(r) || ERRNO_IS_NEG_PRIVILEGE(r)) && ownership_ok)
92✔
349
                        r = RET_NERRNO(fchown(fd, uid, gid));
1,137✔
350
                if (r < 0)
92✔
UNCOV
351
                        return r;
×
352
        }
353

354
        return 0;
355
}
356

357
typedef enum CredentialSearchPath {
358
        CREDENTIAL_SEARCH_PATH_TRUSTED,
359
        CREDENTIAL_SEARCH_PATH_ENCRYPTED,
360
        CREDENTIAL_SEARCH_PATH_ALL,
361
        _CREDENTIAL_SEARCH_PATH_MAX,
362
        _CREDENTIAL_SEARCH_PATH_INVALID = -EINVAL,
363
} CredentialSearchPath;
364

365
static int credential_search_path(const ExecParameters *params, CredentialSearchPath path, char ***ret) {
7,818✔
366
        _cleanup_strv_free_ char **l = NULL;
7,818✔
367
        int r;
7,818✔
368

369
        assert(params);
7,818✔
370
        assert(path >= 0 && path < _CREDENTIAL_SEARCH_PATH_MAX);
7,818✔
371
        assert(ret);
7,818✔
372

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

377
        if (IN_SET(path, CREDENTIAL_SEARCH_PATH_ENCRYPTED, CREDENTIAL_SEARCH_PATH_ALL)) {
7,818✔
378
                r = strv_extend(&l, params->received_encrypted_credentials_directory);
3,913✔
379
                if (r < 0)
3,913✔
380
                        return r;
×
381

382
                _cleanup_strv_free_ char **add = NULL;
3,913✔
383
                r = credential_store_path_encrypted(params->runtime_scope, &add);
3,913✔
384
                if (r < 0)
3,913✔
385
                        return r;
386

387
                r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ false);
3,913✔
388
                if (r < 0)
3,913✔
389
                        return r;
390
        }
391

392
        if (IN_SET(path, CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ALL)) {
7,818✔
393
                r = strv_extend(&l, params->received_credentials_directory);
3,913✔
394
                if (r < 0)
3,913✔
395
                        return r;
×
396

397
                _cleanup_strv_free_ char **add = NULL;
3,913✔
398
                r = credential_store_path(params->runtime_scope, &add);
3,913✔
399
                if (r < 0)
3,913✔
400
                        return r;
401

402
                r = strv_extend_strv_consume(&l, TAKE_PTR(add), /* filter_duplicates= */ false);
3,913✔
403
                if (r < 0)
3,913✔
404
                        return r;
405
        }
406

407
        if (DEBUG_LOGGING) {
7,818✔
408
                _cleanup_free_ char *t = strv_join(l, ":");
15,636✔
409
                log_debug("Credential search path is: %s", strempty(t));
7,818✔
410
        }
411

412
        *ret = TAKE_PTR(l);
7,818✔
413
        return 0;
7,818✔
414
}
415

416
struct load_cred_args {
417
        const ExecContext *context;
418
        const ExecParameters *params;
419
        const char *unit;
420

421
        bool always_ipc;
422

423
        bool encrypted;
424

425
        int write_dfd;
426
        uid_t uid;
427
        gid_t gid;
428
        bool ownership_ok;
429

430
        uint64_t left;
431
};
432

433
static int maybe_decrypt_and_write_credential(
1,137✔
434
                struct load_cred_args *args,
435
                const char *id,
436
                const char *data,
437
                size_t size,
438
                bool graceful) {
439

440
        _cleanup_(iovec_done_erase) struct iovec plaintext = {};
1,137✔
441
        size_t add;
1,137✔
442
        int r;
1,137✔
443

444
        assert(args);
1,137✔
445
        assert(args->write_dfd >= 0);
1,137✔
446
        assert(id);
1,137✔
447
        assert(data || size == 0);
1,137✔
448

449
        if (args->encrypted) {
1,137✔
450
                CredentialFlags flags = 0; /* only allow user creds in user scope */
9✔
451

452
                switch (args->params->runtime_scope) {
9✔
453

454
                case RUNTIME_SCOPE_SYSTEM:
5✔
455
                        /* In system mode talk directly to the TPM – unless we live in a device sandbox
456
                         * which might block TPM device access. */
457

458
                        flags |= CREDENTIAL_ANY_SCOPE;
5✔
459

460
                        if (!args->always_ipc) {
5✔
461
                                r = decrypt_credential_and_warn(
3✔
462
                                                id,
463
                                                now(CLOCK_REALTIME),
464
                                                /* tpm2_device= */ NULL,
465
                                                /* tpm2_signature_path= */ NULL,
466
                                                getuid(),
3✔
467
                                                &IOVEC_MAKE(data, size),
3✔
468
                                                flags,
469
                                                &plaintext);
470
                                break;
3✔
471
                        }
472

473
                        _fallthrough_;
6✔
474

475
                case RUNTIME_SCOPE_USER:
476
                        /* In per user mode we'll not have access to the machine secret, nor to the TPM (most
477
                         * likely), hence go via the IPC service instead. Do this if we are run in root's
478
                         * per-user invocation too, to minimize differences and because isolating this logic
479
                         * into a separate process is generally a good thing anyway. */
480
                        r = ipc_decrypt_credential(
6✔
481
                                        id,
482
                                        now(CLOCK_REALTIME),
483
                                        getuid(),
6✔
484
                                        &IOVEC_MAKE(data, size),
6✔
485
                                        flags,
486
                                        &plaintext);
487
                        break;
488

489
                default:
×
490
                        assert_not_reached();
×
491
                }
492
                if (r < 0) {
9✔
493
                        if (graceful) {
×
494
                                log_warning_errno(r, "Unable to decrypt credential '%s', skipping: %m", id);
×
495
                                return 0;
×
496
                        }
497

498
                        return r;
499
                }
500

501
                data = plaintext.iov_base;
9✔
502
                size = plaintext.iov_len;
9✔
503
        }
504

505
        add = strlen(id) + size;
1,137✔
506
        if (add > args->left)
1,137✔
507
                return -E2BIG;
508

509
        r = write_credential(args->write_dfd, id, data, size, args->uid, args->gid, args->ownership_ok);
1,137✔
510
        if (r < 0)
1,137✔
511
                return log_debug_errno(r, "Failed to write credential '%s': %m", id);
×
512

513
        args->left -= add;
1,137✔
514

515
        return 0;
1,137✔
516
}
517

518
static int load_credential_glob(
7,810✔
519
                struct load_cred_args *args,
520
                const ExecImportCredential *ic,
521
                char * const *search_path,
522
                ReadFullFileFlags flags) {
523

524
        int r;
7,810✔
525

526
        assert(args);
7,810✔
527
        assert(args->write_dfd >= 0);
7,810✔
528
        assert(ic);
7,810✔
529
        assert(search_path);
7,810✔
530

531
        STRV_FOREACH(d, search_path) {
43,020✔
532
                _cleanup_strv_free_ char **paths = NULL;
×
533
                _cleanup_free_ char *j = NULL;
35,210✔
534

535
                j = path_join(*d, ic->glob);
35,210✔
536
                if (!j)
35,210✔
537
                        return -ENOMEM;
538

539
                r = safe_glob(j, /* flags = */ 0, &paths);
35,210✔
540
                if (r == -ENOENT)
35,210✔
541
                        continue;
34,433✔
542
                if (r < 0)
777✔
543
                        return r;
544

545
                STRV_FOREACH(p, paths) {
1,805✔
546
                        _cleanup_free_ char *fn = NULL;
1,028✔
547
                        _cleanup_(erase_and_freep) char *data = NULL;
1,028✔
548
                        size_t size;
1,028✔
549

550
                        r = path_extract_filename(*p, &fn);
1,028✔
551
                        if (r < 0)
1,028✔
552
                                return log_debug_errno(r, "Failed to extract filename from '%s': %m", *p);
×
553

554
                        if (ic->rename) {
1,028✔
555
                                _cleanup_free_ char *renamed = NULL;
×
556

557
                                renamed = strjoin(ic->rename, fn + strlen(ic->glob) - !!endswith(ic->glob, "*"));
182✔
558
                                if (!renamed)
182✔
559
                                        return log_oom_debug();
×
560

561
                                free_and_replace(fn, renamed);
182✔
562
                        }
563

564
                        if (!credential_name_valid(fn)) {
1,028✔
565
                                log_debug("Skipping credential with invalid name: %s", fn);
5✔
566
                                continue;
5✔
567
                        }
568

569
                        if (faccessat(args->write_dfd, fn, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
1,023✔
570
                                log_debug("Skipping credential with duplicated ID %s at %s", fn, *p);
10✔
571
                                continue;
10✔
572
                        }
573
                        if (errno != ENOENT)
1,013✔
574
                                return log_debug_errno(errno, "Failed to test if credential %s exists: %m", fn);
×
575

576
                        /* path is absolute, hence pass AT_FDCWD as nop dir fd here */
577
                        r = read_full_file_full(
1,013✔
578
                                        AT_FDCWD,
579
                                        *p,
580
                                        UINT64_MAX,
581
                                        args->encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
1,013✔
582
                                        flags,
583
                                        NULL,
584
                                        &data, &size);
585
                        if (r < 0)
1,013✔
586
                                return log_debug_errno(r, "Failed to read credential '%s': %m", *p);
×
587

588
                        r = maybe_decrypt_and_write_credential(args, fn, data, size, /* graceful= */ true);
1,013✔
589
                        if (r < 0)
1,013✔
590
                                return r;
591
                }
592
        }
593

594
        return 0;
595
}
596

597
static int load_credential(
34✔
598
                struct load_cred_args *args,
599
                const char *id,
600
                int read_dfd,
601
                const char *path) {
602

603
        ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
34✔
604
        _cleanup_strv_free_ char **search_path = NULL;
×
605
        _cleanup_free_ char *bindname = NULL;
34✔
606
        const char *source = NULL;
34✔
607
        bool missing_ok;
34✔
608
        _cleanup_(erase_and_freep) char *data = NULL;
34✔
609
        size_t size, maxsz;
34✔
610
        int r;
34✔
611

612
        assert(args);
34✔
613
        assert(args->context);
34✔
614
        assert(args->params);
34✔
615
        assert(args->unit);
34✔
616
        assert(args->write_dfd >= 0);
34✔
617
        assert(id);
34✔
618
        assert(read_dfd >= 0 || read_dfd == AT_FDCWD);
34✔
619
        assert(path);
34✔
620

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

627
                if (!filename_is_valid(path)) /* safety check */
4✔
628
                        return -EINVAL;
629

630
                missing_ok = true;
631
                source = path;
632

633
        } else if (path_is_absolute(path)) {
30✔
634
                /* If this is an absolute path, read the data directly from it, and support AF_UNIX
635
                 * sockets */
636

637
                if (!path_is_valid(path)) /* safety check */
22✔
638
                        return -EINVAL;
639

640
                flags |= READ_FULL_FILE_CONNECT_SOCKET;
22✔
641

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

647
                missing_ok = false;
648
                source = path;
649

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

655
                r = credential_search_path(args->params, CREDENTIAL_SEARCH_PATH_ALL, &search_path);
8✔
656
                if (r < 0)
8✔
657
                        return r;
658

659
                missing_ok = true;
660
        } else
661
                return -EINVAL;
662

663
        if (args->encrypted) {
34✔
664
                flags |= READ_FULL_FILE_UNBASE64;
3✔
665
                maxsz = CREDENTIAL_ENCRYPTED_SIZE_MAX;
3✔
666
        } else
667
                maxsz = CREDENTIAL_SIZE_MAX;
668

669
        if (search_path)
34✔
670
                STRV_FOREACH(d, search_path) {
44✔
671
                        _cleanup_free_ char *j = NULL;
44✔
672

673
                        j = path_join(*d, path);
44✔
674
                        if (!j)
44✔
675
                                return -ENOMEM;
×
676

677
                        r = read_full_file_full(
44✔
678
                                        AT_FDCWD, j, /* path is absolute, hence pass AT_FDCWD as nop dir fd here */
679
                                        UINT64_MAX,
680
                                        maxsz,
681
                                        flags,
682
                                        NULL,
683
                                        &data, &size);
684
                        if (r != -ENOENT)
44✔
685
                                break;
686
                }
687
        else if (source)
26✔
688
                r = read_full_file_full(
26✔
689
                                read_dfd, source,
690
                                UINT64_MAX,
691
                                maxsz,
692
                                flags,
693
                                bindname,
694
                                &data, &size);
695
        else
696
                assert_not_reached();
×
697

698
        if (r == -ENOENT && (missing_ok || hashmap_contains(args->context->set_credentials, id))) {
34✔
699
                /* Make a missing inherited credential non-fatal, let's just continue. After all apps
700
                 * will get clear errors if we don't pass such a missing credential on as they
701
                 * themselves will get ENOENT when trying to read them, which should not be much
702
                 * worse than when we handle the error here and make it fatal.
703
                 *
704
                 * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
705
                 * we are fine, too. */
706
                log_full_errno(hashmap_contains(args->context->set_credentials, id) ? LOG_DEBUG : LOG_INFO,
1✔
707
                               r, "Couldn't read inherited credential '%s', skipping: %m", path);
708
                return 0;
1✔
709
        }
710
        if (r < 0)
33✔
711
                return log_debug_errno(r, "Failed to read credential '%s': %m", path);
×
712

713
        return maybe_decrypt_and_write_credential(args, id, data, size, /* graceful= */ false);
33✔
714
}
715

716
static int load_cred_recurse_dir_cb(
6✔
717
                RecurseDirEvent event,
718
                const char *path,
719
                int dir_fd,
720
                int inode_fd,
721
                const struct dirent *de,
722
                const struct statx *sx,
723
                void *userdata) {
724

725
        struct load_cred_args *args = ASSERT_PTR(userdata);
6✔
726
        _cleanup_free_ char *sub_id = NULL;
6✔
727
        int r;
6✔
728

729
        assert(path);
6✔
730
        assert(de);
6✔
731

732
        if (event != RECURSE_DIR_ENTRY)
6✔
733
                return RECURSE_DIR_CONTINUE;
734

735
        if (!IN_SET(de->d_type, DT_REG, DT_SOCK))
4✔
736
                return RECURSE_DIR_CONTINUE;
737

738
        sub_id = strreplace(path, "/", "_");
4✔
739
        if (!sub_id)
4✔
740
                return -ENOMEM;
741

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

745
        if (faccessat(args->write_dfd, sub_id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
4✔
746
                log_debug("Skipping credential with duplicated ID %s at %s", sub_id, path);
×
747
                return RECURSE_DIR_CONTINUE;
×
748
        }
749
        if (errno != ENOENT)
4✔
750
                return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sub_id);
×
751

752
        r = load_credential(args,
8✔
753
                            sub_id,
754
                            dir_fd, de->d_name);
4✔
755
        if (r < 0)
4✔
756
                return r;
×
757

758
        return RECURSE_DIR_CONTINUE;
759
}
760

761
static bool device_nodes_restricted(
2,025✔
762
                const ExecContext *c,
763
                const CGroupContext *cgroup_context) {
764

765
        assert(c);
2,025✔
766
        assert(cgroup_context);
2,025✔
767

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

772
        if (c->private_devices)
2,025✔
773
                return true;
774

775
        if (cgroup_context_has_device_policy(cgroup_context))
1,853✔
776
                return true;
564✔
777

778
        return false;
779
}
780

781
static int acquire_credentials(
2,025✔
782
                const ExecContext *context,
783
                const CGroupContext *cgroup_context,
784
                const ExecParameters *params,
785
                const char *unit,
786
                int dfd,
787
                uid_t uid,
788
                gid_t gid,
789
                bool ownership_ok) {
790

791
        int r;
2,025✔
792

793
        assert(context);
2,025✔
794
        assert(cgroup_context);
2,025✔
795
        assert(params);
2,025✔
796
        assert(unit);
2,025✔
797
        assert(dfd >= 0);
2,025✔
798

799
        struct load_cred_args args = {
4,050✔
800
                .context = context,
801
                .params = params,
802
                .unit = unit,
803
                .always_ipc = device_nodes_restricted(context, cgroup_context),
2,025✔
804
                .write_dfd = dfd,
805
                .uid = uid,
806
                .gid = gid,
807
                .ownership_ok = ownership_ok,
808
                .left = CREDENTIALS_TOTAL_SIZE_MAX,
809
        };
810

811
        /* First, load credentials off disk (or acquire via AF_UNIX socket) */
812
        ExecLoadCredential *lc;
2,025✔
813
        HASHMAP_FOREACH(lc, context->load_credentials) {
4,081✔
814
                _cleanup_close_ int sub_fd = -EBADF;
2,056✔
815

816
                args.encrypted = lc->encrypted;
31✔
817

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

823
                if (path_is_absolute(lc->path)) {
31✔
824
                        sub_fd = open(lc->path, O_DIRECTORY|O_CLOEXEC);
23✔
825
                        if (sub_fd < 0 && !IN_SET(errno,
23✔
826
                                                  ENOTDIR,  /* Not a directory */
827
                                                  ENOENT))  /* Doesn't exist? */
828
                                return log_debug_errno(errno, "Failed to open credential source '%s': %m", lc->path);
×
829
                }
830

831
                if (sub_fd < 0)
22✔
832
                        /* Regular file (incl. a credential passed in from higher up) */
833
                        r = load_credential(&args,
30✔
834
                                            lc->id,
30✔
835
                                            AT_FDCWD, lc->path);
30✔
836
                else
837
                        /* Directory */
838
                        r = recurse_dir(sub_fd,
1✔
839
                                        /* path= */ lc->id, /* recurse_dir() will suffix the subdir paths from here to the top-level id */
1✔
840
                                        /* statx_mask= */ 0,
841
                                        /* n_depth_max= */ UINT_MAX,
842
                                        RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE,
843
                                        load_cred_recurse_dir_cb,
844
                                        &args);
845
                if (r < 0)
31✔
846
                        return r;
847
        }
848

849
        /* Next, look for system credentials and credentials in the credentials store. Note that these do not
850
         * override any credentials found earlier. */
851
        ExecImportCredential *ic;
2,025✔
852
        ORDERED_SET_FOREACH(ic, context->import_credentials) {
5,930✔
853
                _cleanup_free_ char **search_path = NULL;
3,905✔
854

855
                r = credential_search_path(params, CREDENTIAL_SEARCH_PATH_TRUSTED, &search_path);
3,905✔
856
                if (r < 0)
3,905✔
857
                        return r;
858

859
                args.encrypted = false;
3,905✔
860

861
                r = load_credential_glob(
3,905✔
862
                                &args,
863
                                ic,
864
                                search_path,
865
                                READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER);
866
                if (r < 0)
3,905✔
867
                        return r;
868

869
                search_path = strv_free(search_path);
3,905✔
870

871
                r = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ENCRYPTED, &search_path);
3,905✔
872
                if (r < 0)
3,905✔
873
                        return r;
874

875
                args.encrypted = true;
3,905✔
876

877
                r = load_credential_glob(
3,905✔
878
                                &args,
879
                                ic,
880
                                search_path,
881
                                READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_UNBASE64);
882
                if (r < 0)
3,905✔
883
                        return r;
884
        }
885

886
        /* Finally, we add in literally specified credentials. If the credentials already exist, we'll not
887
         * add them, so that they can act as a "default" if the same credential is specified multiple times. */
888
        ExecSetCredential *sc;
2,025✔
889
        HASHMAP_FOREACH(sc, context->set_credentials) {
2,118✔
890
                args.encrypted = sc->encrypted;
93✔
891

892
                if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
93✔
893
                        log_debug("Skipping credential with duplicated ID %s", sc->id);
2✔
894
                        continue;
2✔
895
                }
896
                if (errno != ENOENT)
91✔
897
                        return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sc->id);
×
898

899
                r = maybe_decrypt_and_write_credential(&args, sc->id, sc->data, sc->size, /* graceful= */ false);
91✔
900
                if (r < 0)
91✔
901
                        return r;
902
        }
903

904
        return 0;
2,025✔
905
}
906

907
static int credentials_dir_finalize_permissions(int dfd, uid_t uid, gid_t gid, bool ownership_ok) {
2,025✔
908
        int r;
2,025✔
909

910
        assert(dfd >= 0);
2,025✔
911

912
        r = fd_acl_make_read_only(dfd); /* Take away the "w" bit */
2,025✔
913
        if (r < 0)
2,025✔
914
                return r;
915

916
        /* After we created all keys with the right perms, also make sure the credential store as a whole is
917
         * accessible */
918

919
        if (uid_is_valid(uid) && uid != getuid()) {
2,025✔
920
                r = fd_add_uid_acl_permission(dfd, uid, ACL_READ | ACL_EXECUTE);
673✔
921
                if (r < 0) {
673✔
922
                        if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
×
923
                                return r;
924

925
                        if (!ownership_ok)
×
926
                                return r;
927

928
                        if (fchown(dfd, uid, gid) < 0)
×
929
                                return -errno;
×
930
                }
931
        }
932

933
        return 0;
934
}
935

936
static int setup_credentials_plain_dir(
4✔
937
                const ExecContext *context,
938
                const CGroupContext *cgroup_context,
939
                const ExecParameters *params,
940
                const char *unit,
941
                const char *cred_dir,
942
                uid_t uid,
943
                gid_t gid) {
944

945
        _cleanup_free_ char *t = NULL, *workspace = NULL;
4✔
NEW
946
        _cleanup_(rm_rf_safep) const char *workspace_rm = NULL;
×
947
        _cleanup_close_ int dfd = -EBADF;
4✔
948
        int r;
4✔
949

950
        assert(context);
4✔
951
        assert(params);
4✔
952
        assert(unit);
4✔
953
        assert(cred_dir);
4✔
954

955
        /* Temporary workspace, that remains inaccessible all the time. We prepare stuff there before moving
956
         * it into place, so that users can't access half-initialized credential stores. */
957
        t = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "systemd/temporary-credentials");
4✔
958
        if (!t)
4✔
959
                return -ENOMEM;
960

961
        r = mkdir_label(t, 0700);
4✔
962
        if (r < 0 && r != -EEXIST)
4✔
963
                return r;
964

965
        workspace = path_join(t, unit);
4✔
966
        if (!workspace)
4✔
967
                return -ENOMEM;
968

969
        dfd = open_mkdir(workspace, O_CLOEXEC|O_EXCL, 0700);
4✔
970
        if (dfd < 0)
4✔
NEW
971
                return log_debug_errno(dfd, "Failed to create workspace for credentials: %m");
×
972
        workspace_rm = workspace;
4✔
973

974
        (void) label_fix_full(dfd, /* inode_path = */ NULL, cred_dir, /* flags = */ 0);
4✔
975

976
        r = acquire_credentials(context, cgroup_context, params, unit, dfd, uid, gid, /* ownership_ok = */ false);
4✔
977
        if (r < 0)
4✔
978
                return r;
979

980
        r = RET_NERRNO(rename(workspace, cred_dir));
4✔
NEW
981
        if (r >= 0)
×
982
                workspace_rm = NULL;
4✔
983
        if (r == -EEXIST) {
4✔
NEW
984
                log_debug_errno(r, "Credential dir '%s' already populated, exchanging with workspace.", cred_dir);
×
NEW
985
                r = RET_NERRNO(renameat2(AT_FDCWD, workspace, AT_FDCWD, cred_dir, RENAME_EXCHANGE));
×
986
        }
987
        if (r < 0)
4✔
NEW
988
                return log_debug_errno(r, "Failed to move credentials workspace into place: %m");
×
989

990
        /* rename() requires both the source and target to be writable, hence lock down write permission
991
         * as last step. */
992
        r = credentials_dir_finalize_permissions(dfd, uid, gid, /* ownership_ok = */ false);
4✔
993
        if (r < 0)
4✔
NEW
994
                return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m");
×
995

996
        return 0;
997
}
998

999
static int setup_credentials_internal(
2,027✔
1000
                const ExecContext *context,
1001
                const CGroupContext *cgroup_context,
1002
                const ExecParameters *params,
1003
                const char *unit,
1004
                const char *cred_dir,
1005
                uid_t uid,
1006
                gid_t gid) {
1007

1008
        _cleanup_close_ int fs_fd = -EBADF, mfd = -EBADF, dfd = -EBADF;
4,054✔
1009
        bool dir_mounted;
2,027✔
1010
        int r;
2,027✔
1011

1012
        assert(context);
2,027✔
1013
        assert(params);
2,027✔
1014
        assert(unit);
2,027✔
1015
        assert(cred_dir);
2,027✔
1016

1017
        if (!FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH)) {
2,027✔
1018
                /* We may reuse the previous credential dir */
1019
                r = dir_is_empty(cred_dir, /* ignore_hidden_or_backup = */ false);
5✔
1020
                if (r < 0)
5✔
1021
                        return r;
1022
                if (r == 0) {
5✔
1023
                        log_debug("Credential dir for unit '%s' already set up, skipping.", unit);
2✔
1024
                        return 0;
2✔
1025
                }
1026
        }
1027

1028
        r = path_is_mount_point(cred_dir);
2,025✔
1029
        if (r < 0)
2,025✔
NEW
1030
                return log_debug_errno(r, "Failed to determine if '%s' is a mountpoint: %m", cred_dir);
×
1031
        dir_mounted = r > 0;
2,025✔
1032

1033
        mfd = fsmount_credentials_fs(&fs_fd);
2,025✔
1034
        if (ERRNO_IS_NEG_PRIVILEGE(mfd) && !dir_mounted) {
2,025✔
1035
                log_debug_errno(mfd, "Lacking privilege to mount credentials fs, falling back to plain directory.");
4✔
1036
                return setup_credentials_plain_dir(context, cgroup_context, params, unit, cred_dir, uid, gid);
4✔
1037
        }
1038
        if (mfd < 0)
2,021✔
NEW
1039
                return log_debug_errno(mfd, "Failed to mount credentials fs: %m");
×
1040

1041
        dfd = fd_reopen(mfd, O_DIRECTORY|O_CLOEXEC);
2,021✔
1042
        if (dfd < 0)
2,021✔
1043
                return dfd;
1044

1045
        (void) label_fix_full(dfd, /* inode_path = */ NULL, cred_dir, /* flags = */ 0);
2,021✔
1046

1047
        r = acquire_credentials(context, cgroup_context, params, unit, dfd, uid, gid, /* ownership_ok = */ true);
2,021✔
1048
        if (r < 0)
2,021✔
1049
                return r;
1050

1051
        r = credentials_dir_finalize_permissions(dfd, uid, gid, /* ownership_ok = */ true);
2,021✔
1052
        if (r < 0)
2,021✔
NEW
1053
                return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m");
×
1054

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

1060
        if (fsconfig(fs_fd, FSCONFIG_SET_FLAG, "ro", NULL, 0) < 0)
2,021✔
NEW
1061
                return -errno;
×
1062

1063
        if (fsconfig(fs_fd, FSCONFIG_CMD_RECONFIGURE, NULL, NULL, 0) < 0)
2,021✔
NEW
1064
                return -errno;
×
1065

1066
        log_debug("Successfully reconfigured credentials fs to be read only.");
2,021✔
1067

1068
        if (dir_mounted) {
2,021✔
1069
                /* Firstly, try to move beneath the existing mount, which guarantees strictly atomic replacement
1070
                 * (needs kernel >= 6.5) */
1071
                r = move_mount(mfd, "", AT_FDCWD, cred_dir, MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_BENEATH);
5✔
1072
                if (r >= 0)
5✔
1073
                        return umount_verbose(LOG_DEBUG, cred_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
5✔
NEW
1074
                if (errno != EINVAL)
×
NEW
1075
                        return log_debug_errno(errno, "Failed to move credentials fs into place: %m");
×
1076

NEW
1077
                log_debug_errno(errno, "Unable to move credentials fs beneath existing mount '%s', unmounting instead: %m",
×
1078
                                cred_dir);
1079

NEW
1080
                r = umount_verbose(LOG_DEBUG, cred_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
×
UNCOV
1081
                if (r < 0)
×
1082
                        return r;
1083
        }
1084

1085
        r = move_mount(mfd, "", AT_FDCWD, cred_dir, MOVE_MOUNT_F_EMPTY_PATH);
2,016✔
1086
        if (r < 0)
2,016✔
NEW
1087
                return log_debug_errno(errno, "Failed to move credentials fs into place: %m");
×
1088

1089
        return 0;
1090
}
1091

1092
int exec_setup_credentials(
9,960✔
1093
                const ExecContext *context,
1094
                const CGroupContext *cgroup_context,
1095
                const ExecParameters *params,
1096
                const char *unit,
1097
                uid_t uid,
1098
                gid_t gid) {
1099

1100
        _cleanup_free_ char *p = NULL, *q = NULL;
9,960✔
1101
        int r;
9,960✔
1102

1103
        assert(context);
9,960✔
1104
        assert(params);
9,960✔
1105
        assert(unit);
9,960✔
1106

1107
        if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context))
9,960✔
1108
                return 0;
7,933✔
1109

1110
        if (!params->prefix[EXEC_DIRECTORY_RUNTIME])
2,027✔
1111
                return -EINVAL;
1112

1113
        /* This is where we'll place stuff when we are done; the main credentials directory is world-readable,
1114
         * and the subdir we mount over with a read-only file system readable by the service's user. */
1115
        q = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials");
2,027✔
1116
        if (!q)
2,027✔
1117
                return -ENOMEM;
1118

1119
        r = mkdir_label(q, 0755); /* top-level dir: world readable/searchable */
2,027✔
1120
        if (r < 0 && r != -EEXIST)
2,027✔
1121
                return r;
1122

1123
        p = path_join(q, unit);
2,027✔
1124
        if (!p)
2,027✔
1125
                return -ENOMEM;
1126

1127
        r = mkdir_label(p, 0700); /* per-unit dir: private to user */
2,027✔
1128
        if (r < 0 && r != -EEXIST)
2,027✔
1129
                return r;
1130

1131
        r = setup_credentials_internal(context, cgroup_context, params, unit, p, uid, gid);
2,027✔
1132

1133
        /* If the credentials dir is empty and not a mount point, then there's no point in having it. Let's
1134
         * try to remove it. This matters in particular if we created the dir as mount point but then didn't
1135
         * actually end up mounting anything on it. In that case we'd rather have ENOENT than EACCESS being
1136
         * seen by users when trying access this inode. */
1137
        (void) rmdir(p);
2,027✔
1138
        return r;
2,027✔
1139
}
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