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

systemd / systemd / 29132483780

10 Jul 2026 08:43PM UTC coverage: 72.912% (+0.2%) from 72.702%
29132483780

push

github

bluca
man: run forgotten 'update-man-rules'

344600 of 472622 relevant lines covered (72.91%)

1365091.83 hits per line

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

72.73
/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);
115✔
114
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
115✔
115
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
115✔
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) {
115✔
138
        assert(c);
115✔
139

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

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

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

153
        set_free(c->names);
115✔
154
        uid_range_free(c->uid_range);
115✔
155
}
115✔
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) {
115✔
222
        _cleanup_free_ char *passwd_path = NULL;
115✔
223
        _cleanup_fclose_ FILE *f = NULL;
115✔
224
        struct passwd *pw;
115✔
225
        int r;
115✔
226

227
        assert(c);
115✔
228

229
        r = chase_and_fopen_unlocked("/etc/passwd", arg_root, CHASE_PREFIX_ROOT, "re", &passwd_path, &f);
115✔
230
        if (r == -ENOENT)
115✔
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) {
115✔
265
        _cleanup_free_ char *group_path = NULL;
115✔
266
        _cleanup_fclose_ FILE *f = NULL;
115✔
267
        struct group *gr;
115✔
268
        int r;
115✔
269

270
        assert(c);
115✔
271

272
        r = chase_and_fopen_unlocked("/etc/group", arg_root, CHASE_PREFIX_ROOT, "re", &group_path, &f);
115✔
273
        if (r == -ENOENT)
115✔
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_REFLINK);
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(
882✔
363
                Context *c,
364
                const struct group *gr,
365
                FILE *group) {
366

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

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

374
        a = ordered_hashmap_get(c->members, gr->gr_name);
882✔
375
        if (a) {
882✔
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);
877✔
408
}
409

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

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

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

422
        a = ordered_hashmap_get(c->members, sg->sg_namp);
872✔
423
        if (a) {
872✔
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);
867✔
456
}
457
#endif
458

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

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

471
static int write_temporary_passwd(
115✔
472
                Context *c,
473
                const char *passwd_path,
474
                FILE **ret_tmpfile,
475
                char **ret_tmpfile_path) {
476

477
        _cleanup_fclose_ FILE *original = NULL, *passwd = NULL;
115✔
478
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL;
115✔
479
        struct passwd *pw = NULL;
115✔
480
        Item *i;
115✔
481
        int r;
115✔
482

483
        assert(c);
115✔
484
        assert(ret_tmpfile);
115✔
485
        assert(ret_tmpfile_path);
115✔
486

487
        if (ordered_hashmap_isempty(c->todo_uids))
115✔
488
                goto done;
27✔
489

490
        if (arg_dry_run) {
88✔
491
                log_info("Would write /etc/passwd%s", glyph(GLYPH_ELLIPSIS));
×
492
                goto done;
×
493
        }
494

495
        r = fopen_temporary_label("/etc/passwd", passwd_path, &passwd, &passwd_tmp);
88✔
496
        if (r < 0)
88✔
497
                return log_debug_errno(r, "Failed to open temporary copy of %s: %m", passwd_path);
×
498

499
        original = fopen(passwd_path, "re");
88✔
500
        if (original) {
88✔
501

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

511
                while ((r = fgetpwent_sane(original, &pw)) > 0) {
170✔
512
                        i = ordered_hashmap_get(c->users, pw->pw_name);
160✔
513
                        if (i && i->todo_user)
160✔
514
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
515
                                                       "%s: User \"%s\" already exists.",
516
                                                       passwd_path, pw->pw_name);
517

518
                        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(pw->pw_uid)))
160✔
519
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
520
                                                       "%s: Detected collision for UID " UID_FMT ".",
521
                                                       passwd_path, pw->pw_uid);
522

523
                        /* Make sure we keep the NIS entries (if any) at the end. */
524
                        if (IN_SET(pw->pw_name[0], '+', '-'))
160✔
525
                                break;
526

527
                        r = putpwent_sane(pw, passwd);
155✔
528
                        if (r < 0)
155✔
529
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
530
                                                       pw->pw_name);
531
                }
532
                if (r < 0)
15✔
533
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
534

535
        } else {
536
                if (errno != ENOENT)
73✔
537
                        return log_debug_errno(errno, "Failed to open %s: %m", passwd_path);
×
538
                if (fchmod(fileno(passwd), 0644) < 0)
73✔
539
                        return log_debug_errno(errno, "Failed to fchmod %s: %m", passwd_tmp);
×
540
        }
541

542
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
357✔
543
                _cleanup_free_ char *creds_shell = NULL, *cn = NULL;
269✔
544

545
                struct passwd n = {
1,076✔
546
                        .pw_name = i->name,
269✔
547
                        .pw_uid = i->uid,
269✔
548
                        .pw_gid = i->gid,
269✔
549
                        .pw_gecos = (char*) strempty(i->description),
269✔
550

551
                        /* "x" means the password is stored in the shadow file */
552
                        .pw_passwd = (char*) PASSWORD_SEE_SHADOW,
553

554
                        /* We default to the root directory as home */
555
                        .pw_dir = i->home ?: (char*) "/",
269✔
556

557
                        /* Initialize the shell to nologin, with one exception:
558
                         * for root we patch in something special */
559
                        .pw_shell = (char*) pick_shell(i),
269✔
560
                };
561

562
                /* Try to pick up the shell for this account via the credentials logic */
563
                cn = strjoin("passwd.shell.", i->name);
269✔
564
                if (!cn)
269✔
565
                        return -ENOMEM;
566

567
                r = read_credential(cn, (void**) &creds_shell, NULL);
269✔
568
                if (r < 0)
269✔
569
                        log_debug_errno(r, "Couldn't read credential '%s', ignoring: %m", cn);
269✔
570
                else
571
                        n.pw_shell = creds_shell;
×
572

573
                r = putpwent_sane(&n, passwd);
269✔
574
                if (r < 0)
269✔
575
                        return log_debug_errno(r, "Failed to add new user \"%s\" to temporary passwd file: %m",
×
576
                                               i->name);
577
        }
578

579
        /* Append the remaining NIS entries if any */
580
        while (pw) {
88✔
581
                r = putpwent_sane(pw, passwd);
5✔
582
                if (r < 0)
5✔
583
                        return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
584
                                               pw->pw_name);
585

586
                r = fgetpwent_sane(original, &pw);
5✔
587
                if (r < 0)
5✔
588
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
589
                if (r == 0)
5✔
590
                        break;
591
        }
592

593
        r = fflush_sync_and_check(passwd);
88✔
594
        if (r < 0)
88✔
595
                return log_debug_errno(r, "Failed to flush %s: %m", passwd_tmp);
×
596

597
done:
88✔
598
        *ret_tmpfile = TAKE_PTR(passwd);
115✔
599
        *ret_tmpfile_path = TAKE_PTR(passwd_tmp);
115✔
600
        return 0;
115✔
601
}
602

603
static int write_temporary_shadow(
115✔
604
                Context *c,
605
                const char *shadow_path,
606
                FILE **ret_tmpfile,
607
                char **ret_tmpfile_path) {
608

609
        _cleanup_fclose_ FILE *original = NULL, *shadow = NULL;
115✔
610
        _cleanup_(unlink_and_freep) char *shadow_tmp = NULL;
115✔
611
        struct spwd *sp = NULL;
115✔
612
        long lstchg;
115✔
613
        Item *i;
115✔
614
        int r;
115✔
615

616
        assert(c);
115✔
617
        assert(ret_tmpfile);
115✔
618
        assert(ret_tmpfile_path);
115✔
619

620
        if (ordered_hashmap_isempty(c->todo_uids))
115✔
621
                goto done;
27✔
622

623
        if (arg_dry_run) {
88✔
624
                log_info("Would write /etc/shadow%s", glyph(GLYPH_ELLIPSIS));
×
625
                goto done;
×
626
        }
627

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

632
        lstchg = (long) (source_date_epoch_or_now() / USEC_PER_DAY);
88✔
633

634
        original = fopen(shadow_path, "re");
88✔
635
        if (original) {
88✔
636

637
                r = copy_rights_with_fallback(fileno(original), fileno(shadow), shadow_tmp);
5✔
638
                if (r < 0)
5✔
639
                        return log_debug_errno(r, "Failed to copy permissions from %s to %s: %m",
×
640
                                               shadow_path, shadow_tmp);
641

642
                while ((r = fgetspent_sane(original, &sp)) > 0) {
135✔
643
                        i = ordered_hashmap_get(c->users, sp->sp_namp);
130✔
644
                        if (i && i->todo_user) {
130✔
645
                                /* we will update the existing entry */
646
                                sp->sp_lstchg = lstchg;
×
647

648
                                /* only the /etc/shadow stage is left, so we can
649
                                 * safely remove the item from the todo set */
650
                                i->todo_user = false;
×
651
                                ordered_hashmap_remove(c->todo_uids, UID_TO_PTR(i->uid));
×
652
                        }
653

654
                        /* Make sure we keep the NIS entries (if any) at the end. */
655
                        if (IN_SET(sp->sp_namp[0], '+', '-'))
130✔
656
                                break;
657

658
                        r = putspent_sane(sp, shadow);
130✔
659
                        if (r < 0)
130✔
660
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary shadow file: %m",
×
661
                                                       sp->sp_namp);
662

663
                }
664
                if (r < 0)
5✔
665
                        return log_debug_errno(r, "Failed to read %s: %m", shadow_path);
×
666

667
        } else {
668
                if (errno != ENOENT)
83✔
669
                        return log_debug_errno(errno, "Failed to open %s: %m", shadow_path);
×
670
                if (fchmod(fileno(shadow), 0000) < 0)
83✔
671
                        return log_debug_errno(errno, "Failed to fchmod %s: %m", shadow_tmp);
×
672
        }
