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

systemd / systemd / 24165447443

08 Apr 2026 10:44PM UTC coverage: 72.303% (+0.1%) from 72.175%
24165447443

push

github

bluca
compress: write sparse files when decompressing to regular files

Core dumps are often very sparse, containing large zero-filled regions
whose actual disk usage can be significantly reduced by preserving
holes. Previously, decompress_stream() always wrote dense output,
expanding all zero regions into allocated disk blocks.

Each decompression backend (xz, lz4, zstd) now auto-detects whether the
output fd is suitable for sparse writes via a shared should_sparse()
helper. The check requires both S_ISREG (regular file) and !O_APPEND,
since O_APPEND causes write() to ignore the file position set by
lseek(), which would collapse the holes and corrupt the output. For
pipes, sockets, and append-mode files, dense writes are preserved via
loop_write_full() with USEC_INFINITY timeout, matching the original
behavior. After sparse decompression, finalize_sparse() sets the final
file size to account for any trailing holes.

This is transparent to callers — all public signatures are unchanged.
coredumpctl benefits automatically:
- coredumpctl debug: temp file in /var/tmp is now sparse
- coredumpctl dump -o file: output file is now sparse
- coredumpctl dump > file: redirected stdout is now sparse
- coredumpctl dump | ...: pipe output unchanged (dense)
- coredumpctl dump >> file: append mode, falls back to dense

Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-developed-by: Codex (GPT-5) <noreply@openai.com>

123 of 132 new or added lines in 2 files covered. (93.18%)

5704 existing lines in 82 files now uncovered.

319660 of 442111 relevant lines covered (72.3%)

1196031.58 hits per line

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

71.94
/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) {
×
UNCOV
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:
×
UNCOV
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✔
UNCOV
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✔
UNCOV
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;
×
UNCOV
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)
×
UNCOV
206
                sym_audit_log_acct_message(
×
207
                                c->audit_fd,
208
                                type,
209
                                program_invocation_short_name,
210
                                op,
UNCOV
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✔
UNCOV
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✔
UNCOV
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✔
UNCOV
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

UNCOV
321
                return -errno;
×
322
        }
323

324
        if (fstat(src, &st) < 0)
42✔
UNCOV
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✔
UNCOV
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✔
UNCOV
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✔
UNCOV
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

484
        if (ordered_hashmap_isempty(c->todo_uids))
115✔
485
                goto done;
27✔
486

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

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

496
        original = fopen(passwd_path, "re");
88✔
497
        if (original) {
88✔
498

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

613
        assert(c);
115✔
614

615
        if (ordered_hashmap_isempty(c->todo_uids))
115✔
616
                goto done;
27✔
617

618
        if (arg_dry_run) {
88✔
619
                log_info("Would write /etc/shadow%s", glyph(GLYPH_ELLIPSIS));
×
UNCOV
620
                goto done;
×
621
        }
622

623
        r = fopen_temporary_label("/etc/shadow", shadow_path, &shadow, &shadow_tmp);
88✔
624
        if (r < 0)
88✔
UNCOV
625
                return log_debug_errno(r, "Failed to open temporary copy of %s: %m", shadow_path);
×
626

627
        lstchg = (long) (source_date_epoch_or_now() / USEC_PER_DAY);
88✔
628

629
        original = fopen(shadow_path, "re");
88✔
630
        if (original) {
88✔
631

632
                r = copy_rights_with_fallback(fileno(original), fileno(shadow), shadow_tmp);
5✔
633
                if (r < 0)
5✔
UNCOV
634
                        return log_debug_errno(r, "Failed to copy permissions from %s to %s: %m",
×
635
                                               shadow_path, shadow_tmp);
636

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

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

649
                        /* Make sure we keep the NIS entries (if any) at the end. */
650
                        if (IN_SET(sp->sp_namp[0], '+', '-'))
130✔
651
                                break;
652

653
                        r = putspent_sane(sp, shadow);
130✔
654
                        if (r < 0)
130✔
UNCOV
655
                                return log_debug_errno(r, "Failed to add existing user \"%s\" to temporary shadow file: %m",
×
656
                                                       sp->sp_namp);
657

658
                }
659
                if (r < 0)
5✔
UNCOV
660
                        return log_debug_errno(r, "Failed to read %s: %m", shadow_path);
×
661

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

669
        ORDERED_HASHMAP_FOREACH(i, c->todo_uids) {
357✔
670
                _cleanup_(erase_and_freep) char *creds_password = NULL;
269✔
671
                bool is_hashed;
269✔
672

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

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

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

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

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

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

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

725
        r = fflush_sync_and_check(shadow);
88✔
726
        if (r < 0)
88✔
UNCOV
727
                return log_debug_errno(r, "Failed to flush %s: %m", shadow_tmp);
×
728

729
done:
88✔
730
        *ret_tmpfile = TAKE_PTR(shadow);
115✔
731
        *ret_tmpfile_path = TAKE_PTR(shadow_tmp);
115✔
732
        return 0;
115✔
733
}
734

735
static int write_temporary_group(
115✔
736
                Context *c,
737
                const char *group_path,
738
                FILE **ret_tmpfile,
739
                char **ret_tmpfile_path) {
740

741
        _cleanup_fclose_ FILE *original = NULL, *group = NULL;
115✔
742
        _cleanup_(unlink_and_freep) char *group_tmp = NULL;
115✔
743
        bool group_changed = false;
115✔
744
        struct group *gr = NULL;
115✔
745
        Item *i;
115✔
746
        int r;
115✔
747

748
        assert(c);
115✔
749

750
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
115✔
751
                goto done;
26✔
752

753
        if (arg_dry_run) {
89✔
754
                log_info("Would write /etc/group%s", glyph(GLYPH_ELLIPSIS));
×
UNCOV
755
                goto done;
×
756
        }
757

758
        r = fopen_temporary_label("/etc/group", group_path, &group, &group_tmp);
89✔
759
        if (r < 0)
89✔
UNCOV
760
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", group_path);
×
761

762
        original = fopen(group_path, "re");
89✔
763
        if (original) {
89✔
764

765
                r = copy_rights_with_fallback(fileno(original), fileno(group), group_tmp);
16✔
766
                if (r < 0)
16✔
UNCOV
767
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
768
                                               group_path, group_tmp);
769

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

776
                        i = ordered_hashmap_get(c->groups, gr->gr_name);
368✔
777
                        if (i && i->todo_group)
368✔
UNCOV
778
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
779
                                                       "%s: Group \"%s\" already exists.",
780
                                                       group_path, gr->gr_name);
781

782
                        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gr->gr_gid)))
368✔
UNCOV
783
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
784
                                                       "%s: Detected collision for GID " GID_FMT ".",
785
                                                       group_path, gr->gr_gid);
786

787
                        /* Make sure we keep the NIS entries (if any) at the end. */
788
                        if (IN_SET(gr->gr_name[0], '+', '-'))
368✔
789
                                break;
790

791
                        r = putgrent_with_members(c, gr, group);
363✔
792
                        if (r < 0)
363✔
UNCOV
793
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
794
                                                       gr->gr_name);
795
                        if (r > 0)
363✔
UNCOV
796
                                group_changed = true;
×
797
                }
798
                if (r < 0)
16✔
UNCOV
799
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
800

801
        } else {
802
                if (errno != ENOENT)
73✔
UNCOV
803
                        return log_error_errno(errno, "Failed to open %s: %m", group_path);
×
804
                if (fchmod(fileno(group), 0644) < 0)
73✔
UNCOV
805
                        return log_error_errno(errno, "Failed to fchmod %s: %m", group_tmp);
×
806
        }
807

808
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
608✔
809
                struct group n = {
519✔
810
                        .gr_name = i->name,
519✔
811
                        .gr_gid = i->gid,
519✔
812
                        .gr_passwd = (char*) PASSWORD_SEE_SHADOW,
813
                };
814

815
                r = putgrent_with_members(c, &n, group);
519✔
816
                if (r < 0)
519✔
UNCOV
817
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary group file: %m",
×
818
                                               gr->gr_name);
819

820
                group_changed = true;
519✔
821
        }
822

823
        /* Append the remaining NIS entries if any */
