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