673

674
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
357✔
675
                _cleanup_(erase_and_freep) char *creds_password = NULL;
269✔
676
                bool is_hashed;
269✔
677

678
                struct spwd n = {
538✔
679
                        .sp_namp = i->name,
269✔
680
                        .sp_lstchg = lstchg,
681
                        .sp_min = -1,
682
                        .sp_max = -1,
683
                        .sp_warn = -1,
684
                        .sp_inact = -1,
685
                        .sp_expire = i->locked ? 1 : -1, /* Negative expiration means "unset". Expiration 0 or 1 means "locked" */
269✔
686
                        .sp_flag = ULONG_MAX, /* this appears to be what everybody does ... */
687
                };
688

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

693
                if (creds_password && !is_hashed) {
269✔
694
                        _cleanup_(erase_and_freep) char* plaintext_password = TAKE_PTR(creds_password);
×
695
                        r = hash_password(plaintext_password, &creds_password);
×
696
                        if (r < 0)
×
697
                                return log_debug_errno(r, "Failed to hash password: %m");
×
698
                }
699

700
                if (creds_password)
269✔
701
                        n.sp_pwdp = creds_password;
×
702
                else if (streq(i->name, "root"))
269✔
703
                        /* Let firstboot set the password later */
704
                        n.sp_pwdp = (char*) PASSWORD_UNPROVISIONED;
10✔
705
                else
706
                        n.sp_pwdp = (char*) PASSWORD_LOCKED_AND_INVALID;
259✔
707

708
                r = putspent_sane(&n, shadow);
269✔
709
                if (r < 0)
269✔
710
                        return log_debug_errno(r, "Failed to add new user \"%s\" to temporary shadow file: %m",
×
711
                                               i->name);
712
        }
713

714
        /* Append the remaining NIS entries if any */
715
        while (sp) {
88✔
716
                r = putspent_sane(sp, shadow);
×
717
                if (r < 0)
×
718
                        return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary shadow file: %m",
×
719
                                               sp->sp_namp);
720

721
                r = fgetspent_sane(original, &sp);
×
722
                if (r < 0)
×
723
                        return log_debug_errno(r, "Failed to read %s: %m", shadow_path);
×
724
                if (r == 0)
×
725
                        break;
726
        }
727
        if (!IN_SET(errno, 0, ENOENT))
88✔
728
                return -errno;
×
729

730
        r = fflush_sync_and_check(shadow);
88✔
731
        if (r < 0)
88✔
732
                return log_debug_errno(r, "Failed to flush %s: %m", shadow_tmp);
×
733

734
done:
88✔
735
        *ret_tmpfile = TAKE_PTR(shadow);
115✔
736
        *ret_tmpfile_path = TAKE_PTR(shadow_tmp);
115✔
737
        return 0;
115✔
738
}
739

740
static int write_temporary_group(
115✔
741
                Context *c,
742
                const char *group_path,
743
                FILE **ret_tmpfile,
744
                char **ret_tmpfile_path) {
745

746
        _cleanup_fclose_ FILE *original = NULL, *group = NULL;
115✔
747
        _cleanup_(unlink_and_freep) char *group_tmp = NULL;
115✔
748
        bool group_changed = false;
115✔
749
        struct group *gr = NULL;
115✔
750
        Item *i;
115✔
751
        int r;
115✔
752

753
        assert(c);
115✔
754
        assert(ret_tmpfile);
115✔
755
        assert(ret_tmpfile_path);
115✔
756

757
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
115✔
758
                goto done;
26✔
759

760
        if (arg_dry_run) {
89✔
761
                log_info("Would write /etc/group%s", glyph(GLYPH_ELLIPSIS));
×
762
                goto done;
×
763
        }
764

765
        r = fopen_temporary_label("/etc/group", group_path, &group, &group_tmp);
89✔
766
        if (r < 0)
89✔
767
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", group_path);
×
768

769
        original = fopen(group_path, "re");
89✔
770
        if (original) {
89✔
771

772
                r = copy_rights_with_fallback(fileno(original), fileno(group), group_tmp);
16✔
773
                if (r < 0)
16✔
774
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
775
                                               group_path, group_tmp);
776

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

783
                        i = ordered_hashmap_get(c->groups, gr->gr_name);
368✔
784
                        if (i && i->todo_group)
368✔
785
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
786
                                                       "%s: Group \"%s\" already exists.",
787
                                                       group_path, gr->gr_name);
788

789
                        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gr->gr_gid)))
368✔
790
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
791
                                                       "%s: Detected collision for GID " GID_FMT ".",
792
                                                       group_path, gr->gr_gid);
793

794
                        /* Make sure we keep the NIS entries (if any) at the end. */
795
                        if (IN_SET(gr->gr_name[0], '+', '-'))
368✔
796
                                break;
797

798
                        r = putgrent_with_members(c, gr, group);
363✔
799
                        if (r < 0)
363✔
800
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
801
                                                       gr->gr_name);
802
                        if (r > 0)
363✔
803
                                group_changed = true;
×
804
                }
805
                if (r < 0)
16✔
806
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
807

808
        } else {
809
                if (errno != ENOENT)
73✔
810
                        return log_error_errno(errno, "Failed to open %s: %m", group_path);
×
811
                if (fchmod(fileno(group), 0644) < 0)
73✔
812
                        return log_error_errno(errno, "Failed to fchmod %s: %m", group_tmp);
×
813
        }
814

815
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
608✔
816
                struct group n = {
519✔
817
                        .gr_name = i->name,
519✔
818
                        .gr_gid = i->gid,
519✔
819
                        .gr_passwd = (char*) PASSWORD_SEE_SHADOW,
820
                };
821

822
                r = putgrent_with_members(c, &n, group);
519✔
823
                if (r < 0)
519✔
824
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary group file: %m",
×
825
                                               n.gr_name);
826

827
                group_changed = true;
519✔
828
        }
829

830
        /* Append the remaining NIS entries if any */
831
        while (gr) {
99✔
832
                r = putgrent_sane(gr, group);
15✔
833
                if (r < 0)
15✔
834
                        return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
835
                                               gr->gr_name);
836

837
                r = fgetgrent_sane(original, &gr);
15✔
838
                if (r < 0)
15✔
839
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
840
                if (r == 0)
15✔
841
                        break;
842
        }
843

844
        r = fflush_sync_and_check(group);
89✔
845
        if (r < 0)
89✔
846
                return log_error_errno(r, "Failed to flush %s: %m", group_tmp);
×
847

848
done:
89✔
849
        if (group_changed) {
115✔
850
                *ret_tmpfile = TAKE_PTR(group);
89✔
851
                *ret_tmpfile_path = TAKE_PTR(group_tmp);
89✔
852
        } else {
853
                *ret_tmpfile = NULL;
26✔
854
                *ret_tmpfile_path = NULL;
26✔
855
        }
856
        return 0;
857
}
858

859
static int write_temporary_gshadow(
115✔
860
                Context *c,
861
                const char * gshadow_path,
862
                FILE **ret_tmpfile,
863
                char **ret_tmpfile_path) {
864

865
#if ENABLE_GSHADOW
866
        _cleanup_fclose_ FILE *original = NULL, *gshadow = NULL;
115✔
867
        _cleanup_(unlink_and_freep) char *gshadow_tmp = NULL;
115✔
868
        bool group_changed = false;
115✔
869
        Item *i;
115✔
870
        int r;
115✔
871

872
        assert(c);
115✔
873
        assert(ret_tmpfile);
115✔
874
        assert(ret_tmpfile_path);
115✔
875

876
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
115✔
877
                goto done;
26✔
878

879
        if (arg_dry_run) {
89✔
880
                log_info("Would write /etc/gshadow%s", glyph(GLYPH_ELLIPSIS));
×
881
                goto done;
×
882
        }
883

884
        r = fopen_temporary_label("/etc/gshadow", gshadow_path, &gshadow, &gshadow_tmp);
89✔
885
        if (r < 0)
89✔
886
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", gshadow_path);
×
887

888
        original = fopen(gshadow_path, "re");
89✔
889
        if (original) {
89✔
890
                struct sgrp *sg;
6✔
891

892
                r = copy_rights_with_fallback(fileno(original), fileno(gshadow), gshadow_tmp);
6✔
893
                if (r < 0)
6✔
894
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
895
                                               gshadow_path, gshadow_tmp);
896

897
                while ((r = fgetsgent_sane(original, &sg)) > 0) {
359✔
898

899
                        i = ordered_hashmap_get(c->groups, sg->sg_namp);
353✔
900
                        if (i && i->todo_group)
353✔
901
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
902
                                                       "%s: Group \"%s\" already exists.",
903
                                                       gshadow_path, sg->sg_namp);
904

905
                        r = putsgent_with_members(c, sg, gshadow);
353✔
906
                        if (r < 0)
353✔
907
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary gshadow file: %m",
×
908
                                                       sg->sg_namp);
909
                        if (r > 0)
353✔
910
                                group_changed = true;
×
911
                }
912
                if (r < 0)
6✔
913
                        return r;
914

915
        } else {
916
                if (errno != ENOENT)
83✔
917
                        return log_error_errno(errno, "Failed to open %s: %m", gshadow_path);
×
918
                if (fchmod(fileno(gshadow), 0000) < 0)
83✔
919
                        return log_error_errno(errno, "Failed to fchmod %s: %m", gshadow_tmp);
×
920
        }