824
        while (gr) {
99✔
825
                r = putgrent_sane(gr, group);
15✔
826
                if (r < 0)
15✔
UNCOV
827
                        return log_error_errno(r, "Failed to add existing group \"%s\" to temporary group file: %m",
×
828
                                               gr->gr_name);
829

830
                r = fgetgrent_sane(original, &gr);
15✔
831
                if (r < 0)
15✔
UNCOV
832
                        return log_error_errno(r, "Failed to read %s: %m", group_path);
×
833
                if (r == 0)
15✔
834
                        break;
835
        }
836

837
        r = fflush_sync_and_check(group);
89✔
838
        if (r < 0)
89✔
UNCOV
839
                return log_error_errno(r, "Failed to flush %s: %m", group_tmp);
×
840

841
done:
89✔
842
        if (group_changed) {
115✔
843
                *ret_tmpfile = TAKE_PTR(group);
89✔
844
                *ret_tmpfile_path = TAKE_PTR(group_tmp);
89✔
845
        } else {
846
                *ret_tmpfile = NULL;
26✔
847
                *ret_tmpfile_path = NULL;
26✔
848
        }
849
        return 0;
850
}
851

852
static int write_temporary_gshadow(
115✔
853
                Context *c,
854
                const char * gshadow_path,
855
                FILE **ret_tmpfile,
856
                char **ret_tmpfile_path) {
857

858
#if ENABLE_GSHADOW
859
        _cleanup_fclose_ FILE *original = NULL, *gshadow = NULL;
115✔
860
        _cleanup_(unlink_and_freep) char *gshadow_tmp = NULL;
115✔
861
        bool group_changed = false;
115✔
862
        Item *i;
115✔
863
        int r;
115✔
864

865
        assert(c);
115✔
866

867
        if (ordered_hashmap_isempty(c->todo_gids) && ordered_hashmap_isempty(c->members))
115✔
868
                goto done;
26✔
869

870
        if (arg_dry_run) {
89✔
871
                log_info("Would write /etc/gshadow%s", glyph(GLYPH_ELLIPSIS));
×
UNCOV
872
                goto done;
×
873
        }
874

875
        r = fopen_temporary_label("/etc/gshadow", gshadow_path, &gshadow, &gshadow_tmp);
89✔
876
        if (r < 0)
89✔
UNCOV
877
                return log_error_errno(r, "Failed to open temporary copy of %s: %m", gshadow_path);
×
878

879
        original = fopen(gshadow_path, "re");
89✔
880
        if (original) {
89✔
881
                struct sgrp *sg;
6✔
882

883
                r = copy_rights_with_fallback(fileno(original), fileno(gshadow), gshadow_tmp);
6✔
884
                if (r < 0)
6✔
UNCOV
885
                        return log_error_errno(r, "Failed to copy permissions from %s to %s: %m",
×
886
                                               gshadow_path, gshadow_tmp);
887

888
                while ((r = fgetsgent_sane(original, &sg)) > 0) {
359✔
889

890
                        i = ordered_hashmap_get(c->groups, sg->sg_namp);
353✔
891
                        if (i && i->todo_group)
353✔
UNCOV
892
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
893
                                                       "%s: Group \"%s\" already exists.",
894
                                                       gshadow_path, sg->sg_namp);
895

896
                        r = putsgent_with_members(c, sg, gshadow);
353✔
897
                        if (r < 0)
353✔
UNCOV
898
                                return log_error_errno(r, "Failed to add existing group \"%s\" to temporary gshadow file: %m",
×
899
                                                       sg->sg_namp);
900
                        if (r > 0)
353✔
UNCOV
901
                                group_changed = true;
×
902
                }
903
                if (r < 0)
6✔
904
                        return r;
905

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

913
        ORDERED_HASHMAP_FOREACH(i, c->todo_gids) {
608✔
914
                struct sgrp n = {
519✔
915
                        .sg_namp = i->name,
519✔
916
                        .sg_passwd = (char*) PASSWORD_LOCKED_AND_INVALID,
917
                };
918

919
                r = putsgent_with_members(c, &n, gshadow);
519✔
920
                if (r < 0)
519✔
UNCOV
921
                        return log_error_errno(r, "Failed to add new group \"%s\" to temporary gshadow file: %m",
×
922
                                               n.sg_namp);
923

924
                group_changed = true;
519✔
925
        }
926

927
        r = fflush_sync_and_check(gshadow);
89✔
928
        if (r < 0)
89✔
UNCOV
929
                return log_error_errno(r, "Failed to flush %s: %m", gshadow_tmp);
×
930

931
done:
89✔
932
        if (group_changed) {
115✔
933
                *ret_tmpfile = TAKE_PTR(gshadow);
89✔
934
                *ret_tmpfile_path = TAKE_PTR(gshadow_tmp);
89✔
935
        } else
936
#endif
937
        {
938
                *ret_tmpfile = NULL;
26✔
939
                *ret_tmpfile_path = NULL;
26✔
940
        }
941
        return 0;
942
}
943

944
static int write_files(Context *c) {
115✔
945
        _cleanup_fclose_ FILE *passwd = NULL, *group = NULL, *shadow = NULL, *gshadow = NULL;
345✔
946
        _cleanup_(unlink_and_freep) char *passwd_tmp = NULL, *group_tmp = NULL, *shadow_tmp = NULL, *gshadow_tmp = NULL;
115✔
947
        int r;
115✔
948

949
        _cleanup_free_ char *passwd_path = path_join(arg_root, "/etc/passwd");
230✔
950
        if (!passwd_path)
115✔
UNCOV
951
                return log_oom();
×
952

953
        _cleanup_free_ char *shadow_path = path_join(arg_root, "/etc/shadow");
230✔
954
        if (!shadow_path)
115✔
UNCOV
955
                return log_oom();
×
956

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

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

965
        assert(c);
115✔
966

967
        r = write_temporary_group(c, group_path, &group, &group_tmp);
115✔
968
        if (r < 0)
115✔
969
                return r;
970

971
        r = write_temporary_gshadow(c, gshadow_path, &gshadow, &gshadow_tmp);
115✔
972
        if (r < 0)
115✔
973
                return r;
974

975
        r = write_temporary_passwd(c, passwd_path, &passwd, &passwd_tmp);
115✔
976
        if (r < 0)
115✔
977
                return r;
978

979
        r = write_temporary_shadow(c, shadow_path, &shadow, &shadow_tmp);
115✔
980
        if (r < 0)
115✔
981
                return r;
982

983
        /* Make a backup of the old files */
984
        if (group) {
115✔
985
                r = make_backup("/etc/group", group_path);
89✔
986
                if (r < 0)
89✔
UNCOV
987
                        return log_error_errno(r, "Failed to backup %s: %m", group_path);
×
988
        }
989
        if (gshadow) {
115✔
990
                r = make_backup("/etc/gshadow", gshadow_path);
89✔
991
                if (r < 0)
89✔
UNCOV
992
                        return log_error_errno(r, "Failed to backup %s: %m", gshadow_path);
×
993
        }
994

995
        if (passwd) {
115✔
996
                r = make_backup("/etc/passwd", passwd_path);
88✔
997
                if (r < 0)
88✔
UNCOV
998
                        return log_error_errno(r, "Failed to backup %s: %m", passwd_path);
×
999
        }
1000
        if (shadow) {
115✔
1001
                r = make_backup("/etc/shadow", shadow_path);
88✔
1002
                if (r < 0)
88✔
UNCOV
1003
                        return log_error_errno(r, "Failed to backup %s: %m", shadow_path);
×
1004
        }
1005

1006
        /* And make the new files count */
1007
        if (group) {
115✔
1008
                r = rename_and_apply_smack_floor_label(group_tmp, group_path);
89✔
1009
                if (r < 0)
89✔
UNCOV
1010
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1011
                                               group_tmp, group_path);
1012
                group_tmp = mfree(group_tmp);
89✔
1013
        }
1014
        /* OK, we have written the group entries successfully */
1015
        log_audit_accounts(c, ADD_GROUP);
115✔
1016
        if (gshadow) {
115✔
1017
                r = rename_and_apply_smack_floor_label(gshadow_tmp, gshadow_path);
89✔
1018
                if (r < 0)
89✔
UNCOV
1019
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1020
                                               gshadow_tmp, gshadow_path);
1021

1022
                gshadow_tmp = mfree(gshadow_tmp);
89✔
1023
        }
1024

