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

systemd / systemd / 14895667988

07 May 2025 08:57PM UTC coverage: 72.225% (-0.007%) from 72.232%
14895667988

push

github

yuwata
network: log_link_message_debug_errno() automatically append %m if necessary

Follow-up for d28746ef5.
Fixes CID#1609753.

0 of 1 new or added line in 1 file covered. (0.0%)

20297 existing lines in 338 files now uncovered.

297407 of 411780 relevant lines covered (72.22%)

695716.85 hits per line

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

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

3
#include "alloc-util.h"
4
#include "chase.h"
5
#include "fd-util.h"
6
#include "fileio.h"
7
#include "format-util.h"
8
#include "json-util.h"
9
#include "log.h"
10
#include "nspawn.h"
11
#include "nspawn-bind-user.h"
12
#include "path-util.h"
13
#include "user-util.h"
14
#include "userdb.h"
15

16
static int check_etc_passwd_collisions(
28✔
17
                const char *directory,
18
                const char *name,
19
                uid_t uid) {
20

21
        _cleanup_fclose_ FILE *f = NULL;
28✔
22
        int r;
28✔
23

24
        assert(directory);
28✔
25
        assert(name || uid_is_valid(uid));
28✔
26

27
        r = chase_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
28✔
28
        if (r == -ENOENT)
28✔
29
                return 0; /* no user database? then no user, hence no collision */
30
        if (r < 0)
24✔
UNCOV
31
                return log_error_errno(r, "Failed to open /etc/passwd of container: %m");
×
32

33
        for (;;) {
183✔
34
                struct passwd *pw;
207✔
35

36
                r = fgetpwent_sane(f, &pw);
207✔
37
                if (r < 0)
207✔
38
                        return log_error_errno(r, "Failed to iterate through /etc/passwd of container: %m");
24✔
39
                if (r == 0) /* EOF */
207✔
40
                        return 0; /* no collision */
41

42
                if (name && streq_ptr(pw->pw_name, name))
184✔
43
                        return 1; /* name collision */
44
                if (uid_is_valid(uid) && pw->pw_uid == uid)
183✔
45
                        return 1; /* UID collision */
46
        }
47
}
48

49
static int check_etc_group_collisions(
27✔
50
                const char *directory,
51
                const char *name,
52
                gid_t gid) {
53

54
        _cleanup_fclose_ FILE *f = NULL;
27✔
55
        int r;
27✔
56

57
        assert(directory);
27✔
58
        assert(name || gid_is_valid(gid));
41✔
59

60
        r = chase_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
27✔
61
        if (r == -ENOENT)
27✔
62
                return 0; /* no group database? then no group, hence no collision */
63
        if (r < 0)
27✔
UNCOV
64
                return log_error_errno(r, "Failed to open /etc/group of container: %m");
×
65

66
        for (;;) {
532✔
67
                struct group *gr;
559✔
68

69
                r = fgetgrent_sane(f, &gr);
559✔
70
                if (r < 0)
559✔
71
                        return log_error_errno(r, "Failed to iterate through /etc/group of container: %m");
27✔
72
                if (r == 0)
559✔
73
                        return 0; /* no collision */
74

75
                if (name && streq_ptr(gr->gr_name, name))
533✔
76
                        return 1; /* name collision */
77
                if (gid_is_valid(gid) && gr->gr_gid == gid)
532✔
78
                        return 1; /* gid collision */
79
        }
80
}
81

82
static int convert_user(
14✔
83
                const char *directory,
84
                UserRecord *u,
85
                GroupRecord *g,
86
                uid_t allocate_uid,
87
                UserRecord **ret_converted_user,
88
                GroupRecord **ret_converted_group) {
89

90
        _cleanup_(group_record_unrefp) GroupRecord *converted_group = NULL;
14✔
UNCOV
91
        _cleanup_(user_record_unrefp) UserRecord *converted_user = NULL;
×
92
        _cleanup_free_ char *h = NULL;
14✔
93
        sd_json_variant *p, *hp = NULL, *ssh = NULL;
14✔
94
        int r;
14✔
95

96
        assert(u);
14✔
97
        assert(g);
14✔
98
        assert(user_record_gid(u) == g->gid);
14✔
99

100
        r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
14✔
101
        if (r < 0)
14✔
102
                return r;
103
        if (r > 0)
14✔
104
                return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
1✔
105
                                       "Sorry, the user '%s' already exists in the container.", u->user_name);
106

107
        r = check_etc_group_collisions(directory, g->group_name, GID_INVALID);
13✔
108
        if (r < 0)
13✔
109
                return r;
