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

systemd / systemd / 14630481637

23 Apr 2025 07:04PM UTC coverage: 72.178% (-0.002%) from 72.18%
14630481637

push

github

DaanDeMeyer
mkosi: Run clangd within the tools tree instead of the build container

Running within the build sandbox has a number of disadvantages:
- We have a separate clangd cache for each distribution/release combo
- It requires to build the full image before clangd can be used
- It breaks every time the image becomes out of date and requires a
  rebuild
- We can't look at system headers as we don't have the knowledge to map
  them from inside the build sandbox to the corresponding path on the host

Instead, let's have mkosi.clangd run clangd within the tools tree. We
already require building systemd for both the host and the target anyway,
and all the dependencies to build systemd are installed in the tools tree
already for that, as well as clangd since it's installed together with the
other clang tooling we install in the tools tree. Unlike the previous approach,
this approach only requires the mkosi tools tree to be built upfront, which has
a much higher chance of not invalidating its cache. We can also trivially map
system header lookups from within the sandbox to the path within mkosi.tools
on the host so that starts working as well.

297054 of 411557 relevant lines covered (72.18%)

686269.58 hits per line

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

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

3
#include <getopt.h>
4

5
#include "alloc-util.h"
6
#include "audit-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 "env-util.h"
15
#include "fd-util.h"
16
#include "fileio.h"
17
#include "format-util.h"
18
#include "fs-util.h"
19
#include "hashmap.h"
20
#include "image-policy.h"
21
#include "libcrypt-util.h"
22
#include "main-func.h"
23
#include "memory-util.h"
24
#include "mount-util.h"
25
#include "pager.h"
26
#include "parse-argument.h"
27
#include "path-util.h"
28
#include "pretty-print.h"
29
#include "selinux-util.h"
30
#include "set.h"
31
#include "smack-util.h"
32
#include "specifier.h"
33
#include "stat-util.h"
34
#include "string-util.h"
35
#include "strv.h"
36
#include "sync-util.h"
37
#include "tmpfile-util-label.h"
38
#include "uid-classification.h"
39
#include "uid-range.h"
40
#include "user-util.h"
41
#include "utf8.h"
42
#include "verbs.h"
43

44
typedef enum ItemType {
45
        ADD_USER =   'u',
46
        ADD_GROUP =  'g',
47
        ADD_MEMBER = 'm',
48
        ADD_RANGE =  'r',
49
} ItemType;
50

51
static const char* item_type_to_string(ItemType t) {
×
52
        switch (t) {
×
53
        case ADD_USER:
54
                return "user";
55
        case ADD_GROUP:
×
56
                return "group";
×
57
        case ADD_MEMBER:
×
58
                return "member";
×
59
        case ADD_RANGE:
×
60
                return "range";
×
61
        default:
×
62
                assert_not_reached();
×
63
        }
64
}
65

66
typedef struct Item {
67
        ItemType type;
68

69
        char *name;
70
        char *group_name;
71
        char *uid_path;
72
        char *gid_path;
73
        char *description;
74
        char *home;
75
        char *shell;
76

77
        gid_t gid;
78
        uid_t uid;
79

80
        char *filename;
81
        unsigned line;
82

83
        bool gid_set;
84

85
        /* When set the group with the specified GID must exist
86
         * and the check if a UID clashes with the GID is skipped.
87
         */
88
        bool id_set_strict;
89

90
        bool uid_set;
91

92
        bool locked;
93

94
        bool todo_user;
95
        bool todo_group;
96
} Item;
97

98
static char *arg_root = NULL;
99
static char *arg_image = NULL;
100
static CatFlags arg_cat_flags = CAT_CONFIG_OFF;
101
static const char *arg_replace = NULL;
102
static bool arg_dry_run = false;
103
static bool arg_inline = false;
104
static PagerFlags arg_pager_flags = 0;
105
static ImagePolicy *arg_image_policy = NULL;
106

107
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
113✔
108
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
113✔
109
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
113✔
110

111
typedef struct Context {
112
        int audit_fd;
113

114
        OrderedHashmap *users, *groups;
115
        OrderedHashmap *todo_uids, *todo_gids;
116
        OrderedHashmap *members;
117

118
        Hashmap *database_by_uid, *database_by_username;
119
        Hashmap *database_by_gid, *database_by_groupname;
120

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

124
        uid_t search_uid;
125
        UIDRange *uid_range;
126

127
        UGIDAllocationRange login_defs;
128
        bool login_defs_need_warning;
129
} Context;
130

131
static void context_done(Context *c) {
113✔
132
        assert(c);
113✔
133

134
        c->audit_fd = close_audit_fd(c->audit_fd);
113✔
135

136
        ordered_hashmap_free(c->groups);
113✔
137
        ordered_hashmap_free(c->users);
113✔
138
        ordered_hashmap_free(c->members);
113✔
139
        ordered_hashmap_free(c->todo_uids);
113✔
140
        ordered_hashmap_free(c->todo_gids);
113✔
141

142
        hashmap_free(c->database_by_uid);
113✔
143
        hashmap_free(c->database_by_username);
113✔
144
        hashmap_free(c->database_by_gid);
113✔
145
        hashmap_free(c->database_by_groupname);
113✔
146

147
        set_free(c->names);
113✔
148
        uid_range_free(c->uid_range);
113✔
149
}
113✔
150

151
static void maybe_emit_login_defs_warning(Context *c) {
45✔
152
        assert(c);
45✔
153

154
        if (!c->login_defs_need_warning)
45✔
155
                return;
156

157
        if (c->login_defs.system_alloc_uid_min != SYSTEM_ALLOC_UID_MIN ||
44✔
158
            c->login_defs.system_uid_max != SYSTEM_UID_MAX)
159
                log_warning("login.defs specifies UID allocation range "UID_FMT"–"UID_FMT
×
160
                            " that is different than the built-in defaults ("UID_FMT"–"UID_FMT")",
161
                            c->login_defs.system_alloc_uid_min, c->login_defs.system_uid_max,
162
                            (uid_t) SYSTEM_ALLOC_UID_MIN, (uid_t) SYSTEM_UID_MAX);
163
        if (c->login_defs.system_alloc_gid_min != SYSTEM_ALLOC_GID_MIN ||
44✔
164
            c->login_defs.system_gid_max != SYSTEM_GID_MAX)
165
                log_warning("login.defs specifies GID allocation range "GID_FMT"–"GID_FMT
×
166
                            " that is different than the built-in defaults ("GID_FMT"–"GID_FMT")",
167
                            c->login_defs.system_alloc_gid_min, c->login_defs.system_gid_max,
168
                            (gid_t) SYSTEM_ALLOC_GID_MIN, (gid_t) SYSTEM_GID_MAX);
169

170
        c->login_defs_need_warning = false;
44✔
171
}
172

173
static void log_audit_accounts(Context *c, ItemType what) {
226✔
174
#if HAVE_AUDIT
175
        assert(c);
226✔
176
        assert(IN_SET(what, ADD_USER, ADD_GROUP));
226✔
177

178
        if (arg_dry_run || c->audit_fd < 0)
226✔
179
                return;
226✔
180

181
        Item *i;
×
182
        int type = what == ADD_USER ? AUDIT_ADD_USER : AUDIT_ADD_GROUP;
×
183
        const char *op = what == ADD_USER ? "adding-user" : "adding-group";
×
184

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

200
        ORDERED_HASHMAP_FOREACH(i, what == ADD_USER ? c->todo_uids : c->todo_gids)
×
201
                audit_log_acct_message(
×
202
                                c->audit_fd,
203
                                type,
204
                                program_invocation_short_name,
205
                                op,
206
                                i->name,
×
207
                                /* id= */ (unsigned) -1,
208
                                /* host= */ NULL,
209
                                /* addr= */ NULL,
210
                                /* tty= */ NULL,
211
                                /* success= */ 1);
212
#endif
213
}
214

215
static int load_user_database(Context *c) {
113✔
216
        _cleanup_free_ char *passwd_path = NULL;
113✔
217
        _cleanup_fclose_ FILE *f = NULL;
113✔
218
        struct passwd *pw;
113✔
219
        int r;
113✔
220

221
        assert(c);
113✔
222

223
        r = chase_and_fopen_unlocked("/etc/passwd", arg_root, CHASE_PREFIX_ROOT, "re", &passwd_path, &f);
113✔
224
        if (r == -ENOENT)
113✔
225
                return 0;
226
        if (r < 0)
37✔
227
                return r;
228

229
        while ((r = fgetpwent_sane(f, &pw)) > 0) {
254✔
230

231
                char *n = strdup(pw->pw_name);
217✔
232
                if (!n)
217✔
233
                        return -ENOMEM;
234

235
                /* Note that we use trivial_hash_ops_free here, so identical strings can exist in the set. */
236
                r = set_ensure_consume(&c->names, &trivial_hash_ops_free, n);
217✔
237
                if (r < 0)
217✔
238
                        return r;
239
                assert(r > 0);  /* The set uses pointer comparisons, so n must not be in the set. */
217✔
240

241
                r = hashmap_ensure_put(&c->database_by_username, &string_hash_ops, n, UID_TO_PTR(pw->pw_uid));
217✔
242
                if (r == -EEXIST)
217✔
243
                        log_debug_errno(r, "%s: user '%s' is listed twice, ignoring duplicate uid.",
×
244
                                        passwd_path, n);
245
                else if (r < 0)
217✔
246
                        return r;
247

248
                r = hashmap_ensure_put(&c->database_by_uid, /* hash_ops= */ NULL, UID_TO_PTR(pw->pw_uid), n);
217✔
249
                if (r == -EEXIST)
217✔
250
                        log_debug_errno(r, "%s: uid "UID_FMT" is listed twice, ignoring duplicate name.",
260✔
251
                                        passwd_path, pw->pw_uid);
252
                else if (r < 0)
211✔
253
                        return r;
254
        }
255
        return r;
256
}
257