921

922
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
608✔
923
                struct sgrp n = {
519✔
924
                        .sg_namp = i->name,
519✔
925
                        .sg_passwd = (char*) PASSWORD_LOCKED_AND_INVALID,
926
                };
927

928
                r = putsgent_with_members(c, &n, gshadow);
519✔
929
                if (r < 0)
519✔
930
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary gshadow file: %m",
×
931
                                               n.sg_namp);
932

933
                group_changed = true;
519✔
934
        }
935

936
        r = fflush_sync_and_check(gshadow);
89✔
937
        if (r < 0)
89✔
938
                return log_error_errno(r, "Failed to flush %s: %m", gshadow_tmp);
×
939

940
done:
89✔
941
        if (group_changed) {
115✔
942
                *ret_tmpfile = TAKE_PTR(gshadow);
89✔
943
                *ret_tmpfile_path = TAKE_PTR(gshadow_tmp);
89✔
944
        } else
945
#endif
946
        {
947
                *ret_tmpfile = NULL;
26✔
948
                *ret_tmpfile_path = NULL;
26✔
949
        }
950
        return 0;
951
}
952

953
static int write_files(Context *c) {
115✔
954
        _cleanup_fclose_ FILE *passwd = NULL, *group = NULL, *shadow = NULL, *gshadow = NULL;
345✔
955
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL, *group_tmp = NULL, *shadow_tmp = NULL, *gshadow_tmp = NULL;
115✔
956
        int r;
115✔
957

958
        _cleanup_free_ char *passwd_path = path_join(arg_root, "/etc/passwd");
230✔
959
        if (!passwd_path)
115✔
960
                return log_oom();
×
961

962
        _cleanup_free_ char *shadow_path = path_join(arg_root, "/etc/shadow");
230✔
963
        if (!shadow_path)
115✔
964
                return log_oom();
×
965

966
        _cleanup_free_ char *group_path = path_join(arg_root, "/etc/group");
230✔
967
        if (!group_path)
115✔
968
                return log_oom();
×
969

970
        _cleanup_free_ char *gshadow_path = path_join(arg_root, "/etc/gshadow");
230✔
971
        if (!gshadow_path)
115✔
972
                return log_oom();
×
973

974
        assert(c);
115✔
975

976
        r = write_temporary_group(c, group_path, &group, &group_tmp);
115✔
977
        if (r < 0)
115✔
978
                return r;
979

980
        r = write_temporary_gshadow(c, gshadow_path, &gshadow, &gshadow_tmp);
115✔
981
        if (r < 0)
115✔
982
                return r;
983

984
        r = write_temporary_passwd(c, passwd_path, &passwd, &passwd_tmp);
115✔
985
        if (r < 0)
115✔
986
                return r;
987

988
        r = write_temporary_shadow(c, shadow_path, &shadow, &shadow_tmp);
115✔
989
        if (r < 0)
115✔
990
                return r;
991

992
        /* Make a backup of the old files */
993
        if (group) {
115✔
994
                r = make_backup("/etc/group", group_path);
89✔
995
                if (r < 0)
89✔
996
                        return log_error_errno(r, "Failed to backup %s: %m", group_path);
×
997
        }
998
        if (gshadow) {
115✔
999
                r = make_backup("/etc/gshadow", gshadow_path);
89✔
1000
                if (r < 0)
89✔
1001
                        return log_error_errno(r, "Failed to backup %s: %m", gshadow_path);
×
1002
        }
1003

1004
        if (passwd) {
115✔
1005
                r = make_backup("/etc/passwd", passwd_path);
88✔
1006
                if (r < 0)
88✔
1007
                        return log_error_errno(r, "Failed to backup %s: %m", passwd_path);
×
1008
        }
1009
        if (shadow) {
115✔
1010
                r = make_backup("/etc/shadow", shadow_path);
88✔
1011
                if (r < 0)
88✔
1012
                        return log_error_errno(r, "Failed to backup %s: %m", shadow_path);
×
1013
        }
1014

1015
        /* And make the new files count */
1016
        if (group) {
115✔
1017
                r = rename_and_apply_smack_floor_label(group_tmp, group_path);
89✔
1018
                if (r < 0)
89✔
1019
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1020
                                               group_tmp, group_path);
1021
                group_tmp = mfree(group_tmp);
89✔
1022
        }
1023
        /* OK, we have written the group entries successfully */
1024
        log_audit_accounts(c, ADD_GROUP);
115✔
1025
        if (gshadow) {
115✔
1026
                r = rename_and_apply_smack_floor_label(gshadow_tmp, gshadow_path);
89✔
1027
                if (r < 0)
89✔
1028
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1029
                                               gshadow_tmp, gshadow_path);
1030

1031
                gshadow_tmp = mfree(gshadow_tmp);
89✔
1032
        }
1033

1034
        if (passwd) {
115✔
1035
                r = rename_and_apply_smack_floor_label(passwd_tmp, passwd_path);
88✔
1036
                if (r < 0)
88✔
1037
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1038
                                               passwd_tmp, passwd_path);
1039

1040
                passwd_tmp = mfree(passwd_tmp);
88✔
1041
        }
1042
        /* OK, we have written the user entries successfully */
1043
        log_audit_accounts(c, ADD_USER);
115✔
1044
        if (shadow) {
115✔
1045
                r = rename_and_apply_smack_floor_label(shadow_tmp, shadow_path);
88✔
1046
                if (r < 0)
88✔
1047
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1048
                                               shadow_tmp, shadow_path);
1049

1050
                shadow_tmp = mfree(shadow_tmp);
88✔
1051
        }
1052

1053
        return 0;
1054
}
1055

1056
static int uid_is_ok(
289✔
1057
                Context *c,
1058
                uid_t uid,
1059
                const char *name,
1060
                bool check_with_gid) {
1061

1062
        int r;
289✔
1063
        assert(c);
289✔
1064

1065
        /* Let's see if we already have assigned the UID a second time */
1066
        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(uid)))
289✔
1067
                return 0;
1068

1069
        /* Try to avoid using uids that are already used by a group
1070
         * that doesn't have the same name as our new user. */
1071
        if (check_with_gid) {
284✔
1072
                Item *i;
204✔
1073

1074
                i = ordered_hashmap_get(c->todo_gids, GID_TO_PTR(uid));
204✔
1075
                if (i && !streq(i->name, name))
204✔
1076
                        return 0;
1077
        }
1078

1079
        /* Let's check the files directly */
1080
        if (hashmap_contains(c->database_by_uid, UID_TO_PTR(uid)))
274✔
1081
                return 0;
1082

1083
        if (check_with_gid) {
274✔
1084
                const char *n;
194✔
1085

1086
                n = hashmap_get(c->database_by_gid, GID_TO_PTR(uid));
194✔
1087
                if (n && !streq(n, name))
194✔
1088
                        return 0;
1089
        }
1090

1091
        /* Let's also check via NSS, to avoid UID clashes over LDAP and such, just in case */
1092
        if (!arg_root) {
269✔
1093
                _cleanup_free_ struct group *g = NULL;
6✔
1094

1095
                r = getpwuid_malloc(uid, /* ret= */ NULL);
6✔
1096
                if (r >= 0)
6✔
1097
                        return 0;
1098
                if (r != -ESRCH)
6✔
1099
                        log_warning_errno(r, "Unexpected failure while looking up UID '" UID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
×
1100

1101
                if (check_with_gid) {
6✔
1102
                        r = getgrgid_malloc((gid_t) uid, &g);
6✔
1103
                        if (r >= 0) {
6✔
1104
                                if (!streq(g->gr_name, name))
×
1105
                                        return 0;
1106
                        } else if (r != -ESRCH)
6✔
1107
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
6✔
1108
                }
1109
        }
1110

1111
        return 1;
1112
}
1113

1114
static int root_stat(const char *p, struct stat *ret_st) {
×
1115
        return chase_and_stat(p, arg_root, CHASE_PREFIX_ROOT, /* ret_path= */ NULL, ret_st);
×
1116
}
1117

1118
static int read_id_from_file(Item *i, uid_t *ret_uid, gid_t *ret_gid) {
74✔
1119
        struct stat st;
74✔
1120
        bool found_uid = false, found_gid = false;
74✔
1121
        uid_t uid = 0;
74✔
1122
        gid_t gid = 0;
74✔
1123

1124
        assert(i);
74✔
1125

1126
        /* First, try to get the GID directly */
1127
        if (ret_gid && i->gid_path && root_stat(i->gid_path, &st) >= 0) {
74✔
1128
                gid = st.st_gid;
×
1129
                found_gid = true;
×
1130
        }
1131

1132
        /* Then, try to get the UID directly */
1133
        if ((ret_uid || (ret_gid && !found_gid))
74✔
1134
            && i->uid_path
74✔
1135
            && root_stat(i->uid_path, &st) >= 0) {
×
1136

1137
                uid = st.st_uid;
×
1138
                found_uid = true;
×
1139

1140
                /* If we need the gid, but had no success yet, also derive it from the UID path */
1141
                if (ret_gid && !found_gid) {
×
1142
                        gid = st.st_gid;
×
1143
                        found_gid = true;
×
1144
                }
1145
        }
1146

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

1150
                if (found_gid) {
×
1151
                        uid = (uid_t) gid;
1152
                        found_uid = true;
1153
                } else if (root_stat(i->gid_path, &st) >= 0) {
×
1154
                        uid = (uid_t) st.st_gid;
×
1155
                        found_uid = true;
×
1156
                }
1157
        }
