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

systemd / systemd / 15986406979

30 Jun 2025 05:03PM UTC coverage: 72.045% (-0.09%) from 72.13%
15986406979

push

github

bluca
man/systemd-sysext: list ephemeral/ephemeral-import in the list of options

ephemeral/ephemeral-import are described as possible '--mutable' options but
not present in the list. Note, "systemd-sysext --help" lists them correctly.

300514 of 417119 relevant lines covered (72.05%)

708586.28 hits per line

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

82.4
/src/nspawn/nspawn-bind-user.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <grp.h>
4
#include <pwd.h>
5
#include <unistd.h>
6

7
#include "alloc-util.h"
8
#include "chase.h"
9
#include "fd-util.h"
10
#include "fileio.h"
11
#include "format-util.h"
12
#include "json-util.h"
13
#include "log.h"
14
#include "nspawn-mount.h"
15
#include "nspawn.h"
16
#include "nspawn-bind-user.h"
17
#include "path-util.h"
18
#include "string-util.h"
19
#include "strv.h"
20
#include "user-util.h"
21
#include "userdb.h"
22

23
static int check_etc_passwd_collisions(
68✔
24
                const char *directory,
25
                const char *name,
26
                uid_t uid) {
27

28
        _cleanup_fclose_ FILE *f = NULL;
68✔
29
        int r;
68✔
30

31
        assert(directory);
68✔
32
        assert(name || uid_is_valid(uid));
68✔
33

34
        r = chase_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
68✔
35
        if (r == -ENOENT)
68✔
36
                return 0; /* no user database? then no user, hence no collision */
37
        if (r < 0)
64✔
38
                return log_error_errno(r, "Failed to open /etc/passwd of container: %m");
×
39

40
        for (;;) {
543✔
41
                struct passwd *pw;
607✔
42

43
                r = fgetpwent_sane(f, &pw);
607✔
44
                if (r < 0)
607✔
45
                        return log_error_errno(r, "Failed to iterate through /etc/passwd of container: %m");
64✔
46
                if (r == 0) /* EOF */
607✔
47
                        return 0; /* no collision */
48

49
                if (name && streq_ptr(pw->pw_name, name))
544✔
50
                        return 1; /* name collision */
51
                if (uid_is_valid(uid) && pw->pw_uid == uid)
543✔
52
                        return 1; /* UID collision */
53
        }
54
}
55

56
static int check_etc_group_collisions(
67✔
57
                const char *directory,
58
                const char *name,
59
                gid_t gid) {
60

61
        _cleanup_fclose_ FILE *f = NULL;
67✔
62
        int r;
67✔
63

64
        assert(directory);
67✔
65
        assert(name || gid_is_valid(gid));
101✔
66

67
        r = chase_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
67✔
68
        if (r == -ENOENT)
67✔
69
                return 0; /* no group database? then no group, hence no collision */
70
        if (r < 0)
67✔
71
                return log_error_errno(r, "Failed to open /etc/group of container: %m");
×
72

73
        for (;;) {
1,452✔
74
                struct group *gr;
1,519✔
75

76
                r = fgetgrent_sane(f, &gr);
1,519✔
77
                if (r < 0)
1,519✔
78
                        return log_error_errno(r, "Failed to iterate through /etc/group of container: %m");
67✔
79
                if (r == 0)
1,519✔
80
                        return 0; /* no collision */
81

82
                if (name && streq_ptr(gr->gr_name, name))
1,453✔
83
                        return 1; /* name collision */
84
                if (gid_is_valid(gid) && gr->gr_gid == gid)
1,452✔
85
                        return 1; /* gid collision */
86
        }
87
}
88

89
static int convert_user(
34✔
90
                const char *directory,
91
                UserRecord *u,
92
                GroupRecord *g,
93
                uid_t allocate_uid,
94
                const char *shell,
95
                bool shell_copy,
96
                UserRecord **ret_converted_user,
97
                GroupRecord **ret_converted_group) {
98

99
        _cleanup_(group_record_unrefp) GroupRecord *converted_group = NULL;
34✔
100
        _cleanup_(user_record_unrefp) UserRecord *converted_user = NULL;
×
101
        _cleanup_free_ char *h = NULL;
34✔
102
        sd_json_variant *p, *hp = NULL, *ssh = NULL;
34✔
103
        int r;
34✔
104

105
        assert(u);
34✔
106
        assert(g);
34✔
107
        assert(user_record_gid(u) == g->gid);
34✔
108

109
        if (shell_copy)
34✔
110
                shell = u->shell;
6✔
111

112
        r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
34✔
113
        if (r < 0)
34✔
114
                return r;
115
        if (r > 0)
34✔
116
                return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
1✔
117
                                       "Sorry, the user '%s' already exists in the container.", u->user_name);
118