258
static int load_group_database(Context *c) {
113✔
259
        _cleanup_free_ char *group_path = NULL;
113✔
260
        _cleanup_fclose_ FILE *f = NULL;
113✔
261
        struct group *gr;
113✔
262
        int r;
113✔
263

264
        assert(c);
113✔
265

266
        r = chase_and_fopen_unlocked("/etc/group", arg_root, CHASE_PREFIX_ROOT, "re", &group_path, &f);
113✔
267
        if (r == -ENOENT)
113✔
268
                return 0;
269
        if (r < 0)
37✔
270
                return r;
271

272
        while ((r = fgetgrent_sane(f, &gr)) > 0) {
457✔
273
                char *n = strdup(gr->gr_name);
420✔
274
                if (!n)
420✔
275
                        return -ENOMEM;
276

277
                /* Note that we use trivial_hash_ops_free here, so identical strings can exist in the set. */
278
                r = set_ensure_consume(&c->names, &trivial_hash_ops_free, n);
420✔
279
                if (r < 0)
420✔
280
                        return r;
281
                assert(r > 0);  /* The set uses pointer comparisons, so n must not be in the set. */
420✔
282

283
                r = hashmap_ensure_put(&c->database_by_groupname, &string_hash_ops, n, GID_TO_PTR(gr->gr_gid));
420✔
284
                if (r == -EEXIST)
420✔
285
                        log_debug_errno(r, "%s: group '%s' is listed twice, ignoring duplicate gid.",
×
286
                                        group_path, n);
287
                else if (r < 0)
420✔
288
                        return r;
289

290
                r = hashmap_ensure_put(&c->database_by_gid, /* hash_ops= */ NULL, GID_TO_PTR(gr->gr_gid), n);
420✔
291
                if (r == -EEXIST)
420✔
292
                        log_debug_errno(r, "%s: gid "GID_FMT" is listed twice, ignoring duplicate name.",
469✔
293
                                        group_path, gr->gr_gid);
294
                else if (r < 0)
408✔
295
                        return r;
296
        }
297
        return r;
298
}
299

300
static int make_backup(const char *target, const char *x) {
348✔
301
        _cleanup_(unlink_and_freep) char *dst_tmp = NULL;
×
302
        _cleanup_fclose_ FILE *dst = NULL;
348✔
303
        _cleanup_close_ int src = -EBADF;
348✔
304
        const char *backup;
348✔
305
        struct stat st;
348✔
306
        int r;
348✔
307

308
        assert(target);
348✔
309
        assert(x);
348✔
310

311
        src = open(x, O_RDONLY|O_CLOEXEC|O_NOCTTY);
348✔
312
        if (src < 0) {
348✔
313
                if (errno == ENOENT) /* No backup necessary... */
312✔
314
                        return 0;
315

316
                return -errno;
×
317
        }
318

319
        if (fstat(src, &st) < 0)
36✔
320
                return -errno;
×
321

322
        r = fopen_temporary_label(
36✔
323
                        target,   /* The path for which to the look up the label */
324
                        x,        /* Where we want the file actually to end up */
325
                        &dst,     /* The temporary file we write to */
326
                        &dst_tmp);
327
        if (r < 0)
36✔
328
                return r;
329

330
        r = copy_bytes(src, fileno(dst), UINT64_MAX, COPY_REFLINK);
36✔
331
        if (r < 0)
36✔
332
                return r;
333

334
        backup = strjoina(x, "-");
180✔
335

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

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

345
        r = fsync_full(fileno(dst));
36✔
346
        if (r < 0)
36✔
347
                return r;
348

349
        if (rename(dst_tmp, backup) < 0)
36✔
350
                return errno;
×
351

352
        dst_tmp = mfree(dst_tmp); /* disable the unlink_and_freep() hook now that the file has been renamed */
36✔
353
        return 0;
36✔
354
}
355

356
static int putgrent_with_members(
753✔
357
                Context *c,
358
                const struct group *gr,
359
                FILE *group) {
360

361
        char **a;
753✔
362
        int r;
753✔
363

364
        assert(c);
753✔
365
        assert(gr);
753✔
366
        assert(group);
753✔
367

368
        a = ordered_hashmap_get(c->members, gr->gr_name);
753✔
369
        if (a) {
753✔
370
                _cleanup_strv_free_ char **l = NULL;
10✔
371
                bool added = false;
10✔
372

373
                l = strv_copy(gr->gr_mem);
10✔
374
                if (!l)
10✔
375
                        return -ENOMEM;
376

377
                STRV_FOREACH(i, a) {
20✔
378
                        if (strv_contains(l, *i))
10✔
379
                                continue;
5✔
380

381
                        r = strv_extend(&l, *i);
5✔
382
                        if (r < 0)
5✔
383
                                return r;
384

385
                        added = true;
386
                }
387

388
                if (added) {
10✔
389
                        struct group t;
5✔
390

391
                        strv_sort_uniq(l);
5✔
392

393
                        t = *gr;
5✔
394
                        t.gr_mem = l;
5✔
395

396
                        r = putgrent_sane(&t, group);
5✔
397
                        return r < 0 ? r : 1;
10✔
398
                }
399
        }
400

401
        return putgrent_sane(gr, group);
748✔
402
}
403

404
#if ENABLE_GSHADOW
405
static int putsgent_with_members(
743✔
406
                Context *c,
407
                const struct sgrp *sg,
408
                FILE *gshadow) {
409

410
        char **a;
743✔
411
        int r;
743✔
412

413
        assert(sg);
743✔
414
        assert(gshadow);
743✔
415

416
        a = ordered_hashmap_get(c->members, sg->sg_namp);
743✔
417
        if (a) {
743✔
418
                _cleanup_strv_free_ char **l = NULL;
10✔
419
                bool added = false;
10✔
420

421
                l = strv_copy(sg->sg_mem);
10✔
422
                if (!l)
10✔
423
                        return -ENOMEM;
424

425
                STRV_FOREACH(i, a) {
20✔
426
                        if (strv_contains(l, *i))
10✔
427
                                continue;
5✔
428

429
                        r = strv_extend(&l, *i);
5✔
430
                        if (r < 0)
5✔
431
                                return r;
432

433
                        added = true;
434
                }
435

436
                if (added) {
10✔
437
                        struct sgrp t;
5✔
438

439
                        strv_sort_uniq(l);
5✔
440

441
                        t = *sg;
5✔
442
                        t.sg_mem = l;
5✔
443

444
                        r = putsgent_sane(&t, gshadow);
5✔
445
                        return r < 0 ? r : 1;
10✔
446
                }
447
        }
448

449
        return putsgent_sane(sg, gshadow);
738✔
450
}
451
#endif
452

453
static const char* pick_shell(const Item *i) {
270✔
454
        assert(i);
270✔
455

456
        if (i->type != ADD_USER)
270✔
457
                return NULL;
458
        if (i->shell)
268✔
459
                return i->shell;
460
        if (i->uid_set && i->uid == 0)
248✔
461
                return default_root_shell(arg_root);
10✔
462
        return NOLOGIN;
463
}
464

465
static int write_temporary_passwd(
113✔
466
                Context *c,
467
                const char *passwd_path,
468
                FILE **ret_tmpfile,
469
                char **ret_tmpfile_path) {
470

471
        _cleanup_fclose_ FILE *original = NULL, *passwd = NULL;
113✔
472
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL;
113✔
473
        struct passwd *pw = NULL;
113✔
474
        Item *i;
113✔
475
        int r;
113✔
476

477
        assert(c);
113✔
478

479
        if (ordered_hashmap_isempty(c->todo_uids))
113✔
480
                return 0;
481

482
        if (arg_dry_run) {
87✔
483
                log_info("Would write /etc/passwd%s", glyph(GLYPH_ELLIPSIS));
×
484
                return 0;
×
485
        }
486

487
        r = fopen_temporary_label("/etc/passwd", passwd_path, &passwd, &passwd_tmp);
87✔
488
        if (r < 0)
87✔
489
                return log_debug_errno(r, "Failed to open temporary copy of %s: %m", passwd_path);
×
490

491
        original = fopen(passwd_path, "re");
87✔
492
        if (original) {
87✔
493

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

503
                while ((r = fgetpwent_sane(original, &pw)) > 0) {
138✔
504
                        i = ordered_hashmap_get(c->users, pw->pw_name);
129✔
505
                        if (i && i->todo_user)
129✔
506
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
507
                                                       "%s: User \"%s\" already exists.",
508
                                                       passwd_path, pw->pw_name);
509

510
                        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(pw->pw_uid)))
129✔
511
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
512
                                                       "%s: Detected collision for UID " UID_FMT ".",
513
                                                       passwd_path, pw->pw_uid);
514

515
                        /* Make sure we keep the NIS entries (if any) at the end. */
516
                        if (IN_SET(pw->pw_name[0], '+', '-'))
129✔
517
                                break;
518

519
                        r = putpwent_sane(pw, passwd);
124✔
520
                        if (r < 0)
124✔
521
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
522
                                                       pw->pw_name);
523
                }
524
                if (r < 0)
14✔
525
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
526

527
        } else {
528
                if (errno != ENOENT)
73✔
529
                        return log_debug_errno(errno, "Failed to open %s: %m", passwd_path);
×
530
                if (fchmod(fileno(passwd), 0644) < 0)
73✔
531
                        return log_debug_errno(errno, "Failed to fchmod %s: %m", passwd_tmp);
×
532
        }
533

534
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
355✔
535
                _cleanup_free_ char *creds_shell = NULL, *cn = NULL;
268✔
536