110
        if (r > 0)
13✔
111
                return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
1✔
112
                                       "Sorry, the group '%s' already exists in the container.", g->group_name);
113

114
        h = path_join("/run/host/home/", u->user_name);
12✔
115
        if (!h)
12✔
UNCOV
116
                return log_oom();
×
117

118
        /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
119
        p = sd_json_variant_by_key(u->json, "privileged");
12✔
120
        if (p) {
12✔
121
                hp = sd_json_variant_by_key(p, "hashedPassword");
12✔
122
                ssh = sd_json_variant_by_key(p, "sshAuthorizedKeys");
12✔
123
        }
124

125
        r = user_record_build(
36✔
126
                        &converted_user,
127
                        SD_JSON_BUILD_OBJECT(
24✔
128
                                        SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(u->user_name)),
129
                                        SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
130
                                        SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
131
                                        SD_JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(u->disposition))),
132
                                        SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)),
133
                                        SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
134
                                        SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT(
135
                                                                           SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)),
136
                                                                           SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh))))));
137
        if (r < 0)
12✔
UNCOV
138
                return log_error_errno(r, "Failed to build container user record: %m");
×
139

140
        r = group_record_build(
36✔
141
                        &converted_group,
142
                        SD_JSON_BUILD_OBJECT(
12✔
143
                                        SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(g->group_name)),
144
                                        SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(allocate_uid)),
145
                                        SD_JSON_BUILD_PAIR_CONDITION(g->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(g->disposition))),
146
                                        SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn"))));
147
        if (r < 0)
12✔
UNCOV
148
                return log_error_errno(r, "Failed to build container group record: %m");
×
149

150
        *ret_converted_user = TAKE_PTR(converted_user);
12✔
151
        *ret_converted_group = TAKE_PTR(converted_group);
12✔
152

153
        return 0;
12✔
154
}
155

156
static int find_free_uid(const char *directory, uid_t max_uid, uid_t *current_uid) {
14✔
157
        int r;
14✔
158

159
        assert(directory);
14✔
160
        assert(current_uid);
14✔
161

UNCOV
162
        for (;; (*current_uid)++) {
×
163
                if (*current_uid > MAP_UID_MAX || *current_uid > max_uid)
14✔
UNCOV
164
                        return log_error_errno(
×
165
                                        SYNTHETIC_ERRNO(EBUSY),
166
                                        "No suitable available UID in range " UID_FMT "…" UID_FMT " in container detected, can't map user.",
167
                                        MAP_UID_MIN, MAP_UID_MAX);
168

169
                r = check_etc_passwd_collisions(directory, NULL, *current_uid);
14✔
170
                if (r < 0)
14✔
171
                        return r;
172
                if (r > 0) /* already used */
14✔
UNCOV
173
                        continue;
×
174

175
                /* We want to use the UID also as GID, hence check for it in /etc/group too */
176
                r = check_etc_group_collisions(directory, NULL, (gid_t) *current_uid);
14✔
177
                if (r <= 0)
14✔
178
                        return r;
179
        }
180
}
181

182
BindUserContext* bind_user_context_free(BindUserContext *c) {
6✔
183
        if (!c)
6✔
184
                return NULL;
185

186
        FOREACH_ARRAY(d, c->data, c->n_data) {
13✔
187
                user_record_unref(d->host_user);
7✔
188
                group_record_unref(d->host_group);
7✔
189
                user_record_unref(d->payload_user);
7✔
190
                group_record_unref(d->payload_group);
7✔
191
        }
192

193
        return mfree(c);
6✔
194
}
195

