1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include "format-util.h"
7 #include "nspawn-bind-user.h"
10 #include "user-util.h"
13 #define MAP_UID_START 60514
14 #define MAP_UID_END 60577
16 static int check_etc_passwd_collisions(
17 const char *directory
,
21 _cleanup_fclose_
FILE *f
= NULL
;
25 assert(name
|| uid_is_valid(uid
));
27 r
= chase_symlinks_and_fopen_unlocked("/etc/passwd", directory
, CHASE_PREFIX_ROOT
, "re", &f
, NULL
);
29 return 0; /* no user database? then no user, hence no collision */
31 return log_error_errno(r
, "Failed to open /etc/passwd of container: %m");
36 r
= fgetpwent_sane(f
, &pw
);
38 return log_error_errno(r
, "Failed to iterate through /etc/passwd of container: %m");
40 return 0; /* no collision */
42 if (name
&& streq_ptr(pw
->pw_name
, name
))
43 return 1; /* name collision */
44 if (uid_is_valid(uid
) && pw
->pw_uid
== uid
)
45 return 1; /* UID collision */
49 static int check_etc_group_collisions(
50 const char *directory
,
54 _cleanup_fclose_
FILE *f
= NULL
;
58 assert(name
|| gid_is_valid(gid
));
60 r
= chase_symlinks_and_fopen_unlocked("/etc/group", directory
, CHASE_PREFIX_ROOT
, "re", &f
, NULL
);
62 return 0; /* no group database? then no group, hence no collision */
64 return log_error_errno(r
, "Failed to open /etc/group of container: %m");
69 r
= fgetgrent_sane(f
, &gr
);
71 return log_error_errno(r
, "Failed to iterate through /etc/group of container: %m");
73 return 0; /* no collision */
75 if (name
&& streq_ptr(gr
->gr_name
, name
))
76 return 1; /* name collision */
77 if (gid_is_valid(gid
) && gr
->gr_gid
== gid
)
78 return 1; /* gid collision */
82 static int convert_user(
83 const char *directory
,
87 UserRecord
**ret_converted_user
,
88 GroupRecord
**ret_converted_group
) {
90 _cleanup_(group_record_unrefp
) GroupRecord
*converted_group
= NULL
;
91 _cleanup_(user_record_unrefp
) UserRecord
*converted_user
= NULL
;
92 _cleanup_free_
char *h
= NULL
;
93 JsonVariant
*p
, *hp
= NULL
;
98 assert(u
->gid
== g
->gid
);
100 r
= check_etc_passwd_collisions(directory
, u
->user_name
, UID_INVALID
);
104 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
105 "Sorry, the user '%s' already exists in the container.", u
->user_name
);
107 r
= check_etc_group_collisions(directory
, g
->group_name
, GID_INVALID
);
111 return log_error_errno(SYNTHETIC_ERRNO(EBUSY
),
112 "Sorry, the group '%s' already exists in the container.", g
->group_name
);
114 h
= path_join("/run/host/home/", u
->user_name
);
118 /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
119 p
= json_variant_by_key(u
->json
, "privileged");
121 hp
= json_variant_by_key(p
, "hashedPassword");
123 r
= user_record_build(
126 JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(u
->user_name
)),
127 JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(allocate_uid
)),
128 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid
)),
129 JSON_BUILD_PAIR_CONDITION(u
->disposition
>= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(u
->disposition
))),
130 JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(h
)),
131 JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.NSpawn")),
132 JSON_BUILD_PAIR_CONDITION(!strv_isempty(u
->hashed_password
), "privileged", JSON_BUILD_OBJECT(
133 JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_VARIANT(hp
))))));
135 return log_error_errno(r
, "Failed to build container user record: %m");
137 r
= group_record_build(
140 JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g
->group_name
)),
141 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid
)),
142 JSON_BUILD_PAIR_CONDITION(g
->disposition
>= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(g
->disposition
))),
143 JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.NSpawn"))));
145 return log_error_errno(r
, "Failed to build container group record: %m");
147 *ret_converted_user
= TAKE_PTR(converted_user
);
148 *ret_converted_group
= TAKE_PTR(converted_group
);
153 static int find_free_uid(const char *directory
, uid_t max_uid
, uid_t
*current_uid
) {
159 for (;; (*current_uid
) ++) {
160 if (*current_uid
> MAP_UID_END
|| *current_uid
> max_uid
)
161 return log_error_errno(
162 SYNTHETIC_ERRNO(EBUSY
),
163 "No suitable available UID in range " UID_FMT
"…" UID_FMT
" in container detected, can't map user.",
164 MAP_UID_START
, MAP_UID_END
);
166 r
= check_etc_passwd_collisions(directory
, NULL
, *current_uid
);
169 if (r
> 0) /* already used */
172 /* We want to use the UID also as GID, hence check for it in /etc/group too */
173 r
= check_etc_group_collisions(directory
, NULL
, (gid_t
) *current_uid
);
176 if (r
== 0) /* free! yay! */
181 BindUserContext
* bind_user_context_free(BindUserContext
*c
) {
185 assert(c
->n_data
== 0 || c
->data
);
187 for (size_t i
= 0; i
< c
->n_data
; i
++) {
188 user_record_unref(c
->data
[i
].host_user
);
189 group_record_unref(c
->data
[i
].host_group
);
190 user_record_unref(c
->data
[i
].payload_user
);
191 group_record_unref(c
->data
[i
].payload_group
);
197 int bind_user_prepare(
198 const char *directory
,
202 CustomMount
**custom_mounts
,
203 size_t *n_custom_mounts
,
204 BindUserContext
**ret
) {
206 _cleanup_(bind_user_context_freep
) BindUserContext
*c
= NULL
;
207 uid_t current_uid
= MAP_UID_START
;
211 assert(custom_mounts
);
212 assert(n_custom_mounts
);
215 /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
216 * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
217 * to include an appropriate bind mount mapping.
219 * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
220 * new BindUserContext for the user records */
222 if (strv_isempty(bind_user
)) {
227 c
= new0(BindUserContext
, 1);
231 STRV_FOREACH(n
, bind_user
) {
232 _cleanup_(user_record_unrefp
) UserRecord
*u
= NULL
, *cu
= NULL
;
233 _cleanup_(group_record_unrefp
) GroupRecord
*g
= NULL
, *cg
= NULL
;
234 _cleanup_free_
char *sm
= NULL
, *sd
= NULL
;
237 r
= userdb_by_name(*n
, USERDB_DONT_SYNTHESIZE
, &u
);
239 return log_error_errno(r
, "Failed to resolve user '%s': %m", *n
);
241 /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
242 * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
243 * even if we wanted, we couldn't override the root or nobody user records. Note we also
244 * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
245 * out root/nobody too, hence these checks might appear redundant — but they actually are
246 * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
247 * and the user/group databases fully synthesized at runtime. Moreover, the name of the
248 * user/group name of the "nobody" account differs between distros, hence a check by numeric
250 if (u
->uid
== 0 || streq(u
->user_name
, "root"))
251 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'root' user not supported, sorry.");
252 if (u
->uid
== UID_NOBODY
|| STR_IN_SET(u
->user_name
, NOBODY_USER_NAME
, "nobody"))
253 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Mapping 'nobody' user not supported, sorry.");
255 if (u
->uid
>= uid_shift
&& u
->uid
< uid_shift
+ uid_range
)
256 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "UID of user '%s' to map is already in container UID range, refusing.", u
->user_name
);
258 r
= groupdb_by_gid(u
->gid
, USERDB_DONT_SYNTHESIZE
, &g
);
260 return log_error_errno(r
, "Failed to resolve group of user '%s': %m", u
->user_name
);
262 if (g
->gid
>= uid_shift
&& g
->gid
< uid_shift
+ uid_range
)
263 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "GID of group '%s' to map is already in container GID range, refusing.", g
->group_name
);
265 /* We want to synthesize exactly one user + group from the host into the container. This only
266 * makes sense if the user on the host has its own private group. We can't reasonably check
267 * this, so we just check of the name of user and group match.
269 * One of these days we might want to support users in a shared/common group too, but it's
270 * not clear to me how this would have to be mapped, precisely given that the common group
271 * probably already exists in the container. */
272 if (!streq(u
->user_name
, g
->group_name
))
273 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
274 "Sorry, mapping users without private groups is currently not supported.");
276 r
= find_free_uid(directory
, uid_range
, ¤t_uid
);
280 r
= convert_user(directory
, u
, g
, current_uid
, &cu
, &cg
);
284 if (!GREEDY_REALLOC(c
->data
, c
->n_data
+ 1))
287 sm
= strdup(u
->home_directory
);
291 sd
= strdup(cu
->home_directory
);
295 cm
= reallocarray(*custom_mounts
, sizeof(CustomMount
), *n_custom_mounts
+ 1);
301 (*custom_mounts
)[(*n_custom_mounts
)++] = (CustomMount
) {
302 .type
= CUSTOM_MOUNT_BIND
,
303 .source
= TAKE_PTR(sm
),
304 .destination
= TAKE_PTR(sd
),
307 c
->data
[c
->n_data
++] = (BindUserData
) {
308 .host_user
= TAKE_PTR(u
),
309 .host_group
= TAKE_PTR(g
),
310 .payload_user
= TAKE_PTR(cu
),
311 .payload_group
= TAKE_PTR(cg
),
321 static int write_and_symlink(
327 WriteStringFileFlags extra_flags
) {
329 _cleanup_free_
char *j
= NULL
, *f
= NULL
, *p
= NULL
, *q
= NULL
;
335 assert(uid_is_valid(uid
));
338 r
= json_variant_format(v
, JSON_FORMAT_NEWLINE
, &j
);
340 return log_error_errno(r
, "Failed to format user record JSON: %m");
342 f
= strjoin(name
, suffix
);
346 p
= path_join(root
, "/run/host/userdb/", f
);
350 if (asprintf(&q
, "%s/run/host/userdb/" UID_FMT
"%s", root
, uid
, suffix
) < 0)
353 if (symlink(f
, q
) < 0)
354 return log_error_errno(errno
, "Failed to create symlink '%s': %m", q
);
356 r
= userns_lchown(q
, 0, 0);
358 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", q
);
360 r
= write_string_file(p
, j
, WRITE_STRING_FILE_CREATE
|extra_flags
);
362 return log_error_errno(r
, "Failed to write %s: %m", p
);
364 r
= userns_lchown(p
, 0, 0);
366 return log_error_errno(r
, "Failed to adjust access mode of '%s': %m", p
);
372 const BindUserContext
*c
,
375 static const UserRecordLoadFlags strip_flags
= /* Removes privileged info */
376 USER_RECORD_REQUIRE_REGULAR
|
377 USER_RECORD_STRIP_PRIVILEGED
|
378 USER_RECORD_ALLOW_PER_MACHINE
|
379 USER_RECORD_ALLOW_BINDING
|
380 USER_RECORD_ALLOW_SIGNATURE
|
381 USER_RECORD_PERMISSIVE
;
382 static const UserRecordLoadFlags shadow_flags
= /* Extracts privileged info */
383 USER_RECORD_STRIP_REGULAR
|
384 USER_RECORD_ALLOW_PRIVILEGED
|
385 USER_RECORD_STRIP_PER_MACHINE
|
386 USER_RECORD_STRIP_BINDING
|
387 USER_RECORD_STRIP_SIGNATURE
|
388 USER_RECORD_EMPTY_OK
|
389 USER_RECORD_PERMISSIVE
;
394 if (!c
|| c
->n_data
== 0)
397 r
= userns_mkdir(root
, "/run/host", 0755, 0, 0);
399 return log_error_errno(r
, "Failed to create /run/host: %m");
401 r
= userns_mkdir(root
, "/run/host/home", 0755, 0, 0);
403 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
405 r
= userns_mkdir(root
, "/run/host/userdb", 0755, 0, 0);
407 return log_error_errno(r
, "Failed to create /run/host/userdb: %m");
409 for (size_t i
= 0; i
< c
->n_data
; i
++) {
410 _cleanup_(group_record_unrefp
) GroupRecord
*stripped_group
= NULL
, *shadow_group
= NULL
;
411 _cleanup_(user_record_unrefp
) UserRecord
*stripped_user
= NULL
, *shadow_user
= NULL
;
412 const BindUserData
*d
= c
->data
+ i
;
414 /* First, write shadow (i.e. privileged) data for group record */
415 r
= group_record_clone(d
->payload_group
, shadow_flags
, &shadow_group
);
417 return log_error_errno(r
, "Failed to extract privileged information from group record: %m");
419 if (!json_variant_is_blank_object(shadow_group
->json
)) {
420 r
= write_and_symlink(
423 d
->payload_group
->group_name
,
424 d
->payload_group
->gid
,
426 WRITE_STRING_FILE_MODE_0600
);
431 /* Second, write main part of group record. */
432 r
= group_record_clone(d
->payload_group
, strip_flags
, &stripped_group
);
434 return log_error_errno(r
, "Failed to strip privileged information from group record: %m");
436 r
= write_and_symlink(
438 stripped_group
->json
,
439 d
->payload_group
->group_name
,
440 d
->payload_group
->gid
,
446 /* Third, write out user shadow data. i.e. extract privileged info from user record */
447 r
= user_record_clone(d
->payload_user
, shadow_flags
, &shadow_user
);
449 return log_error_errno(r
, "Failed to extract privileged information from user record: %m");
451 if (!json_variant_is_blank_object(shadow_user
->json
)) {
452 r
= write_and_symlink(
455 d
->payload_user
->user_name
,
456 d
->payload_user
->uid
,
458 WRITE_STRING_FILE_MODE_0600
);
463 /* Finally write out the main part of the user record */
464 r
= user_record_clone(d
->payload_user
, strip_flags
, &stripped_user
);
466 return log_error_errno(r
, "Failed to strip privileged information from user record: %m");
468 r
= write_and_symlink(
471 d
->payload_user
->user_name
,
472 d
->payload_user
->uid
,