537
                struct passwd n = {
1,072✔
538
                        .pw_name = i->name,
268✔
539
                        .pw_uid = i->uid,
268✔
540
                        .pw_gid = i->gid,
268✔
541
                        .pw_gecos = (char*) strempty(i->description),
268✔
542

543
                        /* "x" means the password is stored in the shadow file */
544
                        .pw_passwd = (char*) PASSWORD_SEE_SHADOW,
545

546
                        /* We default to the root directory as home */
547
                        .pw_dir = i->home ?: (char*) "/",
268✔
548

549
                        /* Initialize the shell to nologin, with one exception:
550
                         * for root we patch in something special */
551
                        .pw_shell = (char*) pick_shell(i),
268✔
552
                };
553

554
                /* Try to pick up the shell for this account via the credentials logic */
555
                cn = strjoin("passwd.shell.", i->name);
268✔
556
                if (!cn)
268✔
557
                        return -ENOMEM;
558

559
                r = read_credential(cn, (void**) &creds_shell, NULL);
268✔
560
                if (r < 0)
268✔
561
                        log_debug_errno(r, "Couldn't read credential '%s', ignoring: %m", cn);
268✔
562
                else
563
                        n.pw_shell = creds_shell;
×
564

565
                r = putpwent_sane(&n, passwd);
268✔
566
                if (r < 0)
268✔
567
                        return log_debug_errno(r, "Failed to add new user \"%s\" to temporary passwd file: %m",
×
568
                                               i->name);
569
        }
570

571
        /* Append the remaining NIS entries if any */
572
        while (pw) {
87✔
573
                r = putpwent_sane(pw, passwd);
5✔
574
                if (r < 0)
5✔
575
                        return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary passwd file: %m",
×
576
                                               pw->pw_name);
577

578
                r = fgetpwent_sane(original, &pw);
5✔
579
                if (r < 0)
5✔
580
                        return log_debug_errno(r, "Failed to read %s: %m", passwd_path);
×
581
                if (r == 0)
5✔
582
                        break;
583
        }
584

585
        r = fflush_sync_and_check(passwd);
87✔
586
        if (r < 0)
87✔
587
                return log_debug_errno(r, "Failed to flush %s: %m", passwd_tmp);
×
588

589
        *ret_tmpfile = TAKE_PTR(passwd);
87✔
590
        *ret_tmpfile_path = TAKE_PTR(passwd_tmp);
87✔
591

592
        return 0;
87✔
593
}
594

595
static usec_t epoch_or_now(void) {
87✔
596
        uint64_t epoch;
87✔
597

598
        if (secure_getenv_uint64("SOURCE_DATE_EPOCH", &epoch) >= 0) {
87✔
599
                if (epoch > UINT64_MAX/USEC_PER_SEC) /* Overflow check */
×
600
                        return USEC_INFINITY;
87✔
601
                return (usec_t) epoch * USEC_PER_SEC;
×
602
        }
603

604
        return now(CLOCK_REALTIME);
87✔
605
}
606

607
static int write_temporary_shadow(
113✔
608
                Context *c,
609
                const char *shadow_path,
610
                FILE **ret_tmpfile,
611
                char **ret_tmpfile_path) {
612

613
        _cleanup_fclose_ FILE *original = NULL, *shadow = NULL;
113✔
614
        _cleanup_(unlink_and_freep) char *shadow_tmp = NULL;
113✔
615
        struct spwd *sp = NULL;
113✔
616
        long lstchg;
113✔
617
        Item *i;
113✔
618
        int r;
113✔
619

620
        assert(c);
113✔
621

622
        if (ordered_hashmap_isempty(c->todo_uids))
113✔
623
                return 0;
624

625
        if (arg_dry_run) {
87✔
626
                log_info("Would write /etc/shadow%s", glyph(GLYPH_ELLIPSIS));
×
627
                return 0;
×
628
        }
629

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

634
        lstchg = (long) (epoch_or_now() / USEC_PER_DAY);
87✔
635

636
        original = fopen(shadow_path, "re");
87✔
637
        if (original) {
87✔
638

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

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

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

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

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

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

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

676
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
355✔
677
                _cleanup_(erase_and_freep) char *creds_password = NULL;
268✔
678
                bool is_hashed;
268✔
679

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

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

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

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

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

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

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

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

736
        *ret_tmpfile = TAKE_PTR(shadow);
87✔
737
        *ret_tmpfile_path = TAKE_PTR(shadow_tmp);
87✔
738

739
        return 0;
87✔
740
}
741

742
static int write_temporary_group(
113✔
743
                Context *c,
744
                const char *group_path,
745
                FILE **ret_tmpfile,
746
                char **ret_tmpfile_path) {
747

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

755
        assert(c);
113✔
756

757
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
113✔
758
                return 0;
759

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

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

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

772
                r = copy_rights_with_fallback(fileno(original), fileno(group), group_tmp);
14✔
773
                if (r < 0)
14✔
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) {
251✔
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);
242✔
784
                        if (i && i->todo_group)
242✔
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)))
242✔
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], '+', '-'))
242✔
796
                                break;
797

798
                        r = putgrent_with_members(c, gr, group);
237✔
799
                        if (r < 0)
237✔
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)
237✔
803
                                group_changed = true;
×
804
                }
805
                if (r < 0)
14✔
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) {
603✔
816
                struct group n = {
516✔
817
                        .gr_name = i->name,
516✔
818
                        .gr_gid = i->gid,
516✔
819
                        .gr_passwd = (char*) PASSWORD_SEE_SHADOW,
820
                };
821

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

827
                group_changed = true;
516✔
828
        }
829

830
        /* Append the remaining NIS entries if any */
831
        while (gr) {
97✔
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);
87✔
845
        if (r < 0)
87✔
846
                return log_error_errno(r, "Failed to flush %s: %m", group_tmp);
×
847

848
        if (group_changed) {
87✔
849
                *ret_tmpfile = TAKE_PTR(group);
87✔
850
                *ret_tmpfile_path = TAKE_PTR(group_tmp);
87✔
851
        }
852
        return 0;
853
}
854

855
static int write_temporary_gshadow(
113✔
856
                Context *c,
857
                const char * gshadow_path,
858
                FILE **ret_tmpfile,
859
                char **ret_tmpfile_path) {
860

861
#if ENABLE_GSHADOW
862
        _cleanup_fclose_ FILE *original = NULL, *gshadow = NULL;
113✔
863
        _cleanup_(unlink_and_freep) char *gshadow_tmp = NULL;
113✔
864
        bool group_changed = false;
113✔
865
        Item *i;
113✔
866
        int r;
113✔
867

868
        assert(c);
113✔
869

870
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
113✔
871
                return 0;
872

873
        if (arg_dry_run) {
87✔
874
                log_info("Would write /etc/gshadow%s", glyph(GLYPH_ELLIPSIS));
×
875
                return 0;
×
876
        }
877

878
        r = fopen_temporary_label("/etc/gshadow", gshadow_path, &gshadow, &gshadow_tmp);
87✔
879
        if (r < 0)
87✔
880
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", gshadow_path);
×
881

882
        original = fopen(gshadow_path, "re");
87✔
883
        if (original) {
87✔
884
                struct sgrp *sg;
4✔
885

886
                r = copy_rights_with_fallback(fileno(original), fileno(gshadow), gshadow_tmp);
4✔
887
                if (r < 0)
4✔
888
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
889
                                               gshadow_path, gshadow_tmp);
890

891
                while ((r = fgetsgent_sane(original, &sg)) > 0) {
231✔
892

893
                        i = ordered_hashmap_get(c->groups, sg->sg_namp);
227✔
894
                        if (i && i->todo_group)
227✔
895
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
896
                                                       "%s: Group \"%s\" already exists.",
897
                                                       gshadow_path, sg->sg_namp);
898

899
                        r = putsgent_with_members(c, sg, gshadow);
227✔
900
                        if (r < 0)
227✔
901
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary gshadow file: %m",
×
902
                                                       sg->sg_namp);
903
                        if (r > 0)
227✔
904
                                group_changed = true;
×
905
                }
906
                if (r < 0)
4✔
907
                        return r;
908

909
        } else {
910
                if (errno != ENOENT)
83✔
911
                        return log_error_errno(errno, "Failed to open %s: %m", gshadow_path);
×
912
                if (fchmod(fileno(gshadow), 0000) < 0)
83✔
913
                        return log_error_errno(errno, "Failed to fchmod %s: %m", gshadow_tmp);
×
914
        }
915

916
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
603✔
917
                struct sgrp n = {
516✔
918
                        .sg_namp = i->name,
516✔
919
                        .sg_passwd = (char*) PASSWORD_LOCKED_AND_INVALID,
920
                };
921

922
                r = putsgent_with_members(c, &n, gshadow);
516✔
923
                if (r < 0)
516✔
924
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary gshadow file: %m",
×
925
                                               n.sg_namp);
926

927
                group_changed = true;
516✔
928
        }
929

930
        r = fflush_sync_and_check(gshadow);
87✔
931
        if (r < 0)
87✔
932
                return log_error_errno(r, "Failed to flush %s: %m", gshadow_tmp);
×
933

934
        if (group_changed) {
87✔
935
                *ret_tmpfile = TAKE_PTR(gshadow);
87✔
936
                *ret_tmpfile_path = TAKE_PTR(gshadow_tmp);
87✔
937
        }
938
#endif
939
        return 0;
940
}
941