1158

1159
        if (ret_uid) {
74✔
1160
                if (!found_uid)
41✔
1161
                        return 0;
74✔
1162

1163
                *ret_uid = uid;
×
1164
        }
1165

1166
        if (ret_gid) {
33✔
1167
                if (!found_gid)
33✔
1168
                        return 0;
1169

1170
                *ret_gid = gid;
×
1171
        }
1172

1173
        return 1;
1174
}
1175

1176
static int add_user(Context *c, Item *i) {
299✔
1177
        void *z;
299✔
1178
        int r;
299✔
1179

1180
        assert(c);
299✔
1181
        assert(i);
299✔
1182

1183
        /* Check the database directly */
1184
        z = hashmap_get(c->database_by_username, i->name);
299✔
1185
        if (z) {
299✔
1186
                log_debug("User %s already exists.", i->name);
30✔
1187
                i->uid = PTR_TO_UID(z);
30✔
1188
                i->uid_set = true;
30✔
1189
                return 0;
30✔
1190
        }
1191

1192
        if (!arg_root) {
269✔
1193
                _cleanup_free_ struct passwd *p = NULL;
6✔
1194

1195
                /* Also check NSS */
1196
                r = getpwnam_malloc(i->name, &p);
6✔
1197
                if (r >= 0) {
6✔
1198
                        log_debug("User %s already exists.", i->name);
×
1199
                        i->uid = p->pw_uid;
×
1200
                        i->uid_set = true;
×
1201

1202
                        r = free_and_strdup(&i->description, p->pw_gecos);
×
1203
                        if (r < 0)
×
1204
                                return log_oom();
×
1205

1206
                        return 0;
1207
                }
1208
                if (r != -ESRCH)
6✔
1209
                        log_warning_errno(r, "Unexpected failure while looking up user '%s' via NSS, assuming it doesn't exist: %m", i->name);
6✔
1210
        }
1211

1212
        /* Try to use the suggested numeric UID */
1213
        if (i->uid_set) {
269✔
1214
                r = uid_is_ok(c, i->uid, i->name, !i->id_set_strict);
233✔
1215
                if (r < 0)
233✔
1216
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1217
                if (r == 0) {
233✔
1218
                        log_info("Suggested user ID " UID_FMT " for %s already used.", i->uid, i->name);
5✔
1219
                        i->uid_set = false;
5✔
1220
                }
1221
        }
1222

1223
        /* If that didn't work, try to read it from the specified path */
1224
        if (!i->uid_set) {
269✔
1225
                uid_t candidate;
41✔
1226

1227
                if (read_id_from_file(i, &candidate, NULL) > 0) {
41✔
1228

1229
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
1230
                                log_debug("User ID " UID_FMT " of file not suitable for %s.", candidate, i->name);
×
1231
                        else {
1232
                                r = uid_is_ok(c, candidate, i->name, true);
×
1233
                                if (r < 0)
×
1234
                                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1235
                                else if (r > 0) {
×
1236
                                        i->uid = candidate;
×
1237
                                        i->uid_set = true;
×
1238
                                } else
1239
                                        log_debug("User ID " UID_FMT " of file for %s is already used.", candidate, i->name);
×
1240
                        }
1241
                }
1242
        }
1243

1244
        /* Otherwise, try to reuse the group ID */
1245
        if (!i->uid_set && i->gid_set) {
269✔
1246
                r = uid_is_ok(c, (uid_t) i->gid, i->name, true);
41✔
1247
                if (r < 0)
41✔
1248
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1249
                if (r > 0) {
41✔
1250
                        i->uid = (uid_t) i->gid;
26✔
1251
                        i->uid_set = true;
26✔
1252
                }
1253
        }
1254

1255
        /* And if that didn't work either, let's try to find a free one */
1256
        if (!i->uid_set) {
269✔
1257
                maybe_emit_login_defs_warning(c);
15✔
1258

1259
                for (;;) {
15✔
1260
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
15✔
1261
                        if (r < 0)
15✔
1262
                                return log_error_errno(r, "No free user ID available for %s.", i->name);
×
1263

1264
                        r = uid_is_ok(c, c->search_uid, i->name, true);
15✔
1265
                        if (r < 0)
15✔
1266
                                return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1267
                        else if (r > 0)
15✔
1268
                                break;
1269
                }
1270

1271
                i->uid_set = true;
15✔
1272
                i->uid = c->search_uid;
15✔
1273
        }
1274

1275
        r = ordered_hashmap_ensure_put(&c->todo_uids, NULL, UID_TO_PTR(i->uid), i);
269✔
1276
        if (r == -EEXIST)
269✔
1277
                return log_error_errno(r, "Requested user %s with UID " UID_FMT " and gid" GID_FMT " to be created is duplicated "
×
1278
                                       "or conflicts with another user.", i->name, i->uid, i->gid);
1279
        if (r == -ENOMEM)
269✔
1280
                return log_oom();
×
1281
        if (r < 0)
269✔
1282
                return log_error_errno(r, "Failed to store user %s with UID " UID_FMT " and GID " GID_FMT " to be created: %m",
×
1283
                                       i->name, i->uid, i->gid);
1284

1285
        i->todo_user = true;
269✔
1286
        log_info("Creating user '%s' (%s) with UID " UID_FMT " and GID " GID_FMT ".",
510✔
1287
                 i->name, strna(i->description), i->uid, i->gid);
1288

1289
        return 0;
1290
}
1291

1292
static int gid_is_ok(
805✔
1293
                Context *c,
1294
                gid_t gid,
1295
                const char *groupname,
1296
                bool check_with_uid) {
1297

1298
        Item *user;
805✔
1299
        char *username;
805✔
1300
        int r;
805✔
1301

1302
        assert(c);
805✔
1303
        assert(groupname);
805✔
1304

1305
        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gid)))
805✔
1306
                return 0;
1307

1308
        /* Avoid reusing gids that are already used by a different user */
1309
        if (check_with_uid) {
740✔
1310
                user = ordered_hashmap_get(c->todo_uids, UID_TO_PTR(gid));
406✔
1311
                if (user && !streq(user->name, groupname))
406✔
1312
                        return 0;
1313
        }
1314

1315
        if (hashmap_contains(c->database_by_gid, GID_TO_PTR(gid)))
740✔
1316
                return 0;
1317

1318
        if (check_with_uid) {
520✔
1319
                username = hashmap_get(c->database_by_uid, UID_TO_PTR(gid));
186✔
1320
                if (username && !streq(username, groupname))
186✔
1321
                        return 0;
1322
        }
1323

1324
        if (!arg_root) {
520✔
1325
                r = getgrgid_malloc(gid, /* ret= */ NULL);
8✔
1326
                if (r >= 0)
8✔
1327
                        return 0;
1328
                if (r != -ESRCH)
8✔
1329
                        log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1330

1331
                if (check_with_uid) {
8✔
1332
                        r = getpwuid_malloc(gid, /* ret= */ NULL);
8✔
1333
                        if (r >= 0)
8✔
1334
                                return 0;
1335
                        if (r != -ESRCH)
8✔
1336
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1337
                }
1338
        }
1339

1340
        return 1;
1341
}
1342

1343
static int get_gid_by_name(
650✔
1344
                Context *c,
1345
                const char *name,
1346
                gid_t *ret_gid) {
1347

1348
        void *z;
650✔
1349
        int r;
650✔
1350

1351
        assert(c);
650✔
1352
        assert(ret_gid);
650✔
1353

1354
        /* Check the database directly */
1355
        z = hashmap_get(c->database_by_groupname, name);
650✔
1356
        if (z) {
650✔
1357
                *ret_gid = PTR_TO_GID(z);
69✔
1358
                return 0;
69✔
1359
        }
1360

1361
        /* Also check NSS */
1362
        if (!arg_root) {
581✔
1363
                _cleanup_free_ struct group *g = NULL;
8✔
1364

1365
                r = getgrnam_malloc(name, &g);
8✔
1366
                if (r >= 0) {
8✔
1367
                        *ret_gid = g->gr_gid;
×
1368
                        return 0;
×
1369
                }
1370
                if (r != -ESRCH)
8✔
1371
                        log_warning_errno(r, "Unexpected failure while looking up group '%s' via NSS, assuming it doesn't exist: %m", name);
8✔
1372
        }
1373

1374
        return -ENOENT;
1375
}
1376

1377
static int add_group(Context *c, Item *i) {
644✔
1378
        int r;
644✔
1379

1380
        assert(c);
644✔
1381
        assert(i);
644✔
1382

1383
        r = get_gid_by_name(c, i->name, &i->gid);
644✔
1384
        if (r != -ENOENT) {
644✔
1385
                if (r < 0)
64✔
1386
                        return r;
1387
                log_debug("Group %s already exists.", i->name);
64✔
1388
                i->gid_set = true;
64✔
1389
                return 0;
64✔
1390
        }
1391

1392
        /* Try to use the suggested numeric GID */
1393
        if (i->gid_set) {
580✔
1394
                r = gid_is_ok(c, i->gid, i->name, false);
394✔
1395
                if (r < 0)
394✔
1396
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1397
                if (i->id_set_strict) {
394✔
1398
                        /* If we require the GID to already exist we can return here:
1399
                         * r > 0: means the GID does not exist -> fail
1400
                         * r == 0: means the GID exists -> nothing more to do.
1401
                         */
1402
                        if (r > 0)
61✔
1403
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
1404
                                                       "Failed to create %s: please create GID " GID_FMT,
1405
                                                       i->name, i->gid);
1406
                        if (r == 0)
60✔
1407
                                return 0;
1408
                }
1409
                if (r == 0) {
333✔
1410
                        log_info("Suggested group ID " GID_FMT " for %s already used.", i->gid, i->name);
×
1411
                        i->gid_set = false;
×
1412
                }
1413
        }
