1mod 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
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 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 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 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 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
280fn 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
294fn 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
308fn 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
320fn 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
334fn 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
344fn 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
354enum Strategy {
356 Nothing,
358 FromUID(libc::uid_t, bool),
363}
364
365fn 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
384fn 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
401fn 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}