942
static int write_files(Context *c) {
113✔
943
        _cleanup_fclose_ FILE *passwd = NULL, *group = NULL, *shadow = NULL, *gshadow = NULL;
339✔
944
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL, *group_tmp = NULL, *shadow_tmp = NULL, *gshadow_tmp = NULL;
113✔
945
        int r;
113✔
946

947
        const char
113✔
948
                *passwd_path = prefix_roota(arg_root, "/etc/passwd"),
221✔
949
                *shadow_path = prefix_roota(arg_root, "/etc/shadow"),
221✔
950
                *group_path = prefix_roota(arg_root, "/etc/group"),
221✔
951
                *gshadow_path = prefix_roota(arg_root, "/etc/gshadow");
221✔
952

953
        assert(c);
113✔
954

955
        r = write_temporary_group(c, group_path, &group, &group_tmp);
113✔
956
        if (r < 0)
113✔
957
                return r;
958

959
        r = write_temporary_gshadow(c, gshadow_path, &gshadow, &gshadow_tmp);
113✔
960
        if (r < 0)
113✔
961
                return r;
962

963
        r = write_temporary_passwd(c, passwd_path, &passwd, &passwd_tmp);
113✔
964
        if (r < 0)
113✔
965
                return r;
966

967
        r = write_temporary_shadow(c, shadow_path, &shadow, &shadow_tmp);
113✔
968
        if (r < 0)
113✔
969
                return r;
970

971
        /* Make a backup of the old files */
972
        if (group) {
113✔
973
                r = make_backup("/etc/group", group_path);
87✔
974
                if (r < 0)
87✔
975
                        return log_error_errno(r, "Failed to backup %s: %m", group_path);
×
976
        }
977
        if (gshadow) {
113✔
978
                r = make_backup("/etc/gshadow", gshadow_path);
87✔
979
                if (r < 0)
87✔
980
                        return log_error_errno(r, "Failed to backup %s: %m", gshadow_path);
×
981
        }
982

983
        if (passwd) {
113✔
984
                r = make_backup("/etc/passwd", passwd_path);
87✔
985
                if (r < 0)
87✔
986
                        return log_error_errno(r, "Failed to backup %s: %m", passwd_path);
×
987
        }
988
        if (shadow) {
113✔
989
                r = make_backup("/etc/shadow", shadow_path);
87✔
990
                if (r < 0)
87✔
991
                        return log_error_errno(r, "Failed to backup %s: %m", shadow_path);
×
992
        }
993

994
        /* And make the new files count */
995
        if (group) {
113✔
996
                r = rename_and_apply_smack_floor_label(group_tmp, group_path);
87✔
997
                if (r < 0)
87✔
998
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
999
                                               group_tmp, group_path);
1000
                group_tmp = mfree(group_tmp);
87✔
1001
        }
1002
        /* OK, we have written the group entries successfully */
1003
        log_audit_accounts(c, ADD_GROUP);
113✔
1004
        if (gshadow) {
113✔
1005
                r = rename_and_apply_smack_floor_label(gshadow_tmp, gshadow_path);
87✔
1006
                if (r < 0)
87✔
1007
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1008
                                               gshadow_tmp, gshadow_path);
1009

1010
                gshadow_tmp = mfree(gshadow_tmp);
87✔
1011
        }
1012

1013
        if (passwd) {
113✔
1014
                r = rename_and_apply_smack_floor_label(passwd_tmp, passwd_path);
87✔
1015
                if (r < 0)
87✔
1016
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1017
                                               passwd_tmp, passwd_path);
1018

1019
                passwd_tmp = mfree(passwd_tmp);
87✔
1020
        }
1021
        /* OK, we have written the user entries successfully */
1022
        log_audit_accounts(c, ADD_USER);
113✔
1023
        if (shadow) {
113✔
1024
                r = rename_and_apply_smack_floor_label(shadow_tmp, shadow_path);
87✔
1025
                if (r < 0)
87✔
1026
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1027
                                               shadow_tmp, shadow_path);
1028

1029
                shadow_tmp = mfree(shadow_tmp);
87✔
1030
        }
1031

1032
        return 0;
1033
}
1034

