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

systemd / systemd / 29968961921

22 Jul 2026 03:11PM UTC coverage: 73.067% (+0.2%) from 72.837%
29968961921

push

github

keszybz
homed: verify privileged worker changes

Workers may return a newer embedded identity while processing
an owner update. Accept self-modifiable changes, but require
privileged changes to carry a trusted signature.

Follow-up for 70a5db582

16 of 36 new or added lines in 1 file covered. (44.44%)

1697 existing lines in 49 files now uncovered.

346625 of 474391 relevant lines covered (73.07%)

1335649.2 hits per line

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

73.53
/src/sysusers/sysusers.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <stdlib.h>
4
#include <sys/stat.h>
5

6
#include "alloc-util.h"
7
#include "build.h"
8
#include "chase.h"
9
#include "conf-files.h"
10
#include "constants.h"
11
#include "copy.h"
12
#include "creds-util.h"
13
#include "dissect-image.h"
14
#include "dlopen-note.h"
15
#include "errno-util.h"
16
#include "extract-word.h"
17
#include "fd-util.h"
18
#include "fileio.h"
19
#include "format-table.h"
20
#include "format-util.h"
21
#include "fs-util.h"
22
#include "hashmap.h"
23
#include "image-policy.h"
24
#include "install-file.h"
25
#include "label-util.h"
26
#include "libaudit-util.h"
27
#include "libcrypt-util.h"
28
#include "log.h"
29
#include "loop-util.h"
30
#include "main-func.h"
31
#include "mount-util.h"
32
#include "options.h"
33
#include "pager.h"
34
#include "parse-argument.h"
35
#include "path-util.h"
36
#include "pretty-print.h"
37
#include "set.h"
38
#include "smack-util.h"
39
#include "specifier.h"
40
#include "string-util.h"
41
#include "strv.h"
42
#include "sync-util.h"
43
#include "time-util.h"
44
#include "tmpfile-util.h"
45
#include "uid-classification.h"
46
#include "uid-range.h"
47
#include "user-util.h"
48
#include "verbs.h"
49

50
typedef enum ItemType {
51
        ADD_USER =   'u',
52
        ADD_GROUP =  'g',
53
        ADD_MEMBER = 'm',
54
        ADD_RANGE =  'r',
55
} ItemType;
56

57
static const char* item_type_to_string(ItemType t) {
×
58
        switch (t) {
×
59
        case ADD_USER:
60
                return "user";
61
        case ADD_GROUP:
×
62
                return "group";
×
63
        case ADD_MEMBER:
×
64
                return "member";
×
65
        case ADD_RANGE:
×
66
                return "range";
×
67
        default:
×
68
                assert_not_reached();
×
69
        }
70
}
71

72
typedef struct Item {
73
        ItemType type;
74

75
        char *name;
76
        char *group_name;
77
        char *uid_path;
78
        char *gid_path;
79
        char *description;
80
        char *home;
81
        char *shell;
82

83
        gid_t gid;
84
        uid_t uid;
85

86
        char *filename;
87
        unsigned line;
88

89
        bool gid_set;
90

91
        /* When set the group with the specified GID must exist
92
         * and the check if a UID clashes with the GID is skipped.
93
         */
94
        bool id_set_strict;
95

96
        bool uid_set;
97

98
        bool locked;
99

100
        bool todo_user;
101
        bool todo_group;
102
} Item;
103

104
static char *arg_root = NULL;
105
static char *arg_image = NULL;
106
static CatFlags arg_cat_flags = CAT_CONFIG_OFF;
107
static const char *arg_replace = NULL;
108
static bool arg_dry_run = false;
109
static bool arg_inline = false;
110
static PagerFlags arg_pager_flags = 0;
111
static ImagePolicy *arg_image_policy = NULL;
112

113
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
117✔
114
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
117✔
115
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
117✔
116

117
typedef struct Context {
118
        int audit_fd;
119

120
        OrderedHashmap *users, *groups;
121
        OrderedHashmap *todo_uids, *todo_gids;
122
        OrderedHashmap *members;
123

124
        Hashmap *database_by_uid, *database_by_username;
125
        Hashmap *database_by_gid, *database_by_groupname;
126

127
        /* A helper set to hold names that are used by database_by_{uid,gid,username,groupname} above. */
128
        Set *names;
129

130
        uid_t search_uid;
131
        UIDRange *uid_range;
132

133
        UGIDAllocationRange login_defs;
134
        bool login_defs_need_warning;
135
} Context;
136

137
static void context_done(Context *c) {
117✔
138
        assert(c);
117✔
139

140
        c->audit_fd = close_audit_fd(c->audit_fd);
117✔
141

142
        ordered_hashmap_free(c->groups);
117✔
143
        ordered_hashmap_free(c->users);
117✔
144
        ordered_hashmap_free(c->members);
117✔
145
        ordered_hashmap_free(c->todo_uids);
117✔
146
        ordered_hashmap_free(c->todo_gids);
117✔
147

148
        hashmap_free(c->database_by_uid);
117✔
149
        hashmap_free(c->database_by_username);
117✔
150
        hashmap_free(c->database_by_gid);
117✔
151
        hashmap_free(c->database_by_groupname);
117✔
152

153
        set_free(c->names);
117✔
154
        uid_range_free(c->uid_range);
117✔
155
}
117✔
156

157
static void maybe_emit_login_defs_warning(Context *c) {
48✔
158
        assert(c);
48✔
159

160
        if (!c->login_defs_need_warning)
48✔
161
                return;
162

163
        if (c->login_defs.system_alloc_uid_min != SYSTEM_ALLOC_UID_MIN ||
46✔
164
            c->login_defs.system_uid_max != SYSTEM_UID_MAX)
46✔
165
                log_warning("login.defs specifies UID allocation range "UID_FMT"–"UID_FMT
×
166
                            " that is different than the built-in defaults ("UID_FMT"–"UID_FMT")",
167
                            c->login_defs.system_alloc_uid_min, c->login_defs.system_uid_max,
168
                            (uid_t) SYSTEM_ALLOC_UID_MIN, (uid_t) SYSTEM_UID_MAX);
169
        if (c->login_defs.system_alloc_gid_min != SYSTEM_ALLOC_GID_MIN ||
46✔
170
            c->login_defs.system_gid_max != SYSTEM_GID_MAX)
46✔
171
                log_warning("login.defs specifies GID allocation range "GID_FMT"–"GID_FMT
×
172
                            " that is different than the built-in defaults ("GID_FMT"–"GID_FMT")",
173
                            c->login_defs.system_alloc_gid_min, c->login_defs.system_gid_max,
174
                            (gid_t) SYSTEM_ALLOC_GID_MIN, (gid_t) SYSTEM_GID_MAX);
175

176
        c->login_defs_need_warning = false;
46✔
177
}
178

179
static void log_audit_accounts(Context *c, ItemType what) {
230✔
180
#if HAVE_AUDIT
181
        assert(c);
230✔
182
        assert(IN_SET(what, ADD_USER, ADD_GROUP));
230✔
183

184
        if (arg_dry_run || c->audit_fd < 0)
230✔
185
                return;
230✔
186

187
        Item *i;
×
188
        int type = what == ADD_USER ? AUDIT_ADD_USER : AUDIT_ADD_GROUP;
×
189
        const char *op = what == ADD_USER ? "adding-user" : "adding-group";
×
190

191
        /* Notes:
192
         *
193
         * The op must not contain whitespace. The format with a dash matches what Fedora shadow-utils uses.
194
         *
195
         * We send id == -1, even though we know the number, in particular on success. This is because if we
196
         * send the id, the generated audit message will not contain the name. The name seems more useful
197
         * than the number, hence send just the name:
198
         *
199
         * type=ADD_USER msg=audit(01/10/2025 16:02:00.639:3854) :
200
         *   pid=3846380 uid=root auid=zbyszek ses=2 msg='op=adding-user id=unknown(952) exe=systemd-sysusers ... res=success'
201
         * vs.
202
         * type=ADD_USER msg=audit(01/10/2025 16:03:15.457:3908) :
203
         *   pid=3846607 uid=root auid=zbyszek ses=2 msg='op=adding-user acct=foo5 exe=systemd-sysusers ... res=success'
204
         */
205

206
        ORDERED_HASHMAP_FOREACH(i, what == ADD_USER ? c->todo_uids : c->todo_gids)
×
207
                sym_audit_log_acct_message(
×
208
                                c->audit_fd,
209
                                type,
210
                                program_invocation_short_name,
211
                                op,
212
                                i->name,
×
213
                                /* id= */ (unsigned) -1,
214
                                /* host= */ NULL,
215
                                /* addr= */ NULL,
216
                                /* tty= */ NULL,
217
                                /* success= */ 1);
218
#endif
219
}
220

221
static int load_user_database(Context *c) {
117✔
222
        _cleanup_free_ char *passwd_path = NULL;
117✔
223
        _cleanup_fclose_ FILE *f = NULL;
117✔
224
        struct passwd *pw;
117✔
225
        int r;
117✔
226

227
        assert(c);
117✔
228

229
        r = chase_and_fopen_unlocked("/etc/passwd", arg_root, CHASE_PREFIX_ROOT, "re", &passwd_path, &f);
117✔
230
        if (r == -ENOENT)
117✔
231
                return 0;
232
        if (r < 0)
39✔
233
                return r;
234

235
        while ((r = fgetpwent_sane(f, &pw)) > 0) {
313✔
236

237
                char *n = strdup(pw->pw_name);
274✔
238
                if (!n)
274✔
239
                        return -ENOMEM;
240

241
                /* Note that we use trivial_hash_ops_free here, so identical strings can exist in the set. */
242
                r = set_ensure_consume(&c->names, &trivial_hash_ops_free, n);
274✔
243
                if (r < 0)
274✔
244
                        return r;
245
                assert(r > 0);  /* The set uses pointer comparisons, so n must not be in the set. */
274✔
246

247
                r = hashmap_ensure_put(&c->database_by_username, &string_hash_ops, n, UID_TO_PTR(pw->pw_uid));
274✔
248
                if (r == -EEXIST)
274✔
249
                        log_debug_errno(r, "%s: user '%s' is listed twice, ignoring duplicate uid.",
×
250
                                        passwd_path, n);
251
                else if (r < 0)
274✔
252
                        return r;
253

254
                r = hashmap_ensure_put(&c->database_by_uid, /* hash_ops= */ NULL, UID_TO_PTR(pw->pw_uid), n);
274✔
255
                if (r == -EEXIST)
274✔
256
                        log_debug_errno(r, "%s: uid "UID_FMT" is listed twice, ignoring duplicate name.",
274✔
257
                                        passwd_path, pw->pw_uid);
258
                else if (r < 0)
268✔
259
                        return r;
260
        }
261
        return r;
262
}
263

264
static int load_group_database(Context *c) {
117✔
265
        _cleanup_free_ char *group_path = NULL;
117✔
266
        _cleanup_fclose_ FILE *f = NULL;
117✔
267
        struct group *gr;
117✔
268
        int r;
117✔
269

270
        assert(c);
117✔
271

272
        r = chase_and_fopen_unlocked("/etc/group", arg_root, CHASE_PREFIX_ROOT, "re", &group_path, &f);
117✔
273
        if (r == -ENOENT)
117✔
274
                return 0;
275
        if (r < 0)
39✔
276
                return r;
277

278
        while ((r = fgetgrent_sane(f, &gr)) > 0) {
587✔
279
                char *n = strdup(gr->gr_name);
548✔
280
                if (!n)
548✔
281
                        return -ENOMEM;
282

283
                /* Note that we use trivial_hash_ops_free here, so identical strings can exist in the set. */
284
                r = set_ensure_consume(&c->names, &trivial_hash_ops_free, n);
548✔
285
                if (r < 0)
548✔
286
                        return r;
287
                assert(r > 0);  /* The set uses pointer comparisons, so n must not be in the set. */
548✔
288

289
                r = hashmap_ensure_put(&c->database_by_groupname, &string_hash_ops, n, GID_TO_PTR(gr->gr_gid));
548✔
290
                if (r == -EEXIST)
548✔
291
                        log_debug_errno(r, "%s: group '%s' is listed twice, ignoring duplicate gid.",
×
292
                                        group_path, n);
293
                else if (r < 0)
548✔
294
                        return r;
295

296
                r = hashmap_ensure_put(&c->database_by_gid, /* hash_ops= */ NULL, GID_TO_PTR(gr->gr_gid), n);
548✔
297
                if (r == -EEXIST)
548✔
298
                        log_debug_errno(r, "%s: gid "GID_FMT" is listed twice, ignoring duplicate name.",
548✔
299
                                        group_path, gr->gr_gid);
300
                else if (r < 0)
536✔
301
                        return r;
302
        }
303
        return r;
304
}
305