1025
        if (passwd) {
115✔
1026
                r = rename_and_apply_smack_floor_label(passwd_tmp, passwd_path);
88✔
1027
                if (r < 0)
88✔
UNCOV
1028
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1029
                                               passwd_tmp, passwd_path);
1030

1031
                passwd_tmp = mfree(passwd_tmp);
88✔
1032
        }
1033
        /* OK, we have written the user entries successfully */
1034
        log_audit_accounts(c, ADD_USER);
115✔
1035
        if (shadow) {
115✔
1036
                r = rename_and_apply_smack_floor_label(shadow_tmp, shadow_path);
88✔
1037
                if (r < 0)
88✔
UNCOV
1038
                        return log_error_errno(r, "Failed to rename %s to %s: %m",
×
1039
                                               shadow_tmp, shadow_path);
1040

1041
                shadow_tmp = mfree(shadow_tmp);
88✔
1042
        }
1043

1044
        return 0;
1045
}
1046

1047
static int uid_is_ok(
289✔
1048
                Context *c,
1049
                uid_t uid,
1050
                const char *name,
1051
                bool check_with_gid) {
1052

1053
        int r;
289✔
1054
        assert(c);
289✔
1055

1056
        /* Let's see if we already have assigned the UID a second time */
1057
        if (ordered_hashmap_contains(c->todo_uids, UID_TO_PTR(uid)))
289✔
1058
                return 0;
1059

1060
        /* Try to avoid using uids that are already used by a group
1061
         * that doesn't have the same name as our new user. */
1062
        if (check_with_gid) {
284✔
1063
                Item *i;
204✔
1064

1065
                i = ordered_hashmap_get(c->todo_gids, GID_TO_PTR(uid));
204✔
1066
                if (i && !streq(i->name, name))
204✔
1067
                        return 0;
1068
        }
1069

1070
        /* Let's check the files directly */
1071
        if (hashmap_contains(c->database_by_uid, UID_TO_PTR(uid)))
274✔
1072
                return 0;
1073

1074
        if (check_with_gid) {
274✔
1075
                const char *n;
194✔
1076

1077
                n = hashmap_get(c->database_by_gid, GID_TO_PTR(uid));
194✔
1078
                if (n && !streq(n, name))
194✔
1079
                        return 0;
1080
        }
1081

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

1086
                r = getpwuid_malloc(uid, /* ret= */ NULL);
6✔
1087
                if (r >= 0)
6✔
1088
                        return 0;
1089
                if (r != -ESRCH)
6✔
UNCOV
1090
                        log_warning_errno(r, "Unexpected failure while looking up UID '" UID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
×
1091

1092
                if (check_with_gid) {
6✔
1093
                        r = getgrgid_malloc((gid_t) uid, &g);
6✔
1094
                        if (r >= 0) {
6✔
UNCOV
1095
                                if (!streq(g->gr_name, name))
×
1096
                                        return 0;
1097
                        } else if (r != -ESRCH)
6✔
1098
                                log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", uid);
6✔
1099
                }
1100
        }
1101

1102
        return 1;
1103
}
1104

1105
static int root_stat(const char *p, struct stat *ret_st) {
×
UNCOV
1106
        return chase_and_stat(p, arg_root, CHASE_PREFIX_ROOT, /* ret_path= */ NULL, ret_st);
×
1107
}
1108

1109
static int read_id_from_file(Item *i, uid_t *ret_uid, gid_t *ret_gid) {
74✔
1110
        struct stat st;
74✔
1111
        bool found_uid = false, found_gid = false;
74✔
1112
        uid_t uid = 0;
74✔
1113
        gid_t gid = 0;
74✔
1114

1115
        assert(i);
74✔
1116

1117
        /* First, try to get the GID directly */
1118
        if (ret_gid && i->gid_path && root_stat(i->gid_path, &st) >= 0) {
74✔
1119
                gid = st.st_gid;
×
UNCOV
1120
                found_gid = true;
×
1121
        }
1122

1123
        /* Then, try to get the UID directly */
1124
        if ((ret_uid || (ret_gid && !found_gid))
74✔
1125
            && i->uid_path
74✔
UNCOV
1126
            && root_stat(i->uid_path, &st) >= 0) {
×
1127

1128
                uid = st.st_uid;
×
UNCOV
1129
                found_uid = true;
×
1130

1131
                /* If we need the gid, but had no success yet, also derive it from the UID path */
1132
                if (ret_gid && !found_gid) {
×
1133
                        gid = st.st_gid;
×
UNCOV
1134
                        found_gid = true;
×
1135
                }
1136
        }
1137

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

UNCOV
1141
                if (found_gid) {
×
1142
                        uid = (uid_t) gid;
1143
                        found_uid = true;
1144
                } else if (root_stat(i->gid_path, &st) >= 0) {
×
1145
                        uid = (uid_t) st.st_gid;
×
UNCOV
1146
                        found_uid = true;
×
1147
                }
1148
        }
1149

1150
        if (ret_uid) {
74✔
1151
                if (!found_uid)
41✔
1152
                        return 0;
74✔
1153

UNCOV
1154
                *ret_uid = uid;
×
1155
        }
1156

1157
        if (ret_gid) {
33✔
1158
                if (!found_gid)
33✔
1159
                        return 0;
1160

UNCOV
1161
                *ret_gid = gid;
×
1162
        }
1163

1164
        return 1;
1165
}
1166

1167
static int add_user(Context *c, Item *i) {
299✔
1168
        void *z;
299✔
1169
        int r;
299✔
1170

1171
        assert(c);
299✔
1172
        assert(i);
299✔
1173

1174
        /* Check the database directly */
1175
        z = hashmap_get(c->database_by_username, i->name);
299✔
1176
        if (z) {
299✔
1177
                log_debug("User %s already exists.", i->name);
30✔
1178
                i->uid = PTR_TO_UID(z);
30✔
1179
                i->uid_set = true;
30✔
1180
                return 0;
30✔
1181
        }
1182

1183
        if (!arg_root) {
269✔
1184
                _cleanup_free_ struct passwd *p = NULL;
6✔
1185

1186
                /* Also check NSS */
1187
                r = getpwnam_malloc(i->name, &p);
6✔
1188
                if (r >= 0) {
6✔
1189
                        log_debug("User %s already exists.", i->name);
×
1190
                        i->uid = p->pw_uid;
×
UNCOV
1191
                        i->uid_set = true;
×
1192

1193
                        r = free_and_strdup(&i->description, p->pw_gecos);
×
1194
                        if (r < 0)
×
UNCOV
1195
                                return log_oom();
×
1196

1197
                        return 0;
1198
                }
1199
                if (r != -ESRCH)
6✔
1200
                        log_warning_errno(r, "Unexpected failure while looking up user '%s' via NSS, assuming it doesn't exist: %m", i->name);
6✔
1201
        }
1202

1203
        /* Try to use the suggested numeric UID */
1204
        if (i->uid_set) {
269✔
1205
                r = uid_is_ok(c, i->uid, i->name, !i->id_set_strict);
233✔
1206
                if (r < 0)
233✔
UNCOV
1207
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1208
                if (r == 0) {
233✔
1209
                        log_info("Suggested user ID " UID_FMT " for %s already used.", i->uid, i->name);
5✔
1210
                        i->uid_set = false;
5✔
1211
                }
1212
        }
1213

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

1218
                if (read_id_from_file(i, &candidate, NULL) > 0) {
41✔
1219

1220
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
UNCOV
1221
                                log_debug("User ID " UID_FMT " of file not suitable for %s.", candidate, i->name);
×
1222
                        else {
1223
                                r = uid_is_ok(c, candidate, i->name, true);
×
1224
                                if (r < 0)
×
1225
                                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1226
                                else if (r > 0) {
×
1227
                                        i->uid = candidate;
×
UNCOV
1228
                                        i->uid_set = true;
×
1229
                                } else
UNCOV
1230
                                        log_debug("User ID " UID_FMT " of file for %s is already used.", candidate, i->name);
×
1231
                        }
1232
                }
1233
        }
1234

1235
        /* Otherwise, try to reuse the group ID */
1236
        if (!i->uid_set && i->gid_set) {
269✔
1237
                r = uid_is_ok(c, (uid_t) i->gid, i->name, true);
41✔
1238
                if (r < 0)
41✔
UNCOV
1239
                        return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1240
                if (r > 0) {
41✔
1241
                        i->uid = (uid_t) i->gid;
26✔
1242
                        i->uid_set = true;
26✔
1243
                }
1244
        }