1035
static int uid_is_ok(
288✔
1036
                Context *c,
1037
                uid_t uid,
1038
                const char *name,
1039
                bool check_with_gid) {
1040

1041
        int r;
288✔
1042
        assert(c);
288✔
1043

1044
        /* Let's see if we already have assigned the UID a second time */
1045
        if (ordered_hashmap_get(c->todo_uids, UID_TO_PTR(uid)))
288✔
1046
                return 0;
1047

1048
        /* Try to avoid using uids that are already used by a group
1049
         * that doesn't have the same name as our new user. */
1050
        if (check_with_gid) {
283✔
1051
                Item *i;
203✔
1052

1053
                i = ordered_hashmap_get(c->todo_gids, GID_TO_PTR(uid));
203✔
1054
                if (i && !streq(i->name, name))
203✔
1055
                        return 0;
1056
        }
1057

1058
        /* Let's check the files directly */
1059
        if (hashmap_contains(c->database_by_uid, UID_TO_PTR(uid)))
273✔
1060
                return 0;
1061

1062
        if (check_with_gid) {
273✔
1063
                const char *n;
193✔
1064

1065
                n = hashmap_get(c->database_by_gid, GID_TO_PTR(uid));
193✔
1066
                if (n && !streq(n, name))
193✔
1067
                        return 0;
1068
        }
1069

1070
        /* Let's also check via NSS, to avoid UID clashes over LDAP and such, just in case */
1071
        if (!arg_root) {
268✔
1072
                _cleanup_free_ struct group *g = NULL;
5✔
1073

1074
                r = getpwuid_malloc(uid, /* ret= */ NULL);
5✔
1075
                if (r >= 0)
5✔
1076
                        return 0;
1077
                if (r != -ESRCH)
5✔
1078
                        log_warning_errno(r, "Unexpected failure while looking up UID '" UID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
×
1079

1080
                if (check_with_gid) {
5✔
1081
                        r = getgrgid_malloc((gid_t) uid, &g);
5✔
1082
                        if (r >= 0) {
5✔
1083
                                if (!streq(g->gr_name, name))
×
1084
                                        return 0;
1085
                        } else if (r != -ESRCH)
5✔
1086
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
5✔
1087
                }
1088
        }
1089

1090
        return 1;
1091
}
1092

1093
static int root_stat(const char *p, struct stat *ret_st) {
×
1094
        return chase_and_stat(p, arg_root, CHASE_PREFIX_ROOT, /* ret_path= */ NULL, ret_st);
×
1095
}
1096

1097
static int read_id_from_file(Item *i, uid_t *ret_uid, gid_t *ret_gid) {
70✔
1098
        struct stat st;
70✔
1099
        bool found_uid = false, found_gid = false;
70✔
1100
        uid_t uid = 0;
70✔
1101
        gid_t gid = 0;
70✔
1102

1103
        assert(i);
70✔
1104

1105
        /* First, try to get the GID directly */
1106
        if (ret_gid && i->gid_path && root_stat(i->gid_path, &st) >= 0) {
70✔
1107
                gid = st.st_gid;
×
1108
                found_gid = true;
×
1109
        }
1110

1111
        /* Then, try to get the UID directly */
1112
        if ((ret_uid || (ret_gid && !found_gid))
70✔
1113
            && i->uid_path
70✔
1114
            && root_stat(i->uid_path, &st) >= 0) {
×
1115

1116
                uid = st.st_uid;
×
1117
                found_uid = true;
×
1118

1119
                /* If we need the gid, but had no success yet, also derive it from the UID path */
1120
                if (ret_gid && !found_gid) {
×
1121
                        gid = st.st_gid;
×
1122
                        found_gid = true;
×
1123
                }
1124
        }
1125

1126
        /* If that didn't work yet, then let's reuse the GID as UID */
1127
        if (ret_uid && !found_uid && i->gid_path) {
70✔
1128

1129
                if (found_gid) {
×
1130
                        uid = (uid_t) gid;
1131
                        found_uid = true;
1132
                } else if (root_stat(i->gid_path, &st) >= 0) {
×
1133
                        uid = (uid_t) st.st_gid;
×
1134
                        found_uid = true;
×
1135
                }
1136
        }
1137

1138
        if (ret_uid) {
70✔
1139
                if (!found_uid)
40✔
1140
                        return 0;
70✔
1141

1142
                *ret_uid = uid;
×
1143
        }
1144

1145
        if (ret_gid) {
30✔
1146
                if (!found_gid)
30✔
1147
                        return 0;
1148

1149
                *ret_gid = gid;
×
1150
        }
1151

1152
        return 1;
1153
}
1154

1155
static int add_user(Context *c, Item *i) {
297✔
1156
        void *z;
297✔
1157
        int r;
297✔
1158

1159
        assert(c);
297✔
1160
        assert(i);
297✔
1161

1162
        /* Check the database directly */
1163
        z = hashmap_get(c->database_by_username, i->name);
297✔
1164
        if (z) {
297✔
1165
                log_debug("User %s already exists.", i->name);
29✔
1166
                i->uid = PTR_TO_UID(z);
29✔
1167
                i->uid_set = true;
29✔
1168
                return 0;
29✔
1169
        }
1170

1171
        if (!arg_root) {
268✔
1172
                _cleanup_free_ struct passwd *p = NULL;
5✔
1173

1174
                /* Also check NSS */
1175
                r = getpwnam_malloc(i->name, &p);
5✔
1176
                if (r >= 0) {
5✔
1177
                        log_debug("User %s already exists.", i->name);
×
1178
                        i->uid = p->pw_uid;
×
1179
                        i->uid_set = true;
×
1180

1181
                        r = free_and_strdup(&i->description, p->pw_gecos);
×
1182
                        if (r < 0)
×
1183
                                return log_oom();
×
1184

1185
                        return 0;
1186
                }
1187
                if (r != -ESRCH)
5✔
1188
                        log_warning_errno(r, "Unexpected failure while looking up user '%s' via NSS, assuming it doesn't exist: %m", i->name);
5✔
1189
        }
1190

1191
        /* Try to use the suggested numeric UID */
1192
        if (i->uid_set) {
268✔
1193
                r = uid_is_ok(c, i->uid, i->name, !i->id_set_strict);
233✔
1194
                if (r < 0)
233✔
1195
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1196
                if (r == 0) {
233✔
1197
                        log_info("Suggested user ID " UID_FMT " for %s already used.", i->uid, i->name);
5✔
1198
                        i->uid_set = false;
5✔
1199
                }
1200
        }
1201

1202
        /* If that didn't work, try to read it from the specified path */
1203
        if (!i->uid_set) {
268✔
1204
                uid_t candidate;
40✔
1205

1206
                if (read_id_from_file(i, &candidate, NULL) > 0) {
40✔
1207

1208
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
1209
                                log_debug("User ID " UID_FMT " of file not suitable for %s.", candidate, i->name);
×
1210
                        else {
1211
                                r = uid_is_ok(c, candidate, i->name, true);
×
1212
                                if (r < 0)
×
1213
                                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1214
                                else if (r > 0) {
×
1215
                                        i->uid = candidate;
×
1216
                                        i->uid_set = true;
×
1217
                                } else
1218
                                        log_debug("User ID " UID_FMT " of file for %s is already used.", candidate, i->name);
40✔
1219
                        }
1220
                }
1221
        }
1222

1223
        /* Otherwise, try to reuse the group ID */
1224
        if (!i->uid_set && i->gid_set) {
268✔
1225
                r = uid_is_ok(c, (uid_t) i->gid, i->name, true);
40✔
1226
                if (r < 0)
40✔
1227
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1228
                if (r > 0) {
40✔
1229
                        i->uid = (uid_t) i->gid;
25✔
1230
                        i->uid_set = true;
25✔
1231
                }
1232
        }
1233

1234
        /* And if that didn't work either, let's try to find a free one */
1235
        if (!i->uid_set) {
268✔
1236
                maybe_emit_login_defs_warning(c);
15✔
1237

1238
                for (;;) {
15✔
1239
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
15✔
1240
                        if (r < 0)
15✔
1241
                                return log_error_errno(r, "No free user ID available for %s.", i->name);
×
1242

1243
                        r = uid_is_ok(c, c->search_uid, i->name, true);
15✔
1244
                        if (r < 0)
15✔
1245
                                return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1246
                        else if (r > 0)
15✔
1247
                                break;
1248
                }
1249

1250
                i->uid_set = true;
15✔
1251
                i->uid = c->search_uid;
15✔
1252
        }
1253

1254
        r = ordered_hashmap_ensure_put(&c->todo_uids, NULL, UID_TO_PTR(i->uid), i);
268✔
1255
        if (r == -EEXIST)
268✔
1256
                return log_error_errno(r, "Requested user %s with UID " UID_FMT " and gid" GID_FMT " to be created is duplicated "
×
1257
                                       "or conflicts with another user.", i->name, i->uid, i->gid);
1258
        if (r == -ENOMEM)
268✔
1259
                return log_oom();
×
1260
        if (r < 0)
268✔
1261
                return log_error_errno(r, "Failed to store user %s with UID " UID_FMT " and GID " GID_FMT " to be created: %m",
×
1262
                                       i->name, i->uid, i->gid);
1263

1264
        i->todo_user = true;
268✔
1265
        log_info("Creating user '%s' (%s) with UID " UID_FMT " and GID " GID_FMT ".",
538✔
1266
                 i->name, strna(i->description), i->uid, i->gid);
1267

1268
        return 0;
1269
}
1270

1271
static int gid_is_ok(
720✔
1272
                Context *c,
1273
                gid_t gid,
1274
                const char *groupname,
1275
                bool check_with_uid) {
1276

1277
        Item *user;
720✔
1278
        char *username;
720✔
1279
        int r;
720✔
1280

1281
        assert(c);
720✔
1282
        assert(groupname);
720✔
1283

1284
        if (ordered_hashmap_get(c->todo_gids, GID_TO_PTR(gid)))
720✔
1285
                return 0;
1286

1287
        /* Avoid reusing gids that are already used by a different user */
1288
        if (check_with_uid) {
655✔
1289
                user = ordered_hashmap_get(c->todo_uids, UID_TO_PTR(gid));
321✔
1290
                if (user && !streq(user->name, groupname))
321✔
1291
                        return 0;
1292
        }
1293

1294
        if (hashmap_contains(c->database_by_gid, GID_TO_PTR(gid)))
655✔
1295
                return 0;
1296

1297
        if (check_with_uid) {
517✔
1298
                username = hashmap_get(c->database_by_uid, UID_TO_PTR(gid));
183✔
1299
                if (username && !streq(username, groupname))
183✔
1300
                        return 0;
1301
        }
1302

1303
        if (!arg_root) {
517✔
1304
                r = getgrgid_malloc(gid, /* ret= */ NULL);
5✔
1305
                if (r >= 0)
5✔
1306
                        return 0;
1307
                if (r != -ESRCH)
5✔
1308
                        log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1309

1310
                if (check_with_uid) {
5✔
1311
                        r = getpwuid_malloc(gid, /* ret= */ NULL);
5✔
1312
                        if (r >= 0)
5✔
1313
                                return 0;
1314
                        if (r != -ESRCH)
5✔
1315
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1316
                }
1317
        }
1318

1319
        return 1;
1320
}
1321

1322
static int get_gid_by_name(
645✔
1323
                Context *c,
1324
                const char *name,
1325
                gid_t *ret_gid) {
1326

1327
        void *z;
645✔
1328
        int r;
645✔
1329

1330
        assert(c);
645✔
1331
        assert(ret_gid);
645✔
1332

1333
        /* Check the database directly */
1334
        z = hashmap_get(c->database_by_groupname, name);
645✔
1335
        if (z) {
645✔
1336
                *ret_gid = PTR_TO_GID(z);
67✔
1337
                return 0;
67✔
1338
        }
1339

1340
        /* Also check NSS */
1341
        if (!arg_root) {
578✔
1342
                _cleanup_free_ struct group *g = NULL;
5✔
1343

1344
                r = getgrnam_malloc(name, &g);
5✔
1345
                if (r >= 0) {
5✔
1346
                        *ret_gid = g->gr_gid;
×
1347
                        return 0;
×
1348
                }
1349
                if (r != -ESRCH)
5✔
1350
                        log_warning_errno(r, "Unexpected failure while looking up group '%s' via NSS, assuming it doesn't exist: %m", name);
5✔
1351
        }
1352

1353
        return -ENOENT;
1354
}
1355

1356
static int add_group(Context *c, Item *i) {
639✔
1357
        int r;
639✔
1358

1359
        assert(c);
639✔
1360
        assert(i);
639✔
1361

1362
        r = get_gid_by_name(c, i->name, &i->gid);
639✔
1363
        if (r != -ENOENT) {
639✔
1364
                if (r < 0)
62✔
1365
                        return r;
1366
                log_debug("Group %s already exists.", i->name);
62✔
1367
                i->gid_set = true;
62✔
1368
                return 0;
62✔
1369
        }
1370

1371
        /* Try to use the suggested numeric GID */
1372
        if (i->gid_set) {
577✔
1373
                r = gid_is_ok(c, i->gid, i->name, false);
394✔
1374
                if (r < 0)
394✔
1375
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1376
                if (i->id_set_strict) {
394✔
1377
                        /* If we require the GID to already exist we can return here:
1378
                         * r > 0: means the GID does not exist -> fail
1379
                         * r == 0: means the GID exists -> nothing more to do.
1380
                         */
1381
                        if (r > 0)
61✔
1382
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
1383
                                                       "Failed to create %s: please create GID " GID_FMT,
1384
                                                       i->name, i->gid);
1385
                        if (r == 0)
60✔
1386
                                return 0;
1387
                }
1388
                if (r == 0) {
333✔
1389
                        log_info("Suggested group ID " GID_FMT " for %s already used.", i->gid, i->name);
×
1390
                        i->gid_set = false;
×
1391
                }
1392
        }
1393

1394
        /* Try to reuse the numeric uid, if there's one */
1395
        if (!i->gid_set && i->uid_set) {
516✔
1396
                r = gid_is_ok(c, (gid_t) i->uid, i->name, true);
158✔
1397
                if (r < 0)
158✔
1398
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1399
                if (r > 0) {
158✔
1400
                        i->gid = (gid_t) i->uid;
153✔
1401
                        i->gid_set = true;
153✔
1402
                }
1403
        }
1404

1405
        /* If that didn't work, try to read it from the specified path */
1406
        if (!i->gid_set) {
516✔
1407
                gid_t candidate;
30✔
1408

1409
                if (read_id_from_file(i, NULL, &candidate) > 0) {
30✔
1410

1411
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
1412
                                log_debug("Group ID " GID_FMT " of file not suitable for %s.", candidate, i->name);
×
1413
                        else {
1414
                                r = gid_is_ok(c, candidate, i->name, true);
×
1415
                                if (r < 0)
×
1416
                                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1417
                                else if (r > 0) {
×
1418
                                        i->gid = candidate;
×
1419
                                        i->gid_set = true;
×
1420
                                } else
1421
                                        log_debug("Group ID " GID_FMT " of file for %s already used.", candidate, i->name);
30✔
1422
                        }
1423
                }
1424
        }
1425

1426
        /* And if that didn't work either, let's try to find a free one */
1427
        if (!i->gid_set) {
516✔
1428
                maybe_emit_login_defs_warning(c);
30✔
1429

1430
                for (;;) {
168✔
1431
                        /* We look for new GIDs in the UID pool! */
1432
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
168✔
1433
                        if (r < 0)
168✔
1434
                                return log_error_errno(r, "No free group ID available for %s.", i->name);
×
1435

1436
                        r = gid_is_ok(c, c->search_uid, i->name, true);
168✔
1437
                        if (r < 0)
168✔
1438
                                return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1439
                        else if (r > 0)
168✔
1440
                                break;
1441
                }
1442

1443
                i->gid_set = true;
30✔
1444
                i->gid = c->search_uid;
30✔
1445
        }
1446

1447
        r = ordered_hashmap_ensure_put(&c->todo_gids, NULL, GID_TO_PTR(i->gid), i);
516✔
1448
        if (r == -EEXIST)
516✔
1449
                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);
×
1450
        if (r == -ENOMEM)
