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

systemd / systemd / 24373971178

14 Apr 2026 12:08AM UTC coverage: 72.291% (+0.2%) from 72.107%
24373971178

push

github

web-flow
hwdb: Add extended SteelSeries Arctis headset device support (#41628)

Add USB device IDs for additional SteelSeries Arctis headset models to
the sound card hardware database.

Newly added device IDs:

- Arctis Nova 7x v2 (22AD)
- Arctis Nova 7 Diablo IV (22A9)
- Arctis Nova 7X (22A4)
- Arctis Nova 7X (22A5)
- Arctis Nova 7P V2 (22A7)

321138 of 444232 relevant lines covered (72.29%)

1277257.29 hits per line

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

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

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

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

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

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

71
typedef struct Item {
72
        ItemType type;
73

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

82
        gid_t gid;
83
        uid_t uid;
84

85
        char *filename;
86
        unsigned line;
87

88
        bool gid_set;
89

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

95
        bool uid_set;
96

97
        bool locked;
98

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

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

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

116
typedef struct Context {
117
        int audit_fd;
118

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

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

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

129
        uid_t search_uid;
130
        UIDRange *uid_range;
131

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

136
static void context_done(Context *c) {
115✔
137
        assert(c);
115✔
138

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

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

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

152
        set_free(c->names);
115✔
153
        uid_range_free(c->uid_range);
115✔
154
}
115✔
155

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

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

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

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

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

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

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

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

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

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

226
        assert(c);
115✔
227

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

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

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

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

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

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

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

269
        assert(c);
115✔
270

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

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

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

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

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

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

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

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

321
                return -errno;
×
322
        }
323

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

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

335
        r = copy_bytes(src, fileno(dst), UINT64_MAX, COPY_REFLINK);
42✔
336
        if (r < 0)
42✔
337
                return r;
338

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

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

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

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

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

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

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

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

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

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

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

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

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

390
                        added = true;
391
                }
392

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

396
                        strv_sort_uniq(l);
5✔
397

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

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

406
        return putgrent_sane(gr, group);
877✔
407
}
408

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

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

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

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

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

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

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

438
                        added = true;
439
                }
440

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

444
                        strv_sort_uniq(l);
5✔
445

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

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

454
        return putsgent_sane(sg, gshadow);
867✔
455
}
456
#endif
457

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

826
                group_changed = true;
519✔
827
        }
828

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

932
                group_changed = true;
519✔
933
        }
934

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

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

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

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

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

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

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

973
        assert(c);
115✔
974

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

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

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

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

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

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

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

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

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

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

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

1052
        return 0;
1053
}
1054

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

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

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

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

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

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

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

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

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

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

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

1110
        return 1;
1111
}
1112

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

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

1123
        assert(i);
74✔
1124

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

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

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

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

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

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

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

1162
                *ret_uid = uid;
×
1163
        }
1164

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

1169
                *ret_gid = gid;
×
1170
        }
1171

1172
        return 1;
1173
}
1174

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1288
        return 0;
1289
}
1290

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

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

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

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

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

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

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

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

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

1339
        return 1;
1340
}
1341

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

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

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

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

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

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

1373
        return -ENOENT;
1374
}
1375

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1478
        return 0;
1479
}
1480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1575
        assert(c);
115✔
1576

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

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

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

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

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

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

1614
        return 0;
115✔
1615
}
1616

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1720
        return true;
1721
}
1722

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1827
                path_simplify(resolved_home);
141✔
1828

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

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

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

1844
                path_simplify(resolved_shell);
21✔
1845

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

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

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

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

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

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

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

1877
                return 0;
1878

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

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

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

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

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

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

1907
                return 0;
1908
        }
1909

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2023
                return 0;
2✔
2024
        }
2025

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

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

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

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

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

2053
        pager_open(arg_pager_flags);
×
2054

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

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

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

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

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

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

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

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

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

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

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

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

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

2104
        OptionParser state = { argc, argv };
115✔
2105
        const char *arg;
115✔
2106

2107
        FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c)
378✔
2108
                switch (c) {
148✔
2109

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

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

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

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

2124
                OPTION_GROUP("Options"):
2125
                        break;
2126

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

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

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

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

2153
                        arg_replace = arg;
35✔
2154
                        break;
35✔
2155

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

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

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

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

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

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

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

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

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

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

2196
        assert(c);
43✔
2197

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

2207
                pos++;
47✔
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)
204✔
2225
                if (p && path_equal(*f, p)) {
114✔
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));
120✔
2233

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

2238
        return 0;
2239
}
2240

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

2246
        assert(c);
115✔
2247

2248
        r = get_credentials_dir(&d);
115✔
2249
        if (r == -ENXIO)
115✔
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[]) {
115✔
2263
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
115✔
2264
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
×
2265
        _cleanup_close_ int lock = -EBADF;
115✔
2266
        _cleanup_(context_done) Context c = {
115✔
2267
                .audit_fd = -EBADF,
2268
                .search_uid = UID_INVALID,
2269
        };
2270

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

2274
        char **args = NULL;
115✔
2275
        r = parse_argv(argc, argv, &args);
115✔
2276
        if (r <= 0)
115✔
2277
                return r;
2278

2279
        log_setup();
115✔
2280

2281
        if (arg_cat_flags != CAT_CONFIG_OFF)
115✔
2282
                return cat_config();
×
2283

2284
        if (should_bypass("SYSTEMD_SYSUSERS"))
115✔
2285
                return 0;
2286

2287
        umask(0022);
115✔
2288

2289
        r = mac_init();
115✔
2290
        if (r < 0)
115✔
2291
                return r;
2292

2293
        if (arg_image) {
115✔
2294
                assert(!arg_root);
×
2295

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

2312
                arg_root = strdup(mounted_dir);
×
2313
                if (!arg_root)
×
2314
                        return log_oom();
×
2315
        }
2316

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

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

2332
        r = read_credential_lines(&c);
115✔
2333
        if (r < 0)
115✔
2334
                return r;
2335

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

2344
        if (!c.uid_range) {
115✔
2345
                /* Default to default range of SYSTEMD_UID_MIN..SYSTEM_UID_MAX. */
2346
                r = read_login_defs(&c.login_defs, NULL, arg_root);
115✔
2347
                if (r < 0)
115✔
2348
                        return log_error_errno(r, "Failed to read %s%s: %m",
×
2349
                                               strempty(arg_root), "/etc/login.defs");
2350

2351
                c.login_defs_need_warning = true;
115✔
2352

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

2366
        r = add_implicit(&c);
115✔
2367
        if (r < 0)
115✔
2368
                return r;
2369

2370
        if (!arg_dry_run) {
115✔
2371
                lock = take_etc_passwd_lock(arg_root);
115✔
2372
                if (lock < 0)
115✔
2373
                        return log_error_errno(lock, "Failed to take /etc/passwd lock: %m");
×
2374
        }
2375

2376
        r = load_user_database(&c);
115✔
2377
        if (r < 0)
115✔
2378
                return log_error_errno(r, "Failed to load user database: %m");
×
2379

2380
        r = load_group_database(&c);
115✔
2381
        if (r < 0)
115✔
2382
                return log_error_errno(r, "Failed to read group database: %m");
×
2383

2384
        ORDERED_HASHMAP_FOREACH(i, c.groups)
494✔
2385
                (void) process_item(&c, i);
379✔
2386

2387
        ORDERED_HASHMAP_FOREACH(i, c.users)
416✔
2388
                (void) process_item(&c, i);
301✔
2389

2390
        return write_files(&c);
115✔
2391
}
2392

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