119
        r = check_etc_group_collisions(directory, g->group_name, GID_INVALID);
33✔
120
        if (r < 0)
33✔
121
                return r;
122
        if (r > 0)
33✔
123
                return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
1✔
124
                                       "Sorry, the group '%s' already exists in the container.", g->group_name);
125

126
        h = path_join("/run/host/home/", u->user_name);
32✔
127
        if (!h)
32✔
128
                return log_oom();
×
129

130
        /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
131
        p = sd_json_variant_by_key(u->json, "privileged");
32✔
132
        if (p) {
32✔
133
                hp = sd_json_variant_by_key(p, "hashedPassword");
32✔
134
                ssh = sd_json_variant_by_key(p, "sshAuthorizedKeys");
32✔
135
        }
136

137
        r = user_record_build(
96✔
138
                        &converted_user,
139
                        SD_JSON_BUILD_OBJECT(
64✔
140
                                        SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(u->user_name)),
141
                                        SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
142
                                        SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
143
                                        SD_JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(u->disposition))),
144
                                        SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)),
145
                                        SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
146
                                        JSON_BUILD_PAIR_STRING_NON_EMPTY("shell", shell),
147
                                        SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT(
148
                                                                           SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)),
149
                                                                           SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh))))));
150
        if (r < 0)
32✔
151
                return log_error_errno(r, "Failed to build container user record: %m");
×
152

153
        r = group_record_build(
96✔
154
                        &converted_group,
155
                        SD_JSON_BUILD_OBJECT(
32✔
156
                                        SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(g->group_name)),
157
                                        SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
158
                                        SD_JSON_BUILD_PAIR_CONDITION(g->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(g->disposition))),
159
                                        SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn"))));
160
        if (r < 0)
32✔
161
                return log_error_errno(r, "Failed to build container group record: %m");
×
162

163
        *ret_converted_user = TAKE_PTR(converted_user);
32✔
164
        *ret_converted_group = TAKE_PTR(converted_group);
32✔
165

166
        return 0;
32✔
167
}
168

169
static int find_free_uid(const char *directory, uid_t max_uid, uid_t *current_uid) {
34✔
170
        int r;
34✔
171

172
        assert(directory);
34✔
173
        assert(current_uid);
34✔
174

175
        for (;; (*current_uid)++) {
×
176
                if (*current_uid > MAP_UID_MAX || *current_uid > max_uid)
34✔
177
                        return log_error_errno(
×
178
                                        SYNTHETIC_ERRNO(EBUSY),
179
                                        "No suitable available UID in range " UID_FMT "…" UID_FMT " in container detected, can't map user.",
180
                                        MAP_UID_MIN, MAP_UID_MAX);
181

182
                r = check_etc_passwd_collisions(directory, NULL, *current_uid);
34✔
183
                if (r < 0)
34✔
184
                        return r;
185
                if (r > 0) /* already used */
34✔
186
                        continue;
×
187

188
                /* We want to use the UID also as GID, hence check for it in /etc/group too */
189
                r = check_etc_group_collisions(directory, NULL, (gid_t) *current_uid);
34✔
190
                if (r <= 0)
34✔
191
                        return r;
192
        }
193
}
194

195
BindUserContext* bind_user_context_free(BindUserContext *c) {
13✔
196
        if (!c)
13✔
197
                return NULL;
198

199
        FOREACH_ARRAY(d, c->data, c->n_data) {
30✔
200
                user_record_unref(d->host_user);
17✔
201
                group_record_unref(d->host_group);
17✔
202
                user_record_unref(d->payload_user);
17✔
203
                group_record_unref(d->payload_group);
17✔
204
        }
205

206
        return mfree(c);
13✔
207
}
208

