doc: remove older ChangeLog items
[coreutils.git] / src / chroot.c
blobe12454c84a94aed7fdcdecacf9a28337139a5c1b
1 /* chroot -- run command or shell with special root directory
2 Copyright (C) 1995-2024 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 <ctype.h>
21 #include <getopt.h>
22 #include <stdio.h>
23 #include <sys/types.h>
24 #include <pwd.h>
25 #include <grp.h>
27 #include "system.h"
28 #include "ignore-value.h"
29 #include "mgetgroups.h"
30 #include "quote.h"
31 #include "root-dev-ino.h"
32 #include "userspec.h"
33 #include "xstrtol.h"
35 /* The official name of this program (e.g., no 'g' prefix). */
36 #define PROGRAM_NAME "chroot"
38 #define AUTHORS proper_name ("Roland McGrath")
40 #ifndef MAXGID
41 # define MAXGID GID_T_MAX
42 #endif
44 static inline bool uid_unset (uid_t uid) { return uid == (uid_t) -1; }
45 static inline bool gid_unset (gid_t gid) { return gid == (gid_t) -1; }
46 #define uid_set(x) (!uid_unset (x))
47 #define gid_set(x) (!gid_unset (x))
49 enum
51 GROUPS = UCHAR_MAX + 1,
52 USERSPEC,
53 SKIP_CHDIR
56 static struct option const long_opts[] =
58 {"groups", required_argument, nullptr, GROUPS},
59 {"userspec", required_argument, nullptr, USERSPEC},
60 {"skip-chdir", no_argument, nullptr, SKIP_CHDIR},
61 {GETOPT_HELP_OPTION_DECL},
62 {GETOPT_VERSION_OPTION_DECL},
63 {nullptr, 0, nullptr, 0}
66 #if ! HAVE_SETGROUPS
67 /* At least Interix lacks supplemental group support. */
68 static int
69 setgroups (size_t size, MAYBE_UNUSED gid_t const *list)
71 if (size == 0)
73 /* Return success when clearing supplemental groups
74 as ! HAVE_SETGROUPS should only be the case on
75 platforms that don't support supplemental groups. */
76 return 0;
78 else
80 errno = ENOTSUP;
81 return -1;
84 #endif
86 /* Determine the group IDs for the specified supplementary GROUPS,
87 which is a comma separated list of supplementary groups (names or numbers).
88 Allocate an array for the parsed IDs and store it in PGIDS,
89 which may be allocated even on parse failure.
90 Update the number of parsed groups in PN_GIDS on success.
91 Upon any failure return nonzero, and issue diagnostic if SHOW_ERRORS is true.
92 Otherwise return zero. */
94 static int
95 parse_additional_groups (char const *groups, GETGROUPS_T **pgids,
96 size_t *pn_gids, bool show_errors)
98 GETGROUPS_T *gids = nullptr;
99 size_t n_gids_allocated = 0;
100 size_t n_gids = 0;
101 char *buffer = xstrdup (groups);
102 char const *tmp;
103 int ret = 0;
105 for (tmp = strtok (buffer, ","); tmp; tmp = strtok (nullptr, ","))
107 struct group *g;
108 uintmax_t value;
110 if (xstrtoumax (tmp, nullptr, 10, &value, "") == LONGINT_OK
111 && value <= MAXGID)
113 while (isspace (to_uchar (*tmp)))
114 tmp++;
115 if (*tmp != '+')
117 /* Handle the case where the name is numeric. */
118 g = getgrnam (tmp);
119 if (g != nullptr)
120 value = g->gr_gid;
122 /* Flag that we've got a group from the number. */
123 g = (struct group *) (intptr_t) ! nullptr;
125 else
127 g = getgrnam (tmp);
128 if (g != nullptr)
129 value = g->gr_gid;
132 if (g == nullptr)
134 ret = -1;
136 if (show_errors)
138 error (0, errno, _("invalid group %s"), quote (tmp));
139 continue;
142 break;
145 if (n_gids == n_gids_allocated)
146 gids = X2NREALLOC (gids, &n_gids_allocated);
147 gids[n_gids++] = value;
150 if (ret == 0 && n_gids == 0)
152 if (show_errors)
153 error (0, 0, _("invalid group list %s"), quote (groups));
154 ret = -1;
157 *pgids = gids;
159 if (ret == 0)
160 *pn_gids = n_gids;
162 free (buffer);
163 return ret;
166 /* Return whether the passed path is equivalent to "/".
167 Note we don't compare against get_root_dev_ino() as "/"
168 could be bind mounted to a separate location. */
170 static bool
171 is_root (char const *dir)
173 char *resolved = canonicalize_file_name (dir);
174 bool is_res_root = resolved && STREQ ("/", resolved);
175 free (resolved);
176 return is_res_root;
179 void
180 usage (int status)
182 if (status != EXIT_SUCCESS)
183 emit_try_help ();
184 else
186 printf (_("\
187 Usage: %s [OPTION] NEWROOT [COMMAND [ARG]...]\n\
188 or: %s OPTION\n\
189 "), program_name, program_name);
191 fputs (_("\
192 Run COMMAND with root directory set to NEWROOT.\n\
194 "), stdout);
196 fputs (_("\
197 --groups=G_LIST specify supplementary groups as g1,g2,..,gN\n\
198 "), stdout);
199 fputs (_("\
200 --userspec=USER:GROUP specify user and group (ID or name) to use\n\
201 "), stdout);
202 printf (_("\
203 --skip-chdir do not change working directory to %s\n\
204 "), quoteaf ("/"));
206 fputs (HELP_OPTION_DESCRIPTION, stdout);
207 fputs (VERSION_OPTION_DESCRIPTION, stdout);
208 fputs (_("\
210 If no command is given, run '\"$SHELL\" -i' (default: '/bin/sh -i').\n\
211 "), stdout);
212 emit_exec_status (PROGRAM_NAME);
213 emit_ancillary_info (PROGRAM_NAME);
215 exit (status);
219 main (int argc, char **argv)
221 int c;
223 /* Input user and groups spec. */
224 char *userspec = nullptr;
225 char const *username = nullptr;
226 char const *groups = nullptr;
227 bool skip_chdir = false;
229 /* Parsed user and group IDs. */
230 uid_t uid = -1;
231 gid_t gid = -1;
232 GETGROUPS_T *out_gids = nullptr;
233 size_t n_gids = 0;
235 initialize_main (&argc, &argv);
236 set_program_name (argv[0]);
237 setlocale (LC_ALL, "");
238 bindtextdomain (PACKAGE, LOCALEDIR);
239 textdomain (PACKAGE);
241 initialize_exit_failure (EXIT_CANCELED);
242 atexit (close_stdout);
244 while ((c = getopt_long (argc, argv, "+", long_opts, nullptr)) != -1)
246 switch (c)
248 case USERSPEC:
250 userspec = optarg;
251 /* Treat 'user:' just like 'user'
252 as we lookup the primary group by default
253 (and support doing so for UIDs as well as names. */
254 size_t userlen = strlen (userspec);
255 if (userlen && userspec[userlen - 1] == ':')
256 userspec[userlen - 1] = '\0';
257 break;
260 case GROUPS:
261 groups = optarg;
262 break;
264 case SKIP_CHDIR:
265 skip_chdir = true;
266 break;
268 case_GETOPT_HELP_CHAR;
270 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
272 default:
273 usage (EXIT_CANCELED);
277 if (argc <= optind)
279 error (0, 0, _("missing operand"));
280 usage (EXIT_CANCELED);
283 char const *newroot = argv[optind];
284 bool is_oldroot = is_root (newroot);
286 if (! is_oldroot && skip_chdir)
288 error (0, 0, _("option --skip-chdir only permitted if NEWROOT is old %s"),
289 quoteaf ("/"));
290 usage (EXIT_CANCELED);
293 if (! is_oldroot)
295 /* We have to look up users and groups twice.
296 - First, outside the chroot to load potentially necessary passwd/group
297 parsing plugins (e.g. NSS);
298 - Second, inside chroot to redo parsing in case IDs are different.
299 Within chroot lookup is the main justification for having
300 the --user option supported by the chroot command itself. */
301 if (userspec)
302 ignore_value (parse_user_spec (userspec, &uid, &gid, nullptr, nullptr));
304 /* If no gid is supplied or looked up, do so now.
305 Also lookup the username for use with getgroups. */
306 if (uid_set (uid) && (! groups || gid_unset (gid)))
308 const struct passwd *pwd;
309 if ((pwd = getpwuid (uid)))
311 if (gid_unset (gid))
312 gid = pwd->pw_gid;
313 username = pwd->pw_name;
317 if (groups && *groups)
318 ignore_value (parse_additional_groups (groups, &out_gids, &n_gids,
319 false));
320 #if HAVE_SETGROUPS
321 else if (! groups && gid_set (gid) && username)
323 int ngroups = xgetgroups (username, gid, &out_gids);
324 if (0 < ngroups)
325 n_gids = ngroups;
327 #endif
330 if (chroot (newroot) != 0)
331 error (EXIT_CANCELED, errno, _("cannot change root directory to %s"),
332 quoteaf (newroot));
334 if (! skip_chdir && chdir ("/"))
335 error (EXIT_CANCELED, errno, _("cannot chdir to root directory"));
337 if (argc == optind + 1)
339 /* No command. Run an interactive shell. */
340 char *shell = getenv ("SHELL");
341 if (shell == nullptr)
342 shell = bad_cast ("/bin/sh");
343 argv[0] = shell;
344 argv[1] = bad_cast ("-i");
345 argv[2] = nullptr;
347 else
349 /* The following arguments give the command. */
350 argv += optind + 1;
353 /* Attempt to set all three: supplementary groups, group ID, user ID.
354 Diagnose any failures. If any have failed, exit before execvp. */
355 if (userspec)
357 bool warn;
358 char const *err = parse_user_spec_warn (userspec, &uid, &gid,
359 nullptr, nullptr, &warn);
360 if (err)
361 error (warn ? 0 : EXIT_CANCELED, 0, "%s", (err));
364 /* If no gid is supplied or looked up, do so now.
365 Also lookup the username for use with getgroups. */
366 if (uid_set (uid) && (! groups || gid_unset (gid)))
368 const struct passwd *pwd;
369 if ((pwd = getpwuid (uid)))
371 if (gid_unset (gid))
372 gid = pwd->pw_gid;
373 username = pwd->pw_name;
375 else if (gid_unset (gid))
377 error (EXIT_CANCELED, errno,
378 _("no group specified for unknown uid: %d"), (int) uid);
382 GETGROUPS_T *gids = out_gids;
383 GETGROUPS_T *in_gids = nullptr;
384 if (groups && *groups)
386 if (parse_additional_groups (groups, &in_gids, &n_gids, !n_gids) != 0)
388 if (! n_gids)
389 return EXIT_CANCELED;
390 /* else look-up outside the chroot worked, then go with those. */
392 else
393 gids = in_gids;
395 #if HAVE_SETGROUPS
396 else if (! groups && gid_set (gid) && username)
398 int ngroups = xgetgroups (username, gid, &in_gids);
399 if (ngroups <= 0)
401 if (! n_gids)
402 error (EXIT_CANCELED, errno,
403 _("failed to get supplemental groups"));
404 /* else look-up outside the chroot worked, then go with those. */
406 else
408 n_gids = ngroups;
409 gids = in_gids;
412 #endif
414 if ((uid_set (uid) || groups) && setgroups (n_gids, gids) != 0)
415 error (EXIT_CANCELED, errno, _("failed to set supplemental groups"));
417 free (in_gids);
418 free (out_gids);
420 if (gid_set (gid) && setgid (gid))
421 error (EXIT_CANCELED, errno, _("failed to set group-ID"));
423 if (uid_set (uid) && setuid (uid))
424 error (EXIT_CANCELED, errno, _("failed to set user-ID"));
426 /* Execute the given command. */
427 execvp (argv[0], argv);
429 int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
430 error (0, errno, _("failed to run command %s"), quote (argv[0]));
431 return exit_status;