1mod 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
32enum UserSpec {
34 NeitherGroupNorUser,
35 UserOnly(String),
36 GroupOnly(String),
37 UserAndGroup(String, String),
38}
39
40struct Options {
41 newroot: PathBuf,
43 skip_chdir: bool,
45 groups: Option<Vec<String>>,
47 userspec: Option<UserSpec>,
49}
50
51fn parse_userspec(spec: &str) -> UserSpec {
56 match spec.split_once(':') {
57 None if spec.is_empty() => UserSpec::NeitherGroupNorUser,
59 None => UserSpec::UserOnly(spec.to_string()),
61 Some(("", "")) => UserSpec::NeitherGroupNorUser,
63 Some(("", grp)) => UserSpec::GroupOnly(grp.to_string()),
65 Some((usr, "")) => UserSpec::UserOnly(usr.to_string()),
67 Some((usr, grp)) => UserSpec::UserAndGroup(usr.to_string(), grp.to_string()),
69 }
70}
71
72fn 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 Err(ChrootError::InvalidGroup(name.to_string()))
81 } else {
82 Ok(vec![name.to_string()])
84 }
85 } else if split.iter().all(|s| s.is_empty()) {
86 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 continue;
98 }
99
100 show!(ChrootError::InvalidGroup(name.to_string()));
103 err = true;
104 } else {
105 if trimmed_name.starts_with(char::is_numeric)
107 && trimmed_name.ends_with(|c: char| !c.is_numeric())
108 {
109 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 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 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 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 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
264fn 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
278fn 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
292fn 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
304fn 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
324fn 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
334fn 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
344enum Strategy {
346 Nothing,
348 FromUID(libc::uid_t, bool),
353}
354
355fn 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
374fn 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
391fn 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}