306
static int make_backup(const char *target, const char *x) {
354✔
307
        _cleanup_(unlink_and_freep) char *dst_tmp = NULL;
×
308
        _cleanup_fclose_ FILE *dst = NULL;
354✔
309
        _cleanup_close_ int src = -EBADF;
354✔
310
        const char *backup;
354✔
311
        struct stat st;
354✔
312
        int r;
354✔
313

314
        assert(target);
354✔
315
        assert(x);
354✔
316

317
        src = open(x, O_RDONLY|O_CLOEXEC|O_NOCTTY);
354✔
318
        if (src < 0) {
354✔
319
                if (errno == ENOENT) /* No backup necessary... */
312✔
320
                        return 0;
321

322
                return -errno;
×
323
        }
324

325
        if (fstat(src, &st) < 0)
42✔
326
                return -errno;
×
327

328
        r = fopen_temporary_label(
42✔
329
                        target,   /* The path for which to the look up the label */
330
                        x,        /* Where we want the file actually to end up */
331
                        &dst,     /* The temporary file we write to */
332
                        &dst_tmp);
333
        if (r < 0)
42✔
334
                return r;
335

336
        r = copy_bytes(src, fileno(dst), UINT64_MAX, /* copy_flags= */ 0);
42✔
337
        if (r < 0)
42✔
338
                return r;
339

340
        backup = strjoina(x, "-");
210✔
341

342
        /* Copy over the access mask. Don't fail on chmod() or chown(). If it stays owned by us and/or
343
         * unreadable by others, then it isn't too bad... */
344
        r = fchmod_and_chown_with_fallback(fileno(dst), dst_tmp, st.st_mode & 07777, st.st_uid, st.st_gid);
42✔
345
        if (r < 0)
42✔
346
                log_warning_errno(r, "Failed to change access mode or ownership of %s: %m", backup);
×
347

348
        if (futimens(fileno(dst), (const struct timespec[2]) { st.st_atim, st.st_mtim }) < 0)
42✔
349
                log_warning_errno(errno, "Failed to fix access and modification time of %s: %m", backup);
×
350

351
        r = fsync_full(fileno(dst));
42✔
352
        if (r < 0)
42✔
353
                return r;
354

355
        if (rename(dst_tmp, backup) < 0)
42✔
356
                return -errno;
×
357

358
        dst_tmp = mfree(dst_tmp); /* disable the unlink_and_freep() hook now that the file has been renamed */
42✔
359
        return 0;
42✔
360
}
361

362
static int putgrent_with_members(
883✔
363
                Context *c,
364
                const struct group *gr,
365
                FILE *group) {
366

367
        char **a;
883✔
368
        int r;
883✔
369

370
        assert(c);
883✔
371
        assert(gr);
883✔
372
        assert(group);
883✔
373

374
        a = ordered_hashmap_get(c->members, gr->gr_name);
883✔
375
        if (a) {
883✔
376
                _cleanup_strv_free_ char **l = NULL;
10✔
377
                bool added = false;
10✔
378

379
                l = strv_copy(gr->gr_mem);
10✔
380
                if (!l)
10✔
381
                        return -ENOMEM;
382

383
                STRV_FOREACH(i, a) {
20✔
384
                        if (strv_contains(l, *i))
10✔
385
                                continue;
5✔
386

387
                        r = strv_extend(&l, *i);
5✔
388
                        if (r < 0)
5✔
389
                                return r;
390

391
                        added = true;
392
                }
393

394
                if (added) {
10✔
395
                        struct group t;
5✔
396

397
                        strv_sort_uniq(l);
5✔
398

399
                        t = *gr;
5✔
400
                        t.gr_mem = l;
5✔
401

402
                        r = putgrent_sane(&t, group);
5✔
403
                        return r < 0 ? r : 1;
10✔
404
                }
405
        }
406

407
        return putgrent_sane(gr, group);
878✔
408
}
409

410
#if ENABLE_GSHADOW
411
static int putsgent_with_members(
873✔
412
                Context *c,
413
                const struct sgrp *sg,
414
                FILE *gshadow) {
415

416
        char **a;
873✔
417
        int r;
873✔
418

419
        assert(sg);
873✔
420
        assert(gshadow);
873✔
421

422
        a = ordered_hashmap_get(c->members, sg->sg_namp);
873✔
423
        if (a) {
873✔
424
                _cleanup_strv_free_ char **l = NULL;
10✔
425
                bool added = false;
10✔
426

427
                l = strv_copy(sg->sg_mem);
10✔
428
                if (!l)
10✔
429
                        return -ENOMEM;
430

431
                STRV_FOREACH(i, a) {
20✔
432
                        if (strv_contains(l, *i))
10✔
433
                                continue;
5✔
434

435
                        r = strv_extend(&l, *i);
5✔
436
                        if (r < 0)
5✔
437
                                return r;
438

439
                        added = true;
440
                }
441

442
                if (added) {
10✔
443
                        struct sgrp t;
5✔
444

445
                        strv_sort_uniq(l);
5✔
446

447
                        t = *sg;
5✔
448
                        t.sg_mem = l;
5✔
449

450
                        r = putsgent_sane(&t, gshadow);
5✔
451
                        return r < 0 ? r : 1;
10✔
452
                }
453
        }
454

455
        return putsgent_sane(sg, gshadow);
868✔
456
}
457
#endif
458

459
static const char* pick_shell(const Item *i) {
274✔
460
        assert(i);
274✔
461

462
        if (i->type != ADD_USER)
274✔
463
                return NULL;
464
        if (i->shell)
272✔
465
                return i->shell;
466
        if (i->uid_set && i->uid == 0)
251✔
467
                return default_root_shell(arg_root);
10✔
468
        return NOLOGIN;
469
}
470

471
static int read_shell_credential(const Item *i, char **ret) {
271✔
472
        _cleanup_free_ char *cn = NULL, *shell = NULL;
271✔
473
        int r;
271✔
474

475
        assert(i);
271✔
476

477
        cn = strjoin("passwd.shell.", i->name);
271✔
478
        if (!cn)
271✔
479
                return -ENOMEM;
480

481
        r = read_credential(cn, (void**) &shell, /* ret_size= */ NULL);
271✔
482
        if (r < 0) {
271✔
483
                log_debug_errno(r, "Couldn't read credential '%s', ignoring: %m", cn);
269✔
484
                if (ret)
269✔
485
                        *ret = NULL;
269✔
486
                return 0;
487
        }
488

489
        path_simplify(shell);
2✔
490
        if (!valid_shell(shell))
2✔
491
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
2✔
492
                                       "Shell credential '%s' specifies invalid login shell '%s'.",
493
                                       cn, shell);
494

UNCOV
495
        if (ret)
×
UNCOV
496
                *ret = TAKE_PTR(shell);
×
497
        return 0;
498
}
499

500
static int write_temporary_passwd(
117✔
501
                Context *c,
502
                const char *passwd_path,
503
                FILE **ret_tmpfile,
504
                char **ret_tmpfile_path) {
505

506
        _cleanup_fclose_ FILE *original = NULL, *passwd = NULL;
117✔
507
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL;
117✔
508
        struct passwd *pw = NULL;
117✔
509
        Item *i;
117✔
510
        int r;
117✔
511

512
        assert(c);
117✔
513
        assert(ret_tmpfile);
117✔
514
        assert(ret_tmpfile_path);
117✔
515

516
        if (ordered_hashmap_isempty(c->todo_uids))
117✔
517
                goto done;
27✔
518

519
        if (arg_dry_run) {
90✔
520
                ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
1✔
521
                        r = read_shell_credential(i, /* ret= */ NULL);
1✔
522
                        if (r < 0)
1✔
523
                                return r;
1✔
524
                }
525

UNCOV
526
                log_info("Would write /etc/passwd%s", glyph(GLYPH_ELLIPSIS));
×
UNCOV
527
                goto done;
×
528
        }
529

530
        r = fopen_temporary_label("/etc/passwd", passwd_path, &passwd, &passwd_tmp);
89✔
531
        if (r < 0)
89✔
UNCOV
532
                return log_debug_errno(r, "Failed to open temporary copy of %s: %m", passwd_path);
×
533

534
        original = fopen(passwd_path, "re");
89✔
535
        if (original) {
89✔
536

537
                /* Allow fallback path for when /proc is not mounted. On any normal system /proc will be
538
                 * mounted, but e.g. when 'dnf --installroot' is used, it might not be. There is no security
539
                 * relevance here, since the environment is ultimately trusted, and not requiring /proc makes
540
                 * it easier to depend on sysusers in packaging scripts and suchlike. */
541
                r = copy_rights_with_fallback(fileno(original), fileno(passwd), passwd_tmp);
15✔
542
                if (r < 0)
15✔
UNCOV
543
                        return log_debug_errno(r, "Failed to copy permissions from %s to %s: %m",
×
544
                                               passwd_path, passwd_tmp);
545

546
                while ((r = fgetpwent_sane(original, &pw)) > 0) {
170✔
547
                        i = ordered_hashmap_get(c->users, pw->pw_name);
160✔
548
                        if (i && i->todo_user)
160✔
UNCOV
549
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
550
                                                       "%s: User \"%s\" already exists.",
551
                                                       passwd_path, pw->pw_name);
552

553
                        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(pw->pw_uid)))
160✔
UNCOV
554
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
555
                                                       "%s: Detected collision for UID " UID_FMT ".",
556
                                                       passwd_path, pw->pw_uid);
557

558
                        /* Make sure we keep the NIS entries (if any) at the end. */
559
                        if (IN_SET(pw->pw_name[0], '+', '-'))
160✔
560
                                break;
561

562
                        r = putpwent_sane(pw, passwd);
155✔
563
                        if (r < 0)
155✔
UNCOV
564
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
565
                                                       pw->pw_name);
566
                }
567
                if (r < 0)
15✔
UNCOV
568
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
569

570
        } else {
571
                if (errno != ENOENT)
74✔
UNCOV
572
                        return log_debug_errno(errno, "Failed to open %s: %m", passwd_path);
×
573
                if (fchmod(fileno(passwd), 0644) < 0)
74✔
UNCOV
574
                        return log_debug_errno(errno, "Failed to fchmod %s: %m", passwd_tmp);
×
575
        }
576

577
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
358✔
578
                _cleanup_free_ char *creds_shell = NULL;
270✔
579

580
                struct passwd n = {
1,080✔
581
                        .pw_name = i->name,
270✔
582
                        .pw_uid = i->uid,
270✔
583
                        .pw_gid = i->gid,
270✔
584
                        .pw_gecos = (char*) strempty(i->description),
270✔
585

586
                        /* "x" means the password is stored in the shadow file */
587
                        .pw_passwd = (char*) PASSWORD_SEE_SHADOW,
588

589
                        /* We default to the root directory as home */
590
                        .pw_dir = i->home ?: (char*) "/",
270✔
591

592
                        /* Initialize the shell to nologin, with one exception:
593
                         * for root we patch in something special */
594
                        .pw_shell = (char*) pick_shell(i),
270✔
595
                };
596

597
                r = read_shell_credential(i, &creds_shell);
270✔
598
                if (r < 0)
270✔
599
                        return r;
600
                if (creds_shell)
269✔
UNCOV
601
                        n.pw_shell = creds_shell;
×
602

603
                r = putpwent_sane(&n, passwd);
269✔
604
                if (r < 0)
269✔
UNCOV
605
                        return log_debug_errno(r, "Failed to add new user \"%s\" to temporary passwd file: %m",
×
606
                                               i->name);
607
        }
608

609
        /* Append the remaining NIS entries if any */
610
        while (pw) {
88✔
611
                r = putpwent_sane(pw, passwd);
5✔
612
                if (r < 0)
5✔
UNCOV
613
                        return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
614
                                               pw->pw_name);
615

616
                r = fgetpwent_sane(original, &pw);
5✔
617
                if (r < 0)
5✔
UNCOV
618
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
619
                if (r == 0)
5✔
620
                        break;
621
        }
622

623
        r = fflush_sync_and_check(passwd);
88✔
624
        if (r < 0)
88✔
625
                return log_debug_errno(r, "Failed to flush %s: %m", passwd_tmp);
×
626

627
done:
88✔
628
        *ret_tmpfile = TAKE_PTR(passwd);