196
int bind_user_prepare(
220✔
197
                const char *directory,
198
                char **bind_user,
199
                uid_t uid_shift,
200
                uid_t uid_range,
201
                CustomMount **custom_mounts,
202
                size_t *n_custom_mounts,
203
                BindUserContext **ret) {
204

205
        _cleanup_(bind_user_context_freep) BindUserContext *c = NULL;
220✔
206
        uid_t current_uid = MAP_UID_MIN;
220✔
207
        int r;
220✔
208

209
        assert(custom_mounts);
220✔
210
        assert(n_custom_mounts);
220✔
211
        assert(ret);
220✔
212

213
        /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
214
         * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
215
         * to include an appropriate bind mount mapping.
216
         *
217
         * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
218
         * new BindUserContext for the user records */
219

220
        if (strv_isempty(bind_user)) {
220✔
221
                *ret = NULL;
210✔
222
                return 0;
210✔
223
        }
224

225
        c = new0(BindUserContext, 1);
10✔
226
        if (!c)
10✔
UNCOV
227
                return log_oom();
×
228

229
        STRV_FOREACH(n, bind_user) {
22✔
230
                _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL;
14✔
231
                _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL;
14✔
232
                _cleanup_free_ char *sm = NULL, *sd = NULL;
14✔
233

234
                r = userdb_by_name(*n, /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u);
14✔
235
                if (r < 0)
14✔
UNCOV
236
                        return log_error_errno(r, "Failed to resolve user '%s': %m", *n);
×
237

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

250
                if (user_record_is_nobody(u))
14✔
UNCOV
251
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'nobody' user not supported, sorry.");
×
252

253
                if (!uid_is_valid(u->uid))
14✔
UNCOV
254
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot bind user with no UID, refusing.");
×
255

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

259
                r = groupdb_by_gid(user_record_gid(u), /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g);
14✔
260
                if (r < 0)
14✔
UNCOV
261
                        return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name);
×
262

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

266
                /* We want to synthesize exactly one user + group from the host into the container. This only
267
                 * makes sense if the user on the host has its own private group. We can't reasonably check
268
                 * this, so we just check of the name of user and group match.
269
                 *
270
                 * One of these days we might want to support users in a shared/common group too, but it's
271
                 * not clear to me how this would have to be mapped, precisely given that the common group
272
                 * probably already exists in the container. */
273
                if (!streq(u->user_name, g->group_name))
14✔
UNCOV
274
                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
×
275
                                               "Sorry, mapping users without private groups is currently not supported.");
276

277
                r = find_free_uid(directory, uid_range, &current_uid);
14✔
278
                if (r < 0)
14✔
279
                        return r;
280

281
                r = convert_user(directory, u, g, current_uid, &cu, &cg);
14✔
282
                if (r < 0)
14✔
283
                        return r;
284

285
                if (!GREEDY_REALLOC(c->data, c->n_data + 1))
12✔
UNCOV
286
                        return log_oom();
×
287

288
                sm = strdup(user_record_home_directory(u));
12✔
289
                if (!sm)
12✔
UNCOV
290
                        return log_oom();
×
291

292
                sd = strdup(user_record_home_directory(cu));
12✔
293
                if (!sd)
12✔
UNCOV
294
                        return log_oom();
×
295

296
                if (!GREEDY_REALLOC(*custom_mounts, *n_custom_mounts + 1))
12✔
UNCOV
297
                        return log_oom();
×
298

299
                (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) {
12✔
300
                        .type = CUSTOM_MOUNT_BIND,
301
                        .source = TAKE_PTR(sm),
12✔
302
                        .destination = TAKE_PTR(sd),
12✔
303
                };
304

305
                c->data[c->n_data++] = (BindUserData) {
12✔
306
                        .host_user = TAKE_PTR(u),
12✔
307
                        .host_group = TAKE_PTR(g),
12✔
308
                        .payload_user = TAKE_PTR(cu),
12✔
309
                        .payload_group = TAKE_PTR(cg),
12✔
310
                };
311

312
                current_uid++;
12✔
313
        }
314

315
        *ret = TAKE_PTR(c);
8✔
316
        return 1;
8✔
317
}
318

319
static int write_and_symlink(
30✔
320
                const char *root,
321
                sd_json_variant *v,
322
                const char *name,
323
                uid_t uid,
324
                const char *suffix,
325
                WriteStringFileFlags extra_flags) {
326

327
        _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL;
30✔
328
        int r;
30✔
329

330
        assert(root);
30✔
331
        assert(v);
30✔
332
        assert(name);
30✔
333
        assert(uid_is_valid(uid));
30✔
334
        assert(suffix);
30✔
335

336
        r = sd_json_variant_format(v, SD_JSON_FORMAT_NEWLINE, &j);
30✔
337
        if (r < 0)
30✔
UNCOV
338
                return log_error_errno(r, "Failed to format user record JSON: %m");
×
339

340
        f = strjoin(name, suffix);
30✔
341
        if (!f)
30✔
UNCOV
342
                return log_oom();
×
343

344
        p = path_join(root, "/run/host/userdb/", f);
30✔
345
        if (!p)
30✔
UNCOV
346
                return log_oom();
×
347

348
        if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0)
30✔
UNCOV
349
                return log_oom();
×
350

351
        if (symlink(f, q) < 0)
30✔
UNCOV
352
                return log_error_errno(errno, "Failed to create symlink '%s': %m", q);
×
353

354
        r = userns_lchown(q, 0, 0);
30✔
355
        if (r < 0)
30✔
UNCOV
356
                return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q);
×
357

358
        r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags);
