split: port ‘split -n N /dev/null’ better to macOS
[coreutils.git] / src / chroot.c
blobb5738df2fdebdd97f2bc3c31e889fa3b671017de
1 /* chroot -- run command or shell with special root directory
2 Copyright (C) 1995-2023 Free Software Foundation, Inc.
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.
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.
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/>. */
17 /* Written by Roland McGrath. */
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>
26 #include "system.h"
27 #include "die.h"
28 #include "error.h"
29 #include "ignore-value.h"
30 #include "mgetgroups.h"
31 #include "quote.h"
32 #include "root-dev-ino.h"
33 #include "userspec.h"
34 #include "xstrtol.h"
36 /* The official name of this program (e.g., no 'g' prefix). */
37 #define PROGRAM_NAME "chroot"
39 #define AUTHORS proper_name ("Roland McGrath")
41 #ifndef MAXGID
42 # define MAXGID GID_T_MAX
43 #endif
45 static inline bool uid_unset (uid_t uid) { return uid == (uid_t) -1; }
46 static inline bool gid_unset (gid_t gid) { return gid == (gid_t) -1; }
47 #define uid_set(x) (!uid_unset (x))
48 #define gid_set(x) (!gid_unset (x))
50 enum
52 GROUPS = UCHAR_MAX + 1,
53 USERSPEC,
54 SKIP_CHDIR
57 static struct option const long_opts[] =
59 {"groups", required_argument, NULL, GROUPS},
60 {"userspec", required_argument, NULL, USERSPEC},
61 {"skip-chdir", no_argument, NULL, SKIP_CHDIR},
62 {GETOPT_HELP_OPTION_DECL},
63 {GETOPT_VERSION_OPTION_DECL},
64 {NULL, 0, NULL, 0}
67 #if ! HAVE_SETGROUPS
68 /* At least Interix lacks supplemental group support. */
69 static int
70 setgroups (size_t size, MAYBE_UNUSED gid_t const *list)
72 if (size == 0)
74 /* Return success when clearing supplemental groups
75 as ! HAVE_SETGROUPS should only be the case on
76 platforms that don't support supplemental groups. */
77 return 0;
79 else
81 errno = ENOTSUP;
82 return -1;
85 #endif
87 /* Determine the group IDs for the specified supplementary GROUPS,
88 which is a comma separated list of supplementary groups (names or numbers).
89 Allocate an array for the parsed IDs and store it in PGIDS,
90 which may be allocated even on parse failure.
91 Update the number of parsed groups in PN_GIDS on success.
92 Upon any failure return nonzero, and issue diagnostic if SHOW_ERRORS is true.
93 Otherwise return zero. */
95 static int
96 parse_additional_groups (char const *groups, GETGROUPS_T **pgids,
97 size_t *pn_gids, bool show_errors)
99 GETGROUPS_T *gids = NULL;
100 size_t n_gids_allocated = 0;
101 size_t n_gids = 0;
102 char *buffer = xstrdup (groups);
103 char const *tmp;
104 int ret = 0;
106 for (tmp = strtok (buffer, ","); tmp; tmp = strtok (NULL, ","))
108 struct group *g;
109 uintmax_t value;
111 if (xstrtoumax (tmp, NULL, 10, &value, "") == LONGINT_OK
112 && value <= MAXGID)
114 while (isspace (to_uchar (*tmp)))
115 tmp++;
116 if (*tmp != '+')
118 /* Handle the case where the name is numeric. */
119 g = getgrnam (tmp);
120 if (g != NULL)
121 value = g->gr_gid;
123 /* Flag that we've got a group from the number. */
124 g = (struct group *) (intptr_t) ! NULL;
126 else
128 g = getgrnam (tmp);
129 if (g != NULL)
130 value = g->gr_gid;
133 if (g == NULL)
135 ret = -1;
137 if (show_errors)
139 error (0, errno, _("invalid group %s"), quote (tmp));
140 continue;
143 break;
146 if (n_gids == n_gids_allocated)
147 gids = X2NREALLOC (gids, &n_gids_allocated);
148 gids[n_gids++] = value;
151 if (ret == 0 && n_gids == 0)
153 if (show_errors)
154 error (0, 0, _("invalid group list %s"), quote (groups));
155 ret = -1;
158 *pgids = gids;
160 if (ret == 0)
161 *pn_gids = n_gids;
163 free (buffer);
164 return ret;
167 /* Return whether the passed path is equivalent to "/".
168 Note we don't compare against get_root_dev_ino() as "/"
169 could be bind mounted to a separate location. */
171 static bool
172 is_root (char const *dir)
174 char *resolved = canonicalize_file_name (dir);
175 bool is_res_root = resolved && STREQ ("/", resolved);
176 free (resolved);
177 return is_res_root;
180 void
181 usage (int status)
183 if (status != EXIT_SUCCESS)
184 emit_try_help ();
185 else
187 printf (_("\
188 Usage: %s [OPTION] NEWROOT [COMMAND [ARG]...]\n\
189 or: %s OPTION\n\
190 "), program_name, program_name);
192 fputs (_("\
193 Run COMMAND with root directory set to NEWROOT.\n\
195 "), stdout);
197 fputs (_("\
198 --groups=G_LIST specify supplementary groups as g1,g2,..,gN\n\
199 "), stdout);
200 fputs (_("\
201 --userspec=USER:GROUP specify user and group (ID or name) to use\n\
202 "), stdout);
203 printf (_("\
204 --skip-chdir do not change working directory to %s\n\
205 "), quoteaf ("/"));
207 fputs (HELP_OPTION_DESCRIPTION, stdout);
208 fputs (VERSION_OPTION_DESCRIPTION, stdout);
209 fputs (_("\
211 If no command is given, run '\"$SHELL\" -i' (default: '/bin/sh -i').\n\
212 "), stdout);
213 emit_exec_status (PROGRAM_NAME);
214 emit_ancillary_info (PROGRAM_NAME);
216 exit (status);
220 main (int argc, char **argv)
222 int c;
224 /* Input user and groups spec. */
225 char *userspec = NULL;
226 char const *username = NULL;
227 char const *groups = NULL;
228 bool skip_chdir = false;
230 /* Parsed user and group IDs. */
231 uid_t uid = -1;
232 gid_t gid = -1;
233 GETGROUPS_T *out_gids = NULL;
234 size_t n_gids = 0;
236 initialize_main (&argc, &argv);
237 set_program_name (argv[0]);
238 setlocale (LC_ALL, "");
239 bindtextdomain (PACKAGE, LOCALEDIR);
240 textdomain (PACKAGE);
242 initialize_exit_failure (EXIT_CANCELED);
243 atexit (close_stdout);
245 while ((c = getopt_long (argc, argv, "+", long_opts, NULL)) != -1)
247 switch (c)
249 case USERSPEC:
251 userspec = optarg;
252 /* Treat 'user:' just like 'user'
253 as we lookup the primary group by default
254 (and support doing so for UIDs as well as names. */
255 size_t userlen = strlen (userspec);
256 if (userlen && userspec[userlen - 1] == ':')
257 userspec[userlen - 1] = '\0';
258 break;
261 case GROUPS:
262 groups = optarg;
263 break;
265 case SKIP_CHDIR:
266 skip_chdir = true;
267 break;
269 case_GETOPT_HELP_CHAR;
271 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
273 default:
274 usage (EXIT_CANCELED);
278 if (argc <= optind)
280 error (0, 0, _("missing operand"));
281 usage (EXIT_CANCELED);
284 char const *newroot = argv[optind];
285 bool is_oldroot = is_root (newroot);
287 if (! is_oldroot && skip_chdir)
289 error (0, 0, _("option --skip-chdir only permitted if NEWROOT is old %s"),
290 quoteaf ("/"));
291 usage (EXIT_CANCELED);
294 if (! is_oldroot)
296 /* We have to look up users and groups twice.
297 - First, outside the chroot to load potentially necessary passwd/group
298 parsing plugins (e.g. NSS);
299 - Second, inside chroot to redo parsing in case IDs are different.
300 Within chroot lookup is the main justification for having
301 the --user option supported by the chroot command itself. */
302 if (userspec)
303 ignore_value (parse_user_spec (userspec, &uid, &gid, NULL, NULL));
305 /* If no gid is supplied or looked up, do so now.
306 Also lookup the username for use with getgroups. */
307 if (uid_set (uid) && (! groups || gid_unset (gid)))
309 const struct passwd *pwd;
310 if ((pwd = getpwuid (uid)))
312 if (gid_unset (gid))
313 gid = pwd->pw_gid;
314 username = pwd->pw_name;
318 if (groups && *groups)
319 ignore_value (parse_additional_groups (groups, &out_gids, &n_gids,
320 false));
321 #if HAVE_SETGROUPS
322 else if (! groups && gid_set (gid) && username)
324 int ngroups = xgetgroups (username, gid, &out_gids);
325 if (0 < ngroups)
326 n_gids = ngroups;
328 #endif
331 if (chroot (newroot) != 0)
332 die (EXIT_CANCELED, errno, _("cannot change root directory to %s"),
333 quoteaf (newroot));
335 if (! skip_chdir && chdir ("/"))
336 die (EXIT_CANCELED, errno, _("cannot chdir to root directory"));
338 if (argc == optind + 1)
340 /* No command. Run an interactive shell. */
341 char *shell = getenv ("SHELL");
342 if (shell == NULL)
343 shell = bad_cast ("/bin/sh");
344 argv[0] = shell;
345 argv[1] = bad_cast ("-i");
346 argv[2] = NULL;
348 else
350 /* The following arguments give the command. */
351 argv += optind + 1;
354 /* Attempt to set all three: supplementary groups, group ID, user ID.
355 Diagnose any failures. If any have failed, exit before execvp. */
356 if (userspec)
358 bool warn;
359 char const *err = parse_user_spec_warn (userspec, &uid, &gid,
360 NULL, NULL, &warn);
361 if (err)
362 error (warn ? 0 : EXIT_CANCELED, 0, "%s", (err));
365 /* If no gid is supplied or looked up, do so now.
366 Also lookup the username for use with getgroups. */
367 if (uid_set (uid) && (! groups || gid_unset (gid)))
369 const struct passwd *pwd;
370 if ((pwd = getpwuid (uid)))
372 if (gid_unset (gid))
373 gid = pwd->pw_gid;
374 username = pwd->pw_name;
376 else if (gid_unset (gid))
378 die (EXIT_CANCELED, errno,
379 _("no group specified for unknown uid: %d"), (int) uid);
383 GETGROUPS_T *gids = out_gids;
384 GETGROUPS_T *in_gids = NULL;
385 if (groups && *groups)
387 if (parse_additional_groups (groups, &in_gids, &n_gids, !n_gids) != 0)
389 if (! n_gids)
390 return EXIT_CANCELED;
391 /* else look-up outside the chroot worked, then go with those. */
393 else
394 gids = in_gids;
396 #if HAVE_SETGROUPS
397 else if (! groups && gid_set (gid) && username)
399 int ngroups = xgetgroups (username, gid, &in_gids);
400 if (ngroups <= 0)
402 if (! n_gids)
403 die (EXIT_CANCELED, errno,
404 _("failed to get supplemental groups"));
405 /* else look-up outside the chroot worked, then go with those. */
407 else
409 n_gids = ngroups;
410 gids = in_gids;
413 #endif
415 if ((uid_set (uid) || groups) && setgroups (n_gids, gids) != 0)
416 die (EXIT_CANCELED, errno, _("failed to set supplemental groups"));
418 free (in_gids);
419 free (out_gids);
421 if (gid_set (gid) && setgid (gid))
422 die (EXIT_CANCELED, errno, _("failed to set group-ID"));
424 if (uid_set (uid) && setuid (uid))
425 die (EXIT_CANCELED, errno, _("failed to set user-ID"));
427 /* Execute the given command. */
428 execvp (argv[0], argv);
430 int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
431 error (0, errno, _("failed to run command %s"), quote (argv[0]));
432 return exit_status;