115✔
629
        *ret_tmpfile_path = TAKE_PTR(passwd_tmp);
115✔
630
        return 0;
115✔
631
}
632

633
static int write_temporary_shadow(
115✔
634
                Context *c,
635
                const char *shadow_path,
636
                FILE **ret_tmpfile,
637
                char **ret_tmpfile_path) {
638

639
        _cleanup_fclose_ FILE *original = NULL, *shadow = NULL;
115✔
640
        _cleanup_(unlink_and_freep) char *shadow_tmp = NULL;
115✔
641
        struct spwd *sp = NULL;
115✔
642
        long lstchg;
115✔
643
        Item *i;
115✔
644
        int r;
115✔
645

646
        assert(c);
115✔
647
        assert(ret_tmpfile);
115✔
648
        assert(ret_tmpfile_path);
115✔
649

650
        if (ordered_hashmap_isempty(c->todo_uids))
115✔
651
                goto done;
27✔
652

653
        if (arg_dry_run) {
88✔
UNCOV
654
                log_info("Would write /etc/shadow%s", glyph(GLYPH_ELLIPSIS));
×
UNCOV
655
                goto done;
×
656
        }
657

658
        r = fopen_temporary_label("/etc/shadow", shadow_path, &shadow, &shadow_tmp);
88✔
659
        if (r < 0)
88✔
660
                return log_debug_errno(r, "Failed to open temporary copy of %s: %m", shadow_path);
×
661

662
        lstchg = (long) (source_date_epoch_or_now() / USEC_PER_DAY);
88✔
663

664
        original = fopen(shadow_path, "re");
88✔
665
        if (original) {
88✔
666

667
                r = copy_rights_with_fallback(fileno(original), fileno(shadow), shadow_tmp);
5✔
668
                if (r < 0)
5✔
669
                        return log_debug_errno(r, "Failed to copy permissions from %s to %s: %m",
×
670
                                               shadow_path, shadow_tmp);
671

672
                while ((r = fgetspent_sane(original, &sp)) > 0) {
135✔
673
                        i = ordered_hashmap_get(c->users, sp->sp_namp);
130✔
674
                        if (i && i->todo_user) {
130✔
675
                                /* we will update the existing entry */
UNCOV
676
                                sp->sp_lstchg = lstchg;
×
677

678
                                /* only the /etc/shadow stage is left, so we can
679
                                 * safely remove the item from the todo set */
UNCOV
680
                                i->todo_user = false;
×
UNCOV
681
                                ordered_hashmap_remove(c->todo_uids, UID_TO_PTR(i->uid));
×
682
                        }
683

684
                        /* Make sure we keep the NIS entries (if any) at the end. */
685
                        if (IN_SET(sp->sp_namp[0], '+', '-'))
130✔
686
                                break;
687

688
                        r = putspent_sane(sp, shadow);
130✔
689
                        if (r < 0)
130✔
UNCOV
690
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary shadow file: %m",
×
691
                                                       sp->sp_namp);
692

693
                }
694
                if (r < 0)
5✔
695
                        return log_debug_errno(r, "Failed to read %s: %m", shadow_path);
×
696

697
        } else {
698
                if (errno != ENOENT)
83✔
UNCOV
699
                        return log_debug_errno(errno, "Failed to open %s: %m", shadow_path);
×
700
                if (fchmod(fileno(shadow), 0000) < 0)
83✔
701
                        return log_debug_errno(errno, "Failed to fchmod %s: %m", shadow_tmp);
×
702
        }
703

704
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
357✔
705
                _cleanup_(erase_and_freep) char *creds_password = NULL;
269✔
706
                bool is_hashed;
269✔
707

708
                struct spwd n = {
538✔
709
                        .sp_namp = i->name,
269✔
710
                        .sp_lstchg = lstchg,
711
                        .sp_min = -1,
712
                        .sp_max = -1,
713
                        .sp_warn = -1,
714
                        .sp_inact = -1,
715
                        .sp_expire = i->locked ? 1 : -1, /* Negative expiration means "unset". Expiration 0 or 1 means "locked" */
269✔
716
                        .sp_flag = ULONG_MAX, /* this appears to be what everybody does ... */
717
                };
718

719
                r = get_credential_user_password(i->name, &creds_password, &is_hashed);
269✔
720
                if (r < 0)
269✔
721
                        log_debug_errno(r, "Couldn't read password credential for user '%s', ignoring: %m", i->name);
269✔
722

723
                if (creds_password && !is_hashed) {
269✔
724
                        _cleanup_(erase_and_freep) char* plaintext_password = TAKE_PTR(creds_password);
×
UNCOV
725
                        r = hash_password(plaintext_password, &creds_password);
×
UNCOV
726
                        if (r < 0)
×
UNCOV
727
                                return log_debug_errno(r, "Failed to hash password: %m");
×
728
                }
729

730
                if (creds_password)
269✔
UNCOV
731
                        n.sp_pwdp = creds_password;
×
732
                else if (streq(i->name, "root"))
269✔
733
                        /* Let firstboot set the password later */
734
                        n.sp_pwdp = (char*) PASSWORD_UNPROVISIONED;
10✔
735
                else
736
                        n.sp_pwdp = (char*) PASSWORD_LOCKED_AND_INVALID;
259✔
737

738
                r = putspent_sane(&n, shadow);
269✔
739
                if (r < 0)
269✔
UNCOV
740
                        return log_debug_errno(r, "Failed to add new user \"%s\" to temporary shadow file: %m",
×
741
                                               i->name);
742
        }
743

744
        /* Append the remaining NIS entries if any */
745
        while (sp) {
88✔
UNCOV
746
                r = putspent_sane(sp, shadow);
×
UNCOV
747
                if (r < 0)
×
UNCOV
748
                        return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary shadow file: %m",
×
749
                                               sp->sp_namp);
750

UNCOV
751
                r = fgetspent_sane(original, &sp);
×
UNCOV
752
                if (r < 0)
×
UNCOV
753
                        return log_debug_errno(r, "Failed to read %s: %m", shadow_path);
×
UNCOV
754
                if (r == 0)
×
755
                        break;
756
        }
757
        if (!IN_SET(errno, 0, ENOENT))
88✔
UNCOV
758
                return -errno;
×
759

760
        r = fflush_sync_and_check(shadow);
88✔
761
        if (r < 0)
88✔
762
                return log_debug_errno(r, "Failed to flush %s: %m", shadow_tmp);
×
763

764
done:
88✔
765
        *ret_tmpfile = TAKE_PTR(shadow);
115✔
766
        *ret_tmpfile_path = TAKE_PTR(shadow_tmp);
115✔
767
        return 0;
115✔
768
}
769

770
static int write_temporary_group(
117✔
771
                Context *c,
772
                const char *group_path,
773
                FILE **ret_tmpfile,
774
                char **ret_tmpfile_path) {
775

776
        _cleanup_fclose_ FILE *original = NULL, *group = NULL;
117✔
777
        _cleanup_(unlink_and_freep) char *group_tmp = NULL;
117✔
778
        bool group_changed = false;
117✔
779
        struct group *gr = NULL;
117✔
780
        Item *i;
117✔
781
        int r;
117✔
782

783
        assert(c);
117✔
784
        assert(ret_tmpfile);
117✔
785
        assert(ret_tmpfile_path);
117✔
786

787
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
117✔
788
                goto done;
26✔
789

790
        if (arg_dry_run) {
91✔
791
                log_info("Would write /etc/group%s", glyph(GLYPH_ELLIPSIS));
2✔
792
                goto done;
1✔
793
        }
794

795
        r = fopen_temporary_label("/etc/group", group_path, &group, &group_tmp);
90✔
796
        if (r < 0)
90✔
UNCOV
797
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", group_path);
×
798

799
        original = fopen(group_path, "re");
90✔
800
        if (original) {
90✔
801

802
                r = copy_rights_with_fallback(fileno(original), fileno(group), group_tmp);
16✔
803
                if (r < 0)
16✔
UNCOV
804
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
805
                                               group_path, group_tmp);
806

807
                while ((r = fgetgrent_sane(original, &gr)) > 0) {
379✔
808
                        /* Safety checks against name and GID collisions. Normally,
809
                         * this should be unnecessary, but given that we look at the
810
                         * entries anyway here, let's make an extra verification
811
                         * step that we don't generate duplicate entries. */
812

813
                        i = ordered_hashmap_get(c->groups, gr->gr_name);
368✔
814
                        if (i && i->todo_group)
368✔
UNCOV
815
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
816
                                                       "%s: Group \"%s\" already exists.",
817
                                                       group_path, gr->gr_name);
818

819
                        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gr->gr_gid)))
368✔
UNCOV
820
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
821
                                                       "%s: Detected collision for GID " GID_FMT ".",
822
                                                       group_path, gr->gr_gid);
823

824
                        /* Make sure we keep the NIS entries (if any) at the end. */
825
                        if (IN_SET(gr->gr_name[0], '+', '-'))
368✔
826
                                break;
827

828
                        r = putgrent_with_members(c, gr, group);
363✔
829
                        if (r < 0)
363✔
UNCOV
830
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
831
                                                       gr->gr_name);
832
                        if (r > 0)
363✔
UNCOV
833
                                group_changed = true;
×
834
                }
835
                if (r < 0)
16✔
UNCOV
836
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
837

838
        } else {
839
                if (errno != ENOENT)
74✔
UNCOV
840
                        return log_error_errno(errno, "Failed to open %s: %m", group_path);
×
841
                if (fchmod(fileno(group), 0644) < 0)
74✔
UNCOV
842
                        return log_error_errno(errno, "Failed to fchmod %s: %m", group_tmp);
×
843
        }
844

845
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
610✔
846
                struct group n = {
520✔
847
                        .gr_name = i->name,
520✔
848
                        .gr_gid = i->gid,
520✔
849
                        .gr_passwd = (char*) PASSWORD_SEE_SHADOW,
850
                };
851

852
                r = putgrent_with_members(c, &n, group);
520✔
853
                if (r < 0)
520✔
UNCOV
854
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary group file: %m",
×
855
                                               n.gr_name);
856

857
                group_changed = true;
520✔
858
        }
859

860
        /* Append the remaining NIS entries if any */
861
        while (gr) {
100✔
862
                r = putgrent_sane(gr, group);
15✔
863
                if (r < 0)
15✔
UNCOV
864
                        return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
865
                                               gr->gr_name);
866

867
                r = fgetgrent_sane(original, &gr);
15✔
868
                if (r < 0)
15✔
UNCOV
869
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
870
                if (r == 0)
15✔
871
                        break;
872
        }
873

874
        r = fflush_sync_and_check(group);
90✔
875
        if (r < 0)
90✔
UNCOV
876
                return log_error_errno(r, "Failed to flush %s: %m", group_tmp);
×
877

878
done:
90✔
879
        if (group_changed) {
117✔
880
                *ret_tmpfile = TAKE_PTR(group);
90✔
881
                *ret_tmpfile_path = TAKE_PTR(group_tmp);
90✔
882
        } else {
883
                *ret_tmpfile = NULL;
27✔
884
                *ret_tmpfile_path = NULL;
27✔
885
        }
886
        return 0;
887
}
888

889
static int write_temporary_gshadow(
117✔
890
                Context *c,
891
                const char * gshadow_path,
892
                FILE **ret_tmpfile,
893
                char **ret_tmpfile_path) {
894

895
#if ENABLE_GSHADOW
896
        _cleanup_fclose_ FILE *original = NULL, *gshadow = NULL;
117✔
897
        _cleanup_(unlink_and_freep) char *gshadow_tmp = NULL;
117✔
898
        bool group_changed = false;
117✔
899
        Item *i;
117✔
900
        int r;
117✔
901

902
        assert(c);
117✔
903
        assert(ret_tmpfile);
117✔
904
        assert(ret_tmpfile_path);
117✔
905

906
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
117✔
907
                goto done;
26✔
908

909
        if (arg_dry_run) {
91✔
910
                log_info("Would write /etc/gshadow%s", glyph(GLYPH_ELLIPSIS));
2✔
911
                goto done;
1✔
912
        }
913

914
        r = fopen_temporary_label("/etc/gshadow", gshadow_path, &gshadow, &gshadow_tmp);
90✔
915
        if (r < 0)
90✔
UNCOV
916
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", gshadow_path);
×
917

918
        original = fopen(gshadow_path, "re");
90✔
919
        if (original) {
90✔
920
                struct sgrp *sg;
6✔
921

922
                r = copy_rights_with_fallback(fileno(original), fileno(gshadow), gshadow_tmp);
6✔
923
                if (r < 0)
6✔
UNCOV
924
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
925
                                               gshadow_path, gshadow_tmp);
926

927
                while ((r = fgetsgent_sane(original, &sg)) > 0) {
359✔
928

929
                        i = ordered_hashmap_get(c->groups, sg->sg_namp);
353✔
930
                        if (i && i->todo_group)
353✔
UNCOV
931
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
932
                                                       "%s: Group \"%s\" already exists.",
933
                                                       gshadow_path, sg->sg_namp);
934

935
                        r = putsgent_with_members(c, sg, gshadow);
353✔
936
                        if (r < 0)
353✔
UNCOV
937
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary gshadow file: %m",
×
938
                                                       sg->sg_namp);