209
int bind_user_prepare(
236✔
210
                const char *directory,
211
                char **bind_user,
212
                const char *bind_user_shell,
213
                bool bind_user_shell_copy,
214
                uid_t uid_shift,
215
                uid_t uid_range,
216
                CustomMount **custom_mounts,
217
                size_t *n_custom_mounts,
218
                BindUserContext **ret) {
219

220
        _cleanup_(bind_user_context_freep) BindUserContext *c = NULL;
236✔
221
        uid_t current_uid = MAP_UID_MIN;
236✔
222
        int r;
236✔
223

224
        assert(custom_mounts);
236✔
225
        assert(n_custom_mounts);
236✔
226
        assert(ret);
236✔
227

228
        /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
229
         * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
230
         * to include an appropriate bind mount mapping.
231
         *
232
         * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
233
         * new BindUserContext for the user records */
234

235
        if (strv_isempty(bind_user)) {
236✔
236
                *ret = NULL;
212✔
237
                return 0;
212✔
238
        }
239

240
        c = new0(BindUserContext, 1);
24✔
241
        if (!c)
24✔
242
                return log_oom();
×
243

244
        STRV_FOREACH(n, bind_user) {
56✔
245
                _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL;
34✔
246
                _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL;
34✔
247
                _cleanup_free_ char *sm = NULL, *sd = NULL;
34✔
248

249
                r = userdb_by_name(*n, /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u);
34✔
250
                if (r < 0)
34✔
251
                        return log_error_errno(r, "Failed to resolve user '%s': %m", *n);
×
252

253
                /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
254
                 * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
255
                 * even if we wanted, we couldn't override the root or nobody user records. Note we also
256
                 * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
257
                 * out root/nobody too, hence these checks might appear redundant — but they actually are
258
                 * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
259
                 * and the user/group databases fully synthesized at runtime. Moreover, the name of the
260
                 * user/group name of the "nobody" account differs between distros, hence a check by numeric
261
                 * UID is safer. */
262
                if (user_record_is_root(u))
34✔
263
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'root' user not supported, sorry.");
×
264

265
                if (user_record_is_nobody(u))
34✔
266
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'nobody' user not supported, sorry.");
×
267

268
                if (!uid_is_valid(u->uid))
34✔
269
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot bind user with no UID, refusing.");
×
270

271
                if (u->uid >= uid_shift && u->uid < uid_shift + uid_range)
34✔
272
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name);
×
273

274
                r = groupdb_by_gid(user_record_gid(u), /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g);
34✔
275
                if (r < 0)
34✔
276
                        return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name);
×
277

278
                if (g->gid >= uid_shift && g->gid < uid_shift + uid_range)
34✔
279
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "GID of group '%s' to map is already in container GID range, refusing.", g->group_name);
×
280

281
                /* We want to synthesize exactly one user + group from the host into the container. This only
282
                 * makes sense if the user on the host has its own private group. We can't reasonably check
283
                 * this, so we just check of the name of user and group match.
284
                 *
285
                 * One of these days we might want to support users in a shared/common group too, but it's
286
                 * not clear to me how this would have to be mapped, precisely given that the common group
287
                 * probably already exists in the container. */
288
                if (!streq(u->user_name, g->group_name))
34✔
289
                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
×
290
                                               "Sorry, mapping users without private groups is currently not supported.");
291

292
                r = find_free_uid(directory, uid_range, &current_uid);
34✔
293
                if (r < 0)
34✔
294
                        return r;
295

296
                r = convert_user(directory, u, g, current_uid, bind_user_shell, bind_user_shell_copy, &cu, &cg);
34✔
297
                if (r < 0)
34✔
298
                        return r;
299

300
                if (!GREEDY_REALLOC(c->data, c->n_data + 1))
32✔
301
                        return log_oom();
×
302

303
                sm = strdup(user_record_home_directory(u));
32✔
304
                if (!sm)
32✔
305
                        return log_oom();
×
306

307
                sd = strdup(user_record_home_directory(cu));
32✔
308
                if (!sd)
32✔
309
                        return log_oom();
×
310

311
                if (!GREEDY_REALLOC(*custom_mounts, *n_custom_mounts + 1))
32✔
312
                        return log_oom();
×
313

314
                (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) {
32✔
315
                        .type = CUSTOM_MOUNT_BIND,
316
                        .source = TAKE_PTR(sm),
32✔
317
                        .destination = TAKE_PTR(sd),
32✔
318
                };
319

320
                c->data[c->n_data++] = (BindUserData) {
32✔
321
                        .host_user = TAKE_PTR(u),
32✔
322
                        .host_group = TAKE_PTR(g),
32✔
323
                        .payload_user = TAKE_PTR(cu),
32✔
324
                        .payload_group = TAKE_PTR(cg),
32✔
325
                };
326

327
                current_uid++;
32✔
328
        }
329

330
        *ret = TAKE_PTR(c);
22✔
331
        return 1;
22✔
332
}
333

334
static int write_and_symlink(
90✔
335
                const char *root,
336
                sd_json_variant *v,
337
                const char *name,
338
                uid_t uid,
339
                const char *suffix,
340
                WriteStringFileFlags extra_flags) {
341

342
        _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL;
90✔
343
        int r;
90✔
344

345
        assert(root);
90✔
346
        assert(v);
90✔
347
        assert(name);
90✔
348
        assert(uid_is_valid(uid));
90✔
349
        assert(suffix);
90✔
350

351
        r = sd_json_variant_format(v, SD_JSON_FORMAT_NEWLINE, &j);
90✔
352
        if (r < 0)
90✔
353
                return log_error_errno(r, "Failed to format user record JSON: %m");
×
354

355
        f = strjoin(name, suffix);
90✔
356
        if (!f)
90✔
357
                return log_oom();
×
358

359
        p = path_join(root, "/run/host/userdb/", f);
90✔
360
        if (!p)
90✔
361
                return log_oom();
×
362

363
        if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0)