1245

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

1250
                for (;;) {
15✔
1251
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
15✔
1252
                        if (r < 0)
15✔
UNCOV
1253
                                return log_error_errno(r, "No free user ID available for %s.", i->name);
×
1254

1255
                        r = uid_is_ok(c, c->search_uid, i->name, true);
15✔
1256
                        if (r < 0)
15✔
UNCOV
1257
                                return log_error_errno(r, "Failed to verify UID " UID_FMT ": %m", i->uid);
×
1258
                        else if (r > 0)
15✔
1259
                                break;
1260
                }
1261

1262
                i->uid_set = true;
15✔
1263
                i->uid = c->search_uid;
15✔
1264
        }
1265

1266
        r = ordered_hashmap_ensure_put(&c->todo_uids, NULL, UID_TO_PTR(i->uid), i);
269✔
1267
        if (r == -EEXIST)
269✔
UNCOV
1268
                return log_error_errno(r, "Requested user %s with UID " UID_FMT " and gid" GID_FMT " to be created is duplicated "
×
1269
                                       "or conflicts with another user.", i->name, i->uid, i->gid);
1270
        if (r == -ENOMEM)
269✔
UNCOV
1271
                return log_oom();
×
1272
        if (r < 0)
269✔
UNCOV
1273
                return log_error_errno(r, "Failed to store user %s with UID " UID_FMT " and GID " GID_FMT " to be created: %m",
×
1274
                                       i->name, i->uid, i->gid);
1275

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

1280
        return 0;
1281
}
1282

1283
static int gid_is_ok(
805✔
1284
                Context *c,
1285
                gid_t gid,
1286
                const char *groupname,
1287
                bool check_with_uid) {
1288

1289
        Item *user;
805✔
1290
        char *username;
805✔
1291
        int r;
805✔
1292

1293
        assert(c);
805✔
1294
        assert(groupname);
805✔
1295

1296
        if (ordered_hashmap_contains(c->todo_gids, GID_TO_PTR(gid)))
805✔
1297
                return 0;
1298

1299
        /* Avoid reusing gids that are already used by a different user */
1300
        if (check_with_uid) {
740✔
1301
                user = ordered_hashmap_get(c->todo_uids, UID_TO_PTR(gid));
406✔
1302
                if (user && !streq(user->name, groupname))
406✔
1303
                        return 0;
1304
        }
1305

1306
        if (hashmap_contains(c->database_by_gid, GID_TO_PTR(gid)))
740✔
1307
                return 0;
1308

1309
        if (check_with_uid) {
520✔
1310
                username = hashmap_get(c->database_by_uid, UID_TO_PTR(gid));
186✔
1311
                if (username && !streq(username, groupname))
186✔
1312
                        return 0;
1313
        }
1314

1315
        if (!arg_root) {
520✔
1316
                r = getgrgid_malloc(gid, /* ret= */ NULL);
8✔
1317
                if (r >= 0)
8✔
1318
                        return 0;
1319
                if (r != -ESRCH)
8✔
UNCOV
1320
                        log_warning_errno(r, "Unexpected failure while looking up GID '" GID_FMT "' via NSS, assuming it doesn't exist: %m", gid);
×
1321

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

1331
        return 1;
1332
}
1333

1334
static int get_gid_by_name(
650✔
1335
                Context *c,
1336
                const char *name,
1337
                gid_t *ret_gid) {
1338

1339
        void *z;
650✔
1340
        int r;
650✔
1341

1342
        assert(c);
650✔
1343
        assert(ret_gid);
650✔
1344

1345
        /* Check the database directly */
1346
        z = hashmap_get(c->database_by_groupname, name);
650✔
1347
        if (z) {
650✔
1348
                *ret_gid = PTR_TO_GID(z);
69✔
1349
                return 0;
69✔
1350
        }
1351

1352
        /* Also check NSS */
1353
        if (!arg_root) {
581✔
1354
                _cleanup_free_ struct group *g = NULL;
8✔
1355

1356
                r = getgrnam_malloc(name, &g);
8✔
1357
                if (r >= 0) {
8✔
1358
                        *ret_gid = g->gr_gid;
×
UNCOV
1359
                        return 0;
×
1360
                }
1361
                if (r != -ESRCH)
8✔
1362
                        log_warning_errno(r, "Unexpected failure while looking up group '%s' via NSS, assuming it doesn't exist: %m", name);
8✔
1363
        }
1364

1365
        return -ENOENT;
1366
}
1367

1368
static int add_group(Context *c, Item *i) {
644✔
1369
        int r;
644✔
1370

1371
        assert(c);
644✔
1372
        assert(i);
644✔
1373

1374
        r = get_gid_by_name(c, i->name, &i->gid);
644✔
1375
        if (r != -ENOENT) {
644✔
1376
                if (r < 0)
64✔
1377
                        return r;
1378
                log_debug("Group %s already exists.", i->name);
64✔
1379
                i->gid_set = true;
64✔
1380
                return 0;
64✔
1381
        }
1382

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

1406
        /* Try to reuse the numeric uid, if there's one */
1407
        if (!i->gid_set && i->uid_set) {
519✔
1408
                r = gid_is_ok(c, (gid_t) i->uid, i->name, true);
158✔
1409
                if (r < 0)
158✔
UNCOV
1410
                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1411
                if (r > 0) {
158✔
1412
                        i->gid = (gid_t) i->uid;
153✔
1413
                        i->gid_set = true;
153✔
1414
                }
1415
        }
1416

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

1421
                if (read_id_from_file(i, NULL, &candidate) > 0) {
33✔
1422

1423
                        if (candidate <= 0 || !uid_range_contains(c->uid_range, candidate))
×
UNCOV
1424
                                log_debug("Group ID " GID_FMT " of file not suitable for %s.", candidate, i->name);
×
1425
                        else {
1426
                                r = gid_is_ok(c, candidate, i->name, true);
×
1427
                                if (r < 0)
×
1428
                                        return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1429
                                else if (r > 0) {
×
1430
                                        i->gid = candidate;
×
UNCOV
1431
                                        i->gid_set = true;
×
1432
                                } else
UNCOV
1433
                                        log_debug("Group ID " GID_FMT " of file for %s already used.", candidate, i->name);
×
1434
                        }
1435
                }
1436
        }
1437

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

1442
                for (;;) {
253✔
1443
                        /* We look for new GIDs in the UID pool! */
1444
                        r = uid_range_next_lower(c->uid_range, &c->search_uid);
253✔
1445
                        if (r < 0)
253✔
UNCOV
1446
                                return log_error_errno(r, "No free group ID available for %s.", i->name);
×
1447

1448
                        r = gid_is_ok(c, c->search_uid, i->name, true);
253✔
1449
                        if (r < 0)
253✔
UNCOV
1450
                                return log_error_errno(r, "Failed to verify GID " GID_FMT ": %m", i->gid);
×
1451
                        else if (r > 0)
253✔
1452
                                break;
1453
                }
1454

1455
                i->gid_set = true;
33✔
1456
                i->gid = c->search_uid;
33✔
1457
        }
1458

1459
        r = ordered_hashmap_ensure_put(&c->todo_gids, NULL, GID_TO_PTR(i->gid), i);
519✔
1460
        if (r == -EEXIST)
519✔
UNCOV
1461
                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);
×
1462
        if (r == -ENOMEM)
519✔
UNCOV
1463
                return log_oom();
×
1464
        if (r < 0)
519✔
UNCOV
1465
                return log_error_errno(r, "Failed to store group %s with GID " GID_FMT " to be created: %m", i->name, i->gid);
×
1466

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

1470
        return 0;
1471
}
1472

1473
static int process_item(Context *c, Item *i) {
680✔
1474
        int r;
680✔
1475

1476
        assert(c);
680✔
1477
        assert(i);
680✔
1478

1479
        switch (i->type) {
680✔
1480

1481
        case ADD_USER: {
301✔
1482
                Item *j = NULL;
301✔
1483

1484
                if (!i->gid_set) {
301✔
1485
                        j = ordered_hashmap_get(c->groups, i->group_name ?: i->name);
237✔
1486

1487
                        /* If that's not a match, also check if the group name
1488
                         * matches a user name in the queue. */
1489
                        if (!j && i->group_name)
237✔
1490
                                j = ordered_hashmap_get(c->users, i->group_name);
11✔
1491
                }
1492

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

1514
                return add_user(c, i);
299✔
1515
        }
1516

1517
        case ADD_GROUP:
379✔
1518
                return add_group(c, i);
379✔
1519

1520
        default:
×
UNCOV
1521
                assert_not_reached();
×
1522
        }
1523
}
1524