939
                        if (r > 0)
353✔
UNCOV
940
                                group_changed = true;
×
941
                }
942
                if (r < 0)
6✔
943
                        return r;
944

945
        } else {
946
                if (errno != ENOENT)
84✔
UNCOV
947
                        return log_error_errno(errno, "Failed to open %s: %m", gshadow_path);
×
948
                if (fchmod(fileno(gshadow), 0000) < 0)
84✔
UNCOV
949
                        return log_error_errno(errno, "Failed to fchmod %s: %m", gshadow_tmp);
×
950
        }
951

952
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
610✔
953
                struct sgrp n = {
520✔
954
                        .sg_namp = i->name,
520✔
955
                        .sg_passwd = (char*) PASSWORD_LOCKED_AND_INVALID,
956
                };
957

958
                r = putsgent_with_members(c, &n, gshadow);
520✔
959
                if (r < 0)
520✔
960
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary gshadow file: %m",
×
961
                                               n.sg_namp);
962

963
                group_changed = true;
520✔
964
        }
965

966
        r = fflush_sync_and_check(gshadow);
90✔
967
        if (r < 0)
90✔
968
                return log_error_errno(r, "Failed to flush %s: %m", gshadow_tmp);
×
969

970
done:
90✔
971
        if (group_changed) {
117✔
972
                *ret_tmpfile = TAKE_PTR(gshadow);
90✔
973
                *ret_tmpfile_path = TAKE_PTR(gshadow_tmp);
90✔
974
        } else
975
#endif
976
        {
977
                *ret_tmpfile = NULL;
27✔
978
                *ret_tmpfile_path = NULL;
27✔
979
        }
980
        return 0;
981
}
982

983
static int write_files(Context *c) {
117✔
984
        _cleanup_fclose_ FILE *passwd = NULL, *group = NULL, *shadow = NULL, *gshadow = NULL;
351✔
985
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL, *group_tmp = NULL, *shadow_tmp = NULL, *gshadow_tmp = NULL;
117✔
986
        int r;
117✔
987

988
        _cleanup_free_ char *passwd_path = path_join(arg_root, "/etc/passwd");
234✔
989
        if (!passwd_path)
117✔
UNCOV
990
                return log_oom();
×
991

992
        _cleanup_free_ char *shadow_path = path_join(arg_root, "/etc/shadow");
234✔
993
        if (!shadow_path)
117✔
UNCOV
994
                return log_oom();
×
995

996
        _cleanup_free_ char *group_path = path_join(arg_root, "/etc/group");
234✔
997
        if (!group_path)
117✔
UNCOV
998
                return log_oom();
×
999

1000
        _cleanup_free_ char *gshadow_path = path_join(arg_root, "/etc/gshadow");
234✔
1001
        if (!gshadow_path)
117✔
UNCOV
1002
                return log_oom();
×
1003

1004
        assert(c);
117✔
1005

1006
        r = write_temporary_group(c, group_path, &group, &group_tmp);
117✔
1007
        if (r < 0)
117✔
1008
                return r;
1009

1010
        r = write_temporary_gshadow(c, gshadow_path, &gshadow, &gshadow_tmp);
117✔
1011
        if (r < 0)
117✔
1012
                return r;
1013

1014
        r = write_temporary_passwd(c, passwd_path, &passwd, &passwd_tmp);
117✔
1015
        if (r < 0)
117✔
1016
                return r;
1017

1018
        r = write_temporary_shadow(c, shadow_path, &shadow, &shadow_tmp);
115✔
1019
        if (r < 0)
115✔
1020
                return r;
1021

1022
        /* Make a backup of the old files */
1023
        if (group) {
115✔
1024
                r = make_backup("/etc/group", group_path);
89✔
1025
                if (r < 0)
89✔
UNCOV
1026
                        return log_error_errno(r, "Failed to backup %s: %m", group_path);
×
1027
        }
1028
        if (gshadow) {
115✔
1029
                r = make_backup("/etc/gshadow", gshadow_path);
89✔
1030
                if (r < 0)
89✔
UNCOV
1031
                        return log_error_errno(r, "Failed to backup %s: %m", gshadow_path);
×
1032
        }
1033

1034
        if (passwd) {
115✔
1035
                r = make_backup("/etc/passwd", passwd_path);
88✔
1036
                if (r < 0)
88✔
1037
                        return log_error_errno(r, "Failed to backup %s: %m", passwd_path);
×
1038
        }
1039
        if (shadow) {
115✔
1040
                r = make_backup("/etc/shadow", shadow_path);
88✔
1041
                if (r < 0)
88✔
UNCOV
1042
                        return log_error_errno(r, "Failed to backup %s: %m", shadow_path);
×
1043
        }
1044

1045
        /* And make the new files count */
1046
        if (group) {
115✔
1047
                r = rename_and_apply_smack_floor_label(group_tmp, group_path);
89✔
1048
                if (r < 0)
89✔
UNCOV
1049
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1050
                                               group_tmp, group_path);
1051
                group_tmp = mfree(group_tmp);
89✔
1052
        }
1053
        /* OK, we have written the group entries successfully */
1054
        log_audit_accounts(c, ADD_GROUP);
115✔
1055
        if (gshadow) {
115✔
1056
                r = rename_and_apply_smack_floor_label(gshadow_tmp, gshadow_path);
89✔
1057
                if (r < 0)
89✔
UNCOV
1058
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1059
                                               gshadow_tmp, gshadow_path);
1060

1061
                gshadow_tmp = mfree(gshadow_tmp);
89✔
1062
        }
1063

1064
        if (passwd) {
115✔
1065
                r = rename_and_apply_smack_floor_label(passwd_tmp, passwd_path);
88✔
1066
                if (r < 0)
88✔
UNCOV
1067
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1068
                                               passwd_tmp, passwd_path);
1069

1070
                passwd_tmp = mfree(passwd_tmp);
88✔
1071
        }
1072
        /* OK, we have written the user entries successfully */
1073
        log_audit_accounts(c, ADD_USER);
115✔
1074
        if (shadow) {
115✔
1075
                r = rename_and_apply_smack_floor_label(shadow_tmp, shadow_path);
88✔
1076
                if (r < 0)
88✔
UNCOV
1077
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1078
                                               shadow_tmp, shadow_path);
1079

1080
                shadow_tmp = mfree(shadow_tmp);
88✔
1081
        }
1082

1083
        return 0;
1084
}
1085

1086
static int uid_is_ok(
291✔
1087
                Context *c,
1088
                uid_t uid,
1089
                const char *name,
1090
                bool check_with_gid) {
1091

1092
        int r;
291✔
1093
        assert(c);
291✔
1094

1095
        /* Let's see if we already have assigned the UID a second time */
1096
        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(uid)))
291✔
1097
                return 0;
1098

1099
        /* Try to avoid using uids that are already used by a group
1100
         * that doesn't have the same name as our new user. */
1101
        if (check_with_gid) {
286✔
1102
                Item *i;
206✔
1103

1104
                i = ordered_hashmap_get(c->todo_gids, GID_TO_PTR(uid));
206✔
1105
                if (i && !streq(i->name, name))
206✔
1106
                        return 0;
1107
        }
1108

1109
        /* Let's check the files directly */
1110
        if (hashmap_contains(c->database_by_uid, UID_TO_PTR(uid)))
276✔
1111
                return 0;
1112

1113
        if (check_with_gid) {
276✔
1114
                const char *n;
196✔
1115

1116
                n = hashmap_get(c->database_by_gid, GID_TO_PTR(uid));
196✔
1117
                if (n && !streq(n, name))
196✔
1118
                        return 0;
1119
        }
1120

1121
        /* Let's also check via NSS, to avoid UID clashes over LDAP and such, just in case */
1122
        if (!arg_root) {
271✔
1123
                _cleanup_free_ struct group *g = NULL;
6✔
1124

1125
                r = getpwuid_malloc(uid, /* ret= */ NULL);
6✔
1126
                if (r >= 0)
6✔
1127
                        return 0;
1128
                if (r != -ESRCH)
6✔
1129
                        log_warning_errno(r, "Unexpected failure while looking up UID '" UID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
×
1130

1131
                if (check_with_gid) {
6✔
1132
                        r = getgrgid_malloc((gid_t) uid, &g);
6✔
1133
                        if (r >= 0) {
6✔
UNCOV
1134
                                if (!streq(g->gr_name, name))
×
1135
                                        return 0;
1136
                        } else if (r != -ESRCH)
6✔
1137
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
6✔
1138
                }
1139
        }
1140

1141
        return 1;
1142
}
1143

UNCOV
1144
static int root_stat(const char *p, struct stat *ret_st) {
×
UNCOV
1145
        return chase_and_stat(p, arg_root, CHASE_PREFIX_ROOT, /* ret_path= */ NULL, ret_st);
×
1146
}
1147

1148
static int read_id_from_file(Item *i, uid_t *ret_uid, gid_t *ret_gid) {
74✔
1149
        struct stat st;
74✔
1150
        bool found_uid = false, found_gid = false;
74✔
1151
        uid_t uid = 0;
74✔
1152
        gid_t gid = 0;
74✔
1153

1154
        assert(i);
74✔
1155

1156
        /* First, try to get the GID directly */
1157
        if (ret_gid && i->gid_path && root_stat(i->gid_path, &st) >= 0) {
74✔
UNCOV
1158
                gid = st.st_gid;
×
UNCOV
1159
                found_gid = true;
×
1160
        }
1161

1162
        /* Then, try to get the UID directly */
1163
        if ((ret_uid || (ret_gid && !found_gid))
74✔
1164
            && i->uid_path
74✔
UNCOV
1165
            && root_stat(i->uid_path, &st) >= 0) {
×
1166

UNCOV
1167
                uid = st.st_uid;
×
UNCOV
1168
                found_uid = true;
×
1169

1170
                /* If we need the gid, but had no success yet, also derive it from the UID path */
UNCOV
1171
                if (ret_gid && !found_gid) {
×
UNCOV
1172
                        gid = st.st_gid;
×
UNCOV
1173
                        found_gid = true;
×
1174
                }
1175
        }
1176

1177
        /* If that didn't work yet, then let's reuse the GID as UID */
1178
        if (ret_uid && !found_uid && i->gid_path) {
74✔
1179

UNCOV
1180
                if (found_gid) {
×
1181
                        uid = (uid_t) gid;
1182
                        found_uid = true;
UNCOV
1183
                } else if (root_stat(i->gid_path, &st) >= 0) {
×
UNCOV
1184
                        uid = (uid_t) st.st_gid;
×
UNCOV
1185
                        found_uid = true;
×
1186
                }
1187
        }
1188

1189
        if (ret_uid) {
74✔
1190
                if (!found_uid)
41✔
1191
                        return 0;
74✔
1192

UNCOV
1193
                *ret_uid = uid;
×
1194
        }
1195

1196
        if (ret_gid) {
33✔
1197
                if (!found_gid)
33✔
1198
                        return 0;
1199

1200
                *ret_gid = gid;
×
1201
        }
1202

1203
        return 1;
1204
}
1205

