1mod 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
32enum UserSpec {
34 NeitherGroupNorUser,
35 UserOnly(String),
36 GroupOnly(String),
37 UserAndGroup(String, String),
38}
39
40struct Options {
41 newroot: PathBuf,
44 chroot_target: Option<PathBuf>,
47 skip_chdir: bool,
49 groups: Option<Vec<String>>,
51 userspec: Option<UserSpec>,
53}
54
55fn parse_userspec(spec: &str) -> UserSpec {
60 match spec.split_once(':') {
61 None if spec.is_empty() => UserSpec::NeitherGroupNorUser,
63 None => UserSpec::UserOnly(spec.to_string()),
65 Some(("", "")) => UserSpec::NeitherGroupNorUser,
67 Some(("", grp)) => UserSpec::GroupOnly(grp.to_string()),
69 Some((usr, "")) => UserSpec::UserOnly(usr.to_string()),
71 Some((usr, grp)) => UserSpec::UserAndGroup(usr.to_string(), grp.to_string()),
73 }
74}
75
76fn 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 Err(ChrootError::InvalidGroup(name.to_string()))
85 } else {
86 Ok(vec![name.to_string()])
88 }
89 } else if split.iter().all(|s| s.is_empty()) {
90 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 continue;
102 }
103
104 show!(ChrootError::InvalidGroup(name.to_string()));
107 err = true;
108 } else {
109 if trimmed_name.starts_with(char::is_numeric)
111 && trimmed_name.ends_with(|c: char| !c.is_numeric())
112 {
113 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 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 if options.skip_chdir {
177 let resolved = canonicalize(
178 &options.newroot,
179 MissingHandling::Normal,
180 ResolveMode::Logical,
181 )
182 .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 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 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
270fn 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
284fn 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
298fn 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
310fn 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
330fn 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
340fn 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
350enum Strategy {
352 Nothing,
354 FromUID(libc::uid_t, bool),
359}
360
361fn 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
380fn 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
397fn 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 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}