1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include "chase-symlinks.h"
4 #include "fd-util.h"
5 #include "fileio.h"
6 #include "format-util.h"
7 #include "nspawn-bind-user.h"
8 #include "nspawn.h"
9 #include "path-util.h"
10 #include "user-util.h"
11 #include "userdb.h"
12 
check_etc_passwd_collisions(const char * directory,const char * name,uid_t uid)13 static int check_etc_passwd_collisions(
14                 const char *directory,
15                 const char *name,
16                 uid_t uid) {
17 
18         _cleanup_fclose_ FILE *f = NULL;
19         int r;
20 
21         assert(directory);
22         assert(name || uid_is_valid(uid));
23 
24         r = chase_symlinks_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
25         if (r == -ENOENT)
26                 return 0; /* no user database? then no user, hence no collision */
27         if (r < 0)
28                 return log_error_errno(r, "Failed to open /etc/passwd of container: %m");
29 
30         for (;;) {
31                 struct passwd *pw;
32 
33                 r = fgetpwent_sane(f, &pw);
34                 if (r < 0)
35                         return log_error_errno(r, "Failed to iterate through /etc/passwd of container: %m");
36                 if (r == 0) /* EOF */
37                         return 0; /* no collision */
38 
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 */
43         }
44 }
45 
check_etc_group_collisions(const char * directory,const char * name,gid_t gid)46 static int check_etc_group_collisions(
47                 const char *directory,
48                 const char *name,
49                 gid_t gid) {
50 
51         _cleanup_fclose_ FILE *f = NULL;
52         int r;
53 
54         assert(directory);
55         assert(name || gid_is_valid(gid));
56 
57         r = chase_symlinks_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
58         if (r == -ENOENT)
59                 return 0; /* no group database? then no group, hence no collision */
60         if (r < 0)
61                 return log_error_errno(r, "Failed to open /etc/group of container: %m");
62 
63         for (;;) {
64                 struct group *gr;
65 
66                 r = fgetgrent_sane(f, &gr);
67                 if (r < 0)
68                         return log_error_errno(r, "Failed to iterate through /etc/group of container: %m");
69                 if (r == 0)
70                         return 0; /* no collision */
71 
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 */
76         }
77 }
78 
convert_user(const char * directory,UserRecord * u,GroupRecord * g,uid_t allocate_uid,UserRecord ** ret_converted_user,GroupRecord ** ret_converted_group)79 static int convert_user(
80                 const char *directory,
81                 UserRecord *u,
82                 GroupRecord *g,
83                 uid_t allocate_uid,
84                 UserRecord **ret_converted_user,
85                 GroupRecord **ret_converted_group) {
86 
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;
91         int r;
92 
93         assert(u);
94         assert(g);
95         assert(u->gid == g->gid);
96 
97         r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
98         if (r < 0)
99                 return r;
100         if (r > 0)
101                 return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
102                                        "Sorry, the user '%s' already exists in the container.", u->user_name);
103 
104         r = check_etc_group_collisions(directory, g->group_name, GID_INVALID);
105         if (r < 0)
106                 return r;
107         if (r > 0)
108                 return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
109                                        "Sorry, the group '%s' already exists in the container.", g->group_name);
110 
111         h = path_join("/run/host/home/", u->user_name);
112         if (!h)
113                 return log_oom();
114 
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");
117         if (p)
118                 hp = json_variant_by_key(p, "hashedPassword");
119 
120         r = user_record_build(
121                         &converted_user,
122                         JSON_BUILD_OBJECT(
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))))));
131         if (r < 0)
132                 return log_error_errno(r, "Failed to build container user record: %m");
133 
134         r = group_record_build(
135                         &converted_group,
136                         JSON_BUILD_OBJECT(
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"))));
141         if (r < 0)
142                 return log_error_errno(r, "Failed to build container group record: %m");
143 
144         *ret_converted_user = TAKE_PTR(converted_user);
145         *ret_converted_group = TAKE_PTR(converted_group);
146 
147         return 0;
148 }
149 
find_free_uid(const char * directory,uid_t max_uid,uid_t * current_uid)150 static int find_free_uid(const char *directory, uid_t max_uid, uid_t *current_uid) {
151         int r;
152 
153         assert(directory);
154         assert(current_uid);
155 
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);
162 
163                 r = check_etc_passwd_collisions(directory, NULL, *current_uid);
164                 if (r < 0)
165                         return r;
166                 if (r > 0) /* already used */
167                         continue;
168 
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);
171                 if (r <= 0)
172                         return r;
173         }
174 }
175 
bind_user_context_free(BindUserContext * c)176 BindUserContext* bind_user_context_free(BindUserContext *c) {
177         if (!c)
178                 return NULL;
179 
180         assert(c->n_data == 0 || c->data);
181 
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);
187         }
188 
189         return mfree(c);
190 }
191 
bind_user_prepare(const char * directory,char ** bind_user,uid_t uid_shift,uid_t uid_range,CustomMount ** custom_mounts,size_t * n_custom_mounts,BindUserContext ** ret)192 int bind_user_prepare(
193                 const char *directory,
194                 char **bind_user,
195                 uid_t uid_shift,
196                 uid_t uid_range,
197                 CustomMount **custom_mounts,
198                 size_t *n_custom_mounts,
199                 BindUserContext **ret) {
200 
201         _cleanup_(bind_user_context_freep) BindUserContext *c = NULL;
202         uid_t current_uid = MAP_UID_MIN;
203         int r;
204 
205         assert(custom_mounts);
206         assert(n_custom_mounts);
207         assert(ret);
208 
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.
212          *
213          * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
214          * new BindUserContext for the user records */
215 
216         if (strv_isempty(bind_user)) {
217                 *ret = NULL;
218                 return 0;
219         }
220 
221         c = new0(BindUserContext, 1);
222         if (!c)
223                 return log_oom();
224 
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;
229                 CustomMount *cm;
230 
231                 r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE, &u);
232                 if (r < 0)
233                         return log_error_errno(r, "Failed to resolve user '%s': %m", *n);
234 
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
243                  * UID is safer. */
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.");
248 
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);
251 
252                 r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE, &g);
253                 if (r < 0)
254                         return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name);
255 
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);
258 
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.
262                  *
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.");
269 
270                 r = find_free_uid(directory, uid_range, &current_uid);
271                 if (r < 0)
272                         return r;
273 
274                 r = convert_user(directory, u, g, current_uid, &cu, &cg);
275                 if (r < 0)
276                         return r;
277 
278                 if (!GREEDY_REALLOC(c->data, c->n_data + 1))
279                         return log_oom();
280 
281                 sm = strdup(u->home_directory);
282                 if (!sm)
283                         return log_oom();
284 
285                 sd = strdup(cu->home_directory);
286                 if (!sd)
287                         return log_oom();
288 
289                 cm = reallocarray(*custom_mounts, sizeof(CustomMount), *n_custom_mounts + 1);
290                 if (!cm)
291                         return log_oom();
292 
293                 *custom_mounts = cm;
294 
295                 (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) {
296                         .type = CUSTOM_MOUNT_BIND,
297                         .source = TAKE_PTR(sm),
298                         .destination = TAKE_PTR(sd),
299                 };
300 
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),
306                 };
307 
308                 current_uid++;
309         }
310 
311         *ret = TAKE_PTR(c);
312         return 1;
313 }
314 
write_and_symlink(const char * root,JsonVariant * v,const char * name,uid_t uid,const char * suffix,WriteStringFileFlags extra_flags)315 static int write_and_symlink(
316                 const char *root,
317                 JsonVariant *v,
318                 const char *name,
319                 uid_t uid,
320                 const char *suffix,
321                 WriteStringFileFlags extra_flags) {
322 
323         _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL;
324         int r;
325 
326         assert(root);
327         assert(v);
328         assert(name);
329         assert(uid_is_valid(uid));
330         assert(suffix);
331 
332         r = json_variant_format(v, JSON_FORMAT_NEWLINE, &j);
333         if (r < 0)
334                 return log_error_errno(r, "Failed to format user record JSON: %m");
335 
336         f = strjoin(name, suffix);
337         if (!f)
338                 return log_oom();
339 
340         p = path_join(root, "/run/host/userdb/", f);
341         if (!p)
342                 return log_oom();
343 
344         if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0)
345                 return log_oom();
346 
347         if (symlink(f, q) < 0)
348                 return log_error_errno(errno, "Failed to create symlink '%s': %m", q);
349 
350         r = userns_lchown(q, 0, 0);
351         if (r < 0)
352                 return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q);
353 
354         r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags);
355         if (r < 0)
356                 return log_error_errno(r, "Failed to write %s: %m", p);
357 
358         r = userns_lchown(p, 0, 0);
359         if (r < 0)
360                 return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
361 
362         return 0;
363 }
364 
bind_user_setup(const BindUserContext * c,const char * root)365 int bind_user_setup(
366                 const BindUserContext *c,
367                 const char *root) {
368 
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;
384         int r;
385 
386         assert(root);
387 
388         if (!c || c->n_data == 0)
389                 return 0;
390 
391         r = userns_mkdir(root, "/run/host", 0755, 0, 0);
392         if (r < 0)
393                 return log_error_errno(r, "Failed to create /run/host: %m");
394 
395         r = userns_mkdir(root, "/run/host/home", 0755, 0, 0);
396         if (r < 0)
397                 return log_error_errno(r, "Failed to create /run/host/userdb: %m");
398 
399         r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0);
400         if (r < 0)
401                 return log_error_errno(r, "Failed to create /run/host/userdb: %m");
402 
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;
407 
408                 /* First, write shadow (i.e. privileged) data for group record */
409                 r = group_record_clone(d->payload_group, shadow_flags, &shadow_group);
410                 if (r < 0)
411                         return log_error_errno(r, "Failed to extract privileged information from group record: %m");
412 
413                 if (!json_variant_is_blank_object(shadow_group->json)) {
414                         r = write_and_symlink(
415                                         root,
416                                         shadow_group->json,
417                                         d->payload_group->group_name,
418                                         d->payload_group->gid,
419                                         ".group-privileged",
420                                         WRITE_STRING_FILE_MODE_0600);
421                         if (r < 0)
422                                 return r;
423                 }
424 
425                 /* Second, write main part of group record. */
426                 r = group_record_clone(d->payload_group, strip_flags, &stripped_group);
427                 if (r < 0)
428                         return log_error_errno(r, "Failed to strip privileged information from group record: %m");
429 
430                 r = write_and_symlink(
431                                 root,
432                                 stripped_group->json,
433                                 d->payload_group->group_name,
434                                 d->payload_group->gid,
435                                 ".group",
436                                 0);
437                 if (r < 0)
438                         return r;
439 
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);
442                 if (r < 0)
443                         return log_error_errno(r, "Failed to extract privileged information from user record: %m");
444 
445                 if (!json_variant_is_blank_object(shadow_user->json)) {
446                         r = write_and_symlink(
447                                         root,
448                                         shadow_user->json,
449                                         d->payload_user->user_name,
450                                         d->payload_user->uid,
451                                         ".user-privileged",
452                                         WRITE_STRING_FILE_MODE_0600);
453                         if (r < 0)
454                                 return r;
455                 }
456 
457                 /* Finally write out the main part of the user record */
458                 r = user_record_clone(d->payload_user, strip_flags, &stripped_user);
459                 if (r < 0)
460                         return log_error_errno(r, "Failed to strip privileged information from user record: %m");
461 
462                 r = write_and_symlink(
463                                 root,
464                                 stripped_user->json,
465                                 d->payload_user->user_name,
466                                 d->payload_user->uid,
467                                 ".user",
468                                 0);
469                 if (r < 0)
470                         return r;
471         }
472 
473         return 1;
474 }
475