1525
static Item* item_free(Item *i) {
683✔
1526
        if (!i)
683✔
1527
                return NULL;
1528

1529
        free(i->name);
683✔
1530
        free(i->group_name);
683✔
1531
        free(i->uid_path);
683✔
1532
        free(i->gid_path);
683✔
1533
        free(i->description);
683✔
1534
        free(i->home);
683✔
1535
        free(i->shell);
683✔
1536
        free(i->filename);
683✔
1537
        return mfree(i);
683✔
1538
}
1539

1540
DEFINE_TRIVIAL_CLEANUP_FUNC(Item*, item_free);
1,376✔
1541
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, char, string_hash_func, string_compare_func, Item, item_free);
680✔
1542

1543
static Item* item_new(ItemType type, const char *name, const char *filename, unsigned line) {
683✔
1544
        assert(name);
683✔
1545
        assert(!!filename == (line > 0));
683✔
1546

1547
        _cleanup_(item_freep) Item *new = new(Item, 1);
1,366✔
1548
        if (!new)
683✔
1549
                return NULL;
1550

1551
        *new = (Item) {
683✔
1552
                .type = type,
1553
                .line = line,
1554
        };
1555

1556
        if (free_and_strdup(&new->name, name) < 0 ||
1,366✔
1557
            free_and_strdup(&new->filename, filename) < 0)
683✔
UNCOV
1558
                return NULL;
×
1559

1560
        return TAKE_PTR(new);
683✔
1561
}
1562

1563
static int add_implicit(Context *c) {
115✔
1564
        char *g, **l;
115✔
1565
        int r;
115✔
1566

1567
        assert(c);
115✔
1568

1569
        /* Implicitly create additional users and groups, if they were listed in "m" lines */
1570
        ORDERED_HASHMAP_FOREACH_KEY(l, g, c->members) {
240✔
1571
                STRV_FOREACH(m, l)
20✔
1572
                        if (!ordered_hashmap_contains(c->users, *m)) {
10✔
UNCOV
1573
                                _cleanup_(item_freep) Item *j =
×
1574
                                        item_new(ADD_USER, *m, /* filename= */ NULL, /* line= */ 0);
5✔
1575
                                if (!j)
5✔
UNCOV
1576
                                        return log_oom();
×
1577

1578
                                r = ordered_hashmap_ensure_put(&c->users, &item_hash_ops, j->name, j);
5✔
1579
                                if (r == -ENOMEM)
5✔
UNCOV
1580
                                        return log_oom();
×
1581
                                if (r < 0)
5✔
UNCOV
1582
                                        return log_error_errno(r, "Failed to add implicit user '%s': %m", j->name);
×
1583

1584
                                log_debug("Adding implicit user '%s' due to m line", j->name);
5✔
1585
                                TAKE_PTR(j);
5✔
1586
                        }
1587

1588
                if (!(ordered_hashmap_contains(c->users, g) ||
10✔
1589
                      ordered_hashmap_contains(c->groups, g))) {
2✔
1590
                        _cleanup_(item_freep) Item *j =
×
1591
                                item_new(ADD_GROUP, g, /* filename= */ NULL, /* line= */ 0);
×
1592
                        if (!j)
×
UNCOV
1593
                                return log_oom();
×
1594

1595
                        r = ordered_hashmap_ensure_put(&c->groups, &item_hash_ops, j->name, j);
×
1596
                        if (r == -ENOMEM)
×
1597
                                return log_oom();
×
1598
                        if (r < 0)
×
UNCOV
1599
                                return log_error_errno(r, "Failed to add implicit group '%s': %m", j->name);
×
1600

1601
                        log_debug("Adding implicit group '%s' due to m line", j->name);
×
UNCOV
1602
                        TAKE_PTR(j);
×
1603
                }
1604
        }
1605

1606
        return 0;
115✔
1607
}
1608

1609
static int item_equivalent(Item *a, Item *b) {
2✔
1610
        int r;
2✔
1611

1612
        assert(a);
2✔
1613
        assert(b);
2✔
1614

1615
        if (a->type != b->type) {
2✔
UNCOV
1616
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1617
                           "Item not equivalent because types differ");
UNCOV
1618
                return false;
×
1619
        }
1620

1621
        if (!streq_ptr(a->name, b->name)) {
2✔
UNCOV
1622
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1623
                           "Item not equivalent because names differ ('%s' vs. '%s')",
1624
                           a->name, b->name);
UNCOV
1625
                return false;
×
1626
        }
1627

1628
        /* Paths were simplified previously, so we can use streq. */
1629
        if (!streq_ptr(a->uid_path, b->uid_path)) {
2✔
UNCOV
1630
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1631
                           "Item not equivalent because UID paths differ (%s vs. %s)",
1632
                           a->uid_path ?: "(unset)", b->uid_path ?: "(unset)");
UNCOV
1633
                return false;
×
1634
        }
1635

1636
        if (!streq_ptr(a->gid_path, b->gid_path)) {
2✔
UNCOV
1637
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1638
                           "Item not equivalent because GID paths differ (%s vs. %s)",
1639
                           a->gid_path ?: "(unset)", b->gid_path ?: "(unset)");
UNCOV
1640
                return false;
×
1641
        }
1642

1643
        if (!streq_ptr(a->description, b->description))  {
2✔
UNCOV
1644
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1645
                           "Item not equivalent because descriptions differ ('%s' vs. '%s')",
1646
                           strempty(a->description), strempty(b->description));
UNCOV
1647
                return false;
×
1648
        }
1649

1650
        if ((a->uid_set != b->uid_set) ||
2✔
1651
            (a->uid_set && a->uid != b->uid)) {
×
UNCOV
1652
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1653
                           "Item not equivalent because UIDs differ (%s vs. %s)",
1654
                           a->uid_set ? FORMAT_UID(a->uid) : "(unset)",
1655
                           b->uid_set ? FORMAT_UID(b->uid) : "(unset)");
UNCOV
1656
                return false;
×
1657
        }
1658

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

1668
        if (!streq_ptr(a->home, b->home)) {
2✔
UNCOV
1669
                log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1670
                           "Item not equivalent because home directories differ ('%s' vs. '%s')",
1671
                           strempty(a->description), strempty(b->description));
UNCOV
1672
                return false;
×
1673
        }
1674

1675
        /* Check if the two paths refer to the same file.
1676
         * If the paths are equal (after normalization), it's obviously the same file.
1677
         * If both paths specify a nologin shell, treat them as the same (e.g. /bin/true and /bin/false).
1678
         * Otherwise, try to resolve the paths, and see if we get the same result, (e.g. /sbin/nologin and
1679
         * /usr/sbin/nologin).
1680
         * If we can't resolve something, treat different paths as different. */
1681

1682
        const char *a_shell = pick_shell(a),
2✔
1683
                   *b_shell = pick_shell(b);
2✔
1684
        if (!path_equal(a_shell, b_shell) &&
2✔
1685
            !(is_nologin_shell(a_shell) && is_nologin_shell(b_shell))) {
×
UNCOV
1686
                _cleanup_free_ char *pa = NULL, *pb = NULL;
×
1687

1688
                r = chase(a_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pa, NULL);
×
1689
                if (r < 0) {
×
UNCOV
1690
                        log_full_errno(ERRNO_IS_RESOURCE(r) ? LOG_ERR : LOG_DEBUG,
×
1691
                                       r, "Failed to look up path '%s%s%s': %m",
1692
                                       strempty(arg_root), arg_root ? "/" : "", a_shell);
UNCOV
1693
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1694
                }
1695

1696
                r = chase(b_shell, arg_root, CHASE_PREFIX_ROOT | CHASE_NONEXISTENT, &pb, NULL);
×
1697
                if (r < 0) {
×
UNCOV
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 ? "/" : "", b_shell);
UNCOV
1701
                        return ERRNO_IS_RESOURCE(r) ? r : false;
×
1702
                }