516✔
1451
                return log_oom();
×
1452
        if (r < 0)
516✔
1453
                return log_error_errno(r, "Failed to store group %s with GID " GID_FMT " to be created: %m", i->name, i->gid);
×
1454

1455
        i->todo_group = true;
516✔
1456
        log_info("Creating group '%s' with GID " GID_FMT ".", i->name, i->gid);
516✔
1457

1458
        return 0;
1459
}
1460

1461
static int process_item(Context *c, Item *i) {
675✔
1462
        int r;
675✔
1463

1464
        assert(c);
675✔
1465
        assert(i);
675✔
1466

1467
        switch (i->type) {
675✔
1468

1469
        case ADD_USER: {
299✔
1470
                Item *j = NULL;
299✔
1471

1472
                if (!i->gid_set) {
299✔
1473
                        j = ordered_hashmap_get(c->groups, i->group_name ?: i->name);
235✔
1474

1475
                        /* If that's not a match, also check if the group name
1476
                         * matches a user name in the queue. */
1477
                        if (!j && i->group_name)
235✔
1478
                                j = ordered_hashmap_get(c->users, i->group_name);
11✔
1479
                }
1480

1481
                if (j && j->todo_group) {
235✔
1482
                        /* When a group with the target name is already in queue,
1483
                         * use the information about the group and do not create
1484
                         * duplicated group entry. */
1485
                        i->gid_set = j->gid_set;
30✔
1486
                        i->gid = j->gid;
30✔
1487
                        i->id_set_strict = true;
30✔
1488
                } else if (i->group_name) {
269✔
1489
                        /* When a group name was given instead of a GID and it's
1490
                         * not in queue, then it must already exist. */
1491
                        r = get_gid_by_name(c, i->group_name, &i->gid);
6✔
1492
                        if (r < 0)
6✔
1493
                                return log_error_errno(r, "Group %s not found.", i->group_name);
1✔
1494
                        i->gid_set = true;
5✔
1495
                        i->id_set_strict = true;
5✔
1496
                } else {
1497
                        r = add_group(c, i);
263✔
1498
                        if (r < 0)
263✔
1499
                                return r;
1500
                }
1501

1502
                return add_user(c, i);
297✔
1503
        }
1504

1505
        case ADD_GROUP:
376✔
1506
                return add_group(c, i);
376✔
1507

1508
        default:
×
1509
                assert_not_reached();
×
1510
        }
1511
}
1512

1513
static Item* item_free(Item *i) {
677✔
1514
        if (!i)
677✔
1515
                return NULL;
1516

1517
        free(i->name);
677✔
1518
        free(i->group_name);
677✔
1519
        free(i->uid_path);
677✔
1520
        free(i->gid_path);
677✔
1521
        free(i->description);
677✔
1522
        free(i->home);
677✔
1523
        free(i->shell);
677✔
1524
        free(i->filename);
677✔
1525
        return mfree(i);
677✔
1526
}
1527

1528
DEFINE_TRIVIAL_CLEANUP_FUNC(Item*, item_free);
1,364✔
1529
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, char, string_hash_func, string_compare_func, Item, item_free);
675✔
1530

1531
static Item* item_new(ItemType type, const char *name, const char *filename, unsigned line) {
677✔
1532
        assert(name);
677✔
1533
        assert(!!filename == (line > 0));
677✔
1534

1535
        _cleanup_(item_freep) Item *new = new(Item, 1);
1,354✔
1536
        if (!new)
677✔
1537
                return NULL;
1538

1539
        *new = (Item) {
677✔
1540
                .type = type,
1541
                .line = line,
1542
        };
1543

1544
        if (free_and_strdup(&new->name, name) < 0 ||
1,354✔
1545
            free_and_strdup(&new->filename, filename) < 0)
677✔
1546
                return NULL;
×
1547

1548
        return TAKE_PTR(new);
677✔
1549
}
1550

1551
static int add_implicit(Context *c) {
113✔
1552
        char *g, **l;
113✔
1553
        int r;
113✔
1554

1555
        assert(c);
113✔
1556

1557
        /* Implicitly create additional users and groups, if they were listed in "m" lines */
1558
        ORDERED_HASHMAP_FOREACH_KEY(l, g, c->members) {
236✔
1559
                STRV_FOREACH(m, l)
20✔
1560
                        if (!ordered_hashmap_get(c->users, *m)) {
10✔
1561
                                _cleanup_(item_freep) Item *j =
×
1562
                                        item_new(ADD_USER, *m, /* filename= */ NULL, /* line= */ 0);
5✔
1563
                                if (!j)
5✔
1564
                                        return log_oom();
×
1565

1566
                                r = ordered_hashmap_ensure_put(&c->users, &item_hash_ops, j->name, j);
5✔
1567
                                if (r == -ENOMEM)
5✔
1568
                                        return log_oom();
×
1569
                                if (r < 0)
5✔
1570
                                        return log_error_errno(r, "Failed to add implicit user '%s': %m", j->name);
×
1571

1572
                                log_debug("Adding implicit user '%s' due to m line", j->name);
5✔
1573
                                TAKE_PTR(j);
5✔
1574
                        }
1575

1576
                if (!(ordered_hashmap_get(c->users, g) ||
10✔
1577
                      ordered_hashmap_get(c->groups, g))) {
2✔
1578
                        _cleanup_(item_freep) Item *j =
×
1579
                                item_new(ADD_GROUP, g, /* filename= */ NULL, /* line= */ 0);
×
1580
                        if (!j)
×
1581
                                return log_oom();
×
1582

1583
                        r = ordered_hashmap_ensure_put(&c->groups, &item_hash_ops, j->name, j);
×
1584
                        if (r == -ENOMEM)
×
1585
                                return log_oom();
×
1586
                        if (r < 0)
×
1587
                                return log_error_errno(r, "Failed to add implicit group '%s': %m", j->name);
×
1588

1589
                        log_debug("Adding implicit group '%s' due to m line", j->name);
×
1590
                        TAKE_PTR(j);
×
1591
                }
1592
        }
1593

1594
        return 0;
113✔
1595
}
1596

1597
static int item_equivalent(Item *a, Item *b) {
1✔
1598
        int r;
1✔
1599

1600
        assert(a);
1✔
1601
        assert(b);
1✔
1602

1603
        if (a->type != b->type) {
1✔
1604
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1605
                           "Item not equivalent because types differ");
1606
                return false;
×
1607
        }
1608

1609
        if (!streq_ptr(a->name, b->name)) {
1✔
1610
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1611
                           "Item not equivalent because names differ ('%s' vs. '%s')",
1612
                           a->name, b->name);
1613
                return false;
×
1614
        }
1615

1616
        /* Paths were simplified previously, so we can use streq. */
1617
        if (!streq_ptr(a->uid_path, b->uid_path)) {
1✔
1618
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1619
                           "Item not equivalent because UID paths differ (%s vs. %s)",
1620
                           a->uid_path ?: "(unset)", b->uid_path ?: "(unset)");
1621
                return false;
×
1622
        }
1623

1624
        if (!streq_ptr(a->gid_path, b->gid_path)) {
1✔
1625
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1626
                           "Item not equivalent because GID paths differ (%s vs. %s)",
1627
                           a->gid_path ?: "(unset)", b->gid_path ?: "(unset)");
1628
                return false;
×
1629
        }
1630

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

1638
        if ((a->uid_set != b->uid_set) ||
1✔
1639
            (a->uid_set && a->uid != b->uid)) {
×
1640
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1641
                           "Item not equivalent because UIDs differ (%s vs. %s)",
1642
                           a->uid_set ? FORMAT_UID(a->uid) : "(unset)",
1643
                           b->uid_set ? FORMAT_UID(b->uid) : "(unset)");
1644
                return false;
×
1645
        }
1646

1647
        if ((a->gid_set != b->gid_set) ||
1✔
1648
            (a->gid_set && a->gid != b->gid)) {
1✔
1649
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1650
                           "Item not equivalent because GIDs differ (%s vs. %s)",
1651
                           a->gid_set ? FORMAT_GID(a->gid) : "(unset)",
1652
                           b->gid_set ? FORMAT_GID(b->gid) : "(unset)");
1653
                return false;
×
1654
        }
1655

1656
        if (!streq_ptr(a->home, b->home)) {
1✔
1657
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1658
                           "Item not equivalent because home directories differ ('%s' vs. '%s')",
1659
                           strempty(a->description), strempty(b->description));
1660
                return false;
×
1661
        }
1662

1663
        /* Check if the two paths refer to the same file.
1664
         * If the paths are equal (after normalization), it's obviously the same file.
1665
         * If both paths specify a nologin shell, treat them as the same (e.g. /bin/true and /bin/false).
1666
         * Otherwise, try to resolve the paths, and see if we get the same result, (e.g. /sbin/nologin and
1667
         * /usr/sbin/nologin).
1668
         * If we can't resolve something, treat different paths as different. */
1669

1670
        const char *a_shell = pick_shell(a),
1✔
1671
                   *b_shell = pick_shell(b);
1✔
1672
        if (!path_equal(a_shell, b_shell) &&
1✔
1673
            !(is_nologin_shell(a_shell) && is_nologin_shell(b_shell))) {
×
1674
                _cleanup_free_ char *pa = NULL, *pb = NULL;
×
1675

1676
                r = chase(a_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pa, NULL);
×
1677
                if (r < 0) {
×
1678
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1679
                                       r, "Failed to look up path '%s%s%s': %m",
1680
                                       strempty(arg_root), arg_root ? "/" : "", a_shell);
1681
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1682
                }
1683

1684
                r = chase(b_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pb, NULL);
×
1685
                if (r < 0) {
×
1686
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1687
                                       r, "Failed to look up path '%s%s%s': %m",
1688
                                       strempty(arg_root), arg_root ? "/" : "", b_shell);
1689
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1690
                }
1691