1414

1415
        /* Try to reuse the numeric uid, if there's one */
1416
        if (!i->gid_set && i->uid_set) {
519✔
1417
                r = gid_is_ok(c, (gid_t) i->uid, i->name, true);
158✔
1418
                if (r < 0)
158✔
1419
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1420
                if (r > 0) {
158✔
1421
                        i->gid = (gid_t) i->uid;
153✔
1422
                        i->gid_set = true;
153✔
1423
                }
1424
        }
1425

1426
        /* If that didn't work, try to read it from the specified path */
1427
        if (!i->gid_set) {
519✔
1428
                gid_t candidate;
33✔
1429

1430
                if (read_id_from_file(i, NULL, &candidate) > 0) {
33✔
1431

1432
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
1433
                                log_debug("Group ID " GID_FMT " of file not suitable for %s.", candidate, i->name);
×
1434
                        else {
1435
                                r = gid_is_ok(c, candidate, i->name, true);
×
1436
                                if (r < 0)
×
1437
                                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1438
                                else if (r > 0) {
×
1439
                                        i->gid = candidate;
×
1440
                                        i->gid_set = true;
×
1441
                                } else
1442
                                        log_debug("Group ID " GID_FMT " of file for %s already used.", candidate, i->name);
×
1443
                        }
1444
                }
1445
        }
1446

1447
        /* And if that didn't work either, let's try to find a free one */
1448
        if (!i->gid_set) {
519✔
1449
                maybe_emit_login_defs_warning(c);
33✔
1450

1451
                for (;;) {
253✔
1452
                        /* We look for new GIDs in the UID pool! */
1453
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
253✔
1454
                        if (r < 0)
253✔
1455
                                return log_error_errno(r, "No free group ID available for %s.", i->name);
×
1456

1457
                        r = gid_is_ok(c, c->search_uid, i->name, true);
253✔
1458
                        if (r < 0)
253✔
1459
                                return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1460
                        else if (r > 0)
253✔
1461
                                break;
1462
                }
1463

1464
                i->gid_set = true;
33✔
1465
                i->gid = c->search_uid;
33✔
1466
        }
1467

1468
        r = ordered_hashmap_ensure_put(&c->todo_gids, NULL, GID_TO_PTR(i->gid), i);
519✔
1469
        if (r == -EEXIST)
519✔
1470
                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);
×
1471
        if (r == -ENOMEM)
519✔
1472
                return log_oom();
×
1473
        if (r < 0)
519✔
1474
                return log_error_errno(r, "Failed to store group %s with GID " GID_FMT " to be created: %m", i->name, i->gid);
×
1475

1476
        i->todo_group = true;
519✔
1477
        log_info("Creating group '%s' with GID " GID_FMT ".", i->name, i->gid);
519✔
1478

1479
        return 0;
1480
}
1481

1482
static int process_item(Context *c, Item *i) {
680✔
1483
        int r;
680✔
1484

1485
        assert(c);
680✔
1486
        assert(i);
680✔
1487

1488
        switch (i->type) {
680✔
1489

1490
        case ADD_USER: {
301✔
1491
                Item *j = NULL;
301✔
1492

1493
                if (!i->gid_set) {
301✔
1494
                        j = ordered_hashmap_get(c->groups, i->group_name ?: i->name);
237✔
1495

1496
                        /* If that's not a match, also check if the group name
1497
                         * matches a user name in the queue. */
1498
                        if (!j && i->group_name)
237✔
1499
                                j = ordered_hashmap_get(c->users, i->group_name);
11✔
1500
                }
1501

1502
                if (j && j->todo_group) {
237✔
1503
                        /* When a group with the target name is already in queue,
1504
                         * use the information about the group and do not create
1505
                         * duplicated group entry. */
1506
                        i->gid_set = j->gid_set;
30✔
1507
                        i->gid = j->gid;
30✔
1508
                        i->id_set_strict = true;
30✔
1509
                } else if (i->group_name) {
271✔
1510
                        /* When a group name was given instead of a GID and it's
1511
                         * not in queue, then it must already exist. */
1512
                        r = get_gid_by_name(c, i->group_name, &i->gid);
6✔
1513
                        if (r < 0)
6✔
1514
                                return log_error_errno(r, "Group %s not found.", i->group_name);
1✔
1515
                        i->gid_set = true;
5✔
1516
                        i->id_set_strict = true;
5✔
1517
                } else {
1518
                        r = add_group(c, i);
265✔
1519
                        if (r < 0)
265✔
1520
                                return r;
1521
                }
1522

1523
                return add_user(c, i);
299✔
1524
        }
1525

1526
        case ADD_GROUP:
379✔
1527
                return add_group(c, i);
379✔
1528

1529
        default:
×
1530
                assert_not_reached();
×
1531
        }
1532
}
1533

1534
static Item* item_free(Item *i) {
683✔
1535
        if (!i)
683✔
1536
                return NULL;
1537

1538
        free(i->name);
683✔
1539
        free(i->group_name);
683✔
1540
        free(i->uid_path);
683✔
1541
        free(i->gid_path);
683✔
1542
        free(i->description);
683✔
1543
        free(i->home);
683✔
1544
        free(i->shell);
683✔
1545
        free(i->filename);
683✔
1546
        return mfree(i);
683✔
1547
}
1548

1549
DEFINE_TRIVIAL_CLEANUP_FUNC(Item*, item_free);
1,376✔
1550
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, char, string_hash_func, string_compare_func, Item, item_free);
680✔
1551

1552
static Item* item_new(ItemType type, const char *name, const char *filename, unsigned line) {
683✔
1553
        assert(name);
683✔
1554
        assert(!!filename == (line > 0));
683✔
1555

1556
        _cleanup_(item_freep) Item *new = new(Item, 1);
1,366✔
1557
        if (!new)
683✔
1558
                return NULL;
1559

1560
        *new = (Item) {
683✔
1561
                .type = type,
1562
                .line = line,
1563
        };
1564

1565
        if (free_and_strdup(&new->name, name) < 0 ||
1,366✔
1566
            free_and_strdup(&new->filename, filename) < 0)
683✔
1567
                return NULL;
1568

1569
        return TAKE_PTR(new);
683✔
1570
}
1571

1572
static int add_implicit(Context *c) {
115✔
1573
        char *g, **l;
115✔
1574
        int r;
115✔
1575

1576
        assert(c);
115✔
1577

1578
        /* Implicitly create additional users and groups, if they were listed in "m" lines */
1579
        ORDERED_HASHMAP_FOREACH_KEY(l, g, c->members) {
240✔
1580
                STRV_FOREACH(m, l)
20✔
1581
                        if (!ordered_hashmap_contains(c->users, *m)) {
10✔
1582
                                _cleanup_(item_freep) Item *j =
×
1583
                                        item_new(ADD_USER, *m, /* filename= */ NULL, /* line= */ 0);
5✔
1584
                                if (!j)
5✔
1585
                                        return log_oom();
×
1586

1587
                                r = ordered_hashmap_ensure_put(&c->users, &item_hash_ops, j->name, j);
5✔
1588
                                if (r == -ENOMEM)
5✔
1589
                                        return log_oom();
×
1590
                                if (r < 0)
5✔
1591
                                        return log_error_errno(r, "Failed to add implicit user '%s': %m", j->name);
×
1592

1593
                                log_debug("Adding implicit user '%s' due to m line", j->name);
5✔
1594
                                TAKE_PTR(j);
5✔
1595
                        }
1596

1597
                if (!(ordered_hashmap_contains(c->users, g) ||
10✔
1598
                      ordered_hashmap_contains(c->groups, g))) {
2✔
1599
                        _cleanup_(item_freep) Item *j =
×
1600
                                item_new(ADD_GROUP, g, /* filename= */ NULL, /* line= */ 0);
×
1601
                        if (!j)
×
1602
                                return log_oom();
×
1603

1604
                        r = ordered_hashmap_ensure_put(&c->groups, &item_hash_ops, j->name, j);
×
1605
                        if (r == -ENOMEM)
×
1606
                                return log_oom();
×
1607
                        if (r < 0)
×
1608
                                return log_error_errno(r, "Failed to add implicit group '%s': %m", j->name);
×
1609

1610
                        log_debug("Adding implicit group '%s' due to m line", j->name);
×
1611
                        TAKE_PTR(j);
×
1612
                }
1613
        }
1614

1615
        return 0;
115✔
1616
}
1617

1618
static int item_equivalent(Item *a, Item *b) {
2✔
1619
        int r;
2✔
1620

1621
        assert(a);
2✔
1622
        assert(b);
2✔
1623

1624
        if (a->type != b->type) {
2✔
1625
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1626
                           "Item not equivalent because types differ");
1627
                return false;
1628
        }
1629

1630
        if (!streq_ptr(a->name, b->name)) {
2✔
1631
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1632
                           "Item not equivalent because names differ ('%s' vs. '%s')",
1633
                           a->name, b->name);
1634
                return false;
1635
        }
1636

1637
        /* Paths were simplified previously, so we can use streq. */
1638
        if (!streq_ptr(a->uid_path, b->uid_path)) {
2✔
1639
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1640
                           "Item not equivalent because UID paths differ (%s vs. %s)",
1641
                           a->uid_path ?: "(unset)", b->uid_path ?: "(unset)");
1642
                return false;
1643
        }
1644