1206
static int add_user(Context *c, Item *i) {
301✔
1207
        void *z;
301✔
1208
        int r;
301✔
1209

1210
        assert(c);
301✔
1211
        assert(i);
301✔
1212

1213
        /* Check the database directly */
1214
        z = hashmap_get(c->database_by_username, i->name);
301✔
1215
        if (z) {
301✔
1216
                log_debug("User %s already exists.", i->name);
30✔
1217
                i->uid = PTR_TO_UID(z);
30✔
1218
                i->uid_set = true;
30✔
1219
                return 0;
30✔
1220
        }
1221

1222
        if (!arg_root) {
271✔
1223
                _cleanup_free_ struct passwd *p = NULL;
6✔
1224

1225
                /* Also check NSS */
1226
                r = getpwnam_malloc(i->name, &p);
6✔
1227
                if (r >= 0) {
6✔
UNCOV
1228
                        log_debug("User %s already exists.", i->name);
×
1229
                        i->uid = p->pw_uid;
×
1230
                        i->uid_set = true;
×
1231

1232
                        r = free_and_strdup(&i->description, p->pw_gecos);
×
1233
                        if (r < 0)
×
1234
                                return log_oom();
×
1235

1236
                        return 0;
1237
                }
1238
                if (r != -ESRCH)
6✔
1239
                        log_warning_errno(r, "Unexpected failure while looking up user '%s' via NSS, assuming it doesn't exist: %m", i->name);
6✔
1240
        }
1241

1242
        /* Try to use the suggested numeric UID */
1243
        if (i->uid_set) {
271✔
1244
                r = uid_is_ok(c, i->uid, i->name, !i->id_set_strict);
235✔
1245
                if (r < 0)
235✔
UNCOV
1246
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1247
                if (r == 0) {
235✔
1248
                        log_info("Suggested user ID " UID_FMT " for %s already used.", i->uid, i->name);
5✔
1249
                        i->uid_set = false;
5✔
1250
                }
1251
        }
1252

1253
        /* If that didn't work, try to read it from the specified path */
1254
        if (!i->uid_set) {
271✔
1255
                uid_t candidate;
41✔
1256

1257
                if (read_id_from_file(i, &candidate, NULL) > 0) {
41✔
1258

UNCOV
1259
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
UNCOV
1260
                                log_debug("User ID " UID_FMT " of file not suitable for %s.", candidate, i->name);
×
1261
                        else {
1262
                                r = uid_is_ok(c, candidate, i->name, true);
×
UNCOV
1263
                                if (r < 0)
×
UNCOV
1264
                                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
UNCOV
1265
                                else if (r > 0) {
×
1266
                                        i->uid = candidate;
×
UNCOV
1267
                                        i->uid_set = true;
×
1268
                                } else
UNCOV
1269
                                        log_debug("User ID " UID_FMT " of file for %s is already used.", candidate, i->name);
×
1270
                        }
1271
                }
1272
        }
1273

1274
        /* Otherwise, try to reuse the group ID */
1275
        if (!i->uid_set && i->gid_set) {
271✔
1276
                r = uid_is_ok(c, (uid_t) i->gid, i->name, true);
41✔
1277
                if (r < 0)
41✔
UNCOV
1278
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1279
                if (r > 0) {
41✔
1280
                        i->uid = (uid_t) i->gid;
26✔
1281
                        i->uid_set = true;
26✔
1282
                }
1283
        }
1284

1285
        /* And if that didn't work either, let's try to find a free one */
1286
        if (!i->uid_set) {
271✔
1287
                maybe_emit_login_defs_warning(c);
15✔
1288

1289
                for (;;) {
15✔
1290
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
15✔
1291
                        if (r < 0)
15✔
UNCOV
1292
                                return log_error_errno(r, "No free user ID available for %s.", i->name);
×
1293

1294
                        r = uid_is_ok(c, c->search_uid, i->name, true);
15✔
1295
                        if (r < 0)
15✔
UNCOV
1296
                                return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1297
                        else if (r > 0)
15✔
1298
                                break;
1299
                }
1300

1301
                i->uid_set = true;
15✔
1302
                i->uid = c->search_uid;
15✔
1303
        }
1304

1305
        r = ordered_hashmap_ensure_put(&c->todo_uids, NULL, UID_TO_PTR(i->uid), i);
271✔
1306
        if (r == -EEXIST)
271✔
UNCOV
1307
                return log_error_errno(r, "Requested user %s with UID " UID_FMT " and gid" GID_FMT " to be created is duplicated "
×
1308
                                       "or conflicts with another user.", i->name, i->uid, i->gid);
1309
        if (r == -ENOMEM)
271✔
UNCOV
1310
                return log_oom();
×
1311
        if (r < 0)
271✔
UNCOV
1312
                return log_error_errno(r, "Failed to store user %s with UID " UID_FMT " and GID " GID_FMT " to be created: %m",
×
1313
                                       i->name, i->uid, i->gid);
1314

1315
        i->todo_user = true;
271✔
1316
        log_info("Creating user '%s' (%s) with UID " UID_FMT " and GID " GID_FMT ".",
512✔
1317
                 i->name, strna(i->description), i->uid, i->gid);
1318

1319
        return 0;
1320
}
1321

1322
static int gid_is_ok(
807✔
1323
                Context *c,
1324
                gid_t gid,
1325
                const char *groupname,
1326
                bool check_with_uid) {
1327

1328
        Item *user;
807✔
1329
        char *username;
807✔
1330
        int r;
807✔
1331

1332
        assert(c);
807✔
1333
        assert(groupname);
807✔
1334

1335
        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gid)))
807✔
1336
                return 0;
1337

1338
        /* Avoid reusing gids that are already used by a different user */
1339
        if (check_with_uid) {
742✔
1340
                user = ordered_hashmap_get(c->todo_uids, UID_TO_PTR(gid));
408✔
1341
                if (user && !streq(user->name, groupname))
408✔
1342
                        return 0;
1343
        }
1344

1345
        if (hashmap_contains(c->database_by_gid, GID_TO_PTR(gid)))
742✔
1346
                return 0;
1347

1348
        if (check_with_uid) {
522✔
1349
                username = hashmap_get(c->database_by_uid, UID_TO_PTR(gid));
188✔
1350
                if (username && !streq(username, groupname))
188✔
1351
                        return 0;
1352
        }
1353

1354
        if (!arg_root) {
522✔
1355
                r = getgrgid_malloc(gid, /* ret= */ NULL);
8✔
1356
                if (r >= 0)
8✔
1357
                        return 0;
1358
                if (r != -ESRCH)
8✔
UNCOV
1359
                        log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1360

1361
                if (check_with_uid) {
8✔
1362
                        r = getpwuid_malloc(gid, /* ret= */ NULL);
8✔
1363
                        if (r >= 0)
8✔
1364
                                return 0;
1365
                        if (r != -ESRCH)
8✔
UNCOV
1366
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1367
                }
1368
        }
1369

1370
        return 1;
1371
}
1372

1373
static int get_gid_by_name(
652✔
1374
                Context *c,
1375
                const char *name,
1376
                gid_t *ret_gid) {
1377

1378
        void *z;
652✔
1379
        int r;
652✔
1380

1381
        assert(c);
652✔
1382
        assert(ret_gid);
652✔
1383

1384
        /* Check the database directly */
1385
        z = hashmap_get(c->database_by_groupname, name);
652✔
1386
        if (z) {
652✔
1387
                *ret_gid = PTR_TO_GID(z);
69✔
1388
                return 0;
69✔
1389
        }
1390

1391
        /* Also check NSS */
1392
        if (!arg_root) {
583✔
1393
                _cleanup_free_ struct group *g = NULL;
8✔
1394

1395
                r = getgrnam_malloc(name, &g);
8✔
1396
                if (r >= 0) {
8✔
UNCOV
1397
                        *ret_gid = g->gr_gid;
×
UNCOV
1398
                        return 0;
×
1399
                }
1400
                if (r != -ESRCH)
8✔
1401
                        log_warning_errno(r, "Unexpected failure while looking up group '%s' via NSS, assuming it doesn't exist: %m", name);
8✔
1402
        }
1403

1404
        return -ENOENT;
1405
}
1406

1407
static int add_group(Context *c, Item *i) {
646✔
1408
        int r;
646✔
1409

1410
        assert(c);
646✔
1411
        assert(i);
646✔
1412

1413
        r = get_gid_by_name(c, i->name, &i->gid);
646✔
1414
        if (r != -ENOENT) {
646✔
1415
                if (r < 0)
64✔
1416
                        return r;
1417
                log_debug("Group %s already exists.", i->name);
64✔
1418
                i->gid_set = true;
64✔
1419
                return 0;
64✔
1420
        }
1421

1422
        /* Try to use the suggested numeric GID */
1423
        if (i->gid_set) {
582✔
1424
                r = gid_is_ok(c, i->gid, i->name, false);
394✔
1425
                if (r < 0)
394✔
UNCOV
1426
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1427
                if (i->id_set_strict) {
394✔
1428
                        /* If we require the GID to already exist we can return here:
1429
                         * r > 0: means the GID does not exist -> fail
1430
                         * r == 0: means the GID exists -> nothing more to do.
1431
                         */
1432
                        if (r > 0)
61✔
1433
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
1434
                                                       "Failed to create %s: please create GID " GID_FMT,
1435
                                                       i->name, i->gid);
1436
                        if (r == 0)
60✔
1437
                                return 0;
1438
                }
1439
                if (r == 0) {
333✔
1440
                        log_info("Suggested group ID " GID_FMT " for %s already used.", i->gid, i->name);
×
UNCOV
1441
                        i->gid_set = false;
×
1442
                }
1443
        }
1444

1445
        /* Try to reuse the numeric uid, if there's one */
1446
        if (!i->gid_set && i->uid_set) {
521✔
1447
                r = gid_is_ok(c, (gid_t) i->uid, i->name, true);
160✔
1448
                if (r < 0)
160✔
UNCOV
1449
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1450
                if (r > 0) {
160✔
1451
                        i->gid = (gid_t) i->uid;
155✔
1452
                        i->gid_set = true;
155✔
1453
                }
1454
        }
1455

1456
        /* If that didn't work, try to read it from the specified path */
1457
        if (!i->gid_set) {
521✔
1458
                gid_t candidate;
33✔
1459

1460
                if (read_id_from_file(i, NULL, &candidate) > 0) {
33✔
1461

UNCOV
1462
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
UNCOV
1463
                                log_debug("Group ID " GID_FMT " of file not suitable for %s.", candidate, i->name);
×
1464
                        else {
UNCOV
1465
                                r = gid_is_ok(c, candidate, i->name, true);
×
UNCOV
1466
                                if (r < 0)
×
UNCOV
1467
                                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
UNCOV
1468
                                else if (r > 0) {
×
UNCOV
1469
                                        i->gid = candidate;
×
1470
                                        i->gid_set = true;
×
1471
                                } else
1472
                                        log_debug("Group ID " GID_FMT " of file for %s already used.", candidate, i->name);
×
1473
                        }
1474
                }
1475
        }
1476

1477
        /* And if that didn't work either, let's try to find a free one */
1478
        if (!i->gid_set) {
521✔
1479
                maybe_emit_login_defs_warning(c);
33✔
1480

1481
                for (;;) {
253✔
1482
                        /* We look for new GIDs in the UID pool! */
1483
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
253✔
1484
                        if (r < 0)
253✔
UNCOV
1485
                                return log_error_errno(r, "No free group ID available for %s.", i->name);
×
1486

1487
                        r = gid_is_ok(c, c->search_uid, i->name, true);
253✔
1488
                        if (r < 0)
253✔
UNCOV
1489
                                return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1490
                        else if (r > 0)
253✔
1491
                                break;
1492
                }
1493

1494
                i->gid_set = true;
33✔
1495
                i->gid = c->search_uid;
33✔
1496
        }
1497

1498
        r = ordered_hashmap_ensure_put(&c->todo_gids, NULL, GID_TO_PTR(i->gid), i);
521✔
1499
        if (r == -EEXIST)
521✔
UNCOV
1500
                return log_error_errno(r, "Requested group %s with GID "GID_FMT " to be created is duplicated or conflicts with another user.", i->name, i->gid);
×
1501
        if (r == -ENOMEM)
521✔
UNCOV
1502
                return log_oom();
×
1503
        if (r < 0)
521✔
UNCOV
1504
                return log_error_errno(r, "Failed to store group %s with GID " GID_FMT " to be created: %m", i->name, i->gid);
×
1505

1506
        i->todo_group = true;
521✔
1507
        log_info("Creating group '%s' with GID " GID_FMT ".", i->name, i->gid);
521✔
1508

1509
        return 0;
1510
}
1511