1692
                if (!path_equal(pa, pb)) {
×
1693
                        log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1694
                                   "Item not equivalent because shells differ ('%s' vs. '%s')",
1695
                                   pa, pb);
1696
                        return false;
×
1697
                }
1698
        }
1699

1700
        return true;
1701
}
1702

1703
static int parse_line(
682✔
1704
                const char *fname,
1705
                unsigned line,
1706
                const char *buffer,
1707
                bool *invalid_config,
1708
                void *context) {
1709

1710
        Context *c = ASSERT_PTR(context);
682✔
1711
        _cleanup_free_ char *action = NULL,
1,364✔
1712
                *name = NULL, *resolved_name = NULL,
×
1713
                *id = NULL, *resolved_id = NULL,
×
1714
                *description = NULL, *resolved_description = NULL,
×
1715
                *home = NULL, *resolved_home = NULL,
×
1716
                *shell = NULL, *resolved_shell = NULL;
682✔
1717
        _cleanup_(item_freep) Item *i = NULL;
682✔
1718
        Item *existing;
682✔
1719
        OrderedHashmap *h;
682✔
1720
        int r;
682✔
1721
        const char *p;
682✔
1722

1723
        assert(fname);
682✔
1724
        assert(line >= 1);
682✔
1725
        assert(buffer);
682✔
1726
        assert(!invalid_config); /* We don't support invalid_config yet. */
682✔
1727

1728
        /* Parse columns */
1729
        p = buffer;
682✔
1730
        r = extract_many_words(&p, NULL, EXTRACT_UNQUOTE,
682✔
1731
                               &action, &name, &id, &description, &home, &shell);
1732
        if (r < 0)
682✔
1733
                return log_syntax(NULL, LOG_ERR, fname, line, r, "Syntax error.");
×
1734
        if (r < 2)
682✔
1735
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1736
                                  "Missing action and name columns.");
1737
        if (!isempty(p))
682✔
1738
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1739
                                  "Trailing garbage.");
1740

1741
        if (isempty(action))
682✔
1742
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1743
                                  "Empty command specification.");
1744

1745
        bool locked = false;
1746
        for (int pos = 1; action[pos]; pos++)
694✔
1747
                if (action[pos] == '!' && !locked)
12✔
1748
                        locked = true;
12✔
1749
                else
1750
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1751
                                          "Unknown modifiers in command '%s'.", action);
1752

1753
        if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE))
682✔
1754
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1755
                                  "Unknown command type '%c'.", action[0]);
1756

1757
        /* Verify name */
1758
        if (empty_or_dash(name))
682✔
1759
                name = mfree(name);
×
1760

1761
        if (name) {
682✔
1762
                r = specifier_printf(name, NAME_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_name);
682✔
1763
                if (r < 0)
682✔
1764
                        return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to replace specifiers in '%s': %m", name);
×
1765

1766
                if (!valid_user_group_name(resolved_name, 0))
682✔
1767
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1768
                                          "'%s' is not a valid user or group name.", resolved_name);
1769
        }
1770

1771
        /* Verify id */
1772
        if (empty_or_dash(id))
682✔
1773
                id = mfree(id);
55✔
1774

1775
        if (id) {
682✔
1776
                r = specifier_printf(id, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_id);
627✔
1777
                if (r < 0)
627✔
1778
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1779
                                          "Failed to replace specifiers in '%s': %m", name);
1780
        }
1781

1782
        /* Verify description */
1783
        if (empty_or_dash(description))
682✔
1784
                description = mfree(description);
631✔
1785

1786
        if (description) {
682✔
1787
                r = specifier_printf(description, LONG_LINE_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_description);
51✔
1788
                if (r < 0)
51✔
1789
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1790
                                          "Failed to replace specifiers in '%s': %m", description);
1791

1792
                if (!valid_gecos(resolved_description))
51✔
1793
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1794
                                          "'%s' is not a valid GECOS field.", resolved_description);
1795
        }
1796

1797
        /* Verify home */
1798
        if (empty_or_dash(home))
682✔
1799
                home = mfree(home);
542✔
1800

1801
        if (home) {
682✔
1802
                r = specifier_printf(home, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_home);
140✔
1803
                if (r < 0)
140✔
1804
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1805
                                          "Failed to replace specifiers in '%s': %m", home);
1806

1807
                path_simplify(resolved_home);
140✔
1808

1809
                if (!valid_home(resolved_home))
140✔
1810
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1811
                                          "'%s' is not a valid home directory field.", resolved_home);
1812
        }
1813

1814
        /* Verify shell */
1815
        if (empty_or_dash(shell))
682✔
1816
                shell = mfree(shell);
661✔
1817

1818
        if (shell) {
682✔
1819
                r = specifier_printf(shell, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_shell);
21✔
1820
                if (r < 0)
21✔
1821
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1822
                                          "Failed to replace specifiers in '%s': %m", shell);
1823

1824
                path_simplify(resolved_shell);
21✔
1825

1826
                if (!valid_shell(resolved_shell))
21✔
1827
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1828
                                          "'%s' is not a valid login shell field.", resolved_shell);
1829
        }
1830

1831
        switch (action[0]) {
682✔
1832

1833
        case ADD_RANGE:
×
1834
                if (locked)
×
1835
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1836
                                          "Flag '!' not permitted on lines of type 'r'.");
1837

1838
                if (resolved_name)
×
1839
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1840
                                          "Lines of type 'r' don't take a name field.");
1841

1842
                if (!resolved_id)
×
1843
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1844
                                          "Lines of type 'r' require an ID range in the third field.");
1845

1846
                if (description || home || shell)
×
1847
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1848
                                          "Lines of type '%c' don't take a %s field.",
1849
                                          action[0],
1850
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1851

1852
                r = uid_range_add_str(&c->uid_range, resolved_id);
×
1853
                if (r < 0)
×
1854
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1855
                                          "Invalid UID range %s.", resolved_id);
1856

1857
                return 0;
1858

1859
        case ADD_MEMBER: {
10✔
1860
                /* Try to extend an existing member or group item */
1861
                if (!name)
10✔
1862
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1863
                                          "Lines of type 'm' require a user name in the second field.");
1864

1865
                if (locked)
10✔
1866
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1867
                                          "Flag '!' not permitted on lines of type 'm'.");
1868

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

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

1877
                if (description || home || shell)
10✔
1878
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1879
                                          "Lines of type '%c' don't take a %s field.",
1880
                                          action[0],
1881
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1882

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

1887
                return 0;
1888
        }
1889

1890
        case ADD_USER:
295✔
1891
                if (!name)
295✔
1892
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1893
                                          "Lines of type 'u' require a user name in the second field.");
1894

1895
                r = ordered_hashmap_ensure_allocated(&c->users, &item_hash_ops);
295✔
1896
                if (r < 0)
295✔
1897
                        return log_oom();
×
1898

1899
                i = item_new(ADD_USER, resolved_name, fname, line);
295✔
1900
                if (!i)
295✔
1901
                        return log_oom();
×
1902

1903
                if (resolved_id) {
295✔
1904
                        if (path_is_absolute(resolved_id))
266✔
1905
                                i->uid_path = path_simplify(TAKE_PTR(resolved_id));
×
1906
                        else {
1907
                                _cleanup_free_ char *uid = NULL, *gid = NULL;
266✔
1908
                                if (split_pair(resolved_id, ":", &uid, &gid) == 0) {
266✔
1909
                                        r = parse_gid(gid, &i->gid);
90✔
1910
                                        if (r < 0) {
90✔
1911
                                                if (valid_user_group_name(gid, 0))
26✔
1912
                                                        i->group_name = TAKE_PTR(gid);
26✔
1913
                                                else
1914
                                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1915
                                                                          "Failed to parse GID: '%s': %m", id);
1916
                                        } else {
1917
                                                i->gid_set = true;
64✔
1918
                                                i->id_set_strict = true;
64✔
1919
                                        }
1920
                                        free_and_replace(resolved_id, uid);
90✔
1921
                                }
1922
                                if (!streq(resolved_id, "-")) {
266✔
1923
                                        r = parse_uid(resolved_id, &i->uid);
251✔
1924
                                        if (r < 0)
251✔
1925
                                                return log_syntax(NULL, LOG_ERR, fname, line, r,
1✔
1926
                                                                  "Failed to parse UID: '%s': %m", id);
1927
                                        i->uid_set = true;
250✔
1928
                                }
1929
                        }
1930
                }
1931

1932
                i->description = TAKE_PTR(resolved_description);
294✔
1933
                i->home = TAKE_PTR(resolved_home);
294✔
1934
                i->shell = TAKE_PTR(resolved_shell);
294✔
1935
                i->locked = locked;
294✔
1936

1937
                h = c->users;
294✔
1938
                break;
294✔
1939

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

1945
                if (locked)
377✔
1946
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1947
                                          "Flag '!' not permitted on lines of type 'g'.");
1948

1949
                if (description || home || shell)
377✔
1950
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1951
                                          "Lines of type '%c' don't take a %s field.",
1952
                                          action[0],
1953
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1954

1955
                r = ordered_hashmap_ensure_allocated(&c->groups, &item_hash_ops);
377✔
1956
                if (r < 0)
377✔
1957
                        return log_oom();
×
1958

1959
                i = item_new(ADD_GROUP, resolved_name, fname, line);
377✔
1960
                if (!i)
377✔
1961
                        return log_oom();
×
1962

1963
                if (resolved_id) {
377✔
1964
                        if (path_is_absolute(resolved_id))
351✔
1965
                                i->gid_path = path_simplify(TAKE_PTR(resolved_id));
×
1966
                        else {
1967
                                r = parse_gid(resolved_id, &i->gid);
351✔
1968
                                if (r < 0)
351✔
1969
                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1970
                                                          "Failed to parse GID: '%s': %m", id);
1971

1972
                                i->gid_set = true;
351✔
1973
                        }
1974
                }