1645
        if (!streq_ptr(a->gid_path, b->gid_path)) {
2✔
1646
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1647
                           "Item not equivalent because GID paths differ (%s vs. %s)",
1648
                           a->gid_path ?: "(unset)", b->gid_path ?: "(unset)");
1649
                return false;
1650
        }
1651

1652
        if (!streq_ptr(a->description, b->description))  {
2✔
1653
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1654
                           "Item not equivalent because descriptions differ ('%s' vs. '%s')",
1655
                           strempty(a->description), strempty(b->description));
1656
                return false;
1657
        }
1658

1659
        if ((a->uid_set != b->uid_set) ||
2✔
1660
            (a->uid_set && a->uid != b->uid)) {
×
1661
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1662
                           "Item not equivalent because UIDs differ (%s vs. %s)",
1663
                           a->uid_set ? FORMAT_UID(a->uid) : "(unset)",
1664
                           b->uid_set ? FORMAT_UID(b->uid) : "(unset)");
1665
                return false;
×
1666
        }
1667

1668
        if ((a->gid_set != b->gid_set) ||
2✔
1669
            (a->gid_set && a->gid != b->gid)) {
1✔
1670
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1671
                           "Item not equivalent because GIDs differ (%s vs. %s)",
1672
                           a->gid_set ? FORMAT_GID(a->gid) : "(unset)",
1673
                           b->gid_set ? FORMAT_GID(b->gid) : "(unset)");
1674
                return false;
×
1675
        }
1676

1677
        if (!streq_ptr(a->home, b->home)) {
2✔
1678
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1679
                           "Item not equivalent because home directories differ ('%s' vs. '%s')",
1680
                           strempty(a->description), strempty(b->description));
1681
                return false;
1682
        }
1683

1684
        /* Check if the two paths refer to the same file.
1685
         * If the paths are equal (after normalization), it's obviously the same file.
1686
         * If both paths specify a nologin shell, treat them as the same (e.g. /bin/true and /bin/false).
1687
         * Otherwise, try to resolve the paths, and see if we get the same result, (e.g. /sbin/nologin and
1688
         * /usr/sbin/nologin).
1689
         * If we can't resolve something, treat different paths as different. */
1690

1691
        const char *a_shell = pick_shell(a),
2✔
1692
                   *b_shell = pick_shell(b);
2✔
1693
        if (!path_equal(a_shell, b_shell) &&
2✔
1694
            !(is_nologin_shell(a_shell) && is_nologin_shell(b_shell))) {
×
1695
                _cleanup_free_ char *pa = NULL, *pb = NULL;
×
1696

1697
                r = chase(a_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pa, NULL);
×
1698
                if (r < 0) {
×
1699
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1700
                                       r, "Failed to look up path '%s%s%s': %m",
1701
                                       strempty(arg_root), arg_root ? "/" : "", a_shell);
1702
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1703
                }
1704

1705
                r = chase(b_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pb, NULL);
×
1706
                if (r < 0) {
×
1707
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1708
                                       r, "Failed to look up path '%s%s%s': %m",
1709
                                       strempty(arg_root), arg_root ? "/" : "", b_shell);
1710
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1711
                }
1712

1713
                if (!path_equal(pa, pb)) {
×
1714
                        log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1715
                                   "Item not equivalent because shells differ ('%s' vs. '%s')",
1716
                                   pa, pb);
1717
                        return false;
1718
                }
1719
        }
1720

1721
        return true;
1722
}
1723

1724
static int parse_line(
688✔
1725
                const char *fname,
1726
                unsigned line,
1727
                const char *buffer,
1728
                bool *invalid_config,
1729
                void *context) {
1730

1731
        Context *c = ASSERT_PTR(context);
688✔
1732
        _cleanup_free_ char *action = NULL,
1,376✔
1733
                *name = NULL, *resolved_name = NULL,
×
1734
                *id = NULL, *resolved_id = NULL,
×
1735
                *description = NULL, *resolved_description = NULL,
×
1736
                *home = NULL, *resolved_home = NULL,
×
1737
                *shell = NULL, *resolved_shell = NULL;
688✔
1738
        _cleanup_(item_freep) Item *i = NULL;
688✔
1739
        Item *existing;
688✔
1740
        OrderedHashmap *h;
688✔
1741
        int r;
688✔
1742
        const char *p;
688✔
1743

1744
        assert(fname);
688✔
1745
        assert(line >= 1);
688✔
1746
        assert(buffer);
688✔
1747
        assert(!invalid_config); /* We don't support invalid_config yet. */
688✔
1748

1749
        /* Parse columns */
1750
        p = buffer;
688✔
1751
        r = extract_many_words(&p, NULL, EXTRACT_UNQUOTE,
688✔
1752
                               &action, &name, &id, &description, &home, &shell);
1753
        if (r < 0)
688✔
1754
                return log_syntax(NULL, LOG_ERR, fname, line, r, "Syntax error.");
×
1755
        if (r < 2)
688✔
1756
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1757
                                  "Missing action and name columns.");
1758
        if (!isempty(p))
688✔
1759
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1760
                                  "Trailing garbage.");
1761

1762
        if (isempty(action))
688✔
1763
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1764
                                  "Empty command specification.");
1765

1766
        bool locked = false;
1767
        for (int pos = 1; action[pos]; pos++)
710✔
1768
                if (action[pos] == '!' && !locked)
22✔
1769
                        locked = true;
22✔
1770
                else
1771
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1772
                                          "Unknown modifiers in command '%s'.", action);
1773

1774
        if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE))
688✔
1775
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1776
                                  "Unknown command type '%c'.", action[0]);
1777

1778
        /* Verify name */
1779
        if (empty_or_dash(name))
688✔
1780
                name = mfree(name);
×
1781

1782
        if (name) {
688✔
1783
                r = specifier_printf(name, NAME_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_name);
688✔
1784
                if (r < 0)
688✔
1785
                        return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to replace specifiers in '%s': %m", name);
×
1786

1787
                if (!valid_user_group_name(resolved_name, 0))
688✔
1788
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1789
                                          "'%s' is not a valid user or group name.", resolved_name);
1790
        }
1791

1792
        /* Verify id */
1793
        if (empty_or_dash(id))
688✔
1794
                id = mfree(id);
61✔
1795

1796
        if (id) {
688✔
1797
                r = specifier_printf(id, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_id);
627✔
1798
                if (r < 0)
627✔
1799
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1800
                                          "Failed to replace specifiers in '%s': %m", name);
1801
        }
1802

1803
        /* Verify description */
1804
        if (empty_or_dash(description))
688✔
1805
                description = mfree(description);
635✔
1806

1807
        if (description) {
688✔
1808
                r = specifier_printf(description, LONG_LINE_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_description);
53✔
1809
                if (r < 0)
53✔
1810
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1811
                                          "Failed to replace specifiers in '%s': %m", description);
1812

1813
                if (!valid_gecos(resolved_description))
53✔
1814
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1815
                                          "'%s' is not a valid GECOS field.", resolved_description);
1816
        }
1817

1818
        /* Verify home */
1819
        if (empty_or_dash(home))
688✔
1820
                home = mfree(home);
547✔
1821

1822
        if (home) {
688✔
1823
                r = specifier_printf(home, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_home);
141✔
1824
                if (r < 0)
141✔
1825
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1826
                                          "Failed to replace specifiers in '%s': %m", home);
1827

1828
                path_simplify(resolved_home);
141✔
1829

1830
                if (!valid_home(resolved_home))
141✔
1831
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1832
                                          "'%s' is not a valid home directory field.", resolved_home);
1833
        }
1834

1835
        /* Verify shell */
1836
        if (empty_or_dash(shell))
688✔
1837
                shell = mfree(shell);
667✔
1838

1839
        if (shell) {
688✔
1840
                r = specifier_printf(shell, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_shell);
21✔
1841
                if (r < 0)
21✔
1842
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1843
                                          "Failed to replace specifiers in '%s': %m", shell);
1844

1845
                path_simplify(resolved_shell);
21✔
1846

1847
                if (!valid_shell(resolved_shell))
21✔
1848
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1849
                                          "'%s' is not a valid login shell field.", resolved_shell);
1850
        }
1851

1852
        switch (action[0]) {
688✔
1853

1854
        case ADD_RANGE:
×
1855
                if (locked)
×
1856
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1857
                                          "Flag '!' not permitted on lines of type 'r'.");
1858

1859
                if (resolved_name)
×
1860
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1861
                                          "Lines of type 'r' don't take a name field.");
1862

1863
                if (!resolved_id)
×
1864
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1865
                                          "Lines of type 'r' require an ID range in the third field.");
1866

1867
                if (description || home || shell)
×
1868
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1869
                                          "Lines of type '%c' don't take a %s field.",
1870
                                          action[0],
1871
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1872

1873
                r = uid_range_add_str(&c->uid_range, resolved_id);
×
1874
                if (r < 0)
×
1875
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1876
                                          "Invalid UID range %s.", resolved_id);
1877

1878
                return 0;
1879

1880
        case ADD_MEMBER: {
10✔
1881
                /* Try to extend an existing member or group item */
1882
                if (!name)
10✔
1883
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1884
                                          "Lines of type 'm' require a user name in the second field.");
1885

1886
                if (locked)
10✔
1887
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1888
                                          "Flag '!' not permitted on lines of type 'm'.");
1889

1890
                if (!resolved_id)
10✔
1891
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1892
                                          "Lines of type 'm' require a group name in the third field.");
1893

1894
                if (!valid_user_group_name(resolved_id, 0))
10✔
1895
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1896
                                               "'%s' is not a valid user or group name.", resolved_id);
