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
;
206 assert(custom_mounts
);
207 assert(n_custom_mounts
);
210 /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
211 * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
212 * to include an appropriate bind mount mapping.
214 * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
215 * new BindUserContext for the user records */
217 if (strv_isempty(bind_user
)) {
222 c
= new0(BindUserContext
, 1);
226 STRV_FOREACH(n
, bind_user
) {
227 _cleanup_(user_record_unrefp
) UserRecord
*u
= NULL
, *cu
= NULL
;
228 _cleanup_(group_record_unrefp
) GroupRecord
*g
= NULL
, *cg
= NULL
;
229 _cleanup_free_
char *sm
= NULL
, *sd
= NULL
;
232 r
= userdb_by_name(*n
, USERDB_DONT_SYNTHESIZE
, &u
);
234 return log_error_errno(r
, "Failed to resolve user '%s': %m", *n
);
236 /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
237 * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
238 * even if we wanted, we couldn't override the root or nobody user records. Note we also
239 * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
240 * out root/nobody too, hence these checks might appear redundant — but they actually are
241 * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
242 * and the user/group databases fully synthesized at runtime. Moreover, the name of the
243 * user/group name of the "nobody" account differs between distros, hence a check by numeric
245 if (u
->uid
== 0 || streq(u
->user_name
, "root"))
246 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'root' user not supported, sorry.");
247 if (u
->uid
== UID_NOBODY
|| STR_IN_SET(u
->user_name
, NOBODY_USER_NAME
, "nobody"))
248 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'nobody' user not supported, sorry.");
250 if (u
->uid
>= uid_shift
&& u
->uid
< uid_shift
+ uid_range
)
251 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "UID of user '%s' to map is already in container UID range, refusing.", u
->user_name
);
253 r
= groupdb_by_gid(u
->gid
, USERDB_DONT_SYNTHESIZE
, &g
);
255 return log_error_errno(r
, "Failed to resolve group of user '%s': %m", u
->user_name
);
257 if (g
->gid
>= uid_shift
&& g
->gid
< uid_shift
+ uid_range
)
258 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "GID of group '%s' to map is already in container GID range, refusing.", g
->group_name
);
260 /* We want to synthesize exactly one user + group from the host into the container. This only
261 * makes sense if the user on the host has its own private group. We can't reasonably check
262 * this, so we just check of the name of user and group match.
264 * One of these days we might want to support users in a shared/common group too, but it's
265 * not clear to me how this would have to be mapped, precisely given that the common group
266 * probably already exists in the container. */
267 if (!streq(u
->user_name
, g
->group_name
))
268 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
269 "Sorry, mapping users without private groups is currently not supported.");
271 r
= find_free_uid(directory
, uid_range
, ¤t_uid
);
275 r
= convert_user(directory
, u
, g
, current_uid
, &cu
, &cg
);
279 if (!GREEDY_REALLOC(c
->data
, c
->n_data
+ 1))
282 sm
= strdup(u
->home_directory
);
286 sd
= strdup(cu
->home_directory
);
290 cm
= reallocarray(*custom_mounts
, sizeof(CustomMount
), *n_custom_mounts
+ 1);
296 (*custom_mounts
)[(*n_custom_mounts
)++] = (CustomMount
) {
297 .type
= CUSTOM_MOUNT_BIND
,
298 .source
= TAKE_PTR(sm
),
299 .destination
= TAKE_PTR(sd
),
302 c
->data
[c
->n_data
++] = (BindUserData
) {
303 .host_user
= TAKE_PTR(u
),
304 .host_group
= TAKE_PTR(g
),
305 .payload_user
= TAKE_PTR(cu
),
306 .payload_group
= TAKE_PTR(cg
),
316 static int write_and_symlink(
322 WriteStringFileFlags extra_flags
) {
324 _cleanup_free_
char *j
= NULL
, *f
= NULL
, *p
= NULL
, *q
= NULL
;
330 assert(uid_is_valid(uid
));
333 r
= json_variant_format(v
, JSON_FORMAT_NEWLINE
, &j
);
335 return log_error_errno(r
, "Failed to format user record JSON: %m");
337 f
= strjoin(name
, suffix
);
341 p
= path_join(root
, "/run/host/userdb/", f
);
345 if (asprintf(&q
, "%s/run/host/userdb/" UID_FMT
"%s", root
, uid
, suffix
) < 0)
348 if (symlink(f
, q
) < 0)
349 return log_error_errno(errno
, "Failed to create symlink '%s': %m", q
);
351 r
= userns_lchown(q
, 0, 0);
353 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", q
);
355 r
= write_string_file(p
, j
, WRITE_STRING_FILE_CREATE
|extra_flags
);
357 return log_error_errno(r
, "Failed to write %s: %m", p
);
359 r
= userns_lchown(p
, 0, 0);
361 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", p
);
367 const BindUserContext
*c
,
370 static const UserRecordLoadFlags strip_flags
= /* Removes privileged info */
371 USER_RECORD_REQUIRE_REGULAR
|
372 USER_RECORD_STRIP_PRIVILEGED
|
373 USER_RECORD_ALLOW_PER_MACHINE
|
374 USER_RECORD_ALLOW_BINDING
|
375 USER_RECORD_ALLOW_SIGNATURE
|
376 USER_RECORD_PERMISSIVE
;
377 static const UserRecordLoadFlags shadow_flags
= /* Extracts privileged info */
378 USER_RECORD_STRIP_REGULAR
|
379 USER_RECORD_ALLOW_PRIVILEGED
|
380 USER_RECORD_STRIP_PER_MACHINE
|
381 USER_RECORD_STRIP_BINDING
|
382 USER_RECORD_STRIP_SIGNATURE
|
383 USER_RECORD_EMPTY_OK
|
384 USER_RECORD_PERMISSIVE
;
389 if (!c
|| c
->n_data
== 0)
392 r
= userns_mkdir(root
, "/run/host", 0755, 0, 0);
394 return log_error_errno(r
, "Failed to create /run/host: %m");
396 r
= userns_mkdir(root
, "/run/host/home", 0755, 0, 0);
398 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
400 r
= userns_mkdir(root
, "/run/host/userdb", 0755, 0, 0);
402 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
404 for (size_t i
= 0; i
< c
->n_data
; i
++) {
405 _cleanup_(group_record_unrefp
) GroupRecord
*stripped_group
= NULL
, *shadow_group
= NULL
;
406 _cleanup_(user_record_unrefp
) UserRecord
*stripped_user
= NULL
, *shadow_user
= NULL
;
407 const BindUserData
*d
= c
->data
+ i
;
409 /* First, write shadow (i.e. privileged) data for group record */
410 r
= group_record_clone(d
->payload_group
, shadow_flags
, &shadow_group
);
412 return log_error_errno(r
, "Failed to extract privileged information from group record: %m");
414 if (!json_variant_is_blank_object(shadow_group
->json
)) {
415 r
= write_and_symlink(
418 d
->payload_group
->group_name
,
419 d
->payload_group
->gid
,
421 WRITE_STRING_FILE_MODE_0600
);
426 /* Second, write main part of group record. */
427 r
= group_record_clone(d
->payload_group
, strip_flags
, &stripped_group
);
429 return log_error_errno(r
, "Failed to strip privileged information from group record: %m");
431 r
= write_and_symlink(
433 stripped_group
->json
,
434 d
->payload_group
->group_name
,
435 d
->payload_group
->gid
,
441 /* Third, write out user shadow data. i.e. extract privileged info from user record */
442 r
= user_record_clone(d
->payload_user
, shadow_flags
, &shadow_user
);
444 return log_error_errno(r
, "Failed to extract privileged information from user record: %m");
446 if (!json_variant_is_blank_object(shadow_user
->json
)) {
447 r
= write_and_symlink(
450 d
->payload_user
->user_name
,
451 d
->payload_user
->uid
,
453 WRITE_STRING_FILE_MODE_0600
);
458 /* Finally write out the main part of the user record */
459 r
= user_record_clone(d
->payload_user
, strip_flags
, &stripped_user
);
461 return log_error_errno(r
, "Failed to strip privileged information from user record: %m");
463 r
= write_and_symlink(
466 d
->payload_user
->user_name
,
467 d
->payload_user
->uid
,