90✔
364
                return log_oom();
×
365

366
        if (symlink(f, q) < 0)
90✔
367
                return log_error_errno(errno, "Failed to create symlink '%s': %m", q);
×
368

369
        r = userns_lchown(q, 0, 0);
90✔
370
        if (r < 0)
90✔
371
                return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q);
×
372

373
        r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags);
90✔
374
        if (r < 0)
90✔
375
                return log_error_errno(r, "Failed to write %s: %m", p);
×
376

377
        r = userns_lchown(p, 0, 0);
90✔
378
        if (r < 0)
90✔
379
                return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
×
380

381
        return 0;
382
}
383

384
int bind_user_setup(
234✔
385
                const BindUserContext *c,
386
                const char *root) {
387

388
        static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
234✔
389
                USER_RECORD_LOAD_MASK_PRIVILEGED|
390
                USER_RECORD_PERMISSIVE;
391
        static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */
234✔
392
                USER_RECORD_EXTRACT_PRIVILEGED|
393
                USER_RECORD_EMPTY_OK|
394
                USER_RECORD_PERMISSIVE;
395
        int r;
234✔
396

397
        assert(root);
234✔
398

399
        if (!c || c->n_data == 0)
234✔
400
                return 0;
401

402
        r = make_run_host(root);
22✔
403
        if (r < 0)
22✔
404
                return r;
405

406
        r = userns_mkdir(root, "/run/host/home", 0755, 0, 0);
22✔
407
        if (r < 0)
22✔
408
                return log_error_errno(r, "Failed to create /run/host/home: %m");
×
409

410
        r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0);
22✔
411
        if (r < 0)
22✔
412
                return log_error_errno(r, "Failed to create /run/host/userdb: %m");
×
413

414
        FOREACH_ARRAY(d, c->data, c->n_data) {
52✔
415
                _cleanup_(group_record_unrefp) GroupRecord *stripped_group = NULL, *shadow_group = NULL;
30✔
416
                _cleanup_(user_record_unrefp) UserRecord *stripped_user = NULL, *shadow_user = NULL;
30✔
417

418
                /* First, write shadow (i.e. privileged) data for group record */
419
                r = group_record_clone(d->payload_group, shadow_flags, &shadow_group);
30✔
420
                if (r < 0)
30✔
421
                        return log_error_errno(r, "Failed to extract privileged information from group record: %m");
×
422

423
                if (!sd_json_variant_is_blank_object(shadow_group->json)) {
30✔
424
                        r = write_and_symlink(
×
425
                                        root,
426
                                        shadow_group->json,
×
427
                                        d->payload_group->group_name,
×
428
                                        d->payload_group->gid,
×
429
                                        ".group-privileged",
430
                                        WRITE_STRING_FILE_MODE_0600);
431
                        if (r < 0)
×
432
                                return r;
433
                }
434

435
                /* Second, write main part of group record. */
436
                r = group_record_clone(d->payload_group, strip_flags, &stripped_group);
30✔
437
                if (r < 0)
30✔
438
                        return log_error_errno(r, "Failed to strip privileged information from group record: %m");
×
439

440
                r = write_and_symlink(
60✔
441
                                root,
442
                                stripped_group->json,
30✔
443
                                d->payload_group->group_name,
30✔
444
                                d->payload_group->gid,
30✔
445
                                ".group",
446
                                0);
447
                if (r < 0)
30✔
448
                        return r;
449

450
                /* Third, write out user shadow data. i.e. extract privileged info from user record */
451
                r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
30✔
452
                if (r < 0)
30✔
453
                        return log_error_errno(r, "Failed to extract privileged information from user record: %m");
×
454

455
                if (!sd_json_variant_is_blank_object(shadow_user->json)) {
30✔
456
                        r = write_and_symlink(
60✔
457
                                        root,
458
                                        shadow_user->json,
30✔
459
                                        d->payload_user->user_name,
30✔
460
                                        d->payload_user->uid,
30✔
461
                                        ".user-privileged",
462
                                        WRITE_STRING_FILE_MODE_0600);
463
                        if (r < 0)
30✔
464
                                return r;
465
                }
466

467
                /* Finally write out the main part of the user record */
468
                r = user_record_clone(d->payload_user, strip_flags, &stripped_user);
30✔
469
                if (r < 0)
30✔
470
                        return log_error_errno(r, "Failed to strip privileged information from user record: %m");
×
471

472
                r = write_and_symlink(
60✔
473
                                root,
474
                                stripped_user->json,
30✔
475
                                d->payload_user->user_name,
30✔
476
                                d->payload_user->uid,
30✔
477
                                ".user",
478
                                0);
479
                if (r < 0)
30✔
480
                        return r;
481
        }
482

483
        return 1;
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc