1 /* chroot -- run command or shell with special root directory
2 Copyright (C) 1995-2022 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. */
22 #include <sys/types.h>
29 #include "ignore-value.h"
30 #include "mgetgroups.h"
32 #include "root-dev-ino.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")
42 # define MAXGID GID_T_MAX
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))
52 GROUPS
= UCHAR_MAX
+ 1,
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
},
68 /* At least Interix lacks supplemental group support. */
70 setgroups (size_t size
, MAYBE_UNUSED gid_t
const *list
)
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. */
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. */
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;
102 char *buffer
= xstrdup (groups
);
106 for (tmp
= strtok (buffer
, ","); tmp
; tmp
= strtok (NULL
, ","))
111 if (xstrtoumax (tmp
, NULL
, 10, &value
, "") == LONGINT_OK
114 while (isspace (to_uchar (*tmp
)))
118 /* Handle the case where the name is numeric. */
123 /* Flag that we've got a group from the number. */
124 g
= (struct group
*) (intptr_t) ! NULL
;
139 error (0, errno
, _("invalid group %s"), quote (tmp
));
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)
154 error (0, 0, _("invalid group list %s"), quote (groups
));
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. */
172 is_root (char const *dir
)
174 char *resolved
= canonicalize_file_name (dir
);
175 bool is_res_root
= resolved
&& STREQ ("/", resolved
);
183 if (status
!= EXIT_SUCCESS
)
188 Usage: %s [OPTION] NEWROOT [COMMAND [ARG]...]\n\
190 "), program_name
, program_name
);
193 Run COMMAND with root directory set to NEWROOT.\n\
198 --groups=G_LIST specify supplementary groups as g1,g2,..,gN\n\
201 --userspec=USER:GROUP specify user and group (ID or name) to use\n\
204 --skip-chdir do not change working directory to %s\n\
207 fputs (HELP_OPTION_DESCRIPTION
, stdout
);
208 fputs (VERSION_OPTION_DESCRIPTION
, stdout
);
211 If no command is given, run '\"$SHELL\" -i' (default: '/bin/sh -i').\n\
213 emit_ancillary_info (PROGRAM_NAME
);
219 main (int argc
, char **argv
)
223 /* Input user and groups spec. */
224 char *userspec
= NULL
;
225 char const *username
= NULL
;
226 char const *groups
= NULL
;
227 bool skip_chdir
= false;
229 /* Parsed user and group IDs. */
232 GETGROUPS_T
*out_gids
= NULL
;
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
, NULL
)) != -1)
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';
268 case_GETOPT_HELP_CHAR
;
270 case_GETOPT_VERSION_CHAR (PROGRAM_NAME
, AUTHORS
);
273 usage (EXIT_CANCELED
);
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"),
290 usage (EXIT_CANCELED
);
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. */
302 ignore_value (parse_user_spec (userspec
, &uid
, &gid
, NULL
, NULL
));
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
)))
313 username
= pwd
->pw_name
;
317 if (groups
&& *groups
)
318 ignore_value (parse_additional_groups (groups
, &out_gids
, &n_gids
,
321 else if (! groups
&& gid_set (gid
) && username
)
323 int ngroups
= xgetgroups (username
, gid
, &out_gids
);
330 if (chroot (newroot
) != 0)
331 die (EXIT_CANCELED
, errno
, _("cannot change root directory to %s"),
334 if (! skip_chdir
&& chdir ("/"))
335 die (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");
342 shell
= bad_cast ("/bin/sh");
344 argv
[1] = bad_cast ("-i");
349 /* The following arguments give the command. */
353 /* Attempt to set all three: supplementary groups, group ID, user ID.
354 Diagnose any failures. If any have failed, exit before execvp. */
357 char const *err
= parse_user_spec (userspec
, &uid
, &gid
, NULL
, NULL
);
359 if (err
&& uid_unset (uid
) && gid_unset (gid
))
360 die (EXIT_CANCELED
, errno
, "%s", (err
));
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
)))
367 const struct passwd
*pwd
;
368 if ((pwd
= getpwuid (uid
)))
372 username
= pwd
->pw_name
;
374 else if (gid_unset (gid
))
376 die (EXIT_CANCELED
, errno
,
377 _("no group specified for unknown uid: %d"), (int) uid
);
381 GETGROUPS_T
*gids
= out_gids
;
382 GETGROUPS_T
*in_gids
= NULL
;
383 if (groups
&& *groups
)
385 if (parse_additional_groups (groups
, &in_gids
, &n_gids
, !n_gids
) != 0)
388 return EXIT_CANCELED
;
389 /* else look-up outside the chroot worked, then go with those. */
395 else if (! groups
&& gid_set (gid
) && username
)
397 int ngroups
= xgetgroups (username
, gid
, &in_gids
);
401 die (EXIT_CANCELED
, errno
,
402 _("failed to get supplemental groups"));
403 /* else look-up outside the chroot worked, then go with those. */
413 if ((uid_set (uid
) || groups
) && setgroups (n_gids
, gids
) != 0)
414 die (EXIT_CANCELED
, errno
, _("failed to set supplemental groups"));
419 if (gid_set (gid
) && setgid (gid
))
420 die (EXIT_CANCELED
, errno
, _("failed to set group-ID"));
422 if (uid_set (uid
) && setuid (uid
))
423 die (EXIT_CANCELED
, errno
, _("failed to set user-ID"));
425 /* Execute the given command. */
426 execvp (argv
[0], argv
);
428 int exit_status
= errno
== ENOENT
? EXIT_ENOENT
: EXIT_CANNOT_INVOKE
;
429 error (0, errno
, _("failed to run command %s"), quote (argv
[0]));