1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
3 #include "chase-symlinks.h"
6 #include "format-util.h"
7 #include "nspawn-bind-user.h"
10 #include "user-util.h"
13 static int check_etc_passwd_collisions(
14 const char *directory
,
18 _cleanup_fclose_
FILE *f
= NULL
;
22 assert(name
|| uid_is_valid(uid
));
24 r
= chase_symlinks_and_fopen_unlocked("/etc/passwd", directory
, CHASE_PREFIX_ROOT
, "re", NULL
, &f
);
26 return 0; /* no user database? then no user, hence no collision */
28 return log_error_errno(r
, "Failed to open /etc/passwd of container: %m");
33 r
= fgetpwent_sane(f
, &pw
);
35 return log_error_errno(r
, "Failed to iterate through /etc/passwd of container: %m");
37 return 0; /* no collision */
39 if (name
&& streq_ptr(pw
->pw_name
, name
))
40 return 1; /* name collision */
41 if (uid_is_valid(uid
) && pw
->pw_uid
== uid
)
42 return 1; /* UID collision */
46 static int check_etc_group_collisions(
47 const char *directory
,
51 _cleanup_fclose_
FILE *f
= NULL
;
55 assert(name
|| gid_is_valid(gid
));
57 r
= chase_symlinks_and_fopen_unlocked("/etc/group", directory
, CHASE_PREFIX_ROOT
, "re", NULL
, &f
);
59 return 0; /* no group database? then no group, hence no collision */
61 return log_error_errno(r
, "Failed to open /etc/group of container: %m");
66 r
= fgetgrent_sane(f
, &gr
);
68 return log_error_errno(r
, "Failed to iterate through /etc/group of container: %m");
70 return 0; /* no collision */
72 if (name
&& streq_ptr(gr
->gr_name
, name
))
73 return 1; /* name collision */
74 if (gid_is_valid(gid
) && gr
->gr_gid
== gid
)
75 return 1; /* gid collision */
79 static int convert_user(
80 const char *directory
,
84 UserRecord
**ret_converted_user
,
85 GroupRecord
**ret_converted_group
) {
87 _cleanup_(group_record_unrefp
) GroupRecord
*converted_group
= NULL
;
88 _cleanup_(user_record_unrefp
) UserRecord
*converted_user
= NULL
;
89 _cleanup_free_
char *h
= NULL
;
90 JsonVariant
*p
, *hp
= NULL
;
95 assert(u
->gid
== g
->gid
);
97 r
= check_etc_passwd_collisions(directory
, u
->user_name
, UID_INVALID
);
101 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
102 "Sorry, the user '%s' already exists in the container.", u
->user_name
);
104 r
= check_etc_group_collisions(directory
, g
->group_name
, GID_INVALID
);
108 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
109 "Sorry, the group '%s' already exists in the container.", g
->group_name
);
111 h
= path_join("/run/host/home/", u
->user_name
);
115 /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
116 p
= json_variant_by_key(u
->json
, "privileged");
118 hp
= json_variant_by_key(p
, "hashedPassword");
120 r
= user_record_build(
123 JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(u
->user_name
)),
124 JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(allocate_uid
)),
125 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid
)),
126 JSON_BUILD_PAIR_CONDITION(u
->disposition
>= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(u
->disposition
))),
127 JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(h
)),
128 JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
129 JSON_BUILD_PAIR_CONDITION(!strv_isempty(u
->hashed_password
), "privileged", JSON_BUILD_OBJECT(
130 JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_VARIANT(hp
))))));
132 return log_error_errno(r
, "Failed to build container user record: %m");
134 r
= group_record_build(
137 JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g
->group_name
)),
138 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid
)),
139 JSON_BUILD_PAIR_CONDITION(g
->disposition
>= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(g
->disposition
))),
140 JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn"))));
142 return log_error_errno(r
, "Failed to build container group record: %m");
144 *ret_converted_user
= TAKE_PTR(converted_user
);
145 *ret_converted_group
= TAKE_PTR(converted_group
);
150 static int find_free_uid(const char *directory
, uid_t max_uid
, uid_t
*current_uid
) {
156 for (;; (*current_uid
) ++) {
157 if (*current_uid
> MAP_UID_MAX
|| *current_uid
> max_uid
)
158 return log_error_errno(
159 SYNTHETIC_ERRNO(EBUSY
),
160 "No suitable available UID in range " UID_FMT
"…" UID_FMT
" in container detected, can't map user.",
161 MAP_UID_MIN
, MAP_UID_MAX
);
163 r
= check_etc_passwd_collisions(directory
, NULL
, *current_uid
);
166 if (r
> 0) /* already used */
169 /* We want to use the UID also as GID, hence check for it in /etc/group too */
170 r
= check_etc_group_collisions(directory
, NULL
, (gid_t
) *current_uid
);
176 BindUserContext
* bind_user_context_free(BindUserContext
*c
) {
180 assert(c
->n_data
== 0 || c
->data
);
182 for (size_t i
= 0; i
< c
->n_data
; i
++) {
183 user_record_unref(c
->data
[i
].host_user
);
184 group_record_unref(c
->data
[i
].host_group
);
185 user_record_unref(c
->data
[i
].payload_user
);
186 group_record_unref(c
->data
[i
].payload_group
);
192 int bind_user_prepare(
193 const char *directory
,
197 CustomMount
**custom_mounts
,
198 size_t *n_custom_mounts
,
199 BindUserContext
**ret
) {
201 _cleanup_(bind_user_context_freep
) BindUserContext
*c
= NULL
;
202 uid_t current_uid
= MAP_UID_MIN
;
205 assert(custom_mounts
);
206 assert(n_custom_mounts
);
209 /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
210 * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
211 * to include an appropriate bind mount mapping.
213 * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
214 * new BindUserContext for the user records */
216 if (strv_isempty(bind_user
)) {
221 c
= new0(BindUserContext
, 1);
225 STRV_FOREACH(n
, bind_user
) {
226 _cleanup_(user_record_unrefp
) UserRecord
*u
= NULL
, *cu
= NULL
;
227 _cleanup_(group_record_unrefp
) GroupRecord
*g
= NULL
, *cg
= NULL
;
228 _cleanup_free_
char *sm
= NULL
, *sd
= NULL
;
231 r
= userdb_by_name(*n
, USERDB_DONT_SYNTHESIZE
, &u
);
233 return log_error_errno(r
, "Failed to resolve user '%s': %m", *n
);
235 /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
236 * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
237 * even if we wanted, we couldn't override the root or nobody user records. Note we also
238 * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
239 * out root/nobody too, hence these checks might appear redundant — but they actually are
240 * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
241 * and the user/group databases fully synthesized at runtime. Moreover, the name of the
242 * user/group name of the "nobody" account differs between distros, hence a check by numeric
244 if (u
->uid
== 0 || streq(u
->user_name
, "root"))
245 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'root' user not supported, sorry.");
246 if (u
->uid
== UID_NOBODY
|| STR_IN_SET(u
->user_name
, NOBODY_USER_NAME
, "nobody"))
247 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'nobody' user not supported, sorry.");
249 if (u
->uid
>= uid_shift
&& u
->uid
< uid_shift
+ uid_range
)
250 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "UID of user '%s' to map is already in container UID range, refusing.", u
->user_name
);
252 r
= groupdb_by_gid(u
->gid
, USERDB_DONT_SYNTHESIZE
, &g
);
254 return log_error_errno(r
, "Failed to resolve group of user '%s': %m", u
->user_name
);
256 if (g
->gid
>= uid_shift
&& g
->gid
< uid_shift
+ uid_range
)
257 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "GID of group '%s' to map is already in container GID range, refusing.", g
->group_name
);
259 /* We want to synthesize exactly one user + group from the host into the container. This only
260 * makes sense if the user on the host has its own private group. We can't reasonably check
261 * this, so we just check of the name of user and group match.
263 * One of these days we might want to support users in a shared/common group too, but it's
264 * not clear to me how this would have to be mapped, precisely given that the common group
265 * probably already exists in the container. */
266 if (!streq(u
->user_name
, g
->group_name
))
267 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
268 "Sorry, mapping users without private groups is currently not supported.");
270 r
= find_free_uid(directory
, uid_range
, ¤t_uid
);
274 r
= convert_user(directory
, u
, g
, current_uid
, &cu
, &cg
);
278 if (!GREEDY_REALLOC(c
->data
, c
->n_data
+ 1))
281 sm
= strdup(u
->home_directory
);
285 sd
= strdup(cu
->home_directory
);
289 cm
= reallocarray(*custom_mounts
, sizeof(CustomMount
), *n_custom_mounts
+ 1);
295 (*custom_mounts
)[(*n_custom_mounts
)++] = (CustomMount
) {
296 .type
= CUSTOM_MOUNT_BIND
,
297 .source
= TAKE_PTR(sm
),
298 .destination
= TAKE_PTR(sd
),
301 c
->data
[c
->n_data
++] = (BindUserData
) {
302 .host_user
= TAKE_PTR(u
),
303 .host_group
= TAKE_PTR(g
),
304 .payload_user
= TAKE_PTR(cu
),
305 .payload_group
= TAKE_PTR(cg
),
315 static int write_and_symlink(
321 WriteStringFileFlags extra_flags
) {
323 _cleanup_free_
char *j
= NULL
, *f
= NULL
, *p
= NULL
, *q
= NULL
;
329 assert(uid_is_valid(uid
));
332 r
= json_variant_format(v
, JSON_FORMAT_NEWLINE
, &j
);
334 return log_error_errno(r
, "Failed to format user record JSON: %m");
336 f
= strjoin(name
, suffix
);
340 p
= path_join(root
, "/run/host/userdb/", f
);
344 if (asprintf(&q
, "%s/run/host/userdb/" UID_FMT
"%s", root
, uid
, suffix
) < 0)
347 if (symlink(f
, q
) < 0)
348 return log_error_errno(errno
, "Failed to create symlink '%s': %m", q
);
350 r
= userns_lchown(q
, 0, 0);
352 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", q
);
354 r
= write_string_file(p
, j
, WRITE_STRING_FILE_CREATE
|extra_flags
);
356 return log_error_errno(r
, "Failed to write %s: %m", p
);
358 r
= userns_lchown(p
, 0, 0);
360 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", p
);
366 const BindUserContext
*c
,
369 static const UserRecordLoadFlags strip_flags
= /* Removes privileged info */
370 USER_RECORD_REQUIRE_REGULAR
|
371 USER_RECORD_STRIP_PRIVILEGED
|
372 USER_RECORD_ALLOW_PER_MACHINE
|
373 USER_RECORD_ALLOW_BINDING
|
374 USER_RECORD_ALLOW_SIGNATURE
|
375 USER_RECORD_PERMISSIVE
;
376 static const UserRecordLoadFlags shadow_flags
= /* Extracts privileged info */
377 USER_RECORD_STRIP_REGULAR
|
378 USER_RECORD_ALLOW_PRIVILEGED
|
379 USER_RECORD_STRIP_PER_MACHINE
|
380 USER_RECORD_STRIP_BINDING
|
381 USER_RECORD_STRIP_SIGNATURE
|
382 USER_RECORD_EMPTY_OK
|
383 USER_RECORD_PERMISSIVE
;
388 if (!c
|| c
->n_data
== 0)
391 r
= userns_mkdir(root
, "/run/host", 0755, 0, 0);
393 return log_error_errno(r
, "Failed to create /run/host: %m");
395 r
= userns_mkdir(root
, "/run/host/home", 0755, 0, 0);
397 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
399 r
= userns_mkdir(root
, "/run/host/userdb", 0755, 0, 0);
401 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
403 for (size_t i
= 0; i
< c
->n_data
; i
++) {
404 _cleanup_(group_record_unrefp
) GroupRecord
*stripped_group
= NULL
, *shadow_group
= NULL
;
405 _cleanup_(user_record_unrefp
) UserRecord
*stripped_user
= NULL
, *shadow_user
= NULL
;
406 const BindUserData
*d
= c
->data
+ i
;
408 /* First, write shadow (i.e. privileged) data for group record */
409 r
= group_record_clone(d
->payload_group
, shadow_flags
, &shadow_group
);
411 return log_error_errno(r
, "Failed to extract privileged information from group record: %m");
413 if (!json_variant_is_blank_object(shadow_group
->json
)) {
414 r
= write_and_symlink(
417 d
->payload_group
->group_name
,
418 d
->payload_group
->gid
,
420 WRITE_STRING_FILE_MODE_0600
);
425 /* Second, write main part of group record. */
426 r
= group_record_clone(d
->payload_group
, strip_flags
, &stripped_group
);
428 return log_error_errno(r
, "Failed to strip privileged information from group record: %m");
430 r
= write_and_symlink(
432 stripped_group
->json
,
433 d
->payload_group
->group_name
,
434 d
->payload_group
->gid
,
440 /* Third, write out user shadow data. i.e. extract privileged info from user record */
441 r
= user_record_clone(d
->payload_user
, shadow_flags
, &shadow_user
);
443 return log_error_errno(r
, "Failed to extract privileged information from user record: %m");
445 if (!json_variant_is_blank_object(shadow_user
->json
)) {
446 r
= write_and_symlink(
449 d
->payload_user
->user_name
,
450 d
->payload_user
->uid
,
452 WRITE_STRING_FILE_MODE_0600
);
457 /* Finally write out the main part of the user record */
458 r
= user_record_clone(d
->payload_user
, strip_flags
, &stripped_user
);
460 return log_error_errno(r
, "Failed to strip privileged information from user record: %m");
462 r
= write_and_symlink(
465 d
->payload_user
->user_name
,
466 d
->payload_user
->uid
,