1 /* chroot -- run command or shell with special root directory
2 Copyright (C) 1995-2023 Free Software Foundation, Inc.
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <https://www.gnu.org/licenses/>. */
16
17 /* Written by Roland McGrath. */
18
19 #include <config.h>
20 #include <getopt.h>
21 #include <stdio.h>
22 #include <sys/types.h>
23 #include <pwd.h>
24 #include <grp.h>
25
26 #include "system.h"
27 #include "ignore-value.h"
28 #include "mgetgroups.h"
29 #include "quote.h"
30 #include "root-dev-ino.h"
31 #include "userspec.h"
32 #include "xstrtol.h"
33
34 /* The official name of this program (e.g., no 'g' prefix). */
35 #define PROGRAM_NAME "chroot"
36
37 #define AUTHORS proper_name ("Roland McGrath")
38
39 #ifndef MAXGID
40 # define MAXGID GID_T_MAX
41 #endif
42
uid_unset(uid_t uid)43 static inline bool uid_unset (uid_t uid) { return uid == (uid_t) -1; }
gid_unset(gid_t gid)44 static inline bool gid_unset (gid_t gid) { return gid == (gid_t) -1; }
45 #define uid_set(x) (!uid_unset (x))
46 #define gid_set(x) (!gid_unset (x))
47
48 enum
49 {
50 GROUPS = UCHAR_MAX + 1,
51 USERSPEC,
52 SKIP_CHDIR
53 };
54
55 static struct option const long_opts[] =
56 {
57 {"groups", required_argument, nullptr, GROUPS},
58 {"userspec", required_argument, nullptr, USERSPEC},
59 {"skip-chdir", no_argument, nullptr, SKIP_CHDIR},
60 {GETOPT_HELP_OPTION_DECL},
61 {GETOPT_VERSION_OPTION_DECL},
62 {nullptr, 0, nullptr, 0}
63 };
64
65 #if ! HAVE_SETGROUPS
66 /* At least Interix lacks supplemental group support. */
67 static int
setgroups(size_t size,MAYBE_UNUSED gid_t const * list)68 setgroups (size_t size, MAYBE_UNUSED gid_t const *list)
69 {
70 if (size == 0)
71 {
72 /* Return success when clearing supplemental groups
73 as ! HAVE_SETGROUPS should only be the case on
74 platforms that don't support supplemental groups. */
75 return 0;
76 }
77 else
78 {
79 errno = ENOTSUP;
80 return -1;
81 }
82 }
83 #endif
84
85 /* Determine the group IDs for the specified supplementary GROUPS,
86 which is a comma separated list of supplementary groups (names or numbers).
87 Allocate an array for the parsed IDs and store it in PGIDS,
88 which may be allocated even on parse failure.
89 Update the number of parsed groups in PN_GIDS on success.
90 Upon any failure return nonzero, and issue diagnostic if SHOW_ERRORS is true.
91 Otherwise return zero. */
92
93 static int
parse_additional_groups(char const * groups,GETGROUPS_T ** pgids,size_t * pn_gids,bool show_errors)94 parse_additional_groups (char const *groups, GETGROUPS_T **pgids,
95 size_t *pn_gids, bool show_errors)
96 {
97 GETGROUPS_T *gids = nullptr;
98 size_t n_gids_allocated = 0;
99 size_t n_gids = 0;
100 char *buffer = xstrdup (groups);
101 char const *tmp;
102 int ret = 0;
103
104 for (tmp = strtok (buffer, ","); tmp; tmp = strtok (nullptr, ","))
105 {
106 struct group *g;
107 uintmax_t value;
108
109 if (xstrtoumax (tmp, nullptr, 10, &value, "") == LONGINT_OK
110 && value <= MAXGID)
111 {
112 while (isspace (to_uchar (*tmp)))
113 tmp++;
114 if (*tmp != '+')
115 {
116 /* Handle the case where the name is numeric. */
117 g = getgrnam (tmp);
118 if (g != nullptr)
119 value = g->gr_gid;
120 }
121 /* Flag that we've got a group from the number. */
122 g = (struct group *) (intptr_t) ! nullptr;
123 }
124 else
125 {
126 g = getgrnam (tmp);
127 if (g != nullptr)
128 value = g->gr_gid;
129 }
130
131 if (g == nullptr)
132 {
133 ret = -1;
134
135 if (show_errors)
136 {
137 error (0, errno, _("invalid group %s"), quote (tmp));
138 continue;
139 }
140
141 break;
142 }
143
144 if (n_gids == n_gids_allocated)
145 gids = X2NREALLOC (gids, &n_gids_allocated);
146 gids[n_gids++] = value;
147 }
148
149 if (ret == 0 && n_gids == 0)
150 {
151 if (show_errors)
152 error (0, 0, _("invalid group list %s"), quote (groups));
153 ret = -1;
154 }
155
156 *pgids = gids;
157
158 if (ret == 0)
159 *pn_gids = n_gids;
160
161 free (buffer);
162 return ret;
163 }
164
165 /* Return whether the passed path is equivalent to "/".
166 Note we don't compare against get_root_dev_ino() as "/"
167 could be bind mounted to a separate location. */
168
169 static bool
is_root(char const * dir)170 is_root (char const *dir)
171 {
172 char *resolved = canonicalize_file_name (dir);
173 bool is_res_root = resolved && STREQ ("/", resolved);
174 free (resolved);
175 return is_res_root;
176 }
177
178 void
usage(int status)179 usage (int status)
180 {
181 if (status != EXIT_SUCCESS)
182 emit_try_help ();
183 else
184 {
185 printf (_("\
186 Usage: %s [OPTION] NEWROOT [COMMAND [ARG]...]\n\
187 or: %s OPTION\n\
188 "), program_name, program_name);
189
190 fputs (_("\
191 Run COMMAND with root directory set to NEWROOT.\n\
192 \n\
193 "), stdout);
194
195 fputs (_("\
196 --groups=G_LIST specify supplementary groups as g1,g2,..,gN\n\
197 "), stdout);
198 fputs (_("\
199 --userspec=USER:GROUP specify user and group (ID or name) to use\n\
200 "), stdout);
201 printf (_("\
202 --skip-chdir do not change working directory to %s\n\
203 "), quoteaf ("/"));
204
205 fputs (HELP_OPTION_DESCRIPTION, stdout);
206 fputs (VERSION_OPTION_DESCRIPTION, stdout);
207 fputs (_("\
208 \n\
209 If no command is given, run '\"$SHELL\" -i' (default: '/bin/sh -i').\n\
210 "), stdout);
211 emit_exec_status (PROGRAM_NAME);
212 emit_ancillary_info (PROGRAM_NAME);
213 }
214 exit (status);
215 }
216
217 int
main(int argc,char ** argv)218 main (int argc, char **argv)
219 {
220 int c;
221
222 /* Input user and groups spec. */
223 char *userspec = nullptr;
224 char const *username = nullptr;
225 char const *groups = nullptr;
226 bool skip_chdir = false;
227
228 /* Parsed user and group IDs. */
229 uid_t uid = -1;
230 gid_t gid = -1;
231 GETGROUPS_T *out_gids = nullptr;
232 size_t n_gids = 0;
233
234 initialize_main (&argc, &argv);
235 set_program_name (argv[0]);
236 setlocale (LC_ALL, "");
237 bindtextdomain (PACKAGE, LOCALEDIR);
238 textdomain (PACKAGE);
239
240 initialize_exit_failure (EXIT_CANCELED);
241 atexit (close_stdout);
242
243 while ((c = getopt_long (argc, argv, "+", long_opts, nullptr)) != -1)
244 {
245 switch (c)
246 {
247 case USERSPEC:
248 {
249 userspec = optarg;
250 /* Treat 'user:' just like 'user'
251 as we lookup the primary group by default
252 (and support doing so for UIDs as well as names. */
253 size_t userlen = strlen (userspec);
254 if (userlen && userspec[userlen - 1] == ':')
255 userspec[userlen - 1] = '\0';
256 break;
257 }
258
259 case GROUPS:
260 groups = optarg;
261 break;
262
263 case SKIP_CHDIR:
264 skip_chdir = true;
265 break;
266
267 case_GETOPT_HELP_CHAR;
268
269 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
270
271 default:
272 usage (EXIT_CANCELED);
273 }
274 }
275
276 if (argc <= optind)
277 {
278 error (0, 0, _("missing operand"));
279 usage (EXIT_CANCELED);
280 }
281
282 char const *newroot = argv[optind];
283 bool is_oldroot = is_root (newroot);
284
285 if (! is_oldroot && skip_chdir)
286 {
287 error (0, 0, _("option --skip-chdir only permitted if NEWROOT is old %s"),
288 quoteaf ("/"));
289 usage (EXIT_CANCELED);
290 }
291
292 if (! is_oldroot)
293 {
294 /* We have to look up users and groups twice.
295 - First, outside the chroot to load potentially necessary passwd/group
296 parsing plugins (e.g. NSS);
297 - Second, inside chroot to redo parsing in case IDs are different.
298 Within chroot lookup is the main justification for having
299 the --user option supported by the chroot command itself. */
300 if (userspec)
301 ignore_value (parse_user_spec (userspec, &uid, &gid, nullptr, nullptr));
302
303 /* If no gid is supplied or looked up, do so now.
304 Also lookup the username for use with getgroups. */
305 if (uid_set (uid) && (! groups || gid_unset (gid)))
306 {
307 const struct passwd *pwd;
308 if ((pwd = getpwuid (uid)))
309 {
310 if (gid_unset (gid))
311 gid = pwd->pw_gid;
312 username = pwd->pw_name;
313 }
314 }
315
316 if (groups && *groups)
317 ignore_value (parse_additional_groups (groups, &out_gids, &n_gids,
318 false));
319 #if HAVE_SETGROUPS
320 else if (! groups && gid_set (gid) && username)
321 {
322 int ngroups = xgetgroups (username, gid, &out_gids);
323 if (0 < ngroups)
324 n_gids = ngroups;
325 }
326 #endif
327 }
328
329 if (chroot (newroot) != 0)
330 error (EXIT_CANCELED, errno, _("cannot change root directory to %s"),
331 quoteaf (newroot));
332
333 if (! skip_chdir && chdir ("/"))
334 error (EXIT_CANCELED, errno, _("cannot chdir to root directory"));
335
336 if (argc == optind + 1)
337 {
338 /* No command. Run an interactive shell. */
339 char *shell = getenv ("SHELL");
340 if (shell == nullptr)
341 shell = bad_cast ("/bin/sh");
342 argv[0] = shell;
343 argv[1] = bad_cast ("-i");
344 argv[2] = nullptr;
345 }
346 else
347 {
348 /* The following arguments give the command. */
349 argv += optind + 1;
350 }
351
352 /* Attempt to set all three: supplementary groups, group ID, user ID.
353 Diagnose any failures. If any have failed, exit before execvp. */
354 if (userspec)
355 {
356 bool warn;
357 char const *err = parse_user_spec_warn (userspec, &uid, &gid,
358 nullptr, nullptr, &warn);
359 if (err)
360 error (warn ? 0 : EXIT_CANCELED, 0, "%s", (err));
361 }
362
363 /* If no gid is supplied or looked up, do so now.
364 Also lookup the username for use with getgroups. */
365 if (uid_set (uid) && (! groups || gid_unset (gid)))
366 {
367 const struct passwd *pwd;
368 if ((pwd = getpwuid (uid)))
369 {
370 if (gid_unset (gid))
371 gid = pwd->pw_gid;
372 username = pwd->pw_name;
373 }
374 else if (gid_unset (gid))
375 {
376 error (EXIT_CANCELED, errno,
377 _("no group specified for unknown uid: %d"), (int) uid);
378 }
379 }
380
381 GETGROUPS_T *gids = out_gids;
382 GETGROUPS_T *in_gids = nullptr;
383 if (groups && *groups)
384 {
385 if (parse_additional_groups (groups, &in_gids, &n_gids, !n_gids) != 0)
386 {
387 if (! n_gids)
388 return EXIT_CANCELED;
389 /* else look-up outside the chroot worked, then go with those. */
390 }
391 else
392 gids = in_gids;
393 }
394 #if HAVE_SETGROUPS
395 else if (! groups && gid_set (gid) && username)
396 {
397 int ngroups = xgetgroups (username, gid, &in_gids);
398 if (ngroups <= 0)
399 {
400 if (! n_gids)
401 error (EXIT_CANCELED, errno,
402 _("failed to get supplemental groups"));
403 /* else look-up outside the chroot worked, then go with those. */
404 }
405 else
406 {
407 n_gids = ngroups;
408 gids = in_gids;
409 }
410 }
411 #endif
412
413 if ((uid_set (uid) || groups) && setgroups (n_gids, gids) != 0)
414 error (EXIT_CANCELED, errno, _("failed to set supplemental groups"));
415
416 free (in_gids);
417 free (out_gids);
418
419 if (gid_set (gid) && setgid (gid))
420 error (EXIT_CANCELED, errno, _("failed to set group-ID"));
421
422 if (uid_set (uid) && setuid (uid))
423 error (EXIT_CANCELED, errno, _("failed to set user-ID"));
424
425 /* Execute the given command. */
426 execvp (argv[0], argv);
427
428 int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
429 error (0, errno, _("failed to run command %s"), quote (argv[0]));
430 return exit_status;
431 }
432