1703

1704
                if (!path_equal(pa, pb)) {
×
UNCOV
1705
                        log_syntax(NULL, LOG_DEBUG, a->filename, a->line, 0,
×
1706
                                   "Item not equivalent because shells differ ('%s' vs. '%s')",
1707
                                   pa, pb);
UNCOV
1708
                        return false;
×
1709
                }
1710
        }
1711

1712
        return true;
1713
}
1714

1715
static int parse_line(
688✔
1716
                const char *fname,
1717
                unsigned line,
1718
                const char *buffer,
1719
                bool *invalid_config,
1720
                void *context) {
1721

1722
        Context *c = ASSERT_PTR(context);
688✔
1723
        _cleanup_free_ char *action = NULL,
1,376✔
1724
                *name = NULL, *resolved_name = NULL,
×
1725
                *id = NULL, *resolved_id = NULL,
×
1726
                *description = NULL, *resolved_description = NULL,
×
UNCOV
1727
                *home = NULL, *resolved_home = NULL,
×
1728
                *shell = NULL, *resolved_shell = NULL;
688✔
1729
        _cleanup_(item_freep) Item *i = NULL;
688✔
1730
        Item *existing;
688✔
1731
        OrderedHashmap *h;
688✔
1732
        int r;
688✔
1733
        const char *p;
688✔
1734

1735
        assert(fname);
688✔
1736
        assert(line >= 1);
688✔
1737
        assert(buffer);
688✔
1738
        assert(!invalid_config); /* We don't support invalid_config yet. */
688✔
1739

1740
        /* Parse columns */
1741
        p = buffer;
688✔
1742
        r = extract_many_words(&p, NULL, EXTRACT_UNQUOTE,
688✔
1743
                               &action, &name, &id, &description, &home, &shell);
1744
        if (r < 0)
688✔
UNCOV
1745
                return log_syntax(NULL, LOG_ERR, fname, line, r, "Syntax error.");
×
1746
        if (r < 2)
688✔
UNCOV
1747
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1748
                                  "Missing action and name columns.");
1749
        if (!isempty(p))
688✔
UNCOV
1750
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1751
                                  "Trailing garbage.");
1752

1753
        if (isempty(action))
688✔
UNCOV
1754
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1755
                                  "Empty command specification.");
1756

1757
        bool locked = false;
1758
        for (int pos = 1; action[pos]; pos++)
709✔
1759
                if (action[pos] == '!' && !locked)
21✔
1760
                        locked = true;
21✔
1761
                else
UNCOV
1762
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1763
                                          "Unknown modifiers in command '%s'.", action);
1764

1765
        if (!IN_SET(action[0], ADD_USER, ADD_GROUP, ADD_MEMBER, ADD_RANGE))
688✔
UNCOV
1766
                return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
×
1767
                                  "Unknown command type '%c'.", action[0]);
1768

1769
        /* Verify name */
1770
        if (empty_or_dash(name))
688✔
UNCOV
1771
                name = mfree(name);
×
1772

1773
        if (name) {
688✔
1774
                r = specifier_printf(name, NAME_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_name);
688✔
1775
                if (r < 0)
688✔
UNCOV
1776
                        return log_syntax(NULL, LOG_ERR, fname, line, r, "Failed to replace specifiers in '%s': %m", name);
×
1777

1778
                if (!valid_user_group_name(resolved_name, 0))
688✔
UNCOV
1779
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1780
                                          "'%s' is not a valid user or group name.", resolved_name);
1781
        }
1782

1783
        /* Verify id */
1784
        if (empty_or_dash(id))
688✔
1785
                id = mfree(id);
61✔
1786

1787
        if (id) {
688✔
1788
                r = specifier_printf(id, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_id);
627✔
1789
                if (r < 0)
627✔
UNCOV
1790
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1791
                                          "Failed to replace specifiers in '%s': %m", name);
1792
        }
1793

1794
        /* Verify description */
1795
        if (empty_or_dash(description))
688✔
1796
                description = mfree(description);
635✔
1797

1798
        if (description) {
688✔
1799
                r = specifier_printf(description, LONG_LINE_MAX, system_and_tmp_specifier_table, arg_root, NULL, &resolved_description);
53✔
1800
                if (r < 0)
53✔
UNCOV
1801
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1802
                                          "Failed to replace specifiers in '%s': %m", description);
1803

1804
                if (!valid_gecos(resolved_description))
53✔
UNCOV
1805
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1806
                                          "'%s' is not a valid GECOS field.", resolved_description);
1807
        }
1808

1809
        /* Verify home */
1810
        if (empty_or_dash(home))
688✔
1811
                home = mfree(home);
547✔
1812

1813
        if (home) {
688✔
1814
                r = specifier_printf(home, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_home);
141✔
1815
                if (r < 0)
141✔
UNCOV
1816
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1817
                                          "Failed to replace specifiers in '%s': %m", home);
1818

1819
                path_simplify(resolved_home);
141✔
1820

1821
                if (!valid_home(resolved_home))
141✔
UNCOV
1822
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1823
                                          "'%s' is not a valid home directory field.", resolved_home);
1824
        }
1825

1826
        /* Verify shell */
1827
        if (empty_or_dash(shell))
688✔
1828
                shell = mfree(shell);
667✔
1829

1830
        if (shell) {
688✔
1831
                r = specifier_printf(shell, PATH_MAX-1, system_and_tmp_specifier_table, arg_root, NULL, &resolved_shell);
21✔
1832
                if (r < 0)
21✔
UNCOV
1833
                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1834
                                          "Failed to replace specifiers in '%s': %m", shell);
1835

1836
                path_simplify(resolved_shell);
21✔
1837

1838
                if (!valid_shell(resolved_shell))
21✔
UNCOV
1839
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1840
                                          "'%s' is not a valid login shell field.", resolved_shell);
1841
        }
1842

1843
        switch (action[0]) {
688✔
1844

1845
        case ADD_RANGE:
×
1846
                if (locked)
×
UNCOV
1847
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1848
                                          "Flag '!' not permitted on lines of type 'r'.");
1849

1850
                if (resolved_name)
×
UNCOV
1851
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1852
                                          "Lines of type 'r' don't take a name field.");
1853

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

1858
                if (description || home || shell)
×
UNCOV
1859
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1860
                                          "Lines of type '%c' don't take a %s field.",
1861
                                          action[0],
1862
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1863

1864
                r = uid_range_add_str(&c->uid_range, resolved_id);
×
1865
                if (r < 0)
×
UNCOV
1866
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1867
                                          "Invalid UID range %s.", resolved_id);
1868

1869
                return 0;
1870

1871
        case ADD_MEMBER: {
10✔
1872
                /* Try to extend an existing member or group item */
1873
                if (!name)
10✔
UNCOV
1874
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1875
                                          "Lines of type 'm' require a user name in the second field.");
1876

1877
                if (locked)
10✔
UNCOV
1878
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1879
                                          "Flag '!' not permitted on lines of type 'm'.");
1880

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

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

1889
                if (description || home || shell)
10✔
UNCOV
1890
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1891
                                          "Lines of type '%c' don't take a %s field.",
1892
                                          action[0],
1893
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1894

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

1899
                return 0;
1900
        }
1901

1902
        case ADD_USER:
298✔
1903
                if (!name)
298✔
UNCOV
1904
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1905
                                          "Lines of type 'u' require a user name in the second field.");
1906

1907
                r = ordered_hashmap_ensure_allocated(&c->users, &item_hash_ops);
298✔
1908
                if (r < 0)
298✔
UNCOV
1909
                        return log_oom();
×
1910

1911
                i = item_new(ADD_USER, resolved_name, fname, line);
298✔
1912
                if (!i)
298✔
UNCOV
1913
                        return log_oom();
×
1914

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

1944
                i->description = TAKE_PTR(resolved_description);
297✔
1945
                i->home = TAKE_PTR(resolved_home);
297✔
1946
                i->shell = TAKE_PTR(resolved_shell);
297✔
1947
                i->locked = locked;
297✔
1948

1949
                h = c->users;
297✔
1950
                break;
297✔
1951

1952
        case ADD_GROUP:
380✔
1953
                if (!name)
380✔
UNCOV
1954
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1955
                                          "Lines of type 'g' require a user name in the second field.");
