use anyhow::{Ok, Result}; #[cfg(unix)] use getopts::Options; use std::env; #[cfg(unix)] use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::{ffi::CStr, process::Command}; #[cfg(any(target_os = "linux", target_os = "android"))] use crate::pty::prepare_pty; use crate::{ defs, utils::{self, umask}, }; use rustix::thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid}; fn print_usage(opts: Options) { let brief = "APatch\n\nUsage: [options] [-] [user [argument...]]".to_string(); print!("{}", opts.usage(&brief)); } fn set_identity(uid: u32, gid: u32) { #[cfg(any(target_os = "linux", target_os = "android"))] let gid = unsafe { Gid::from_raw(gid) }; let uid = unsafe { Uid::from_raw(uid) }; set_thread_res_gid(gid, gid, gid).ok(); set_thread_res_uid(uid, uid, uid).ok(); } #[cfg(not(unix))] pub fn root_shell() -> Result<()> { unimplemented!() } #[cfg(unix)] pub fn root_shell() -> Result<()> { // we are root now, this was set in kernel! let env_args: Vec = env::args().collect(); let args = env_args .iter() .position(|arg| arg == "-c") .map(|i| { let rest = env_args[i + 1..].to_vec(); let mut new_args = env_args[..i].to_vec(); new_args.push("-c".to_string()); if !rest.is_empty() { new_args.push(rest.join(" ")); } new_args }) .unwrap_or_else(|| env_args.clone()); let mut opts = Options::new(); opts.optopt( "c", "command", "pass COMMAND to the invoked shell", "COMMAND", ); opts.optflag("h", "help", "display this help message and exit"); opts.optflag("l", "login", "pretend the shell to be a login shell"); opts.optflag( "p", "preserve-environment", "preserve the entire environment", ); opts.optopt( "s", "shell", "use SHELL instead of the default /system/bin/sh", "SHELL", ); opts.optflag("v", "version", "display version number and exit"); opts.optflag("V", "", "display version code and exit"); opts.optflag( "M", "mount-master", "force run in the global mount namespace", ); opts.optflag("", "no-pty", "Do not allocate a new pseudo terminal."); // Replace -cn with -z, -mm with -M for supporting getopt_long let args = args .into_iter() .map(|e| { if e == "-mm" { "-M".to_string() } else if e == "-cn" { "-z".to_string() } else { e } }) .collect::>(); let matches = match opts.parse(&args[1..]) { Result::Ok(m) => m, Err(f) => { println!("{f}"); print_usage(opts); std::process::exit(-1); } }; if matches.opt_present("h") { print_usage(opts); return Ok(()); } if matches.opt_present("v") { println!("{}:APatch", defs::VERSION_NAME); return Ok(()); } if matches.opt_present("V") { println!("{}", defs::VERSION_CODE); return Ok(()); } let shell = matches.opt_str("s").unwrap_or("/system/bin/sh".to_string()); let mut is_login = matches.opt_present("l"); let preserve_env = matches.opt_present("p"); let mount_master = matches.opt_present("M"); // we've made sure that -c is the last option and it already contains the whole command, no need to construct it again let args = matches .opt_str("c") .map(|cmd| vec!["-c".to_string(), cmd]) .unwrap_or_default(); let mut free_idx = 0; if !matches.free.is_empty() && matches.free[free_idx] == "-" { is_login = true; free_idx += 1; } // use current uid if no user specified, these has been done in kernel! let mut uid = unsafe { libc::getuid() }; let gid = unsafe { libc::getgid() }; if free_idx < matches.free.len() { let name = &matches.free[free_idx]; uid = unsafe { #[cfg(target_arch = "aarch64")] let pw = libc::getpwnam(name.as_ptr()).as_ref(); #[cfg(target_arch = "x86_64")] let pw = libc::getpwnam(name.as_ptr() as *const i8).as_ref(); match pw { Some(pw) => pw.pw_uid, None => name.parse::().unwrap_or(0), } } } // https://github.com/topjohnwu/Magisk/blob/master/native/src/core/su/su_daemon.cpp#L408 let arg0 = if is_login { "-" } else { &shell }; let mut command = &mut Command::new(&shell); if !preserve_env { // This is actually incorrect, I don't know why. // command = command.env_clear(); let pw = unsafe { libc::getpwuid(uid).as_ref() }; if let Some(pw) = pw { let home = unsafe { CStr::from_ptr(pw.pw_dir) }; let pw_name = unsafe { CStr::from_ptr(pw.pw_name) }; let home = home.to_string_lossy(); let pw_name = pw_name.to_string_lossy(); command = command .env("HOME", home.as_ref()) .env("USER", pw_name.as_ref()) .env("LOGNAME", pw_name.as_ref()) .env("SHELL", &shell); } } // add /data/adb/ap/bin to PATH #[cfg(any(target_os = "linux", target_os = "android"))] add_path_to_env(defs::BINARY_DIR)?; // when AP_RC_PATH exists and ENV is not set, set ENV to AP_RC_PATH if PathBuf::from(defs::AP_RC_PATH).exists() && env::var("ENV").is_err() { command = command.env("ENV", defs::AP_RC_PATH); } #[cfg(target_os = "android")] if !matches.opt_present("no-pty") { if let Err(e) = prepare_pty() { log::error!("failed to prepare pty: {:?}", e); } } // escape from the current cgroup and become session leader // WARNING!!! This cause some root shell hang forever! // command = command.process_group(0); command = unsafe { command.pre_exec(move || { umask(0o22); utils::switch_cgroups(); // switch to global mount namespace #[cfg(any(target_os = "linux", target_os = "android"))] let global_namespace_enable = std::fs::read_to_string(defs::GLOBAL_NAMESPACE_FILE).unwrap_or("0".to_string()); if global_namespace_enable.trim() == "1" || mount_master { let _ = utils::switch_mnt_ns(1); } set_identity(uid, gid); Result::Ok(()) }) }; command = command.args(args).arg0(arg0); Err(command.exec().into()) } fn add_path_to_env(path: &str) -> Result<()> { let mut paths = env::var_os("PATH").map_or(Vec::new(), |val| env::split_paths(&val).collect::>()); let new_path = PathBuf::from(path.trim_end_matches('/')); paths.push(new_path); let new_path_env = env::join_paths(paths)?; unsafe { env::set_var("PATH", new_path_env) }; Ok(()) }