30✔
359
        if (r < 0)
30✔
UNCOV
360
                return log_error_errno(r, "Failed to write %s: %m", p);
×
361

362
        r = userns_lchown(p, 0, 0);
30✔
363
        if (r < 0)
30✔
UNCOV
364
                return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
×
365

366
        return 0;
367
}
368

369
int bind_user_setup(
218✔
370
                const BindUserContext *c,
371
                const char *root) {
372

373
        static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
218✔
374
                USER_RECORD_LOAD_MASK_PRIVILEGED|
375
                USER_RECORD_PERMISSIVE;
376
        static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */
218✔
377
                USER_RECORD_EXTRACT_PRIVILEGED|
378
                USER_RECORD_EMPTY_OK|
379
                USER_RECORD_PERMISSIVE;
380
        int r;
218✔
381

382
        assert(root);
218✔
383

384
        if (!c || c->n_data == 0)
218✔
385
                return 0;
386

387
        r = make_run_host(root);
8✔
388
        if (r < 0)
8✔
389
                return r;
390

391
        r = userns_mkdir(root, "/run/host/home", 0755, 0, 0);
8✔
392
        if (r < 0)
8✔
UNCOV
393
                return log_error_errno(r, "Failed to create /run/host/home: %m");
×
394

395
        r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0);
8✔
396
        if (r < 0)
8✔
UNCOV
397
                return log_error_errno(r, "Failed to create /run/host/userdb: %m");
×
398

399
        FOREACH_ARRAY(d, c->data, c->n_data) {
18✔
400
                _cleanup_(group_record_unrefp) GroupRecord *stripped_group = NULL, *shadow_group = NULL;
10✔
401
                _cleanup_(user_record_unrefp) UserRecord *stripped_user = NULL, *shadow_user = NULL;
10✔
402

403
                /* First, write shadow (i.e. privileged) data for group record */
404
                r = group_record_clone(d->payload_group, shadow_flags, &shadow_group);
10✔
405
                if (r < 0)
10✔
UNCOV
406
                        return log_error_errno(r, "Failed to extract privileged information from group record: %m");
×
407

408
                if (!sd_json_variant_is_blank_object(shadow_group->json)) {
10✔
UNCOV
409
                        r = write_and_symlink(
×
410
                                        root,
411
                                        shadow_group->json,
×
412
                                        d->payload_group->group_name,
×
UNCOV
413
                                        d->payload_group->gid,
×
414
                                        ".group-privileged",
415
                                        WRITE_STRING_FILE_MODE_0600);
UNCOV
416
                        if (r < 0)
×
417
                                return r;
418
                }
419

420
                /* Second, write main part of group record. */
421
                r = group_record_clone(d->payload_group, strip_flags, &stripped_group);
10✔
422
                if (r < 0)
10✔
UNCOV
423
                        return log_error_errno(r, "Failed to strip privileged information from group record: %m");
×
424

425
                r = write_and_symlink(
20✔
426
                                root,
427
                                stripped_group->json,
10✔
428
                                d->payload_group->group_name,
10✔
429
                                d->payload_group->gid,
10✔
430
                                ".group",
431
                                0);
432
                if (r < 0)
10✔
433
                        return r;
434

435
                /* Third, write out user shadow data. i.e. extract privileged info from user record */
436
                r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
10✔
437
                if (r < 0)
10✔
UNCOV
438
                        return log_error_errno(r, "Failed to extract privileged information from user record: %m");
×
439

440
                if (!sd_json_variant_is_blank_object(shadow_user->json)) {
10✔
441
                        r = write_and_symlink(
20✔
442
                                        root,
443
                                        shadow_user->json,
10✔
444
                                        d->payload_user->user_name,
10✔
445
                                        d->payload_user->uid,
10✔
446
                                        ".user-privileged",
447
                                        WRITE_STRING_FILE_MODE_0600);
448
                        if (r < 0)
10✔
449
                                return r;
450
                }
451

452
                /* Finally write out the main part of the user record */
453
                r = user_record_clone(d->payload_user, strip_flags, &stripped_user);
10✔
454
                if (r < 0)
10✔
UNCOV
455
                        return log_error_errno(r, "Failed to strip privileged information from user record: %m");
×
456

457
                r = write_and_symlink(
20✔
458
                                root,
459
                                stripped_user->json,
10✔
460
                                d->payload_user->user_name,
10✔
461
                                d->payload_user->uid,
10✔
462
                                ".user",
463
                                0);
464
                if (r < 0)
10✔
465
                        return r;
466
        }
467

468
        return 1;
469
}
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