1512
static int process_item(Context *c, Item *i) {
682✔
1513
        int r;
682✔
1514

1515
        assert(c);
682✔
1516
        assert(i);
682✔
1517

1518
        switch (i->type) {
682✔
1519

1520
        case ADD_USER: {
303✔
1521
                Item *j = NULL;
303✔
1522

1523
                if (!i->gid_set) {
303✔
1524
                        j = ordered_hashmap_get(c->groups, i->group_name ?: i->name);
239✔
1525

1526
                        /* If that's not a match, also check if the group name
1527
                         * matches a user name in the queue. */
1528
                        if (!j && i->group_name)
239✔
1529
                                j = ordered_hashmap_get(c->users, i->group_name);
11✔
1530
                }
1531

1532
                if (j && j->todo_group) {
239✔
1533
                        /* When a group with the target name is already in queue,
1534
                         * use the information about the group and do not create
1535
                         * duplicated group entry. */
1536
                        i->gid_set = j->gid_set;
30✔
1537
                        i->gid = j->gid;
30✔
1538
                        i->id_set_strict = true;
30✔
1539
                } else if (i->group_name) {
273✔
1540
                        /* When a group name was given instead of a GID and it's
1541
                         * not in queue, then it must already exist. */
1542
                        r = get_gid_by_name(c, i->group_name, &i->gid);
6✔
1543
                        if (r < 0)
6✔
1544
                                return log_error_errno(r, "Group %s not found.", i->group_name);
1✔
1545
                        i->gid_set = true;
5✔
1546
                        i->id_set_strict = true;
5✔
1547
                } else {
1548
                        r = add_group(c, i);
267✔
1549
                        if (r < 0)
267✔
1550
                                return r;
1551
                }
1552

1553
                return add_user(c, i);
301✔
1554
        }
1555

1556
        case ADD_GROUP:
379✔
1557
                return add_group(c, i);
379✔
1558

UNCOV
1559
        default:
×
UNCOV
1560
                assert_not_reached();
×
1561
        }
1562
}
1563

1564
static Item* item_free(Item *i) {
685✔
1565
        if (!i)
685✔
1566
                return NULL;
1567

1568
        free(i->name);
685✔
1569
        free(i->group_name);
685✔
1570
        free(i->uid_path);
685✔
1571
        free(i->gid_path);
685✔
1572
        free(i->description);
685✔
1573
        free(i->home);
685✔
1574
        free(i->shell);
685✔
1575
        free(i->filename);
685✔
1576
        return mfree(i);
685✔
1577
}
1578

1579
DEFINE_TRIVIAL_CLEANUP_FUNC(Item*, item_free);
1,380✔
1580
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, char, string_hash_func, string_compare_func, Item, item_free);
682✔
1581

1582
static Item* item_new(ItemType type, const char *name, const char *filename, unsigned line) {
685✔
1583
        assert(name);
685✔
1584
        assert(!!filename == (line > 0));
685✔
1585

1586
        _cleanup_(item_freep) Item *new = new(Item, 1);
1,370✔
1587
        if (!new)
685✔
1588
                return NULL;
1589

1590
        *new = (Item) {
685✔
1591
                .type = type,
1592
                .line = line,
1593
        };
1594

1595
        if (free_and_strdup(&new->name, name) < 0 ||
1,370✔
1596
            free_and_strdup(&new->filename, filename) < 0)
685✔
1597
                return NULL;
1598

1599
        return TAKE_PTR(new);
685✔
1600
}
1601

1602
static int add_implicit(Context *c) {
117✔
1603
        char *g, **l;
117✔
1604
        int r;
117✔
1605

1606
        assert(c);
117✔
1607

1608
        /* Implicitly create additional users and groups, if they were listed in "m" lines */
1609
        ORDERED_HASHMAP_FOREACH_KEY(l, g, c->members) {
244✔
1610
                STRV_FOREACH(m, l)
20✔
1611
                        if (!ordered_hashmap_contains(c->users, *m)) {
10✔
UNCOV
1612
                                _cleanup_(item_freep) Item *j =
×
1613
                                        item_new(ADD_USER, *m, /* filename= */ NULL, /* line= */ 0);
5✔
1614
                                if (!j)
5✔
UNCOV
1615
                                        return log_oom();
×
1616

1617
                                r = ordered_hashmap_ensure_put(&c->users, &item_hash_ops, j->name, j);
5✔
1618
                                if (r == -ENOMEM)
5✔
UNCOV
1619
                                        return log_oom();
×
1620
                                if (r < 0)
5✔
UNCOV
1621
                                        return log_error_errno(r, "Failed to add implicit user '%s': %m", j->name);
×
1622

1623
                                log_debug("Adding implicit user '%s' due to m line", j->name);
5✔
1624
                                TAKE_PTR(j);
5✔
1625
                        }
1626

1627
                if (!(ordered_hashmap_contains(c->users, g) ||
10✔
1628
                      ordered_hashmap_contains(c->groups, g))) {
2✔
UNCOV
1629
                        _cleanup_(item_freep) Item *j =
×
UNCOV
1630
                                item_new(ADD_GROUP, g, /* filename= */ NULL, /* line= */ 0);
×
1631
                        if (!j)
×
UNCOV
1632
                                return log_oom();
×
1633

UNCOV
1634
                        r = ordered_hashmap_ensure_put(&c->groups, &item_hash_ops, j->name, j);
×
UNCOV
1635
                        if (r == -ENOMEM)
×
UNCOV
1636
                                return log_oom();
×
UNCOV
1637
                        if (r < 0)
×
UNCOV
1638
                                return log_error_errno(r, "Failed to add implicit group '%s': %m", j->name);
×
1639

UNCOV
1640
                        log_debug("Adding implicit group '%s' due to m line", j->name);
×
UNCOV
1641
                        TAKE_PTR(j);
×
1642
                }
1643
        }
1644

1645
        return 0;
117✔
1646
}
1647

1648
static int item_equivalent(Item *a, Item *b) {
2✔
1649
        int r;
2✔
1650

1651
        assert(a);
2✔
1652
        assert(b);
2✔
1653

1654
        if (a->type != b->type) {
2✔
UNCOV
1655
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1656
                           "Item not equivalent because types differ");
1657
                return false;
1658
        }
1659

1660
        if (!streq_ptr(a->name, b->name)) {
2✔
1661
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1662
                           "Item not equivalent because names differ ('%s' vs. '%s')",
1663
                           a->name, b->name);
1664
                return false;
1665
        }
1666

1667
        /* Paths were simplified previously, so we can use streq. */
1668
        if (!streq_ptr(a->uid_path, b->uid_path)) {
2✔
UNCOV
1669
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1670
                           "Item not equivalent because UID paths differ (%s vs. %s)",
1671
                           a->uid_path ?: "(unset)", b->uid_path ?: "(unset)");
1672
                return false;
1673
        }
1674

1675
        if (!streq_ptr(a->gid_path, b->gid_path)) {
2✔
UNCOV
1676
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1677
                           "Item not equivalent because GID paths differ (%s vs. %s)",
1678
                           a->gid_path ?: "(unset)", b->gid_path ?: "(unset)");
1679
                return false;
1680
        }
1681

1682
        if (!streq_ptr(a->description, b->description))  {
2✔
UNCOV
1683
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1684
                           "Item not equivalent because descriptions differ ('%s' vs. '%s')",
1685
                           strempty(a->description), strempty(b->description));
1686
                return false;
1687
        }
1688

1689
        if ((a->uid_set != b->uid_set) ||
2✔
UNCOV
1690
            (a->uid_set && a->uid != b->uid)) {
×
UNCOV
1691
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1692
                           "Item not equivalent because UIDs differ (%s vs. %s)",
1693
                           a->uid_set ? FORMAT_UID(a->uid) : "(unset)",
1694
                           b->uid_set ? FORMAT_UID(b->uid) : "(unset)");
1695
                return false;
×
1696
        }
1697

1698
        if ((a->gid_set != b->gid_set) ||
2✔
1699
            (a->gid_set && a->gid != b->gid)) {
1✔
UNCOV
1700
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1701
                           "Item not equivalent because GIDs differ (%s vs. %s)",
1702
                           a->gid_set ? FORMAT_GID(a->gid) : "(unset)",
1703
                           b->gid_set ? FORMAT_GID(b->gid) : "(unset)");
UNCOV
1704
                return false;
×
1705
        }
1706

1707
        if (!streq_ptr(a->home, b->home)) {
2✔
UNCOV
1708
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1709
                           "Item not equivalent because home directories differ ('%s' vs. '%s')",
1710
                           strempty(a->description), strempty(b->description));
1711
                return false;
1712
        }
1713

1714
        /* Check if the two paths refer to the same file.
1715
         * If the paths are equal (after normalization), it's obviously the same file.
1716
         * If both paths specify a nologin shell, treat them as the same (e.g. /bin/true and /bin/false).
1717
         * Otherwise, try to resolve the paths, and see if we get the same result, (e.g. /sbin/nologin and
1718
         * /usr/sbin/nologin).
1719
         * If we can't resolve something, treat different paths as different. */
1720

1721
        const char *a_shell = pick_shell(a),
2✔
1722
                   *b_shell = pick_shell(b);
2✔
1723
        if (!path_equal(a_shell, b_shell) &&
2✔
UNCOV
1724
            !(is_nologin_shell(a_shell) && is_nologin_shell(b_shell))) {
×
UNCOV
1725
                _cleanup_free_ char *pa = NULL, *pb = NULL;
×
1726

UNCOV
1727
                r = chase(a_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pa, NULL);
×
UNCOV
1728
                if (r < 0) {
×
UNCOV
1729
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1730
                                       r, "Failed to look up path '%s%s%s': %m",
1731
                                       strempty(arg_root), arg_root ? "/" : "", a_shell);
UNCOV
1732
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1733
                }
1734

1735
                r = chase(b_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pb, NULL);
×
1736
                if (r < 0) {
×
UNCOV
1737
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1738
                                       r, "Failed to look up path '%s%s%s': %m",
1739
                                       strempty(arg_root), arg_root ? "/" : "", b_shell);
UNCOV
1740
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1741
                }
1742

UNCOV
1743
                if (!path_equal(pa, pb)) {
×
UNCOV
1744
                        log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1745
                                   "Item not equivalent because shells differ ('%s' vs. '%s')",
1746
                                   pa, pb);
1747
                        return false;
1748
                }
1749
        }
1750

1751
        return true;
1752
}
1753

1754
static int parse_line(
690✔
1755
                const char *fname,
1756
                unsigned line,
1757
                const char *buffer,
1758
                bool *invalid_config,
1759
                void *context) {
1760

1761
        Context *c = ASSERT_PTR(context);
690✔
1762
        _cleanup_free_ char *action = NULL,
1,380✔
1763
                *name = NULL, *resolved_name = NULL,
×
UNCOV
1764
                *id = NULL, *resolved_id = NULL,
×
UNCOV
1765
                *description = NULL, *resolved_description = NULL,
×
UNCOV
1766
                *home = NULL, *resolved_home = NULL,
×
1767
                *shell = NULL, *resolved_shell = NULL;
690✔
1768
        _cleanup_(item_freep) Item *i = NULL;
690✔
1769
        Item *existing;
690✔
1770
        OrderedHashmap *h;
690✔
1771
        int r;
690✔
1772
        const char *p;
690✔
1773

1774
        assert(fname);
690✔
1775
        assert(line >= 1);
690✔
1776
        assert(buffer);
690✔
1777
        assert(!invalid_config); /* We don't support invalid_config yet. */
690✔
1778

1779
        /* Parse columns */
1780
        p = buffer;
690✔
1781
        r = extract_many_words(&p, NULL, EXTRACT_UNQUOTE,
690✔
1782
                               &action, &name, &id, &description, &home, &shell);
1783
        if (r < 0)
690✔
UNCOV
1784
                return log_syntax(NULL, LOG_ERR, fname, line, r, "Syntax error.");
×
1785
        if (r < 2)
690✔
UNCOV
1786
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1787
                                  "Missing action and name columns.");
1788
        if (!isempty(p))
690✔
UNCOV
1789
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1790
                                  "Trailing garbage.");
1791

1792
        if (isempty(action))
690✔
UNCOV
1793
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1794
                                  "Empty command specification.");
1795

1796
        bool locked = false;
1797
        for (int pos = 1; action[pos]; pos++)
712✔
1798
                if (action[pos] == '!' && !locked)
22✔
1799
                        locked = true;
22✔
1800
                else
UNCOV
1801
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1802
                                          "Unknown modifiers in command '%s'.", action);
1803

1804
        if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE))
690✔
UNCOV
1805
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1806
                                  "Unknown command type '%c'.", action[0]);
1807

1808
        /* Verify name */
1809
        if (empty_or_dash(name))
690✔
1810
                name = mfree(name);
×
1811

1812
        if (name) {
690✔
1813
                r = specifier_printf(name, NAME_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_name);
690✔
1814
                if (r < 0)
690✔
UNCOV
1815
                        return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to replace specifiers in '%s': %m", name);
×
1816

1817
                if (!valid_user_group_name(resolved_name, 0))
690✔
UNCOV
1818
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1819
                                          "'%s' is not a valid user or group name.", resolved_name);
1820
        }
1821

1822
        /* Verify id */
1823
        if (empty_or_dash(id))
690✔
1824
                id = mfree(id);
61✔
1825

1826
        if (id) {
690✔
1827
                r = specifier_printf(id, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_id);
629✔
1828
                if (r < 0)
629✔
UNCOV
1829
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1830
                                          "Failed to replace specifiers in '%s': %m", name);
1831
        }
1832

1833
        /* Verify description */
1834
        if (empty_or_dash(description))
690✔
1835
                description = mfree(description);
635✔
1836

1837
        if (description) {
690✔
1838
                r = specifier_printf(description, LONG_LINE_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_description);
55✔
1839
                if (r < 0)
55✔
UNCOV
1840
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1841
                                          "Failed to replace specifiers in '%s': %m", description);
1842

1843
                if (!valid_gecos(resolved_description))
55✔
UNCOV
1844
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1845
                                          "'%s' is not a valid GECOS field.", resolved_description);
1846
        }
1847

1848
        /* Verify home */
1849
        if (empty_or_dash(home))
690✔
1850
                home = mfree(home);
547✔
1851

1852
        if (home) {
690✔
1853
                r = specifier_printf(home, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_home);
143✔
1854
                if (r < 0)
143✔
1855
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1856
                                          "Failed to replace specifiers in '%s': %m", home);
1857

1858
                path_simplify(resolved_home);
143✔
1859

1860
                if (!valid_home(resolved_home))
143✔
UNCOV
1861
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1862
                                          "'%s' is not a valid home directory field.", resolved_home);
1863
        }
1864

1865
        /* Verify shell */
1866
        if (empty_or_dash(shell))
690✔
1867
                shell = mfree(shell);
669✔
1868

1869
        if (shell) {
690✔
1870
                r = specifier_printf(shell, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_shell);
21✔
1871
                if (r < 0)
21✔
UNCOV
1872
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1873
                                          "Failed to replace specifiers in '%s': %m", shell);
1874

1875
                path_simplify(resolved_shell);
21✔
1876

1877
                if (!valid_shell(resolved_shell))
21✔
UNCOV
1878
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1879
                                          "'%s' is not a valid login shell field.", resolved_shell);
1880
        }
1881

1882
        switch (action[0]) {
690✔
1883

UNCOV
1884
        case ADD_RANGE:
×
UNCOV
1885
                if (locked)
×
UNCOV
1886
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1887
                                          "Flag '!' not permitted on lines of type 'r'.");
1888

UNCOV
1889
                if (resolved_name)
×
UNCOV
1890
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1891
                                          "Lines of type 'r' don't take a name field.");
1892

UNCOV
1893
                if (!resolved_id)
×
UNCOV
1894
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1895
                                          "Lines of type 'r' require an ID range in the third field.");
1896

UNCOV
1897
                if (description || home || shell)
×
UNCOV
1898
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1899
                                          "Lines of type '%c' don't take a %s field.",
1900
                                          action[0],
1901
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1902

UNCOV
1903
                r = uid_range_add_str(&c->uid_range, resolved_id);
×
UNCOV
1904
                if (r < 0)
×
UNCOV
1905
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1906
                                          "Invalid UID range %s.", resolved_id);
1907

1908
                return 0;
1909

1910
        case ADD_MEMBER: {
10✔
1911
                /* Try to extend an existing member or group item */
1912
                if (!name)
10✔
1913
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1914
                                          "Lines of type 'm' require a user name in the second field.");
1915

1916
                if (locked)
10✔
UNCOV
1917
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1918
                                          "Flag '!' not permitted on lines of type 'm'.");
1919

1920
                if (!resolved_id)
10✔
UNCOV
1921
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1922
                                          "Lines of type 'm' require a group name in the third field.");
1923

1924
                if (!valid_user_group_name(resolved_id, 0))
10✔
UNCOV
1925
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1926
                                               "'%s' is not a valid user or group name.", resolved_id);
1927

1928
                if (description || home || shell)
10✔
UNCOV
1929
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1930
                                          "Lines of type '%c' don't take a %s field.",
1931
                                          action[0],
1932
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1933

1934
                r = string_strv_ordered_hashmap_put(&c->members, resolved_id, resolved_name);
10✔
1935
                if (r < 0)
10✔
UNCOV
1936
                        return log_error_errno(r, "Failed to store mapping for %s: %m", resolved_id);
×
1937

1938
                return 0;
1939
        }
1940

1941
        case ADD_USER:
300✔
1942
                if (!name)
300✔
UNCOV
1943
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1944
                                          "Lines of type 'u' require a user name in the second field.");
1945

1946
                r = ordered_hashmap_ensure_allocated(&c->users, &item_hash_ops);
300✔
1947
                if (r < 0)
300✔
UNCOV
1948
                        return log_oom();
×
1949

1950
                i = item_new(ADD_USER, resolved_name, fname, line);
300✔
1951
                if (!i)
300✔
UNCOV
1952
                        return log_oom();
×
1953

1954
                if (resolved_id) {
300✔
1955
                        if (path_is_absolute(resolved_id))
268✔
UNCOV
1956
                                i->uid_path = path_simplify(TAKE_PTR(resolved_id));
×
1957
                        else {
1958
                                _cleanup_free_ char *uid = NULL, *gid = NULL;
268✔
1959
                                if (split_pair(resolved_id, ":", &uid, &gid) == 0) {
268✔
1960
                                        r = parse_gid(gid, &i->gid);
90✔
1961
                                        if (r < 0) {
90✔
1962
                                                if (valid_user_group_name(gid, 0))
26✔
1963
                                                        i->group_name = TAKE_PTR(gid);
26✔
1964
                                                else
UNCOV
1965
                                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1966
                                                                          "Failed to parse GID: '%s': %m", id);
1967
                                        } else {
1968
                                                i->gid_set = true;
64✔
1969
                                                i->id_set_strict = true;
64✔
1970
                                        }
1971
                                        free_and_replace(resolved_id, uid);
90✔
1972
                                }
1973
                                if (!streq(resolved_id, "-")) {
268✔
1974
                                        r = parse_uid(resolved_id, &i->uid);
253✔
1975
                                        if (r < 0)
253✔
1976
                                                return log_syntax(NULL, LOG_ERR, fname, line, r,
1✔
1977
                                                                  "Failed to parse UID: '%s': %m", id);
1978
                                        i->uid_set = true;
252✔
1979
                                }
1980
                        }
1981
                }
1982

1983
                i->description = TAKE_PTR(resolved_description);
299✔
1984
                i->home = TAKE_PTR(resolved_home);
299✔
1985
                i->shell = TAKE_PTR(resolved_shell);
299✔
1986
                i->locked = locked;
299✔
1987

1988
                h = c->users;
299✔
1989
                break;
299✔
1990

1991
        case ADD_GROUP:
380✔
1992
                if (!name)
380✔
UNCOV
1993
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1994
                                          "Lines of type 'g' require a user name in the second field.");
1995

1996
                if (locked)
380✔
UNCOV
1997
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1998
                                          "Flag '!' not permitted on lines of type 'g'.");
1999

2000
                if (description || home || shell)
380✔
2001
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
2002
                                          "Lines of type '%c' don't take a %s field.",
2003
                                          action[0],
2004
                                          description ? "GECOS" : home ? "home directory" : "login shell");
2005

2006
                r = ordered_hashmap_ensure_allocated(&c->groups, &item_hash_ops);
380✔
2007
                if (r < 0)
380✔
UNCOV
2008
                        return log_oom();
×
2009

2010
                i = item_new(ADD_GROUP, resolved_name, fname, line);
380✔
2011
                if (!i)
380✔
2012
                        return log_oom();
×
2013

2014
                if (resolved_id) {
380✔
2015
                        if (path_is_absolute(resolved_id))
351✔
UNCOV
2016
                                i->gid_path = path_simplify(TAKE_PTR(resolved_id));
×
2017
                        else {
2018
                                r = parse_gid(resolved_id, &i->gid);
351✔
2019
                                if (r < 0)
351✔
UNCOV
2020
                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
2021
                                                          "Failed to parse GID: '%s': %m", id);
2022

2023
                                i->gid_set = true;
351✔
2024
                        }
2025
                }
2026

2027
                h = c->groups;
380✔
2028
                break;
380✔
2029

UNCOV
2030
        default:
×
UNCOV
2031
                assert_not_reached();
×
2032
        }
2033

2034
        existing = ordered_hashmap_get(h, i->name);
679✔
2035
        if (existing) {
679✔
2036
                /* Two functionally-equivalent items are fine */
2037
                r = item_equivalent(i, existing);
2✔
2038
                if (r < 0)
2✔
2039
                        return r;
2040
                if (r == 0) {
2✔
UNCOV
2041
                        if (existing->filename)
×
UNCOV
2042
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2043
                                           "Conflict with earlier configuration for %s '%s' in %s:%u, ignoring line.",
2044
                                           item_type_to_string(i->type),
2045
                                           i->name,
2046
                                           existing->filename, existing->line);
2047
                        else
2048
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2049
                                           "Conflict with earlier configuration for %s '%s', ignoring line.",
2050
                                           item_type_to_string(i->type),
2051
                                           i->name);
2052
                }
2053

2054
                return 0;
2055
        }
2056

2057
        r = ordered_hashmap_put(h, i->name, i);
677✔
2058
        if (r < 0)
677✔
2059
                return log_oom();
×
2060

2061
        i = NULL;
677✔
2062
        return 0;
677✔
2063
}
2064

2065
static int read_config_file(Context *c, const char *fn, bool ignore_enoent) {
137✔
2066
        return conf_file_read(
411✔
2067
                        arg_root,
2068
                        (const char**) CONF_PATHS_STRV("sysusers.d"),
137✔
2069
                        ASSERT_PTR(fn),
137✔
2070
                        parse_line,
2071
                        ASSERT_PTR(c),
137✔
2072
                        ignore_enoent,
2073
                        /* invalid_config= */ NULL);
2074
}
2075

2076
static int cat_config(void) {
×
UNCOV
2077
        _cleanup_strv_free_ char **files = NULL;
×
2078
        int r;
×
2079

UNCOV
2080
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, NULL);
×
UNCOV
2081
        if (r < 0)
×
2082
                return r;
2083

UNCOV
2084
        pager_open(arg_pager_flags);
×
2085

2086
        return cat_files(NULL, files, arg_cat_flags);
×
2087
}
2088

2089
static int help(void) {
×
UNCOV
2090
        _cleanup_free_ char *link = NULL;
×
2091
        _cleanup_(table_unrefp) Table *cmds = NULL, *opts = NULL;
×
2092
        int r;
×
2093

UNCOV
2094
        r = terminal_urlify_man("systemd-sysusers.service", "8", &link);
×
2095
        if (r < 0)
×
UNCOV
2096
                return log_oom();
×
2097

UNCOV
2098
        r = option_parser_get_help_table(&cmds);
×
UNCOV
2099
        if (r < 0)
×
2100
                return r;
2101

UNCOV
2102
        r = option_parser_get_help_table_group("Options", &opts);
×
UNCOV
2103
        if (r < 0)
×
2104
                return r;
2105

UNCOV
2106
        (void) table_sync_column_widths(0, cmds, opts);
×
2107

UNCOV
2108
        printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n"
×
2109
               "\n%sCreates system user and group accounts.%s\n"
2110
               "\nCommands:\n",
2111
               program_invocation_short_name,
2112
               ansi_highlight(),
2113
               ansi_normal());
2114

2115
        r = table_print_or_warn(cmds);
×
2116
        if (r < 0)
×
2117
                return r;
2118

2119
        printf("\nOptions:\n");
×
2120

2121
        r = table_print_or_warn(opts);
×
2122
        if (r < 0)
×
2123
                return r;
2124

UNCOV
2125
        printf("\nSee the %s for details.\n", link);
×
2126
        return 0;
2127
}
2128