1897

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

1904
                r = string_strv_ordered_hashmap_put(&c->members, resolved_id, resolved_name);
10✔
1905
                if (r < 0)
10✔
1906
                        return log_error_errno(r, "Failed to store mapping for %s: %m", resolved_id);
×
1907

1908
                return 0;
1909
        }
1910

1911
        case ADD_USER:
298✔
1912
                if (!name)
298✔
1913
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1914
                                          "Lines of type 'u' require a user name in the second field.");
1915

1916
                r = ordered_hashmap_ensure_allocated(&c->users, &item_hash_ops);
298✔
1917
                if (r < 0)
298✔
1918
                        return log_oom();
×
1919

1920
                i = item_new(ADD_USER, resolved_name, fname, line);
298✔
1921
                if (!i)
298✔
1922
                        return log_oom();
×
1923

1924
                if (resolved_id) {
298✔
1925
                        if (path_is_absolute(resolved_id))
266✔
1926
                                i->uid_path = path_simplify(TAKE_PTR(resolved_id));
×
1927
                        else {
1928
                                _cleanup_free_ char *uid = NULL, *gid = NULL;
266✔
1929
                                if (split_pair(resolved_id, ":", &uid, &gid) == 0) {
266✔
1930
                                        r = parse_gid(gid, &i->gid);
90✔
1931
                                        if (r < 0) {
90✔
1932
                                                if (valid_user_group_name(gid, 0))
26✔
1933
                                                        i->group_name = TAKE_PTR(gid);
26✔
1934
                                                else
1935
                                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1936
                                                                          "Failed to parse GID: '%s': %m", id);
1937
                                        } else {
1938
                                                i->gid_set = true;
64✔
1939
                                                i->id_set_strict = true;
64✔
1940
                                        }
1941
                                        free_and_replace(resolved_id, uid);
90✔
1942
                                }
1943
                                if (!streq(resolved_id, "-")) {
266✔
1944
                                        r = parse_uid(resolved_id, &i->uid);
251✔
1945
                                        if (r < 0)
251✔
1946
                                                return log_syntax(NULL, LOG_ERR, fname, line, r,
1✔
1947
                                                                  "Failed to parse UID: '%s': %m", id);
1948
                                        i->uid_set = true;
250✔
1949
                                }
1950
                        }
1951
                }
1952

1953
                i->description = TAKE_PTR(resolved_description);
297✔
1954
                i->home = TAKE_PTR(resolved_home);
297✔
1955
                i->shell = TAKE_PTR(resolved_shell);
297✔
1956
                i->locked = locked;
297✔
1957

1958
                h = c->users;
297✔
1959
                break;
297✔
1960

1961
        case ADD_GROUP:
380✔
1962
                if (!name)
380✔
1963
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1964
                                          "Lines of type 'g' require a user name in the second field.");
1965

1966
                if (locked)
380✔
1967
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1968
                                          "Flag '!' not permitted on lines of type 'g'.");
1969

1970
                if (description || home || shell)
380✔
1971
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1972
                                          "Lines of type '%c' don't take a %s field.",
1973
                                          action[0],
1974
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1975

1976
                r = ordered_hashmap_ensure_allocated(&c->groups, &item_hash_ops);
380✔
1977
                if (r < 0)
380✔
1978
                        return log_oom();
×
1979

1980
                i = item_new(ADD_GROUP, resolved_name, fname, line);
380✔
1981
                if (!i)
380✔
1982
                        return log_oom();
×
1983

1984
                if (resolved_id) {
380✔
1985
                        if (path_is_absolute(resolved_id))
351✔
1986
                                i->gid_path = path_simplify(TAKE_PTR(resolved_id));
×
1987
                        else {
1988
                                r = parse_gid(resolved_id, &i->gid);
351✔
1989
                                if (r < 0)
351✔
1990
                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1991
                                                          "Failed to parse GID: '%s': %m", id);
1992

1993
                                i->gid_set = true;
351✔
1994
                        }
1995
                }
1996

1997
                h = c->groups;
380✔
1998
                break;
380✔
1999

2000
        default:
×
2001
                assert_not_reached();
×
2002
        }
2003

2004
        existing = ordered_hashmap_get(h, i->name);
677✔
2005
        if (existing) {
677✔
2006
                /* Two functionally-equivalent items are fine */
2007
                r = item_equivalent(i, existing);
2✔
2008
                if (r < 0)
2✔
2009
                        return r;
2010
                if (r == 0) {
2✔
2011
                        if (existing->filename)
×
2012
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2013
                                           "Conflict with earlier configuration for %s '%s' in %s:%u, ignoring line.",
2014
                                           item_type_to_string(i->type),
2015
                                           i->name,
2016
                                           existing->filename, existing->line);
2017
                        else
2018
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2019
                                           "Conflict with earlier configuration for %s '%s', ignoring line.",
2020
                                           item_type_to_string(i->type),
2021
                                           i->name);
2022
                }
2023

2024
                return 0;
2025
        }
2026

2027
        r = ordered_hashmap_put(h, i->name, i);
675✔
2028
        if (r < 0)
675✔
2029
                return log_oom();
×
2030

2031
        i = NULL;
675✔
2032
        return 0;
675✔
2033
}
2034

2035
static int read_config_file(Context *c, const char *fn, bool ignore_enoent) {
135✔
2036
        return conf_file_read(
405✔
2037
                        arg_root,
2038
                        (const char**) CONF_PATHS_STRV("sysusers.d"),
135✔
2039
                        ASSERT_PTR(fn),
135✔
2040
                        parse_line,
2041
                        ASSERT_PTR(c),
135✔
2042
                        ignore_enoent,
2043
                        /* invalid_config= */ NULL);
2044
}
2045

2046
static int cat_config(void) {
×
2047
        _cleanup_strv_free_ char **files = NULL;
×
2048
        int r;
×
2049

2050
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, NULL);
×
2051
        if (r < 0)
×
2052
                return r;
2053

2054
        pager_open(arg_pager_flags);
×
2055

2056
        return cat_files(NULL, files, arg_cat_flags);
×
2057
}
2058

2059
static int help(void) {
×
2060
        _cleanup_free_ char *link = NULL;
×
2061
        _cleanup_(table_unrefp) Table *cmds = NULL, *opts = NULL;
×
2062
        int r;
×
2063

2064
        r = terminal_urlify_man("systemd-sysusers.service", "8", &link);
×
2065
        if (r < 0)
×
2066
                return log_oom();
×
2067

2068
        r = option_parser_get_help_table(&cmds);
×
2069
        if (r < 0)
×
2070
                return r;
2071

2072
        r = option_parser_get_help_table_group("Options", &opts);
×
2073
        if (r < 0)
×
2074
                return r;
2075

2076
        (void) table_sync_column_widths(0, cmds, opts);
×
2077

2078
        printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n"
×
2079
               "\n%sCreates system user and group accounts.%s\n"
2080
               "\nCommands:\n",
2081
               program_invocation_short_name,
2082
               ansi_highlight(),
2083
               ansi_normal());
2084

2085
        r = table_print_or_warn(cmds);
×
2086
        if (r < 0)
×
2087
                return r;
2088

2089
        printf("\nOptions:\n");
×
2090

2091
        r = table_print_or_warn(opts);
×
2092
        if (r < 0)
×
2093
                return r;
2094

2095
        printf("\nSee the %s for details.\n", link);
×
2096
        return 0;
2097
}
2098

2099
static int parse_argv(int argc, char *argv[], char ***ret_args) {
115✔
2100
        int r;
115✔
2101

2102
        assert(argc >= 0);
115✔
2103
        assert(argv);
115✔
2104

2105
        OptionParser opts = { argc, argv };
115✔
2106

2107
        FOREACH_OPTION_OR_RETURN(c, &opts)
378✔
2108
                switch (c) {
148✔
2109

2110
                OPTION_COMMON_CAT_CONFIG:
×
2111
                        arg_cat_flags = CAT_CONFIG_ON;
×
2112
                        break;
×
2113

2114
                OPTION_COMMON_TLDR:
×
2115
                        arg_cat_flags = CAT_TLDR;
×
2116
                        break;
×
2117

2118
                OPTION_COMMON_HELP:
×
2119
                        return help();
×
2120

2121
                OPTION_COMMON_VERSION:
×
2122
                        return version();
×
2123

2124
                OPTION_GROUP("Options"): {}
108✔
2125

2126
                OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"):
108✔
2127
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_root);
108✔
2128
                        if (r < 0)
108✔
2129
                                return r;
2130
                        break;
2131

2132
                OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"):
×
2133
                        r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_image);
×
2134
                        if (r < 0)
×
2135
                                return r;
2136
                        break;
2137

2138
                OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"):
×
2139
                        r = parse_image_policy_argument(opts.arg, &arg_image_policy);
×
2140
                        if (r < 0)
×
2141
                                return r;
2142
                        break;
2143

2144
                OPTION_LONG("replace", "PATH", "Treat arguments as replacement for PATH"):
35✔
2145
                        if (!path_is_absolute(opts.arg))
35✔
2146
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2147
                                                       "The argument to --replace= must be an absolute path.");
2148
                        if (!endswith(opts.arg, ".conf"))
35✔
2149
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2150
                                                       "The argument to --replace= must have the extension '.conf'.");
2151

2152
                        arg_replace = opts.arg;
35✔
2153
                        break;
35✔
2154

2155
                OPTION_LONG("dry-run", NULL, "Just print what would be done"):
×
2156
                        arg_dry_run = true;
×
2157
                        break;
×
2158

