Skip to main content

uu_chroot/
chroot.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (ToDO) NEWROOT Userspec pstatus chdir
7mod error;
8
9use crate::error::ChrootError;
10use clap::{Arg, ArgAction, Command};
11use std::ffi::OsString;
12use std::io::{Error, ErrorKind};
13use std::os::unix::process::CommandExt;
14use std::path::{Path, PathBuf};
15use std::process;
16use uucore::entries::{Locate, Passwd, grp2gid, usr2gid, usr2uid};
17use uucore::error::{UResult, UUsageError};
18use uucore::fs::{MissingHandling, ResolveMode, canonicalize};
19use uucore::libc::{self, setgid, setgroups, setuid};
20use uucore::{format_usage, show};
21
22use uucore::translate;
23
24mod options {
25    pub const NEWROOT: &str = "newroot";
26    pub const GROUPS: &str = "groups";
27    pub const USERSPEC: &str = "userspec";
28    pub const COMMAND: &str = "command";
29    pub const SKIP_CHDIR: &str = "skip-chdir";
30}
31
32/// A user and group specification, where each is optional.
33enum UserSpec {
34    NeitherGroupNorUser,
35    UserOnly(String),
36    GroupOnly(String),
37    UserAndGroup(String, String),
38}
39
40struct Options {
41    /// Path to the new root directory, as the caller spelled it. Used for
42    /// diagnostics.
43    newroot: PathBuf,
44    /// The path actually passed to `chroot(2)`, when it must differ from
45    /// `newroot`. See the `--skip-chdir` handling in `uumain`.
46    chroot_target: Option<PathBuf>,
47    /// Whether to change to the new root directory.
48    skip_chdir: bool,
49    /// List of groups under which the command will be run.
50    groups: Option<Vec<String>>,
51    /// The user and group (each optional) under which the command will be run.
52    userspec: Option<UserSpec>,
53}
54
55/// Parse a user and group from the argument to `--userspec`.
56///
57/// The `spec` must be of the form `[USER][:[GROUP]]`, otherwise an
58/// error is returned.
59fn parse_userspec(spec: &str) -> UserSpec {
60    match spec.split_once(':') {
61        // ""
62        None if spec.is_empty() => UserSpec::NeitherGroupNorUser,
63        // "usr"
64        None => UserSpec::UserOnly(spec.to_string()),
65        // ":"
66        Some(("", "")) => UserSpec::NeitherGroupNorUser,
67        // ":grp"
68        Some(("", grp)) => UserSpec::GroupOnly(grp.to_string()),
69        // "usr:"
70        Some((usr, "")) => UserSpec::UserOnly(usr.to_string()),
71        // "usr:grp"
72        Some((usr, grp)) => UserSpec::UserAndGroup(usr.to_string(), grp.to_string()),
73    }
74}
75
76/// Pre-condition: `list_str` is non-empty.
77fn parse_group_list(list_str: &str) -> Result<Vec<String>, ChrootError> {
78    let split: Vec<&str> = list_str.split(',').collect();
79    if split.len() == 1 {
80        let name = split[0].trim();
81        if name.is_empty() {
82            // --groups=" "
83            // chroot: invalid group ' '
84            Err(ChrootError::InvalidGroup(name.to_string()))
85        } else {
86            // --groups="blah"
87            Ok(vec![name.to_string()])
88        }
89    } else if split.iter().all(|s| s.is_empty()) {
90        // --groups=","
91        // chroot: invalid group list ','
92        Err(ChrootError::InvalidGroupList(list_str.to_string()))
93    } else {
94        let mut result = vec![];
95        let mut err = false;
96        for name in split {
97            let trimmed_name = name.trim();
98            if trimmed_name.is_empty() {
99                if name.is_empty() {
100                    // --groups=","
101                    continue;
102                }
103
104                // --groups=", "
105                // chroot: invalid group ' '
106                show!(ChrootError::InvalidGroup(name.to_string()));
107                err = true;
108            } else {
109                // TODO Figure out a better condition here.
110                if trimmed_name.starts_with(char::is_numeric)
111                    && trimmed_name.ends_with(|c: char| !c.is_numeric())
112                {
113                    // --groups="0trail"
114                    // chroot: invalid group '0trail'
115                    show!(ChrootError::InvalidGroup(name.to_string()));
116                    err = true;
117                } else {
118                    result.push(trimmed_name.to_string());
119                }
120            }
121        }
122        if err {
123            Err(ChrootError::GroupsParsingFailed)
124        } else {
125            Ok(result)
126        }
127    }
128}
129
130impl Options {
131    /// Parse parameters from the command-line arguments.
132    fn from(matches: &clap::ArgMatches) -> UResult<Self> {
133        let newroot = match matches.get_one::<String>(options::NEWROOT) {
134            Some(v) => Path::new(v).to_path_buf(),
135            None => return Err(ChrootError::MissingNewRoot.into()),
136        };
137        let groups = match matches.get_one::<String>(options::GROUPS) {
138            None => None,
139            Some(s) => {
140                if s.is_empty() {
141                    Some(vec![])
142                } else {
143                    Some(parse_group_list(s)?)
144                }
145            }
146        };
147        let skip_chdir = matches.get_flag(options::SKIP_CHDIR);
148        let userspec = matches
149            .get_one::<String>(options::USERSPEC)
150            .map(|s| parse_userspec(s));
151        Ok(Self {
152            newroot,
153            chroot_target: None,
154            skip_chdir,
155            groups,
156            userspec,
157        })
158    }
159}
160
161#[uucore::main]
162pub fn uumain(args: impl uucore::Args) -> UResult<()> {
163    let matches =
164        uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?;
165
166    let mut options = Options::from(&matches)?;
167
168    // We are resolving the path in case it is a symlink or /. or /../
169    //
170    // GNU validates the resolved path but then chroots the original spelling,
171    // which leaves a window: a NEWROOT symlink repointed after the check would
172    // pass the guard and still put the process somewhere else, with a working
173    // directory left outside it because --skip-chdir suppresses the chdir.
174    // Since the guard only succeeds when the resolution is `/`, chrooting the
175    // resolved path is the same destination and closes that window.
176    if options.skip_chdir {
177        let resolved = canonicalize(
178            &options.newroot,
179            MissingHandling::Normal,
180            ResolveMode::Logical,
181        )
182        // A NEWROOT that does not resolve is by definition not old `/`, so treat
183        // an Err as a non-match instead of unwrapping it.
184        .ok();
185        if resolved.as_deref().and_then(|p| p.to_str()) != Some("/") {
186            return Err(UUsageError::new(
187                125,
188                translate!("chroot-error-skip-chdir-only-permitted"),
189            ));
190        }
191        // The guard proved the resolution is `/`, so chrooting it is the same
192        // destination, minus the window. NEWROOT is kept for diagnostics so the
193        // error text still names what the caller asked for.
194        options.chroot_target = resolved;
195    }
196
197    if !options.newroot.is_dir() {
198        return Err(ChrootError::NoSuchDirectory(options.newroot).into());
199    }
200
201    let mut cmd_iter = matches
202        .get_many::<OsString>(options::COMMAND)
203        .into_iter()
204        .flatten();
205    let (chroot_command, args) = match cmd_iter.next() {
206        Some(c) => (c.clone(), cmd_iter.cloned().collect::<Vec<OsString>>()),
207        None => (
208            std::env::var_os("SHELL").unwrap_or_else(|| "/bin/sh".into()),
209            vec!["-i".into()],
210        ),
211    };
212
213    // NOTE: Tests can only trigger code beyond this point if they're invoked with root permissions
214    set_context(&options)?;
215
216    let err = process::Command::new(&chroot_command).args(&args).exec();
217
218    Err(if err.kind() == ErrorKind::NotFound {
219        ChrootError::CommandNotFound(chroot_command, err)
220    } else {
221        ChrootError::CommandFailed(chroot_command, err)
222    }
223    .into())
224}
225
226pub fn uu_app() -> Command {
227    let cmd = Command::new("chroot")
228        .version(uucore::crate_version!())
229        .about(translate!("chroot-about"))
230        .override_usage(format_usage(&translate!("chroot-usage")))
231        .infer_long_args(true)
232        .trailing_var_arg(true);
233    uucore::clap_localization::configure_localized_command(cmd)
234        .arg(
235            Arg::new(options::NEWROOT)
236                .value_hint(clap::ValueHint::DirPath)
237                .hide(true)
238                .required(true)
239                .index(1),
240        )
241        .arg(
242            Arg::new(options::GROUPS)
243                .long(options::GROUPS)
244                .overrides_with(options::GROUPS)
245                .help(translate!("chroot-help-groups"))
246                .value_name("GROUP1,GROUP2..."),
247        )
248        .arg(
249            Arg::new(options::USERSPEC)
250                .long(options::USERSPEC)
251                .help(translate!("chroot-help-userspec"))
252                .value_name("USER:GROUP"),
253        )
254        .arg(
255            Arg::new(options::SKIP_CHDIR)
256                .long(options::SKIP_CHDIR)
257                .help(translate!("chroot-help-skip-chdir"))
258                .action(ArgAction::SetTrue),
259        )
260        .arg(
261            Arg::new(options::COMMAND)
262                .action(ArgAction::Append)
263                .value_hint(clap::ValueHint::CommandName)
264                .value_parser(clap::value_parser!(OsString))
265                .hide(true)
266                .index(2),
267        )
268}
269
270/// Get the UID for the given username, falling back to numeric parsing.
271///
272/// According to the documentation of GNU `chroot`, "POSIX requires that
273/// these commands first attempt to resolve the specified string as a
274/// name, and only once that fails, then try to interpret it as an ID."
275fn name_to_uid(name: &str) -> Result<libc::uid_t, ChrootError> {
276    match usr2uid(name) {
277        Ok(uid) => Ok(uid),
278        Err(_) => name
279            .parse::<libc::uid_t>()
280            .map_err(|_| ChrootError::NoSuchUser),
281    }
282}
283
284/// Get the GID for the given group name, falling back to numeric parsing.
285///
286/// According to the documentation of GNU `chroot`, "POSIX requires that
287/// these commands first attempt to resolve the specified string as a
288/// name, and only once that fails, then try to interpret it as an ID."
289fn name_to_gid(name: &str) -> Result<libc::gid_t, ChrootError> {
290    match grp2gid(name) {
291        Ok(gid) => Ok(gid),
292        Err(_) => name
293            .parse::<libc::gid_t>()
294            .map_err(|_| ChrootError::NoSuchGroup),
295    }
296}
297
298/// Get the list of group IDs for the given user.
299///
300/// According to the GNU documentation, "the supplementary groups are
301/// set according to the system defined list for that user". This
302/// function gets that list.
303fn supplemental_gids(uid: libc::uid_t) -> Vec<libc::gid_t> {
304    match Passwd::locate(uid) {
305        Err(_) => vec![],
306        Ok(passwd) => passwd.belongs_to(),
307    }
308}
309
310/// Set the supplemental group IDs for this process.
311fn set_supplemental_gids(gids: &[libc::gid_t]) -> std::io::Result<()> {
312    #[cfg(any(
313        target_vendor = "apple",
314        target_os = "freebsd",
315        target_os = "openbsd",
316        target_os = "cygwin",
317        target_os = "netbsd"
318    ))]
319    let n = gids.len() as core::ffi::c_int;
320    #[cfg(any(target_os = "linux", target_os = "android"))]
321    let n = gids.len() as libc::size_t;
322    let err = unsafe { setgroups(n, gids.as_ptr()) };
323    if err == 0 {
324        Ok(())
325    } else {
326        Err(Error::last_os_error())
327    }
328}
329
330/// Set the group ID of this process.
331fn set_gid(gid: libc::gid_t) -> std::io::Result<()> {
332    let err = unsafe { setgid(gid) };
333    if err == 0 {
334        Ok(())
335    } else {
336        Err(Error::last_os_error())
337    }
338}
339
340/// Set the user ID of this process.
341fn set_uid(uid: libc::uid_t) -> std::io::Result<()> {
342    let err = unsafe { setuid(uid) };
343    if err == 0 {
344        Ok(())
345    } else {
346        Err(Error::last_os_error())
347    }
348}
349
350/// What to do when the `--groups` argument is missing.
351enum Strategy {
352    /// Do nothing.
353    Nothing,
354    /// Use the list of supplemental groups for the given user.
355    ///
356    /// If the `bool` parameter is `false` and the list of groups for
357    /// the given user is empty, then this will result in an error.
358    FromUID(libc::uid_t, bool),
359}
360
361/// Set supplemental groups when the `--groups` argument is not specified.
362fn handle_missing_groups(strategy: Strategy) -> Result<(), ChrootError> {
363    match strategy {
364        Strategy::Nothing => Ok(()),
365        Strategy::FromUID(uid, false) => {
366            let gids = supplemental_gids(uid);
367            if gids.is_empty() {
368                Err(ChrootError::NoGroupSpecified(uid))
369            } else {
370                set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
371            }
372        }
373        Strategy::FromUID(uid, true) => {
374            let gids = supplemental_gids(uid);
375            set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
376        }
377    }
378}
379
380/// Set supplemental groups for this process.
381fn set_supplemental_gids_with_strategy(
382    strategy: Strategy,
383    groups: Option<&Vec<String>>,
384) -> Result<(), ChrootError> {
385    match groups {
386        None => handle_missing_groups(strategy),
387        Some(groups) => {
388            let mut gids = vec![];
389            for group in groups {
390                gids.push(name_to_gid(group)?);
391            }
392            set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
393        }
394    }
395}
396
397/// Change the root, set the user ID, and set the group IDs for this process.
398fn set_context(options: &Options) -> UResult<()> {
399    match &options.userspec {
400        None | Some(UserSpec::NeitherGroupNorUser) => {
401            let strategy = Strategy::Nothing;
402            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
403            enter_chroot(options, options.skip_chdir)?;
404        }
405        Some(UserSpec::UserOnly(user)) => {
406            let uid = name_to_uid(user)?;
407            let gid = usr2gid(user).map_err(|_| ChrootError::NoGroupSpecified(uid))?;
408            let strategy = Strategy::FromUID(uid, false);
409            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
410            enter_chroot(options, options.skip_chdir)?;
411            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(user.to_owned(), e))?;
412            set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_owned(), e))?;
413        }
414        Some(UserSpec::GroupOnly(group)) => {
415            let gid = name_to_gid(group)?;
416            let strategy = Strategy::Nothing;
417            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
418            enter_chroot(options, options.skip_chdir)?;
419            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_owned(), e))?;
420        }
421        Some(UserSpec::UserAndGroup(user, group)) => {
422            let uid = name_to_uid(user)?;
423            let gid = name_to_gid(group)?;
424            let strategy = Strategy::FromUID(uid, true);
425            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
426            enter_chroot(options, options.skip_chdir)?;
427            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_owned(), e))?;
428            set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_owned(), e))?;
429        }
430    }
431    Ok(())
432}
433
434fn enter_chroot(options: &Options, skip_chdir: bool) -> UResult<()> {
435    // chroot the resolved target when there is one; name the caller's spelling
436    // in the error either way.
437    let target = options.chroot_target.as_deref().unwrap_or(&options.newroot);
438    rustix::process::chroot(target)
439        .map_err(|e| ChrootError::CannotEnter(options.newroot.clone(), e.into()))?;
440    if !skip_chdir {
441        std::env::set_current_dir("/")?;
442    }
443    Ok(())
444}