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::CString;
12use std::io::Error;
13use std::os::unix::prelude::OsStrExt;
14use std::path::{Path, PathBuf};
15use std::process;
16use uucore::entries::{Locate, Passwd, grp2gid, usr2uid};
17use uucore::error::{UResult, UUsageError, set_exit_code};
18use uucore::fs::{MissingHandling, ResolveMode, canonicalize};
19use uucore::libc::{self, chroot, 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 str = "/bin/sh";
162    let default_option: &'static str = "-i";
163    let user_shell = std::env::var("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(format!("{}", options.newroot.display())).into());
186    }
187
188    let commands = match matches.get_many::<String>(options::COMMAND) {
189        Some(v) => v.map(|s| s.as_str()).collect(),
190        None => vec![],
191    };
192
193    // TODO: refactor the args and command matching
194    // See: https://github.com/uutils/coreutils/pull/2365#discussion_r647849967
195    let command: Vec<&str> = match commands.len() {
196        0 => {
197            let shell: &str = match user_shell {
198                Err(_) => default_shell,
199                Ok(ref s) => s.as_ref(),
200            };
201            vec![shell, default_option]
202        }
203        _ => commands,
204    };
205
206    assert!(!command.is_empty());
207    let chroot_command = command[0];
208    let chroot_args = &command[1..];
209
210    // NOTE: Tests can only trigger code beyond this point if they're invoked with root permissions
211    set_context(&options)?;
212
213    let pstatus = match process::Command::new(chroot_command)
214        .args(chroot_args)
215        .status()
216    {
217        Ok(status) => status,
218        Err(e) => {
219            return Err(if e.kind() == std::io::ErrorKind::NotFound {
220                ChrootError::CommandNotFound(command[0].to_string(), e)
221            } else {
222                ChrootError::CommandFailed(command[0].to_string(), e)
223            }
224            .into());
225        }
226    };
227
228    let code = if pstatus.success() {
229        0
230    } else {
231        pstatus.code().unwrap_or(-1)
232    };
233    set_exit_code(code);
234    Ok(())
235}
236
237pub fn uu_app() -> Command {
238    let cmd = Command::new(uucore::util_name())
239        .version(uucore::crate_version!())
240        .about(translate!("chroot-about"))
241        .override_usage(format_usage(&translate!("chroot-usage")))
242        .infer_long_args(true)
243        .trailing_var_arg(true);
244    uucore::clap_localization::configure_localized_command(cmd)
245        .arg(
246            Arg::new(options::NEWROOT)
247                .value_hint(clap::ValueHint::DirPath)
248                .hide(true)
249                .required(true)
250                .index(1),
251        )
252        .arg(
253            Arg::new(options::GROUPS)
254                .long(options::GROUPS)
255                .overrides_with(options::GROUPS)
256                .help(translate!("chroot-help-groups"))
257                .value_name("GROUP1,GROUP2..."),
258        )
259        .arg(
260            Arg::new(options::USERSPEC)
261                .long(options::USERSPEC)
262                .help(translate!("chroot-help-userspec"))
263                .value_name("USER:GROUP"),
264        )
265        .arg(
266            Arg::new(options::SKIP_CHDIR)
267                .long(options::SKIP_CHDIR)
268                .help(translate!("chroot-help-skip-chdir"))
269                .action(ArgAction::SetTrue),
270        )
271        .arg(
272            Arg::new(options::COMMAND)
273                .action(ArgAction::Append)
274                .value_hint(clap::ValueHint::CommandName)
275                .hide(true)
276                .index(2),
277        )
278}
279
280/// Get the UID for the given username, falling back to numeric parsing.
281///
282/// According to the documentation of GNU `chroot`, "POSIX requires that
283/// these commands first attempt to resolve the specified string as a
284/// name, and only once that fails, then try to interpret it as an ID."
285fn name_to_uid(name: &str) -> Result<libc::uid_t, ChrootError> {
286    match usr2uid(name) {
287        Ok(uid) => Ok(uid),
288        Err(_) => name
289            .parse::<libc::uid_t>()
290            .map_err(|_| ChrootError::NoSuchUser),
291    }
292}
293
294/// Get the GID for the given group name, falling back to numeric parsing.
295///
296/// According to the documentation of GNU `chroot`, "POSIX requires that
297/// these commands first attempt to resolve the specified string as a
298/// name, and only once that fails, then try to interpret it as an ID."
299fn name_to_gid(name: &str) -> Result<libc::gid_t, ChrootError> {
300    match grp2gid(name) {
301        Ok(gid) => Ok(gid),
302        Err(_) => name
303            .parse::<libc::gid_t>()
304            .map_err(|_| ChrootError::NoSuchGroup),
305    }
306}
307
308/// Get the list of group IDs for the given user.
309///
310/// According to the GNU documentation, "the supplementary groups are
311/// set according to the system defined list for that user". This
312/// function gets that list.
313fn supplemental_gids(uid: libc::uid_t) -> Vec<libc::gid_t> {
314    match Passwd::locate(uid) {
315        Err(_) => vec![],
316        Ok(passwd) => passwd.belongs_to(),
317    }
318}
319
320/// Set the supplemental group IDs for this process.
321fn set_supplemental_gids(gids: &[libc::gid_t]) -> std::io::Result<()> {
322    #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))]
323    let n = gids.len() as libc::c_int;
324    #[cfg(any(target_os = "linux", target_os = "android"))]
325    let n = gids.len() as libc::size_t;
326    let err = unsafe { setgroups(n, gids.as_ptr()) };
327    if err == 0 {
328        Ok(())
329    } else {
330        Err(Error::last_os_error())
331    }
332}
333
334/// Set the group ID of this process.
335fn set_gid(gid: libc::gid_t) -> std::io::Result<()> {
336    let err = unsafe { setgid(gid) };
337    if err == 0 {
338        Ok(())
339    } else {
340        Err(Error::last_os_error())
341    }
342}
343
344/// Set the user ID of this process.
345fn set_uid(uid: libc::uid_t) -> std::io::Result<()> {
346    let err = unsafe { setuid(uid) };
347    if err == 0 {
348        Ok(())
349    } else {
350        Err(Error::last_os_error())
351    }
352}
353
354/// What to do when the `--groups` argument is missing.
355enum Strategy {
356    /// Do nothing.
357    Nothing,
358    /// Use the list of supplemental groups for the given user.
359    ///
360    /// If the `bool` parameter is `false` and the list of groups for
361    /// the given user is empty, then this will result in an error.
362    FromUID(libc::uid_t, bool),
363}
364
365/// Set supplemental groups when the `--groups` argument is not specified.
366fn handle_missing_groups(strategy: Strategy) -> Result<(), ChrootError> {
367    match strategy {
368        Strategy::Nothing => Ok(()),
369        Strategy::FromUID(uid, false) => {
370            let gids = supplemental_gids(uid);
371            if gids.is_empty() {
372                Err(ChrootError::NoGroupSpecified(uid))
373            } else {
374                set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
375            }
376        }
377        Strategy::FromUID(uid, true) => {
378            let gids = supplemental_gids(uid);
379            set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
380        }
381    }
382}
383
384/// Set supplemental groups for this process.
385fn set_supplemental_gids_with_strategy(
386    strategy: Strategy,
387    groups: Option<&Vec<String>>,
388) -> Result<(), ChrootError> {
389    match groups {
390        None => handle_missing_groups(strategy),
391        Some(groups) => {
392            let mut gids = vec![];
393            for group in groups {
394                gids.push(name_to_gid(group)?);
395            }
396            set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed)
397        }
398    }
399}
400
401/// Change the root, set the user ID, and set the group IDs for this process.
402fn set_context(options: &Options) -> UResult<()> {
403    enter_chroot(&options.newroot, options.skip_chdir)?;
404    match &options.userspec {
405        None | Some(UserSpec::NeitherGroupNorUser) => {
406            let strategy = Strategy::Nothing;
407            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
408        }
409        Some(UserSpec::UserOnly(user)) => {
410            let uid = name_to_uid(user)?;
411            let gid = uid as libc::gid_t;
412            let strategy = Strategy::FromUID(uid, false);
413            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
414            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(user.to_string(), e))?;
415            set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?;
416        }
417        Some(UserSpec::GroupOnly(group)) => {
418            let gid = name_to_gid(group)?;
419            let strategy = Strategy::Nothing;
420            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
421            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?;
422        }
423        Some(UserSpec::UserAndGroup(user, group)) => {
424            let uid = name_to_uid(user)?;
425            let gid = name_to_gid(group)?;
426            let strategy = Strategy::FromUID(uid, true);
427            set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?;
428            set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?;
429            set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?;
430        }
431    }
432    Ok(())
433}
434
435fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> {
436    let err = unsafe {
437        chroot(
438            CString::new(root.as_os_str().as_bytes().to_vec())
439                .map_err(|e| ChrootError::CannotEnter("root".to_string(), e.into()))?
440                .as_bytes_with_nul()
441                .as_ptr()
442                .cast::<libc::c_char>(),
443        )
444    };
445
446    if err == 0 {
447        if !skip_chdir {
448            std::env::set_current_dir("/")?;
449        }
450        Ok(())
451    } else {
452        Err(ChrootError::CannotEnter(format!("{}", root.display()), Error::last_os_error()).into())
453    }
454}