2159
                OPTION_LONG("inline", NULL, "Treat arguments as configuration lines"):
5✔
2160
                        arg_inline = true;
5✔
2161
                        break;
5✔
2162

2163
                OPTION_COMMON_NO_PAGER:
×
2164
                        arg_pager_flags |= PAGER_DISABLE;
×
2165
                        break;
×
2166
                }
2167

2168
        char **args = option_parser_get_args(&opts);
115✔
2169
        size_t n_args = option_parser_get_n_args(&opts);
115✔
2170

2171
        if (arg_replace && arg_cat_flags != CAT_CONFIG_OFF)
115✔
2172
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2173
                                       "Option --replace= is not supported with --cat-config/--tldr.");
2174

2175
        if (arg_inline && arg_cat_flags != CAT_CONFIG_OFF)
115✔
2176
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2177
                                       "Option --inline is not supported with --cat-config/--tldr.");
2178

2179
        if (arg_replace && n_args == 0)
115✔
2180
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2181
                                       "When --replace= is given, some configuration items must be specified.");
2182

2183
        if (arg_image && arg_root)
115✔
2184
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2185
                                       "Use either --root= or --image=, the combination of both is not supported.");
2186

2187
        *ret_args = args;
115✔
2188
        return 1;
115✔
2189
}
2190

2191
static int parse_arguments(Context *c, char **args) {
43✔
2192
        unsigned pos = 1;
43✔
2193
        int r;
43✔
2194

2195
        assert(c);
43✔
2196

2197
        STRV_FOREACH(arg, args) {
90✔
2198
                if (arg_inline)
47✔
2199
                        /* Use (argument):n, where n==1 for the first positional arg */
2200
                        r = parse_line("(argument)", pos, *arg, /* invalid_config= */ NULL, c);
9✔
2201
                else
2202
                        r = read_config_file(c, *arg, /* ignore_enoent= */ false);
38✔
2203
                if (r < 0)
47✔
2204
                        return r;
2205

2206
                pos++;
47✔
2207
        }
2208

2209
        return 0;
2210
}
2211

2212
static int read_config_files(Context *c, char **args) {
90✔
2213
        _cleanup_strv_free_ char **files = NULL;
×
2214
        _cleanup_free_ char *p = NULL;
90✔
2215
        int r;
90✔
2216

2217
        assert(c);
90✔
2218

2219
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, &p);
90✔
2220
        if (r < 0)
90✔
2221
                return r;
2222

2223
        STRV_FOREACH(f, files)
204✔
2224
                if (p && path_equal(*f, p)) {
114✔
2225
                        log_debug("Parsing arguments at position \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
18✔
2226

2227
                        r = parse_arguments(c, args);
18✔
2228
                        if (r < 0)
18✔
2229
                                return r;
2230
                } else {
2231
                        log_debug("Reading config file \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
120✔
2232

2233
                        /* Just warn, ignore result otherwise */
2234
                        (void) read_config_file(c, *f, /* ignore_enoent= */ true);
96✔
2235
                }
2236

2237
        return 0;
2238
}
2239

2240
static int read_credential_lines(Context *c) {
115✔
2241
        _cleanup_free_ char *j = NULL;
115✔
2242
        const char *d;
115✔
2243
        int r;
115✔
2244

2245
        assert(c);
115✔
2246

2247
        r = get_credentials_dir(&d);
115✔
2248
        if (r == -ENXIO)
115✔
2249
                return 0;
2250
        if (r < 0)
1✔
2251
                return log_error_errno(r, "Failed to get credentials directory: %m");
×
2252

2253
        j = path_join(d, "sysusers.extra");
1✔
2254
        if (!j)
1✔
2255
                return log_oom();
×
2256

2257
        (void) read_config_file(c, j, /* ignore_enoent= */ true);
1✔
2258
        return 0;
2259
}
2260

2261
static int run(int argc, char *argv[]) {
115✔
2262
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
115✔
2263
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
×
2264
        _cleanup_close_ int lock = -EBADF;
115✔
2265
        _cleanup_(context_done) Context c = {
115✔
2266
                .audit_fd = -EBADF,
2267
                .search_uid = UID_INVALID,
2268
        };
2269

2270
        Item *i;
115✔
2271
        int r;
115✔
2272

2273
        LIBAUDIT_NOTE(recommended);
115✔
2274
        LIBBLKID_NOTE(recommended);
115✔
2275
        LIBCRYPT_NOTE(recommended);
115✔
2276
        LIBCRYPTSETUP_NOTE(suggested);
115✔
2277
        LIBCRYPTO_NOTE(suggested);
115✔
2278
        LIBMOUNT_NOTE(recommended);
115✔
2279
        LIBSELINUX_NOTE(recommended);
115✔
2280

2281
        char **args = NULL;
115✔
2282
        r = parse_argv(argc, argv, &args);
115✔
2283
        if (r <= 0)
115✔
2284
                return r;
2285

2286
        log_setup();
115✔
2287

2288
        if (arg_cat_flags != CAT_CONFIG_OFF)
115✔
2289
                return cat_config();
×
2290

2291
        if (should_bypass("SYSTEMD_SYSUSERS"))
115✔
2292
                return 0;
2293

2294
        umask(0022);
115✔
2295

2296
        r = mac_init();
115✔
2297
        if (r < 0)
115✔
2298
                return r;
2299

2300
        if (arg_image) {
115✔
2301
                assert(!arg_root);
×
2302

2303
                r = mount_image_privately_interactively(
×
2304
                                arg_image,
2305
                                arg_image_policy,
2306
                                DISSECT_IMAGE_GENERIC_ROOT |
2307
                                DISSECT_IMAGE_REQUIRE_ROOT |
2308
                                DISSECT_IMAGE_VALIDATE_OS |
2309
                                DISSECT_IMAGE_RELAX_VAR_CHECK |
2310
                                DISSECT_IMAGE_FSCK |
2311
                                DISSECT_IMAGE_GROWFS |
2312
                                DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
2313
                                &mounted_dir,
2314
                                /* ret_dir_fd= */ NULL,
2315
                                &loop_device);
2316
                if (r < 0)
×
2317
                        return r;
2318

2319
                arg_root = strdup(mounted_dir);
×
2320
                if (!arg_root)
×
2321
                        return log_oom();
×
2322
        }
2323

2324
        /* Prepare to emit audit events, but only if we're operating on the host system. */
2325
        if (!arg_root)
115✔
2326
                c.audit_fd = open_audit_fd_or_warn();
7✔
2327

2328
        /* If command line arguments are specified along with --replace, read all configuration files and
2329
         * insert the positional arguments at the specified place. Otherwise, if command line arguments are
2330
         * specified, execute just them, and finally, without --replace= or any positional arguments, just
2331
         * read configuration and execute it. */
2332
        if (arg_replace || strv_isempty(args))
115✔
2333
                r = read_config_files(&c, args);
90✔
2334
        else
2335
                r = parse_arguments(&c, args);
25✔
2336
        if (r < 0)
115✔
2337
                return r;
2338

2339
        r = read_credential_lines(&c);
115✔
2340
        if (r < 0)
115✔
2341
                return r;
2342

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

2351
        if (!c.uid_range) {
115✔
2352
                /* Default to default range of SYSTEMD_UID_MIN..SYSTEM_UID_MAX. */
2353
                r = read_login_defs(&c.login_defs, NULL, arg_root);
115✔
2354
                if (r < 0)
115✔
2355
                        return log_error_errno(r, "Failed to read %s%s: %m",
×
2356
                                               strempty(arg_root), "/etc/login.defs");
2357

2358
                c.login_defs_need_warning = true;
115✔
2359

2360
                /* We pick a range that very conservative: we look at compiled-in maximum and the value in
2361
                 * /etc/login.defs. That way the UIDs/GIDs which we allocate will be interpreted correctly,
2362
                 * even if /etc/login.defs is removed later. (The bottom bound doesn't matter much, since
2363
                 * it's only used during allocation, so we use the configured value directly). */
2364
                uid_t begin = c.login_defs.system_alloc_uid_min,
115✔
2365
                      end = MIN3((uid_t) SYSTEM_UID_MAX, c.login_defs.system_uid_max, c.login_defs.system_gid_max);
115✔
2366
                if (begin < end) {
115✔
2367
                        r = uid_range_add(&c.uid_range, begin, end - begin + 1);
115✔
2368
                        if (r < 0)
115✔
2369
                                return log_oom();
×
2370
                }
2371
        }
2372

2373
        r = add_implicit(&c);
115✔
2374
        if (r < 0)
115✔
2375
                return r;
2376

2377
        if (!arg_dry_run) {
115✔
2378
                lock = take_etc_passwd_lock(arg_root);
115✔
2379
                if (lock < 0)
115✔
2380
                        return log_error_errno(lock, "Failed to take /etc/passwd lock: %m");
×
2381
        }
2382

2383
        r = load_user_database(&c);
115✔
2384
        if (r < 0)
115✔
2385
                return log_error_errno(r, "Failed to load user database: %m");
×
2386

2387
        r = load_group_database(&c);
115✔
2388
        if (r < 0)
115✔
2389
                return log_error_errno(r, "Failed to read group database: %m");
×
2390

2391
        ORDERED_HASHMAP_FOREACH(i, c.groups)
494✔
2392
                (void) process_item(&c, i);
379✔
2393

2394
        ORDERED_HASHMAP_FOREACH(i, c.users)
416✔
2395
                (void) process_item(&c, i);
301✔
2396

2397
        return write_files(&c);
115✔
2398
}
2399

2400
DEFINE_MAIN_FUNCTION(run);
115✔
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