1956

1957
                if (locked)
380✔
UNCOV
1958
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1959
                                          "Flag '!' not permitted on lines of type 'g'.");
1960

1961
                if (description || home || shell)
380✔
UNCOV
1962
                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EINVAL),
×
1963
                                          "Lines of type '%c' don't take a %s field.",
1964
                                          action[0],
1965
                                          description ? "GECOS" : home ? "home directory" : "login shell");
1966

1967
                r = ordered_hashmap_ensure_allocated(&c->groups, &item_hash_ops);
380✔
1968
                if (r < 0)
380✔
UNCOV
1969
                        return log_oom();
×
1970

1971
                i = item_new(ADD_GROUP, resolved_name, fname, line);
380✔
1972
                if (!i)
380✔
UNCOV
1973
                        return log_oom();
×
1974

1975
                if (resolved_id) {
380✔
1976
                        if (path_is_absolute(resolved_id))
351✔
UNCOV
1977
                                i->gid_path = path_simplify(TAKE_PTR(resolved_id));
×
1978
                        else {
1979
                                r = parse_gid(resolved_id, &i->gid);
351✔
1980
                                if (r < 0)
351✔
UNCOV
1981
                                        return log_syntax(NULL, LOG_ERR, fname, line, r,
×
1982
                                                          "Failed to parse GID: '%s': %m", id);
1983

1984
                                i->gid_set = true;
351✔
1985
                        }
1986
                }
1987

1988
                h = c->groups;
380✔
1989
                break;
380✔
1990

1991
        default:
×
UNCOV
1992
                assert_not_reached();
×
1993
        }
1994

1995
        existing = ordered_hashmap_get(h, i->name);
677✔
1996
        if (existing) {
677✔
1997
                /* Two functionally-equivalent items are fine */
1998
                r = item_equivalent(i, existing);
2✔
1999
                if (r < 0)
2✔
2000
                        return r;
2001
                if (r == 0) {
2✔
2002
                        if (existing->filename)
×
UNCOV
2003
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2004
                                           "Conflict with earlier configuration for %s '%s' in %s:%u, ignoring line.",
2005
                                           item_type_to_string(i->type),
2006
                                           i->name,
2007
                                           existing->filename, existing->line);
2008
                        else
UNCOV
2009
                                log_syntax(NULL, LOG_WARNING, fname, line, 0,
×
2010
                                           "Conflict with earlier configuration for %s '%s', ignoring line.",
2011
                                           item_type_to_string(i->type),
2012
                                           i->name);
2013
                }
2014

2015
                return 0;
2✔
2016
        }
2017

2018
        r = ordered_hashmap_put(h, i->name, i);
675✔
2019
        if (r < 0)
675✔
UNCOV
2020
                return log_oom();
×
2021

2022
        i = NULL;
675✔
2023
        return 0;
675✔
2024
}
2025

2026
static int read_config_file(Context *c, const char *fn, bool ignore_enoent) {
135✔
2027
        return conf_file_read(
405✔
2028
                        arg_root,
2029
                        (const char**) CONF_PATHS_STRV("sysusers.d"),
135✔
2030
                        ASSERT_PTR(fn),
135✔
2031
                        parse_line,
2032
                        ASSERT_PTR(c),
135✔
2033
                        ignore_enoent,
2034
                        /* invalid_config= */ NULL);
2035
}
2036

2037
static int cat_config(void) {
×
2038
        _cleanup_strv_free_ char **files = NULL;
×
UNCOV
2039
        int r;
×
2040

2041
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, NULL);
×
UNCOV
2042
        if (r < 0)
×
2043
                return r;
2044

UNCOV
2045
        pager_open(arg_pager_flags);
×
2046

UNCOV
2047
        return cat_files(NULL, files, arg_cat_flags);
×
2048
}
2049

2050
static int help(void) {
×
2051
        _cleanup_free_ char *link = NULL;
×
UNCOV
2052
        _cleanup_(table_unrefp) Table *cmds = NULL, *opts = NULL;
×
2053
        int r;
×
2054

2055
        r = terminal_urlify_man("systemd-sysusers.service", "8", &link);
×
UNCOV
2056
        if (r < 0)
×
2057
                return log_oom();
×
2058

UNCOV
2059
        r = option_parser_get_help_table(&cmds);
×
UNCOV
2060
        if (r < 0)
×
2061
                return r;
2062

UNCOV
2063
        r = option_parser_get_help_table_group("Options", &opts);
×
UNCOV
2064
        if (r < 0)
×
2065
                return r;
2066

UNCOV
2067
        (void) table_sync_column_widths(0, cmds, opts);
×
2068

UNCOV
2069
        printf("%s [OPTIONS...] [CONFIGURATION FILE...]\n"
×
2070
               "\n%sCreates system user and group accounts.%s\n"
2071
               "\nCommands:\n",
2072
               program_invocation_short_name,
2073
               ansi_highlight(),
2074
               ansi_normal());
2075

UNCOV
2076
        r = table_print_or_warn(cmds);
×
UNCOV
2077
        if (r < 0)
×
2078
                return r;
2079

UNCOV
2080
        printf("\nOptions:\n");
×
2081

UNCOV
2082
        r = table_print_or_warn(opts);
×
UNCOV
2083
        if (r < 0)
×
2084
                return r;
2085

UNCOV
2086
        printf("\nSee the %s for details.\n", link);
×
2087
        return 0;
2088
}
2089

2090
static int parse_argv(int argc, char *argv[], char ***ret_args) {
115✔
2091
        int r;
115✔
2092

2093
        assert(argc >= 0);
115✔
2094
        assert(argv);
115✔
2095

2096
        OptionParser state = { argc, argv };
115✔
2097
        const char *arg;
115✔
2098

2099
        FOREACH_OPTION(&state, c, &arg, /* on_error= */ return c)
378✔
2100
                switch (c) {
148✔
2101

UNCOV
2102
                OPTION_COMMON_CAT_CONFIG:
×
UNCOV
2103
                        arg_cat_flags = CAT_CONFIG_ON;
×
UNCOV
2104
                        break;
×
2105

UNCOV
2106
                OPTION_COMMON_TLDR:
×
UNCOV
2107
                        arg_cat_flags = CAT_TLDR;
×
UNCOV
2108
                        break;
×
2109

UNCOV
2110
                OPTION_COMMON_HELP:
×
UNCOV
2111
                        return help();
×
2112

UNCOV
2113
                OPTION_COMMON_VERSION:
×
UNCOV
2114
                        return version();
×
2115

2116
                OPTION_GROUP("Options"):
2117
                        break;
2118

2119
                OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"):
108✔
2120
                        r = parse_path_argument(arg, /* suppress_root= */ false, &arg_root);
108✔
2121
                        if (r < 0)
108✔
2122
                                return r;
2123
                        break;
2124

2125
                OPTION_LONG("image", "PATH", "Operate on disk image as filesystem root"):
×
UNCOV
2126
                        r = parse_path_argument(arg, /* suppress_root= */ false, &arg_image);
×
2127
                        if (r < 0)
×
2128
                                return r;
2129
                        break;
2130

2131
                OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"):
×
2132
                        r = parse_image_policy_argument(arg, &arg_image_policy);
×
2133
                        if (r < 0)
×
2134
                                return r;
2135
                        break;
2136

2137
                OPTION_LONG("replace", "PATH", "Treat arguments as replacement for PATH"):
35✔
2138
                        if (!path_is_absolute(arg))
35✔
UNCOV
2139
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2140
                                                       "The argument to --replace= must be an absolute path.");
2141
                        if (!endswith(arg, ".conf"))
35✔
2142
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2143
                                                       "The argument to --replace= must have the extension '.conf'.");
2144

2145
                        arg_replace = arg;
35✔
2146
                        break;
35✔
2147

2148
                OPTION_LONG("dry-run", NULL, "Just print what would be done"):
×
2149
                        arg_dry_run = true;
×
UNCOV
2150
                        break;
×
2151

2152
                OPTION_LONG("inline", NULL, "Treat arguments as configuration lines"):
5✔
2153
                        arg_inline = true;
5✔
2154
                        break;
5✔
2155

UNCOV
2156
                OPTION_COMMON_NO_PAGER:
×
UNCOV
2157
                        arg_pager_flags |= PAGER_DISABLE;
×
2158
                        break;
