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