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

systemd / systemd / 19484860156

18 Nov 2025 10:54PM UTC coverage: 72.449% (-0.05%) from 72.503%
19484860156

push

github

web-flow
Improve systemd-analyze man page and bash completion (#39778)

This updates example output in systemd-analyze's man page after the
tool's output was changed in a previous commit.

Additionally bash completion is added for `systemd-analyze filesystems`
and improved for `systemd-analyze calendar`.

308071 of 425226 relevant lines covered (72.45%)

1286949.26 hits per line

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

86.13
/src/shared/machine-bind-user.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

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

6
#include "alloc-util.h"
7
#include "chase.h"
8
#include "fd-util.h"
9
#include "format-util.h"
10
#include "json-util.h"
11
#include "log.h"
12
#include "machine-bind-user.h"
13
#include "path-util.h"
14
#include "string-util.h"
15
#include "strv.h"
16
#include "user-util.h"
17
#include "userdb.h"
18

19
static int check_etc_passwd_collisions(
68✔
20
                const char *directory,
21
                const char *name,
22
                uid_t uid) {
23

24
        _cleanup_fclose_ FILE *f = NULL;
68✔
25
        int r;
68✔
26

27
        assert(name || uid_is_valid(uid));
68✔
28

29
        if (!directory)
68✔
30
                return 0;
31

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

38
        for (;;) {
543✔
39
                struct passwd *pw;
607✔
40

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

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

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

59
        _cleanup_fclose_ FILE *f = NULL;
67✔
60
        int r;
67✔
61

62
        assert(name || gid_is_valid(gid));
101✔
63

64
        if (!directory)
67✔
65
                return 0;
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 machine: %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 machine: %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
                const char *home_mount_directory,
97
                char **groups,
98
                UserRecord **ret_converted_user,
99
                GroupRecord **ret_converted_group) {
100

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

107
        assert(u);
34✔
108
        assert(g);
34✔
109
        assert(user_record_gid(u) == g->gid);
34✔
110

111
        if (shell_copy)
34✔
112
                shell = u->shell;
6✔
113

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

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

128
        h = path_join(home_mount_directory, u->user_name);
32✔
129
        if (!h)
32✔
130
                return log_oom();
×
131

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

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

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

166
        *ret_converted_user = TAKE_PTR(converted_user);
32✔
167
        *ret_converted_group = TAKE_PTR(converted_group);
32✔
168

169
        return 0;
32✔
170
}
171

172
static int find_free_uid(const char *directory, uid_t *current_uid) {
34✔
173
        int r;
34✔
174

175
        assert(current_uid);
34✔
176

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

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

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

197
MachineBindUserContext* machine_bind_user_context_free(MachineBindUserContext *c) {
13✔
198
        if (!c)
13✔
199
                return NULL;
200

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

208
        return mfree(c);
13✔
209
}
210

211
int machine_bind_user_prepare(
256✔
212
                const char *directory,
213
                char **bind_user,
214
                const char *bind_user_shell,
215
                bool bind_user_shell_copy,
216
                const char *bind_user_home_mount_directory,
217
                char **bind_user_groups,
218
                MachineBindUserContext **ret) {
219

220
        _cleanup_(machine_bind_user_context_freep) MachineBindUserContext *c = NULL;
256✔
221
        uid_t current_uid = MAP_UID_MIN;
256✔
222
        int r;
256✔
223

224
        assert(ret);
256✔
225

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

233
        if (strv_isempty(bind_user)) {
256✔
234
                *ret = NULL;
232✔
235
                return 0;
232✔
236
        }
237

238
        c = new0(MachineBindUserContext, 1);
24✔
239
        if (!c)
24✔
240
                return log_oom();
×
241

242
        STRV_FOREACH(n, bind_user) {
56✔
243
                _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL;
34✔
244
                _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL;
34✔
245

246
                r = userdb_by_name(*n, /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u);
34✔
247
                if (r == -ENOEXEC)
34✔
248
                        return log_error_errno(r, "User '%s' did not pass filter.", *n);
×
249
                if (r < 0)
34✔
250
                        return log_error_errno(r, "Failed to resolve user '%s': %s", *n, STRERROR_USER(r));
×
251

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

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

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

270
                r = groupdb_by_gid(user_record_gid(u), /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g);
34✔
271
                if (r == -ENOEXEC)
34✔
272
                        return log_error_errno(r, "Group of user '%s' did not pass filter.", u->user_name);
×
273
                if (r < 0)
34✔
274
                        return log_error_errno(r, "Failed to resolve group of user '%s': %s",
×
275
                                               u->user_name, STRERROR_GROUP(r));
276

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

288
                r = find_free_uid(directory, &current_uid);
34✔
289
                if (r < 0)
34✔
290
                        return r;
291

292
                r = convert_user(
34✔
293
                                directory,
294
                                u, g,
295
                                current_uid,
296
                                bind_user_shell,
297
                                bind_user_shell_copy,
298
                                bind_user_home_mount_directory,
299
                                bind_user_groups,
300
                                &cu, &cg);
301
                if (r < 0)
34✔
302
                        return r;
303

304
                if (!GREEDY_REALLOC(c->data, c->n_data + 1))
32✔
305
                        return log_oom();
×
306

307
                c->data[c->n_data++] = (MachineBindUserData) {
32✔
308
                        .host_user = TAKE_PTR(u),
32✔
309
                        .host_group = TAKE_PTR(g),
32✔
310
                        .payload_user = TAKE_PTR(cu),
32✔
311
                        .payload_group = TAKE_PTR(cg),
32✔
312
                };
313

314
                current_uid++;
32✔
315
        }
316

317
        *ret = TAKE_PTR(c);
22✔
318
        return 1;
22✔
319
}
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