1975

1976
                h = c->groups;
377✔
1977
                break;
377✔
1978

1979
        default:
×
1980
                assert_not_reached();
×
1981
        }
1982

1983
        existing = ordered_hashmap_get(h, i->name);
671✔
1984
        if (existing) {
671✔
1985
                /* Two functionally-equivalent items are fine */
1986
                r = item_equivalent(i, existing);
1✔
1987
                if (r < 0)
1✔
1988
                        return r;
1989
                if (r == 0) {
1✔
1990
                        if (existing->filename)
×
1991
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
1992
                                           "Conflict with earlier configuration for %s '%s' in %s:%u, ignoring line.",
1993
                                           item_type_to_string(i->type),
1994
                                           i->name,
1995
                                           existing->filename, existing->line);
1996
                        else
1997
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
1998
                                           "Conflict with earlier configuration for %s '%s', ignoring line.",
1999
                                           item_type_to_string(i->type),
2000
                                           i->name);
2001
                }
2002

2003
                return 0;
1✔
2004
        }
2005

2006
        r = ordered_hashmap_put(h, i->name, i);
670✔
2007
        if (r < 0)
670✔
2008
                return log_oom();
×
2009

2010
        i = NULL;
670✔
2011
        return 0;
670✔
2012
}
2013

2014
static int read_config_file(Context *c, const char *fn, bool ignore_enoent) {
132✔
2015
        return conf_file_read(
396✔
2016
                        arg_root,
2017
                        (const char**) CONF_PATHS_STRV("sysusers.d"),
132✔
2018
                        ASSERT_PTR(fn),
132✔
2019
                        parse_line,
2020
                        ASSERT_PTR(c),
132✔
2021
                        ignore_enoent,
2022
                        /* invalid_config= */ NULL);
2023
}
2024

2025
static int cat_config(void) {
×
2026
        _cleanup_strv_free_ char **files = NULL;
×
2027
        int r;
×
2028

2029
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, NULL);
×
2030
        if (r < 0)
×
2031
                return r;
2032

2033
        pager_open(arg_pager_flags);
×
2034

2035
        return cat_files(NULL, files, arg_cat_flags);
×
2036
}
2037

2038
static int help(void) {
×
2039
        _cleanup_free_ char *link = NULL;
×
2040
        int r;
×
2041

2042
        r = terminal_urlify_man("systemd-sysusers.service", "8", &link);
×
2043
        if (r < 0)
×
2044
                return log_oom();
×
2045

2046
        printf("%1$s [OPTIONS...] [CONFIGURATION FILE...]\n"
×
2047
               "\n%2$sCreates system user and group accounts.%4$s\n"
2048
               "\n%3$sCommands:%4$s\n"
2049
               "     --cat-config           Show configuration files\n"
2050
               "     --tldr                 Show non-comment parts of configuration\n"
2051
               "  -h --help                 Show this help\n"
2052
               "     --version              Show package version\n"
2053
               "\n%3$sOptions:%4$s\n"
2054
               "     --root=PATH            Operate on an alternate filesystem root\n"
2055
               "     --image=PATH           Operate on disk image as filesystem root\n"
2056
               "     --image-policy=POLICY  Specify disk image dissection policy\n"
2057
               "     --replace=PATH         Treat arguments as replacement for PATH\n"
2058
               "     --dry-run              Just print what would be done\n"
2059
               "     --inline               Treat arguments as configuration lines\n"
2060
               "     --no-pager             Do not pipe output into a pager\n"
2061
               "\nSee the %5$s for details.\n",
2062
               program_invocation_short_name,
2063
               ansi_highlight(),
2064
               ansi_underline(),
2065
               ansi_normal(),
2066
               link);
2067

2068
        return 0;
2069
}
2070

2071
static int parse_argv(int argc, char *argv[]) {
113✔
2072

2073
        enum {
113✔
2074
                ARG_VERSION = 0x100,
2075
                ARG_CAT_CONFIG,
2076
                ARG_TLDR,
2077
                ARG_ROOT,
2078
                ARG_IMAGE,
2079
                ARG_IMAGE_POLICY,
2080
                ARG_REPLACE,
2081
                ARG_DRY_RUN,
2082
                ARG_INLINE,
2083
                ARG_NO_PAGER,
2084
        };
2085

2086
        static const struct option options[] = {
113✔
2087
                { "help",         no_argument,       NULL, 'h'              },
2088
                { "version",      no_argument,       NULL, ARG_VERSION      },
2089
                { "cat-config",   no_argument,       NULL, ARG_CAT_CONFIG   },
2090
                { "tldr",         no_argument,       NULL, ARG_TLDR         },
2091
                { "root",         required_argument, NULL, ARG_ROOT         },
2092
                { "image",        required_argument, NULL, ARG_IMAGE        },
2093
                { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
2094
                { "replace",      required_argument, NULL, ARG_REPLACE      },
2095
                { "dry-run",      no_argument,       NULL, ARG_DRY_RUN      },
2096
                { "inline",       no_argument,       NULL, ARG_INLINE       },
2097
                { "no-pager",     no_argument,       NULL, ARG_NO_PAGER     },
2098
                {}
2099
        };
2100

2101
        int c, r;
113✔
2102

2103
        assert(argc >= 0);
113✔
2104
        assert(argv);
113✔
2105

2106
        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
260✔
2107

2108
                switch (c) {
147✔
2109

2110
                case 'h':
×
2111
                        return help();
×
2112

2113
                case ARG_VERSION:
×
2114
                        return version();
×
2115

2116
                case ARG_CAT_CONFIG:
×
2117
                        arg_cat_flags = CAT_CONFIG_ON;
×
2118
                        break;
×
2119

2120
                case ARG_TLDR:
×
2121
                        arg_cat_flags = CAT_TLDR;
×
2122
                        break;
×
2123

2124
                case ARG_ROOT:
108✔
2125
                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
108✔
2126
                        if (r < 0)
108✔
2127
                                return r;
2128
                        break;
2129

2130
                case ARG_IMAGE:
×
2131
#ifdef STANDALONE
2132
                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
2133
                                               "This systemd-sysusers version is compiled without support for --image=.");
2134
#else
2135
                        r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
×
2136
                        if (r < 0)
×
2137
                                return r;
2138
                        break;
2139
#endif
2140

2141
                case ARG_IMAGE_POLICY:
×
2142
                        r = parse_image_policy_argument(optarg, &arg_image_policy);
×
2143
                        if (r < 0)
×
2144
                                return r;
2145
                        break;
2146

2147
                case ARG_REPLACE:
35✔
2148
                        if (!path_is_absolute(optarg))
35✔
2149
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2150
                                                       "The argument to --replace= must be an absolute path.");
2151
                        if (!endswith(optarg, ".conf"))
35✔
2152
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2153
                                                       "The argument to --replace= must have the extension '.conf'.");
2154

2155
                        arg_replace = optarg;
35✔
2156
                        break;
35✔
2157

2158
                case ARG_DRY_RUN:
×
2159
                        arg_dry_run = true;
×
2160
                        break;
×
2161

2162
                case ARG_INLINE:
4✔
2163
                        arg_inline = true;
4✔
2164
                        break;
4✔
2165

2166
                case ARG_NO_PAGER:
×
2167
                        arg_pager_flags |= PAGER_DISABLE;
×
2168
                        break;
×
2169

2170
                case '?':
2171
                        return -EINVAL;
2172

2173
                default:
×
2174
                        assert_not_reached();
×
2175
                }
2176

2177
        if (arg_replace && arg_cat_flags != CAT_CONFIG_OFF)
113✔
2178
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2179
                                       "Option --replace= is not supported with --cat-config/--tldr.");
2180

2181
        if (arg_replace && optind >= argc)
113✔
2182
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2183
                                       "When --replace= is given, some configuration items must be specified.");
2184

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

2189
        return 1;
2190
}
2191

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

2196
        assert(c);
41✔
2197

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

2207
                pos++;
44✔
2208
        }
2209

2210
        return 0;
2211
}
2212

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

2218
        assert(c);
90✔
2219

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

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

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

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

2238
        return 0;
2239
}
2240

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

2246
        assert(c);
113✔
2247

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

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

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

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

2273
        Item *i;
113✔
2274
        int r;
113✔
2275

2276
        r = parse_argv(argc, argv);
113✔
2277
        if (r <= 0)
113✔
2278
                return r;
2279

2280
        log_setup();
113✔
2281

2282
        if (arg_cat_flags != CAT_CONFIG_OFF)
113✔
2283
                return cat_config();
×
2284

2285
        if (should_bypass("SYSTEMD_SYSUSERS"))
113✔
2286
                return 0;
2287

2288
        umask(0022);
113✔
2289

2290
        r = mac_init();
113✔
2291
        if (r < 0)
113✔
2292
                return r;
2293

2294
#ifndef STANDALONE
2295
        if (arg_image) {
113✔
2296
                assert(!arg_root);
×
2297

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

2314
                arg_root = strdup(mounted_dir);
×
2315
                if (!arg_root)
×
2316
                        return log_oom();
×
2317
        }
2318
#else
2319
        assert(!arg_image);
2320
#endif
2321

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

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

2337
        r = read_credential_lines(&c);
113✔
2338
        if (r < 0)
113✔
2339
                return r;
2340

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

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

2356
                c.login_defs_need_warning = true;
113✔
2357

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

2371
        r = add_implicit(&c);
113✔
2372
        if (r < 0)
113✔
2373
                return r;
2374

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

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

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

2389
        ORDERED_HASHMAP_FOREACH(i, c.groups)
489✔
2390
                (void) process_item(&c, i);
376✔
2391

2392
        ORDERED_HASHMAP_FOREACH(i, c.users)
412✔
2393
                (void) process_item(&c, i);
299✔
2394

2395
        return write_files(&c);
113✔
2396
}
2397

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

© 2026 Coveralls, Inc