×
2159
                }
2160

2161
        char **args = option_parser_get_args(&state);
115✔
2162
        size_t n_args = option_parser_get_n_args(&state);
115✔
2163

2164
        if (arg_replace && arg_cat_flags != CAT_CONFIG_OFF)
115✔
2165
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2166
                                       "Option --replace= is not supported with --cat-config/--tldr.");
2167

2168
        if (arg_inline && arg_cat_flags != CAT_CONFIG_OFF)
115✔
UNCOV
2169
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2170
                                       "Option --inline is not supported with --cat-config/--tldr.");
2171

2172
        if (arg_replace && n_args == 0)
115✔
2173
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2174
                                       "When --replace= is given, some configuration items must be specified.");
2175

2176
        if (arg_image && arg_root)
115✔
UNCOV
2177
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
2178
                                       "Use either --root= or --image=, the combination of both is not supported.");
2179

2180
        *ret_args = args;
115✔
2181
        return 1;
115✔
2182
}
2183

2184
static int parse_arguments(Context *c, char **args) {
43✔
2185
        unsigned pos = 1;
43✔
2186
        int r;
43✔
2187

2188
        assert(c);
43✔
2189

2190
        STRV_FOREACH(arg, args) {
90✔
2191
                if (arg_inline)
47✔
2192
                        /* Use (argument):n, where n==1 for the first positional arg */
2193
                        r = parse_line("(argument)", pos, *arg, /* invalid_config= */ NULL, c);
9✔
2194
                else
2195
                        r = read_config_file(c, *arg, /* ignore_enoent= */ false);
38✔
2196
                if (r < 0)
47✔
2197
                        return r;
2198

2199
                pos++;
47✔
2200
        }
2201

2202
        return 0;
2203
}
2204

2205
static int read_config_files(Context *c, char **args) {
90✔
UNCOV
2206
        _cleanup_strv_free_ char **files = NULL;
×
2207
        _cleanup_free_ char *p = NULL;
90✔
2208
        int r;
90✔
2209

2210
        assert(c);
90✔
2211

2212
        r = conf_files_list_with_replacement(arg_root, CONF_PATHS_STRV("sysusers.d"), arg_replace, &files, &p);
90✔
2213
        if (r < 0)
90✔
2214
                return r;
2215

2216
        STRV_FOREACH(f, files)
204✔
2217
                if (p && path_equal(*f, p)) {
114✔
2218
                        log_debug("Parsing arguments at position \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
18✔
2219

2220
                        r = parse_arguments(c, args);
18✔
2221
                        if (r < 0)
18✔
2222
                                return r;
2223
                } else {
2224
                        log_debug("Reading config file \"%s\"%s", *f, glyph(GLYPH_ELLIPSIS));
120✔
2225

2226
                        /* Just warn, ignore result otherwise */
2227
                        (void) read_config_file(c, *f, /* ignore_enoent= */ true);
96✔
2228
                }
2229

2230
        return 0;
2231
}
2232

2233
static int read_credential_lines(Context *c) {
115✔
2234
        _cleanup_free_ char *j = NULL;
115✔
2235
        const char *d;
115✔
2236
        int r;
115✔
2237

2238
        assert(c);
115✔
2239

2240
        r = get_credentials_dir(&d);
115✔
2241
        if (r == -ENXIO)
115✔
2242
                return 0;
2243
        if (r < 0)
1✔
UNCOV
2244
                return log_error_errno(r, "Failed to get credentials directory: %m");
×
2245

2246
        j = path_join(d, "sysusers.extra");
1✔
2247
        if (!j)
1✔
UNCOV
2248
                return log_oom();
×
2249

2250
        (void) read_config_file(c, j, /* ignore_enoent= */ true);
1✔
2251
        return 0;
2252
}
2253

2254
static int run(int argc, char *argv[]) {
115✔
2255
        _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
115✔
UNCOV
2256
        _cleanup_(umount_and_freep) char *mounted_dir = NULL;
×
2257
        _cleanup_close_ int lock = -EBADF;
115✔
2258
        _cleanup_(context_done) Context c = {
115✔
2259
                .audit_fd = -EBADF,
2260
                .search_uid = UID_INVALID,
2261
        };
2262

2263
        Item *i;
115✔
2264
        int r;
115✔
2265

2266
        char **args = NULL;
115✔
2267
        r = parse_argv(argc, argv, &args);
115✔
2268
        if (r <= 0)
115✔
2269
                return r;
2270

2271
        log_setup();
115✔
2272

2273
        if (arg_cat_flags != CAT_CONFIG_OFF)
115✔
2274
                return cat_config();
×
2275

2276
        if (should_bypass("SYSTEMD_SYSUSERS"))
115✔
2277
                return 0;
2278

2279
        umask(0022);
115✔
2280

2281
        r = mac_init();
115✔
2282
        if (r < 0)
115✔
2283
                return r;
2284

2285
        if (arg_image) {
115✔
UNCOV
2286
                assert(!arg_root);
×
2287

UNCOV
2288
                r = mount_image_privately_interactively(
×
2289
                                arg_image,
2290
                                arg_image_policy,
2291
                                DISSECT_IMAGE_GENERIC_ROOT |
2292
                                DISSECT_IMAGE_REQUIRE_ROOT |
2293
                                DISSECT_IMAGE_VALIDATE_OS |
2294
                                DISSECT_IMAGE_RELAX_VAR_CHECK |
2295
                                DISSECT_IMAGE_FSCK |
2296
                                DISSECT_IMAGE_GROWFS |
2297
                                DISSECT_IMAGE_ALLOW_USERSPACE_VERITY,
2298
                                &mounted_dir,
2299
                                /* ret_dir_fd= */ NULL,
2300
                                &loop_device);
UNCOV
2301
                if (r < 0)
×
2302
                        return r;
2303

UNCOV
2304
                arg_root = strdup(mounted_dir);
×
2305
                if (!arg_root)
×
UNCOV
2306
                        return log_oom();
×
2307
        }
2308

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

2313
        /* If command line arguments are specified along with --replace, read all configuration files and
2314
         * insert the positional arguments at the specified place. Otherwise, if command line arguments are
2315
         * specified, execute just them, and finally, without --replace= or any positional arguments, just
2316
         * read configuration and execute it. */
2317
        if (arg_replace || strv_isempty(args))
115✔
2318
                r = read_config_files(&c, args);
90✔
2319
        else
2320
                r = parse_arguments(&c, args);
25✔
2321
        if (r < 0)
115✔
2322
                return r;
2323

2324
        r = read_credential_lines(&c);
115✔
2325
        if (r < 0)
115✔
2326
                return r;
2327

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

2336
        if (!c.uid_range) {
115✔
2337
                /* Default to default range of SYSTEMD_UID_MIN..SYSTEM_UID_MAX. */
2338
                r = read_login_defs(&c.login_defs, NULL, arg_root);
115✔
2339
                if (r < 0)
115✔
UNCOV
2340
                        return log_error_errno(r, "Failed to read %s%s: %m",
×
2341
                                               strempty(arg_root), "/etc/login.defs");
2342

2343
                c.login_defs_need_warning = true;
115✔
2344

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

2358
        r = add_implicit(&c);
115✔
2359
        if (r < 0)
115✔
2360
                return r;
2361

2362
        if (!arg_dry_run) {
115✔
2363
                lock = take_etc_passwd_lock(arg_root);
115✔
2364
                if (lock < 0)
115✔
UNCOV
2365
                        return log_error_errno(lock, "Failed to take /etc/passwd lock: %m");
×
2366
        }
2367

2368
        r = load_user_database(&c);
115✔
2369
        if (r < 0)
115✔
UNCOV
2370
                return log_error_errno(r, "Failed to load user database: %m");
×
2371

2372
        r = load_group_database(&c);
115✔
2373
        if (r < 0)
115✔
UNCOV
2374
                return log_error_errno(r, "Failed to read group database: %m");
×
2375

2376
        ORDERED_HASHMAP_FOREACH(i, c.groups)
494✔
2377
                (void) process_item(&c, i);
379✔
2378

2379
        ORDERED_HASHMAP_FOREACH(i, c.users)
416✔
2380
                (void) process_item(&c, i);
301✔
2381

2382
        return write_files(&c);
115✔
2383
}
2384

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

© 2026 Coveralls, Inc