2129
static int parse_argv(int argc, char *argv[], char ***ret_args) {
117✔
2130
        int r;
117✔
2131

2132
        assert(argc >= 0);
117✔
2133
        assert(argv);
117✔
2134

2135
        OptionParser opts = { argc, argv };
117✔
2136

2137
        FOREACH_OPTION_OR_RETURN(c, &opts)
387✔
2138
                switch (c) {
153✔
2139

2140
                OPTION_COMMON_CAT_CONFIG:
×
UNCOV
2141
                        arg_cat_flags = CAT_CONFIG_ON;
×
UNCOV
2142
                        break;
×
2143

UNCOV
2144
                OPTION_COMMON_TLDR:
×
UNCOV
2145
                        arg_cat_flags = CAT_TLDR;
×
2146
                        break;
×
2147

UNCOV
2148
                OPTION_COMMON_HELP:
×
2149
                        return help();
×
2150

UNCOV
2151
                OPTION_COMMON_VERSION:
×
UNCOV
2152
                        return version();
×
2153

2154
                OPTION_GROUP("Options"): {}
110✔
2155

2156
                OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"):
110✔
2157
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
110✔
2158
                        if (r < 0)
110✔
2159
                                return r;
2160
                        break;
2161

UNCOV
2162
                OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"):
×
2163
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
2164
                        if (r < 0)
×
2165
                                return r;
2166
                        break;
2167

UNCOV
2168
                OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"):
×
UNCOV
2169
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
UNCOV
2170
                        if (r < 0)
×
2171
                                return r;
2172
                        break;
2173

2174
                OPTION_LONG("replace", "PATH", "Treat arguments as replacement for PATH"):
35✔
2175
                        if (!path_is_absolute(opts.arg))
35✔
2176
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2177
                                                       "The argument to --replace= must be an absolute path.");
2178
                        if (!endswith(opts.arg, ".conf"))
35✔
UNCOV
2179
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2180
                                                       "The argument to --replace= must have the extension '.conf'.");
2181

2182
                        arg_replace = opts.arg;
35✔
2183
                        break;
35✔
2184

2185
                OPTION_LONG("dry-run", NULL, "Just print what would be done"):
1✔
2186
                        arg_dry_run = true;
1✔
2187
                        break;
1✔
2188

2189
                OPTION_LONG("inline", NULL, "Treat arguments as configuration lines"):
7✔
2190
                        arg_inline = true;
7✔
2191
                        break;
7✔
2192

UNCOV
2193
                OPTION_COMMON_NO_PAGER:
×
UNCOV
2194
                        arg_pager_flags |= PAGER_DISABLE;
×
UNCOV
2195
                        break;
×
2196
                }
2197

2198
        char **args = option_parser_get_args(&opts);
117✔
2199
        size_t n_args = option_parser_get_n_args(&opts);
117✔
2200

2201
        if (arg_replace && arg_cat_flags != CAT_CONFIG_OFF)
117✔
UNCOV
2202
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2203
                                       "Option --replace= is not supported with --cat-config/--tldr.");
2204

2205
        if (arg_inline && arg_cat_flags != CAT_CONFIG_OFF)
117✔
UNCOV
2206
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2207
                                       "Option --inline is not supported with --cat-config/--tldr.");
2208

2209
        if (arg_replace && n_args == 0)
117✔
UNCOV
2210
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2211
                                       "When --replace= is given, some configuration items must be specified.");
2212

2213
        if (arg_image && arg_root)
117✔
UNCOV
2214
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2215
                                       "Use either --root= or --image=, the combination of both is not supported.");
2216

2217
        *ret_args = args;
117✔
2218
        return 1;
117✔
2219
}
2220

2221
static int parse_arguments(Context *c, char **args) {
45✔
2222
        unsigned pos = 1;
45✔
2223
        int r;
45✔
2224

2225
        assert(c);
45✔
2226

2227
        STRV_FOREACH(arg, args) {
94✔
2228
                if (arg_inline)
49✔
2229
                        /* Use (argument):n, where n==1 for the first positional arg */
2230
                        r = parse_line("(argument)", pos, *arg, /* invalid_config= */ NULL, c);
11✔
2231
                else
2232
                        r = read_config_file(c, *arg, /* ignore_enoent= */ false);
38✔
2233
                if (r < 0)
49✔
2234
                        return r;
2235

2236
                pos++;
49✔
2237
        }
2238

2239
        return 0;
2240
}
2241

2242
static int read_config_files(Context *c, char **args) {
90✔
UNCOV
2243
        _cleanup_strv_free_ char **files = NULL;
×
2244
        _cleanup_free_ char *p = NULL;
90✔
2245
        int r;
90✔
2246

2247
        assert(c);
90✔
2248

2249
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, &p);
90✔
2250
        if (r < 0)
90✔
2251
                return r;
2252

2253
        STRV_FOREACH(f, files)
204✔
2254
                if (p && path_equal(*f, p)) {
114✔
2255
                        log_debug("Parsing arguments at position \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
18✔
2256

2257
                        r = parse_arguments(c, args);
18✔
2258
                        if (r < 0)
18✔
2259
                                return r;
2260
                } else {
2261
                        log_debug("Reading config file \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
120✔
2262

2263
                        /* Just warn, ignore result otherwise */
2264
                        (void) read_config_file(c, *f, /* ignore_enoent= */ true);
96✔
2265
                }
2266

2267
        return 0;
2268
}
2269

2270
static int read_credential_lines(Context *c) {
117✔
2271
        _cleanup_free_ char *j = NULL;
117✔
2272
        const char *d;
117✔
2273
        int r;
117✔
2274

2275
        assert(c);
117✔
2276

2277
        r = get_credentials_dir(&d);
117✔
2278
        if (r == -ENXIO)
117✔
2279
                return 0;
2280
        if (r < 0)
3✔
UNCOV
2281
                return log_error_errno(r, "Failed to get credentials directory: %m");
×
2282

2283
        j = path_join(d, "sysusers.extra");
3✔
2284
        if (!j)
3✔
UNCOV
2285
                return log_oom();
×
2286

2287
        (void) read_config_file(c, j, /* ignore_enoent= */ true);
3✔
2288
        return 0;
2289
}
2290

2291
static int run(int argc, char *argv[]) {
117✔
2292
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
117✔
UNCOV
2293
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
×
2294
        _cleanup_close_ int lock = -EBADF;
117✔
2295
        _cleanup_(context_done) Context c = {
117✔
2296
                .audit_fd = -EBADF,
2297
                .search_uid = UID_INVALID,
2298
        };
2299

2300
        Item *i;
117✔
2301
        int r;
117✔
2302

2303
        LIBAUDIT_NOTE(recommended);
117✔
2304
        LIBBLKID_NOTE(recommended);
117✔
2305
        LIBCRYPT_NOTE(recommended);
117✔
2306
        LIBCRYPTSETUP_NOTE(suggested);
117✔
2307
        LIBCRYPTO_NOTE(suggested);
117✔
2308
        LIBMOUNT_NOTE(recommended);
117✔
2309
        LIBSELINUX_NOTE(recommended);
117✔
2310

2311
        char **args = NULL;
117✔
2312
        r = parse_argv(argc, argv, &args);
117✔
2313
        if (r <= 0)
117✔
2314
                return r;
2315

2316
        log_setup();
117✔
2317

2318
        if (arg_cat_flags != CAT_CONFIG_OFF)
117✔
2319
                return cat_config();
×
2320

2321
        if (should_bypass("SYSTEMD_SYSUSERS"))
117✔
2322
                return 0;
2323

2324
        umask(0022);
117✔
2325

2326
        r = mac_init();
117✔
2327
        if (r < 0)
117✔
2328
                return r;
2329

2330
        if (arg_image) {
117✔
UNCOV
2331
                assert(!arg_root);
×
2332

UNCOV
2333
                r = mount_image_privately_interactively(
×
2334
                                arg_image,
2335
                                arg_image_policy,
2336
                                DISSECT_IMAGE_GENERIC_ROOT |
2337
                                DISSECT_IMAGE_REQUIRE_ROOT |
2338
                                DISSECT_IMAGE_VALIDATE_OS |
2339
                                DISSECT_IMAGE_RELAX_VAR_CHECK |
2340
                                DISSECT_IMAGE_FSCK |
2341
                                DISSECT_IMAGE_GROWFS |
2342
                                DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
2343
                                &mounted_dir,
2344
                                /* ret_dir_fd= */ NULL,
2345
                                &loop_device);
UNCOV
2346
                if (r < 0)
×
2347
                        return r;
2348

2349
                arg_root = strdup(mounted_dir);
×
UNCOV
2350
                if (!arg_root)
×
UNCOV
2351
                        return log_oom();
×
2352
        }
2353

2354
        /* Prepare to emit audit events, but only if we're operating on the host system. */
2355
        if (!arg_root)
117✔
2356
                c.audit_fd = open_audit_fd_or_warn();
7✔
2357

2358
        /* If command line arguments are specified along with --replace, read all configuration files and
2359
         * insert the positional arguments at the specified place. Otherwise, if command line arguments are
2360
         * specified, execute just them, and finally, without --replace= or any positional arguments, just
2361
         * read configuration and execute it. */
2362
        if (arg_replace || strv_isempty(args))
117✔
2363
                r = read_config_files(&c, args);
90✔
2364
        else
2365
                r = parse_arguments(&c, args);
27✔
2366
        if (r < 0)
117✔
2367
                return r;
2368

2369
        r = read_credential_lines(&c);
117✔
2370
        if (r < 0)
117✔
2371
                return r;
2372

2373
        /* Let's tell nss-systemd not to synthesize the "root" and "nobody" entries for it, so that our
2374
         * detection whether the names or UID/GID area already used otherwise doesn't get confused. After
2375
         * all, even though nss-systemd synthesizes these users/groups, they should still appear in
2376
         * /etc/passwd and /etc/group, as the synthesizing logic is merely supposed to be fallback for cases
2377
         * where we run with a completely unpopulated /etc. */
2378
        if (setenv("SYSTEMD_NSS_BYPASS_SYNTHETIC", "1", 1) < 0)
117✔
UNCOV
2379
                return log_error_errno(errno, "Failed to set SYSTEMD_NSS_BYPASS_SYNTHETIC environment variable: %m");
×
2380

2381
        if (!c.uid_range) {
117✔
2382
                /* Default to default range of SYSTEMD_UID_MIN..SYSTEM_UID_MAX. */
2383
                r = read_login_defs(&c.login_defs, NULL, arg_root);
117✔
2384
                if (r < 0)
117✔
2385
                        return log_error_errno(r, "Failed to read %s%s: %m",
×
2386
                                               strempty(arg_root), "/etc/login.defs");
2387

2388
                c.login_defs_need_warning = true;
117✔
2389

2390
                /* We pick a range that very conservative: we look at compiled-in maximum and the value in
2391
                 * /etc/login.defs. That way the UIDs/GIDs which we allocate will be interpreted correctly,
2392
                 * even if /etc/login.defs is removed later. (The bottom bound doesn't matter much, since
2393
                 * it's only used during allocation, so we use the configured value directly). */
2394
                uid_t begin = c.login_defs.system_alloc_uid_min,
117✔
2395
                      end = MIN3((uid_t) SYSTEM_UID_MAX, c.login_defs.system_uid_max, c.login_defs.system_gid_max);
117✔
2396
                if (begin < end) {
117✔
2397
                        r = uid_range_add(&c.uid_range, begin, end - begin + 1);
117✔
2398
                        if (r < 0)
117✔
UNCOV
2399
                                return log_oom();
×
2400
                }
2401
        }
2402

2403
        r = add_implicit(&c);
117✔
2404
        if (r < 0)
117✔
2405
                return r;
2406

2407
        if (!arg_dry_run) {
117✔
2408
                lock = take_etc_passwd_lock(arg_root);
116✔
2409
                if (lock < 0)
116✔
UNCOV
2410
                        return log_error_errno(lock, "Failed to take /etc/passwd lock: %m");
×
2411
        }
2412

2413
        r = load_user_database(&c);
117✔
2414
        if (r < 0)
117✔
UNCOV
2415
                return log_error_errno(r, "Failed to load user database: %m");
×
2416

2417
        r = load_group_database(&c);
117✔
2418
        if (r < 0)
117✔
UNCOV
2419
                return log_error_errno(r, "Failed to read group database: %m");
×
2420

2421
        ORDERED_HASHMAP_FOREACH(i, c.groups)
496✔
2422
                (void) process_item(&c, i);
379✔
2423

2424
        ORDERED_HASHMAP_FOREACH(i, c.users)
420✔
2425
                (void) process_item(&c, i);
303✔
2426

2427
        return write_files(&c);
117✔
2428
}
2429

2430
DEFINE_MAIN_FUNCTION(run);
117✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc