Repo cloned
This commit is contained in:
commit
11ea8025b0
214 changed files with 33943 additions and 0 deletions
231
apd/src/apd.rs
Normal file
231
apd/src/apd.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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: <command> [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<String> = 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::<Vec<String>>();
|
||||
|
||||
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::<u32>().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::<Vec<_>>());
|
||||
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(())
|
||||
}
|
||||
15
apd/src/assets.rs
Normal file
15
apd/src/assets.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use const_format::concatcp;
|
||||
|
||||
use crate::{defs::BINARY_DIR, utils};
|
||||
|
||||
pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop");
|
||||
pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox");
|
||||
pub const MAGISKPOLICY_PATH: &str = concatcp!(BINARY_DIR, "magiskpolicy");
|
||||
|
||||
pub fn ensure_binaries() -> Result<()> {
|
||||
utils::ensure_binary(RESETPROP_PATH)?;
|
||||
utils::ensure_binary(BUSYBOX_PATH)?;
|
||||
utils::ensure_binary(MAGISKPOLICY_PATH)?;
|
||||
Ok(())
|
||||
}
|
||||
5
apd/src/banner
Normal file
5
apd/src/banner
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
_ ____ _ _
|
||||
/ \ | _ \ __ _| |_ ___| |__
|
||||
/ _ \ | |_) / _` | __/ __| '_ \
|
||||
/ ___ \| __/ (_| | || (__| | | |
|
||||
/_/ \_\_| \__,_|\__\___|_| |_|
|
||||
162
apd/src/cli.rs
Normal file
162
apd/src/cli.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::{defs, event, module, supercall, utils};
|
||||
|
||||
/// APatch cli
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = defs::VERSION_CODE, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "KEY",
|
||||
help = "Super key for authentication root"
|
||||
)]
|
||||
superkey: Option<String>,
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Manage APatch modules
|
||||
Module {
|
||||
#[command(subcommand)]
|
||||
command: Module,
|
||||
},
|
||||
|
||||
/// Trigger `post-fs-data` event
|
||||
PostFsData,
|
||||
|
||||
/// Trigger `service` event
|
||||
Services,
|
||||
|
||||
/// Trigger `boot-complete` event
|
||||
BootCompleted,
|
||||
|
||||
/// Start uid listener for synchronizing root list
|
||||
UidListener,
|
||||
|
||||
/// SELinux policy Patch tool
|
||||
Sepolicy {
|
||||
#[command(subcommand)]
|
||||
command: Sepolicy,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Module {
|
||||
/// Install module <ZIP>
|
||||
Install {
|
||||
/// module zip file path
|
||||
zip: String,
|
||||
},
|
||||
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// enable module <id>
|
||||
Enable {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// disable module <id>
|
||||
Disable {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// run action for module <id>
|
||||
Action {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// list all modules
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Sepolicy {
|
||||
/// Check if sepolicy statement is supported/valid
|
||||
Check {
|
||||
/// sepolicy statements
|
||||
sepolicy: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
#[cfg(target_os = "android")]
|
||||
android_logger::init_once(
|
||||
Config::default()
|
||||
.with_max_level(LevelFilter::Trace) // limit log level
|
||||
.with_tag("APatchD")
|
||||
.with_filter(
|
||||
android_logger::FilterBuilder::new()
|
||||
.filter_level(LevelFilter::Trace)
|
||||
.filter_module("notify", LevelFilter::Warn)
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
env_logger::init();
|
||||
|
||||
// the kernel executes su with argv[0] = "/system/bin/kp" or "/system/bin/su" or "su" or "kp" and replace it with us
|
||||
let arg0 = std::env::args().next().unwrap_or_default();
|
||||
if arg0.ends_with("kp") || arg0.ends_with("su") {
|
||||
return crate::apd::root_shell();
|
||||
}
|
||||
|
||||
let cli = Args::parse();
|
||||
|
||||
log::info!("command: {:?}", cli.command);
|
||||
|
||||
if let Some(ref _superkey) = cli.superkey {
|
||||
supercall::privilege_apd_profile(&cli.superkey);
|
||||
}
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::PostFsData => event::on_post_data_fs(cli.superkey),
|
||||
|
||||
Commands::BootCompleted => event::on_boot_completed(cli.superkey),
|
||||
|
||||
Commands::UidListener => event::start_uid_listener(),
|
||||
|
||||
Commands::Module { command } => {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
utils::switch_mnt_ns(1)?;
|
||||
}
|
||||
match command {
|
||||
Module::Install { zip } => module::install_module(&zip),
|
||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Action { id } => module::run_action(&id),
|
||||
Module::Enable { id } => module::enable_module(&id),
|
||||
Module::Disable { id } => module::disable_module(&id),
|
||||
Module::List => module::list_modules(),
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Sepolicy { command } => match command {
|
||||
Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy),
|
||||
},
|
||||
|
||||
Commands::Services => event::on_services(cli.superkey),
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
log::error!("Error: {:?}", e);
|
||||
}
|
||||
result
|
||||
}
|
||||
36
apd/src/defs.rs
Normal file
36
apd/src/defs.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use const_format::concatcp;
|
||||
|
||||
pub const ADB_DIR: &str = "/data/adb/";
|
||||
pub const WORKING_DIR: &str = concatcp!(ADB_DIR, "ap/");
|
||||
pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, "bin/");
|
||||
pub const APATCH_LOG_FOLDER: &str = concatcp!(WORKING_DIR, "log/");
|
||||
|
||||
pub const AP_RC_PATH: &str = concatcp!(WORKING_DIR, ".aprc");
|
||||
pub const GLOBAL_NAMESPACE_FILE: &str = concatcp!(ADB_DIR, ".global_namespace_enable");
|
||||
pub const LITEMODE_FILE: &str = concatcp!(ADB_DIR, ".litemode_enable");
|
||||
pub const FORCE_OVERLAYFS_FILE: &str = concatcp!(ADB_DIR, ".overlayfs_enable");
|
||||
pub const AP_OVERLAY_SOURCE: &str = "APatch";
|
||||
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "apd");
|
||||
|
||||
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
|
||||
pub const MODULE_UPDATE_TMP_IMG: &str = concatcp!(WORKING_DIR, "update_tmp.img");
|
||||
|
||||
// warning: this directory should not change, or you need to change the code in module_installer.sh!!!
|
||||
pub const MODULE_UPDATE_TMP_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
pub const MODULE_MOUNT_DIR: &str = concatcp!(ADB_DIR, "modules_mount/");
|
||||
|
||||
pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/");
|
||||
|
||||
pub const TEMP_DIR: &str = "/debug_ramdisk";
|
||||
pub const TEMP_DIR_LEGACY: &str = "/sbin";
|
||||
|
||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const UPDATE_FILE_NAME: &str = "update";
|
||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
pub const PTS_NAME: &str = "pts";
|
||||
|
||||
pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE"));
|
||||
pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME"));
|
||||
606
apd/src/event.rs
Normal file
606
apd/src/event.rs
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
use crate::magic_mount;
|
||||
use crate::module;
|
||||
use crate::supercall::fork_for_result;
|
||||
use crate::utils::{ensure_dir_exists, ensure_file_exists, get_work_dir, switch_cgroups};
|
||||
use crate::{
|
||||
assets, defs, mount, restorecon, supercall,
|
||||
supercall::{init_load_package_uid_config, init_load_su_path, refresh_ap_package_list},
|
||||
utils::{self, ensure_clean_dir},
|
||||
};
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use extattr::{Flags as XattrFlags, lgetxattr, lsetxattr};
|
||||
use libc::SIGPWR;
|
||||
use log::{info, warn};
|
||||
use notify::event::{ModifyKind, RenameMode};
|
||||
use notify::{Config, Event, EventKind, INotifyWatcher, RecursiveMode, Watcher};
|
||||
use rustix::mount::*;
|
||||
use signal_hook::consts::signal::*;
|
||||
use signal_hook::iterator::Signals;
|
||||
use std::ffi::CStr;
|
||||
use std::fs::{remove_dir_all, rename};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, thread};
|
||||
use std::{env, fs, io};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn copy_with_xattr(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
fs::copy(src, dest)?;
|
||||
|
||||
if let Ok(xattr_value) = lgetxattr(src, "security.selinux") {
|
||||
lsetxattr(dest, "security.selinux", &xattr_value, XattrFlags::empty())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_with_xattr(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
for entry in WalkDir::new(src) {
|
||||
let entry = entry?;
|
||||
let rel_path = entry
|
||||
.path()
|
||||
.strip_prefix(src)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
let target_path = dest.join(rel_path);
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target_path)?;
|
||||
} else if entry.file_type().is_file() {
|
||||
copy_with_xattr(entry.path(), &target_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_partition(partition_name: &str, lowerdir: &Vec<String>) -> Result<()> {
|
||||
if lowerdir.is_empty() {
|
||||
warn!("partition: {partition_name} lowerdir is empty");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = format!("/{partition_name}");
|
||||
|
||||
// if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately
|
||||
if Path::new(&partition).read_link().is_ok() {
|
||||
warn!("partition: {partition} is a symlink");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut workdir = None;
|
||||
let mut upperdir = None;
|
||||
let system_rw_dir = Path::new(defs::SYSTEM_RW_DIR);
|
||||
if system_rw_dir.exists() {
|
||||
workdir = Some(system_rw_dir.join(partition_name).join("workdir"));
|
||||
upperdir = Some(system_rw_dir.join(partition_name).join("upperdir"));
|
||||
}
|
||||
|
||||
mount::mount_overlay(&partition, lowerdir, workdir, upperdir)
|
||||
}
|
||||
|
||||
pub fn mount_systemlessly(module_dir: &str, is_img: bool) -> Result<()> {
|
||||
// construct overlay mount params
|
||||
if !is_img {
|
||||
info!("fallback to modules.img");
|
||||
let module_update_dir = defs::MODULE_DIR;
|
||||
let module_dir = defs::MODULE_MOUNT_DIR;
|
||||
let tmp_module_img = defs::MODULE_UPDATE_TMP_IMG;
|
||||
let tmp_module_path = Path::new(tmp_module_img);
|
||||
|
||||
ensure_clean_dir(module_dir)?;
|
||||
info!("- Preparing image");
|
||||
|
||||
let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME);
|
||||
|
||||
if !tmp_module_path.exists() {
|
||||
ensure_file_exists(&module_update_flag)?;
|
||||
}
|
||||
|
||||
if module_update_flag.exists() {
|
||||
if tmp_module_path.exists() {
|
||||
//if it has update, remove tmp file
|
||||
fs::remove_file(tmp_module_path)?;
|
||||
}
|
||||
let total_size = calculate_total_size(Path::new(module_update_dir))?; //create modules adapt size
|
||||
info!(
|
||||
"Total size of files in '{}': {} bytes",
|
||||
tmp_module_path.display(),
|
||||
total_size
|
||||
);
|
||||
let grow_size = 128 * 1024 * 1024 + total_size;
|
||||
fs::File::create(tmp_module_img)
|
||||
.context("Failed to create ext4 image file")?
|
||||
.set_len(grow_size)
|
||||
.context("Failed to extend ext4 image")?;
|
||||
let result = Command::new("mkfs.ext4")
|
||||
.arg("-b")
|
||||
.arg("1024")
|
||||
.arg(tmp_module_img)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()?;
|
||||
ensure!(
|
||||
result.status.success(),
|
||||
"Failed to format ext4 image: {}",
|
||||
String::from_utf8(result.stderr)?
|
||||
);
|
||||
info!("Checking Image");
|
||||
module::check_image(tmp_module_img)?;
|
||||
}
|
||||
info!("- Mounting image");
|
||||
mount::AutoMountExt4::try_new(tmp_module_img, module_dir, false)
|
||||
.with_context(|| "mount module image failed".to_string())?;
|
||||
info!("mounted {} to {}", tmp_module_img, module_dir);
|
||||
let _ = restorecon::setsyscon(module_dir);
|
||||
if module_update_flag.exists() {
|
||||
let command_string = format!(
|
||||
"cp --preserve=context -RP {}* {};",
|
||||
module_update_dir, module_dir
|
||||
);
|
||||
let args = vec!["-c", &command_string];
|
||||
let _ = utils::run_command("sh", &args, None)?.wait()?;
|
||||
}
|
||||
mount_systemlessly(module_dir, true)?;
|
||||
return Ok(());
|
||||
}
|
||||
let module_dir_origin = Path::new(defs::MODULE_DIR);
|
||||
let dir = fs::read_dir(module_dir);
|
||||
let Ok(dir) = dir else {
|
||||
bail!("open {} failed", defs::MODULE_DIR);
|
||||
};
|
||||
|
||||
let mut system_lowerdir: Vec<String> = Vec::new();
|
||||
|
||||
let partition = vec!["vendor", "product", "system_ext", "odm", "oem"];
|
||||
let mut partition_lowerdir: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for ele in &partition {
|
||||
partition_lowerdir.insert((*ele).to_string(), Vec::new());
|
||||
}
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let module = entry.path();
|
||||
if !module.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if let Some(module_name) = module.file_name() {
|
||||
let real_module_path = module_dir_origin.join(module_name);
|
||||
|
||||
let disabled = real_module_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
|
||||
if disabled {
|
||||
info!("module: {} is disabled, ignore!", module.display());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let skip_mount = module.join(defs::SKIP_MOUNT_FILE_NAME).exists();
|
||||
if skip_mount {
|
||||
info!("module: {} skip_mount exist, skip!", module.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
let module_system = Path::new(&module).join("system");
|
||||
if module_system.is_dir() {
|
||||
system_lowerdir.push(format!("{}", module_system.display()));
|
||||
}
|
||||
|
||||
for part in &partition {
|
||||
// if /partition is a mountpoint, we would move it to $MODPATH/$partition when install
|
||||
// otherwise it must be a symlink and we don't need to overlay!
|
||||
let part_path = Path::new(&module).join(part);
|
||||
if part_path.is_dir() {
|
||||
if let Some(v) = partition_lowerdir.get_mut(*part) {
|
||||
v.push(format!("{}", part_path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mount /system first
|
||||
if let Err(e) = mount_partition("system", &system_lowerdir) {
|
||||
warn!("mount system failed: {:#}", e);
|
||||
}
|
||||
|
||||
// mount other partitions
|
||||
for (k, v) in partition_lowerdir {
|
||||
if let Err(e) = mount_partition(&k, &v) {
|
||||
warn!("mount {k} failed: {:#}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn systemless_bind_mount(_module_dir: &str) -> Result<()> {
|
||||
// call magisk mount
|
||||
magic_mount::magic_mount()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_total_size(path: &Path) -> io::Result<u64> {
|
||||
let mut total_size = 0;
|
||||
if path.is_dir() {
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
if file_type.is_file() {
|
||||
total_size += entry.metadata()?.len();
|
||||
} else if file_type.is_dir() {
|
||||
total_size += calculate_total_size(&entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(total_size)
|
||||
}
|
||||
pub fn move_file(module_update_dir: &str, module_dir: &str) -> Result<()> {
|
||||
for entry in fs::read_dir(module_update_dir)? {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name();
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
|
||||
if entry.path().is_dir() {
|
||||
let source_path = Path::new(module_update_dir).join(file_name_str.as_ref());
|
||||
let target_path = Path::new(module_dir).join(file_name_str.as_ref());
|
||||
let update = target_path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
if update {
|
||||
if target_path.exists() {
|
||||
info!(
|
||||
"Removing existing folder in target directory: {}",
|
||||
file_name_str
|
||||
);
|
||||
remove_dir_all(&target_path)?;
|
||||
}
|
||||
|
||||
info!("Moving {} to target directory", file_name_str);
|
||||
rename(&source_path, &target_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn on_post_data_fs(superkey: Option<String>) -> Result<()> {
|
||||
utils::umask(0);
|
||||
use std::process::Stdio;
|
||||
#[cfg(unix)]
|
||||
init_load_package_uid_config(&superkey);
|
||||
|
||||
init_load_su_path(&superkey);
|
||||
|
||||
let args = ["/data/adb/ap/bin/magiskpolicy", "--magisk", "--live"];
|
||||
fork_for_result("/data/adb/ap/bin/magiskpolicy", &args, &superkey);
|
||||
|
||||
info!("Re-privilege apd profile after injecting sepolicy");
|
||||
supercall::privilege_apd_profile(&superkey);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip post-fs-data!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create log environment
|
||||
if !Path::new(defs::APATCH_LOG_FOLDER).exists() {
|
||||
fs::create_dir(defs::APATCH_LOG_FOLDER).expect("Failed to create log folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(defs::APATCH_LOG_FOLDER, permissions)
|
||||
.expect("Failed to set permissions");
|
||||
}
|
||||
let command_string = format!(
|
||||
"rm -rf {}*.old.log; for file in {}*; do mv \"$file\" \"$file.old.log\"; done",
|
||||
defs::APATCH_LOG_FOLDER,
|
||||
defs::APATCH_LOG_FOLDER
|
||||
);
|
||||
let mut args = vec!["-c", &command_string];
|
||||
// for all file to .old
|
||||
let result = utils::run_command("sh", &args, None)?.wait()?;
|
||||
if result.success() {
|
||||
info!("Successfully deleted .old files.");
|
||||
} else {
|
||||
info!("Failed to delete .old files.");
|
||||
}
|
||||
let logcat_path = format!("{}locat.log", defs::APATCH_LOG_FOLDER);
|
||||
let dmesg_path = format!("{}dmesg.log", defs::APATCH_LOG_FOLDER);
|
||||
let bootlog = fs::File::create(dmesg_path)?;
|
||||
args = vec![
|
||||
"-s",
|
||||
"9",
|
||||
"120s",
|
||||
"logcat",
|
||||
"-b",
|
||||
"main,system,crash",
|
||||
"-f",
|
||||
&logcat_path,
|
||||
"logcatcher-bootlog:S",
|
||||
"&",
|
||||
];
|
||||
let _ = unsafe {
|
||||
Command::new("timeout")
|
||||
.process_group(0)
|
||||
.pre_exec(|| {
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
.args(args)
|
||||
.spawn()
|
||||
};
|
||||
args = vec!["-s", "9", "120s", "dmesg", "-w"];
|
||||
let _result = unsafe {
|
||||
Command::new("timeout")
|
||||
.process_group(0)
|
||||
.pre_exec(|| {
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
.args(args)
|
||||
.stdout(Stdio::from(bootlog))
|
||||
.spawn()
|
||||
};
|
||||
|
||||
let key = "KERNELPATCH_VERSION";
|
||||
match env::var(key) {
|
||||
Ok(value) => println!("{}: {}", key, value),
|
||||
Err(_) => println!("{} not found", key),
|
||||
}
|
||||
|
||||
let key = "KERNEL_VERSION";
|
||||
match env::var(key) {
|
||||
Ok(value) => println!("{}: {}", key, value),
|
||||
Err(_) => println!("{} not found", key),
|
||||
}
|
||||
|
||||
let safe_mode = utils::is_safe_mode(superkey.clone());
|
||||
|
||||
if safe_mode {
|
||||
// we should still mount modules.img to `/data/adb/modules` in safe mode
|
||||
// becuase we may need to operate the module dir in safe mode
|
||||
warn!("safe mode, skip common post-fs-data.d scripts");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
// Then exec common post-fs-data scripts
|
||||
if let Err(e) = module::exec_common_scripts("post-fs-data.d", true) {
|
||||
warn!("exec common post-fs-data scripts failed: {}", e);
|
||||
}
|
||||
}
|
||||
let module_update_dir = defs::MODULE_UPDATE_TMP_DIR; //save module place
|
||||
let module_dir = defs::MODULE_DIR; // run modules place
|
||||
let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME); // if update ,there will be renewed modules file
|
||||
assets::ensure_binaries().with_context(|| "binary missing")?;
|
||||
|
||||
if Path::new(defs::MODULE_UPDATE_TMP_DIR).exists() {
|
||||
move_file(module_update_dir, module_dir)?;
|
||||
fs::remove_dir_all(module_update_dir)?;
|
||||
}
|
||||
let is_lite_mode_enabled = Path::new(defs::LITEMODE_FILE).exists();
|
||||
|
||||
if safe_mode {
|
||||
warn!("safe mode, skip post-fs-data scripts and disable all modules!");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = module::prune_modules() {
|
||||
warn!("prune modules failed: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = restorecon::restorecon() {
|
||||
warn!("restorecon failed: {}", e);
|
||||
}
|
||||
|
||||
// load sepolicy.rule
|
||||
if module::load_sepolicy_rule().is_err() {
|
||||
warn!("load sepolicy.rule failed");
|
||||
}
|
||||
if is_lite_mode_enabled {
|
||||
info!("litemode runing skip mount tempfs")
|
||||
} else {
|
||||
if let Err(e) = mount::mount_tmpfs(utils::get_tmp_path()) {
|
||||
warn!("do temp dir mount failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// exec modules post-fs-data scripts
|
||||
// TODO: Add timeout
|
||||
if let Err(e) = module::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec post-fs-data scripts failed: {}", e);
|
||||
}
|
||||
|
||||
// load system.prop
|
||||
if let Err(e) = module::load_system_prop() {
|
||||
warn!("load system.prop failed: {}", e);
|
||||
}
|
||||
|
||||
if utils::should_use_overlayfs()? {
|
||||
// mount module systemlessly by overlay
|
||||
let work_dir = get_work_dir();
|
||||
let tmp_dir = PathBuf::from(work_dir.clone());
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(
|
||||
defs::AP_OVERLAY_SOURCE,
|
||||
&tmp_dir,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)
|
||||
.context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let dir_names = vec!["vendor", "product", "system_ext", "odm", "oem", "system"];
|
||||
let dir = fs::read_dir(module_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let module_path = entry.path();
|
||||
let disabled = module_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
if disabled {
|
||||
info!("module: {} is disabled, ignore!", module_path.display());
|
||||
continue;
|
||||
}
|
||||
if module_path.is_dir() {
|
||||
let module_name = module_path.file_name().unwrap().to_string_lossy();
|
||||
let module_dest = Path::new(&work_dir).join(module_name.as_ref());
|
||||
|
||||
for sub_dir in dir_names.iter() {
|
||||
let sub_dir_path = module_path.join(sub_dir);
|
||||
if sub_dir_path.exists() && sub_dir_path.is_dir() {
|
||||
let sub_dir_dest = module_dest.join(sub_dir);
|
||||
fs::create_dir_all(&sub_dir_dest)?;
|
||||
|
||||
copy_dir_with_xattr(&sub_dir_path, &sub_dir_dest)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = mount_systemlessly(&get_work_dir(), false) {
|
||||
warn!("do systemless mount failed: {}", e);
|
||||
}
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
} else {
|
||||
if !is_lite_mode_enabled {
|
||||
if let Err(e) = systemless_bind_mount(module_dir) {
|
||||
warn!("do systemless bind_mount failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("litemode runing skip magic mount");
|
||||
}
|
||||
}
|
||||
|
||||
info!("remove update flag");
|
||||
let _ = fs::remove_file(module_update_flag);
|
||||
|
||||
|
||||
|
||||
run_stage("post-mount", superkey, true);
|
||||
|
||||
env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_stage(stage: &str, superkey: Option<String>, block: bool) {
|
||||
utils::umask(0);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip {stage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if utils::is_safe_mode(superkey) {
|
||||
warn!("safe mode, skip {stage} scripts");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = module::exec_common_scripts(&format!("{stage}.d"), block) {
|
||||
warn!("Failed to exec common {stage} scripts: {e}");
|
||||
}
|
||||
if let Err(e) = module::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec {stage} scripts: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_services(superkey: Option<String>) -> Result<()> {
|
||||
info!("on_services triggered!");
|
||||
run_stage("service", superkey, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_uid_monitor() {
|
||||
info!("Trigger run_uid_monitor!");
|
||||
|
||||
let mut command = &mut Command::new("/data/adb/apd");
|
||||
{
|
||||
command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(|| {
|
||||
// ignore the error?
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
}
|
||||
command = command.arg("uid-listener");
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.expect("[run_uid_monitor] Failed to run uid monitor");
|
||||
}
|
||||
|
||||
pub fn on_boot_completed(superkey: Option<String>) -> Result<()> {
|
||||
info!("on_boot_completed triggered!");
|
||||
|
||||
run_stage("boot-completed", superkey, false);
|
||||
|
||||
run_uid_monitor();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_uid_listener() -> Result<()> {
|
||||
info!("start_uid_listener triggered!");
|
||||
println!("[start_uid_listener] Registering...");
|
||||
|
||||
// create inotify instance
|
||||
const SYS_PACKAGES_LIST_TMP: &str = "/data/system/packages.list.tmp";
|
||||
let sys_packages_list_tmp = PathBuf::from(&SYS_PACKAGES_LIST_TMP);
|
||||
let dir: PathBuf = sys_packages_list_tmp.parent().unwrap().into();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let tx_clone = tx.clone();
|
||||
let mutex = Arc::new(Mutex::new(()));
|
||||
|
||||
{
|
||||
let mutex_clone = mutex.clone();
|
||||
thread::spawn(move || {
|
||||
let mut signals = Signals::new(&[SIGTERM, SIGINT, SIGPWR]).unwrap();
|
||||
for sig in signals.forever() {
|
||||
log::warn!("[shutdown] Caught signal {sig}, refreshing package list...");
|
||||
let skey = CStr::from_bytes_with_nul(b"su\0")
|
||||
.expect("[shutdown_listener] CStr::from_bytes_with_nul failed");
|
||||
refresh_ap_package_list(&skey, &mutex_clone);
|
||||
break; // 执行一次后退出线程
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut watcher = INotifyWatcher::new(
|
||||
move |ev: notify::Result<Event>| match ev {
|
||||
Ok(Event {
|
||||
kind: EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
|
||||
paths,
|
||||
..
|
||||
}) => {
|
||||
if paths.contains(&sys_packages_list_tmp) {
|
||||
info!("[uid_monitor] System packages list changed, sending to tx...");
|
||||
tx_clone.send(false).unwrap()
|
||||
}
|
||||
}
|
||||
Err(err) => warn!("inotify error: {err}"),
|
||||
_ => (),
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
|
||||
watcher.watch(dir.as_ref(), RecursiveMode::NonRecursive)?;
|
||||
|
||||
let mut debounce = false;
|
||||
while let Ok(delayed) = rx.recv() {
|
||||
if delayed {
|
||||
debounce = false;
|
||||
let skey = CStr::from_bytes_with_nul(b"su\0")
|
||||
.expect("[start_uid_listener] CStr::from_bytes_with_nul failed");
|
||||
refresh_ap_package_list(&skey, &mutex);
|
||||
} else if !debounce {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
debounce = true;
|
||||
tx.send(true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
441
apd/src/installer.sh
Normal file
441
apd/src/installer.sh
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# APatch Module installer script
|
||||
# mostly from module_installer.sh
|
||||
# and util_functions.sh in Magisk
|
||||
############################################
|
||||
|
||||
umask 022
|
||||
|
||||
ui_print() {
|
||||
if $BOOTMODE; then
|
||||
echo "$1"
|
||||
else
|
||||
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
|
||||
fi
|
||||
}
|
||||
|
||||
toupper() {
|
||||
echo "$@" | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
grep_cmdline() {
|
||||
local REGEX="s/^$1=//p"
|
||||
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
|
||||
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
|
||||
} 2>/dev/null | sed -n "$REGEX"
|
||||
}
|
||||
|
||||
grep_prop() {
|
||||
local REGEX="s/^$1=//p"
|
||||
shift
|
||||
local FILES=$@
|
||||
[ -z "$FILES" ] && FILES='/system/build.prop'
|
||||
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1
|
||||
}
|
||||
|
||||
grep_get_prop() {
|
||||
local result=$(grep_prop $@)
|
||||
if [ -z "$result" ]; then
|
||||
# Fallback to getprop
|
||||
getprop "$1"
|
||||
else
|
||||
echo $result
|
||||
fi
|
||||
}
|
||||
|
||||
is_mounted() {
|
||||
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
abort() {
|
||||
ui_print "$1"
|
||||
$BOOTMODE || recovery_cleanup
|
||||
[ ! -z $MODPATH ] && rm -rf $MODPATH
|
||||
rm -rf $TMPDIR
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_title() {
|
||||
local len line1len line2len bar
|
||||
line1len=$(echo -n $1 | wc -c)
|
||||
line2len=$(echo -n $2 | wc -c)
|
||||
len=$line2len
|
||||
[ $line1len -gt $line2len ] && len=$line1len
|
||||
len=$((len + 2))
|
||||
bar=$(printf "%${len}s" | tr ' ' '*')
|
||||
ui_print "$bar"
|
||||
ui_print " $1 "
|
||||
[ "$2" ] && ui_print " $2 "
|
||||
ui_print "$bar"
|
||||
}
|
||||
|
||||
######################
|
||||
# Environment Related
|
||||
######################
|
||||
|
||||
setup_flashable() {
|
||||
ensure_bb
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in `ls /proc/$$/fd`; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_actions() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_cleanup() {
|
||||
:
|
||||
}
|
||||
|
||||
#######################
|
||||
# Installation Related
|
||||
#######################
|
||||
|
||||
# find_block [partname...]
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=`grep_prop DEVNAME $UEVENT`
|
||||
PARTNAME=`grep_prop PARTNAME $UEVENT`
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# setup_mntpoint <mountpoint>
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
# mount_name <partname(s)> <mountpoint> <flag>
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
# mount_ro_ensure <partname(s)> <mountpoint>
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=`grep_cmdline androidboot.slot_suffix`
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=`grep_cmdline androidboot.slot`
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2&>/dev/null
|
||||
umount /system_root 2&>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
SYSTEM_ROOT=false
|
||||
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
|
||||
fi
|
||||
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
|
||||
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
|
||||
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
# Mount sepolicy rules dir locations in recovery (best effort)
|
||||
if ! $BOOTMODE; then
|
||||
mount_name "cache cac" /cache
|
||||
mount_name metadata /metadata
|
||||
mount_name persist /persist
|
||||
fi
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
#################
|
||||
# Module Related
|
||||
#################
|
||||
|
||||
set_perm() {
|
||||
chown $2:$3 $1 || return 1
|
||||
chmod $4 $1 || return 1
|
||||
local CON=$5
|
||||
[ -z $CON ] && CON=u:object_r:system_file:s0
|
||||
chcon $CON $1 || return 1
|
||||
}
|
||||
|
||||
set_perm_recursive() {
|
||||
find $1 -type d 2>/dev/null | while read dir; do
|
||||
set_perm $dir $2 $3 $4 $6
|
||||
done
|
||||
find $1 -type f -o -type l 2>/dev/null | while read file; do
|
||||
set_perm $file $2 $3 $5 $6
|
||||
done
|
||||
}
|
||||
|
||||
mktouch() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
[ -z $2 ] && touch $1 || echo $2 > $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_remove() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
mknod $1 c 0 0
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
|
||||
request_zip_size_check() {
|
||||
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
|
||||
}
|
||||
|
||||
boot_actions() { return; }
|
||||
|
||||
# Require ZIPFILE to be set
|
||||
is_legacy_script() {
|
||||
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
|
||||
return $?
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
# if /system/vendor is a symlink, we need to move it out of $MODPATH/system, otherwise it will be overlayed
|
||||
# if /system/vendor is a normal directory, it is ok to overlay it and we don't need to overlay it separately.
|
||||
if [ ! -e $MODPATH/system/$1 ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ -L "/system/$1" ] && [ "$(readlink -f /system/$1)" = "/$1" ]; then
|
||||
ui_print "- Handle partition /$1"
|
||||
# we create a symlink if module want to access $MODPATH/system/$1
|
||||
# but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)
|
||||
mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1
|
||||
fi
|
||||
}
|
||||
|
||||
# Require OUTFD, ZIPFILE to be set
|
||||
install_module() {
|
||||
rm -rf $TMPDIR
|
||||
mkdir -p $TMPDIR
|
||||
chcon u:object_r:system_file:s0 $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
mount_partitions
|
||||
api_level_arch_detect
|
||||
|
||||
# Setup busybox and binaries
|
||||
if $BOOTMODE; then
|
||||
boot_actions
|
||||
else
|
||||
recovery_actions
|
||||
fi
|
||||
|
||||
# Extract prop file
|
||||
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
|
||||
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
|
||||
|
||||
local MODDIRNAME=modules
|
||||
$BOOTMODE && MODDIRNAME=modules_update
|
||||
local MODULEROOT=$NVBASE/$MODDIRNAME
|
||||
MODID=`grep_prop id $TMPDIR/module.prop`
|
||||
MODNAME=`grep_prop name $TMPDIR/module.prop`
|
||||
MODAUTH=`grep_prop author $TMPDIR/module.prop`
|
||||
MODPATH=$MODULEROOT/$MODID
|
||||
|
||||
# Create mod paths
|
||||
rm -rf $MODPATH
|
||||
mkdir -p $MODPATH
|
||||
|
||||
if is_legacy_script; then
|
||||
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
|
||||
|
||||
# Load install script
|
||||
. $TMPDIR/install.sh
|
||||
|
||||
# Callbacks
|
||||
print_modname
|
||||
on_install
|
||||
|
||||
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
|
||||
$SKIPMOUNT && touch $MODPATH/skip_mount
|
||||
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
|
||||
cp -af $TMPDIR/module.prop $MODPATH/module.prop
|
||||
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
|
||||
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
|
||||
|
||||
ui_print "- Setting permissions"
|
||||
set_permissions
|
||||
else
|
||||
print_title "$MODNAME" "by $MODAUTH"
|
||||
print_title "Powered by APatch"
|
||||
|
||||
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
|
||||
|
||||
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
|
||||
ui_print "- Extracting module files"
|
||||
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
|
||||
|
||||
# Default permissions
|
||||
set_perm_recursive $MODPATH 0 0 0755 0644
|
||||
set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0
|
||||
fi
|
||||
|
||||
# Load customization script
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace $MODPATH$TARGET
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove $MODPATH$TARGET
|
||||
done
|
||||
|
||||
handle_partition vendor
|
||||
handle_partition system_ext
|
||||
handle_partition product
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
|
||||
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
|
||||
fi
|
||||
|
||||
# Remove stuff that doesn't belong to modules and clean up any empty directories
|
||||
rm -rf \
|
||||
$MODPATH/system/placeholder $MODPATH/customize.sh \
|
||||
$MODPATH/README.md $MODPATH/.git*
|
||||
rmdir -p $MODPATH 2>/dev/null
|
||||
|
||||
cd /
|
||||
$BOOTMODE || recovery_cleanup
|
||||
rm -rf $TMPDIR
|
||||
|
||||
ui_print "- Done"
|
||||
}
|
||||
|
||||
##########
|
||||
# Presets
|
||||
##########
|
||||
|
||||
# Detect whether in boot mode
|
||||
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||
|
||||
NVBASE=/data/adb
|
||||
TMPDIR=/dev/tmp
|
||||
POSTFSDATAD=$NVBASE/post-fs-data.d
|
||||
SERVICED=$NVBASE/service.d
|
||||
|
||||
# Some modules dependents on this
|
||||
export MAGISK_VER=27.0
|
||||
export MAGISK_VER_CODE=27000
|
||||
445
apd/src/installer_bind.sh
Normal file
445
apd/src/installer_bind.sh
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# APatch Module installer script
|
||||
# mostly from module_installer.sh
|
||||
# and util_functions.sh in Magisk
|
||||
############################################
|
||||
|
||||
umask 022
|
||||
|
||||
ui_print() {
|
||||
if $BOOTMODE; then
|
||||
echo "$1"
|
||||
else
|
||||
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
|
||||
fi
|
||||
}
|
||||
|
||||
toupper() {
|
||||
echo "$@" | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
grep_cmdline() {
|
||||
local REGEX="s/^$1=//p"
|
||||
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
|
||||
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
|
||||
} 2>/dev/null | sed -n "$REGEX"
|
||||
}
|
||||
|
||||
grep_prop() {
|
||||
local REGEX="s/$1=//p"
|
||||
shift
|
||||
local FILES=$@
|
||||
[ -z "$FILES" ] && FILES='/system/build.prop'
|
||||
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 | xargs
|
||||
}
|
||||
|
||||
grep_get_prop() {
|
||||
local result=$(grep_prop $@)
|
||||
if [ -z "$result" ]; then
|
||||
# Fallback to getprop
|
||||
getprop "$1"
|
||||
else
|
||||
echo $result
|
||||
fi
|
||||
}
|
||||
|
||||
is_mounted() {
|
||||
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
abort() {
|
||||
ui_print "$1"
|
||||
$BOOTMODE || recovery_cleanup
|
||||
[ ! -z $MODPATH ] && rm -rf $MODPATH
|
||||
rm -rf $TMPDIR
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_title() {
|
||||
local len line1len line2len bar
|
||||
line1len=$(echo -n $1 | wc -c)
|
||||
line2len=$(echo -n $2 | wc -c)
|
||||
len=$line2len
|
||||
[ $line1len -gt $line2len ] && len=$line1len
|
||||
len=$((len + 2))
|
||||
bar=$(printf "%${len}s" | tr ' ' '*')
|
||||
ui_print "$bar"
|
||||
ui_print " $1 "
|
||||
[ "$2" ] && ui_print " $2 "
|
||||
ui_print "$bar"
|
||||
}
|
||||
|
||||
check_sepolicy() {
|
||||
/data/adb/apd sepolicy check "$1"
|
||||
return $?
|
||||
}
|
||||
|
||||
######################
|
||||
# Environment Related
|
||||
######################
|
||||
|
||||
setup_flashable() {
|
||||
ensure_bb
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in /proc/$$/fd/*; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_actions() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_cleanup() {
|
||||
:
|
||||
}
|
||||
|
||||
#######################
|
||||
# Installation Related
|
||||
#######################
|
||||
|
||||
# find_block [partname...]
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=`grep_prop DEVNAME $UEVENT`
|
||||
PARTNAME=`grep_prop PARTNAME $UEVENT`
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# setup_mntpoint <mountpoint>
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
# mount_name <partname(s)> <mountpoint> <flag>
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
# mount_ro_ensure <partname(s)> <mountpoint>
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=`grep_cmdline androidboot.slot_suffix`
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=`grep_cmdline androidboot.slot`
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2&>/dev/null
|
||||
umount /system_root 2&>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
SYSTEM_ROOT=false
|
||||
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
|
||||
fi
|
||||
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
|
||||
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
|
||||
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
# Mount sepolicy rules dir locations in recovery (best effort)
|
||||
if ! $BOOTMODE; then
|
||||
mount_name "cache cac" /cache
|
||||
mount_name metadata /metadata
|
||||
mount_name persist /persist
|
||||
fi
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
#################
|
||||
# Module Related
|
||||
#################
|
||||
|
||||
set_perm() {
|
||||
chown $2:$3 $1 || return 1
|
||||
chmod $4 $1 || return 1
|
||||
local CON=$5
|
||||
[ -z $CON ] && CON=u:object_r:system_file:s0
|
||||
chcon $CON $1 || return 1
|
||||
}
|
||||
|
||||
set_perm_recursive() {
|
||||
find $1 -type d 2>/dev/null | while read dir; do
|
||||
set_perm $dir $2 $3 $4 $6
|
||||
done
|
||||
find $1 -type f -o -type l 2>/dev/null | while read file; do
|
||||
set_perm $file $2 $3 $5 $6
|
||||
done
|
||||
}
|
||||
|
||||
mktouch() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
[ -z $2 ] && touch $1 || echo $2 > $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_remove() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
mknod $1 c 0 0
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
|
||||
request_zip_size_check() {
|
||||
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
|
||||
}
|
||||
|
||||
boot_actions() { return; }
|
||||
|
||||
# Require ZIPFILE to be set
|
||||
is_legacy_script() {
|
||||
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
|
||||
return $?
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
PARTITION="$1"
|
||||
REQUIRE_SYMLINK="$2"
|
||||
if [ ! -e "$MODPATH/system/$PARTITION" ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then
|
||||
ui_print "- Handle partition /$PARTITION"
|
||||
ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION"
|
||||
fi
|
||||
}
|
||||
|
||||
# Require OUTFD, ZIPFILE to be set
|
||||
install_module() {
|
||||
rm -rf $TMPDIR
|
||||
mkdir -p $TMPDIR
|
||||
chcon u:object_r:system_file:s0 $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
mount_partitions
|
||||
api_level_arch_detect
|
||||
|
||||
# Setup busybox and binaries
|
||||
if $BOOTMODE; then
|
||||
boot_actions
|
||||
else
|
||||
recovery_actions
|
||||
fi
|
||||
|
||||
# Extract prop file
|
||||
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
|
||||
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
|
||||
|
||||
local MODDIRNAME=modules
|
||||
$BOOTMODE && MODDIRNAME=modules_update
|
||||
local MODULEROOT=$NVBASE/$MODDIRNAME
|
||||
MODID=`grep_prop id $TMPDIR/module.prop`
|
||||
MODNAME=`grep_prop name $TMPDIR/module.prop`
|
||||
MODAUTH=`grep_prop author $TMPDIR/module.prop`
|
||||
MODPATH=$MODULEROOT/$MODID
|
||||
|
||||
# Create mod paths
|
||||
rm -rf $MODPATH
|
||||
mkdir -p $MODPATH
|
||||
|
||||
if is_legacy_script; then
|
||||
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
|
||||
|
||||
# Load install script
|
||||
. $TMPDIR/install.sh
|
||||
|
||||
# Callbacks
|
||||
print_modname
|
||||
on_install
|
||||
|
||||
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
|
||||
$SKIPMOUNT && touch $MODPATH/skip_mount
|
||||
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
|
||||
cp -af $TMPDIR/module.prop $MODPATH/module.prop
|
||||
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
|
||||
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
|
||||
|
||||
ui_print "- Setting permissions"
|
||||
set_permissions
|
||||
else
|
||||
print_title "$MODNAME" "by $MODAUTH"
|
||||
print_title "Powered by APatch"
|
||||
|
||||
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
|
||||
|
||||
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
|
||||
ui_print "- Extracting module files"
|
||||
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
|
||||
|
||||
# Default permissions
|
||||
set_perm_recursive $MODPATH 0 0 0755 0644
|
||||
set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0
|
||||
fi
|
||||
|
||||
# Load customization script
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
handle_partition vendor true
|
||||
handle_partition system_ext true
|
||||
handle_partition product true
|
||||
handle_partition odm false
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
|
||||
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
|
||||
fi
|
||||
|
||||
# Remove stuff that doesn't belong to modules and clean up any empty directories
|
||||
rm -rf \
|
||||
$MODPATH/system/placeholder $MODPATH/customize.sh \
|
||||
$MODPATH/README.md $MODPATH/.git*
|
||||
rmdir -p $MODPATH 2>/dev/null
|
||||
|
||||
cd /
|
||||
$BOOTMODE || recovery_cleanup
|
||||
rm -rf $TMPDIR
|
||||
|
||||
ui_print "- Done"
|
||||
}
|
||||
|
||||
##########
|
||||
# Presets
|
||||
##########
|
||||
|
||||
# Detect whether in boot mode
|
||||
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||
|
||||
NVBASE=/data/adb
|
||||
TMPDIR=/dev/tmp
|
||||
POSTFSDATAD=$NVBASE/post-fs-data.d
|
||||
SERVICED=$NVBASE/service.d
|
||||
|
||||
# Some modules dependents on this
|
||||
export MAGISK_VER=27.0
|
||||
export MAGISK_VER_CODE=27000
|
||||
444
apd/src/magic_mount.rs
Normal file
444
apd/src/magic_mount.rs
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
use crate::defs::{AP_OVERLAY_SOURCE, DISABLE_FILE_NAME, MODULE_DIR, SKIP_MOUNT_FILE_NAME};
|
||||
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
|
||||
use crate::restorecon::{lgetfilecon, lsetfilecon};
|
||||
use crate::utils::ensure_dir_exists;
|
||||
use crate::utils::get_work_dir;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use extattr::lgetxattr;
|
||||
use rustix::fs::{
|
||||
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
|
||||
chmod, chown, mount, move_mount, unmount,
|
||||
};
|
||||
use rustix::mount::mount_change;
|
||||
use rustix::path::Arg;
|
||||
use std::cmp::PartialEq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fs;
|
||||
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
|
||||
use std::os::unix::fs::{FileTypeExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
enum NodeFileType {
|
||||
RegularFile,
|
||||
Directory,
|
||||
Symlink,
|
||||
Whiteout,
|
||||
}
|
||||
|
||||
impl NodeFileType {
|
||||
fn from_file_type(file_type: FileType) -> Option<Self> {
|
||||
if file_type.is_file() {
|
||||
Some(RegularFile)
|
||||
} else if file_type.is_dir() {
|
||||
Some(Directory)
|
||||
} else if file_type.is_symlink() {
|
||||
Some(Symlink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
file_type: NodeFileType,
|
||||
children: HashMap<String, Node>,
|
||||
// the module that owned this node
|
||||
module_path: Option<PathBuf>,
|
||||
replace: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn collect_module_files<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
|
||||
let dir = module_dir.as_ref();
|
||||
let mut has_file = false;
|
||||
for entry in dir.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let node = match self.children.entry(name.clone()) {
|
||||
Entry::Occupied(o) => Some(o.into_mut()),
|
||||
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
|
||||
};
|
||||
|
||||
if let Some(node) = node {
|
||||
has_file |= if node.file_type == Directory {
|
||||
node.collect_module_files(dir.join(&node.name))? || node.replace
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(has_file)
|
||||
}
|
||||
|
||||
fn new_root<T: ToString>(name: T) -> Self {
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let path = entry.path();
|
||||
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
|
||||
Some(Whiteout)
|
||||
} else {
|
||||
NodeFileType::from_file_type(metadata.file_type())
|
||||
};
|
||||
if let Some(file_type) = file_type {
|
||||
let mut replace = false;
|
||||
if file_type == Directory {
|
||||
if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) {
|
||||
if String::from_utf8_lossy(&v) == "y" {
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(Node {
|
||||
name: name.to_string(),
|
||||
file_type,
|
||||
children: Default::default(),
|
||||
module_path: Some(path),
|
||||
replace,
|
||||
skip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_module_files() -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = Path::new(MODULE_DIR);
|
||||
let mut has_file = false;
|
||||
for entry in module_root.read_dir()?.flatten() {
|
||||
if !entry.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.path().join(DISABLE_FILE_NAME).exists()
|
||||
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_system = entry.path().join("system");
|
||||
if !mod_system.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("collecting {}", entry.path().display());
|
||||
|
||||
has_file |= system.collect_module_files(&mod_system)?;
|
||||
}
|
||||
|
||||
if has_file {
|
||||
for (partition, require_symlink) in [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
("oem", false),
|
||||
] {
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
|
||||
let name = partition.to_string();
|
||||
if let Some(node) = system.children.remove(&name) {
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
entry: &DirEntry,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
current: Node,
|
||||
has_tmpfs: bool,
|
||||
) -> Result<()> {
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root file {}!", path.display());
|
||||
}
|
||||
}
|
||||
Symlink => {
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"create module symlink {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(module_path, &work_dir_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root symlink {}!", path.display());
|
||||
}
|
||||
}
|
||||
Directory => {
|
||||
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
|
||||
if !has_tmpfs && !create_tmpfs {
|
||||
for it in &mut current.children {
|
||||
let (name, node) = it;
|
||||
let real_path = path.join(name);
|
||||
let need = match node.file_type {
|
||||
Symlink => true,
|
||||
Whiteout => real_path.exists(),
|
||||
_ => {
|
||||
if let Ok(metadata) = real_path.symlink_metadata() {
|
||||
let file_type = NodeFileType::from_file_type(metadata.file_type())
|
||||
.unwrap_or(Whiteout);
|
||||
file_type != node.file_type || file_type == Symlink
|
||||
} else {
|
||||
// real path not exists
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if need {
|
||||
if current.module_path.is_none() {
|
||||
log::error!(
|
||||
"cannot create tmpfs on {}, ignore: {name}",
|
||||
path.display()
|
||||
);
|
||||
node.skip = true;
|
||||
continue;
|
||||
}
|
||||
create_tmpfs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_tmpfs = has_tmpfs || create_tmpfs;
|
||||
|
||||
if has_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs skeleton for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir_all(&work_dir_path)?;
|
||||
let (metadata, path) = if path.exists() {
|
||||
(path.metadata()?, &path)
|
||||
} else if let Some(module_path) = ¤t.module_path {
|
||||
(module_path.metadata()?, module_path)
|
||||
} else {
|
||||
bail!("cannot mount root dir {}!", path.display());
|
||||
};
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(&work_dir_path, &work_dir_path).context("bind self")?;
|
||||
}
|
||||
|
||||
if path.exists() && !current.replace {
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let result = if let Some(node) = current.children.remove(&name) {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
} else if has_tmpfs {
|
||||
mount_mirror(&path, &work_dir_path, &entry)
|
||||
.with_context(|| format!("mount mirror {}/{name}", path.display()))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.replace {
|
||||
if current.module_path.is_none() {
|
||||
bail!(
|
||||
"dir {} is declared as replaced but it is root!",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
log::debug!("dir {} is replaced", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, node) in current.children.into_iter() {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
{
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"moving tmpfs {} -> {}",
|
||||
work_dir_path.display(),
|
||||
path.display()
|
||||
);
|
||||
move_mount(&work_dir_path, &path).context("move self")?;
|
||||
mount_change(&path, MountPropagationFlags::PRIVATE).context("make self private")?;
|
||||
}
|
||||
}
|
||||
Whiteout => {
|
||||
log::debug!("file {} is removed", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magic_mount() -> Result<()> {
|
||||
match collect_module_files()? {
|
||||
Some(root) => {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
let tmp_dir = PathBuf::from(get_work_dir());
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
&tmp_dir,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)
|
||||
.context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let result = do_magic_mount("/", &tmp_dir, root, false);
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
fs::remove_dir(tmp_dir).ok();
|
||||
result
|
||||
}
|
||||
_ => {
|
||||
log::info!("no modules to mount, skipping!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
18
apd/src/main.rs
Normal file
18
apd/src/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
mod apd;
|
||||
mod assets;
|
||||
mod cli;
|
||||
mod defs;
|
||||
mod event;
|
||||
mod magic_mount;
|
||||
mod module;
|
||||
mod mount;
|
||||
mod package;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
mod pty;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod supercall;
|
||||
mod utils;
|
||||
fn main() -> anyhow::Result<()> {
|
||||
cli::run()
|
||||
}
|
||||
565
apd/src/module.rs
Normal file
565
apd/src/module.rs
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{assets, defs, restorecon};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use const_format::concatcp;
|
||||
use is_executable::is_executable;
|
||||
use java_properties::PropertiesIter;
|
||||
use log::{info, warn};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
use zip_extensions::zip_extract_file_to_memory;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
|
||||
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
|
||||
const INSTALLER_CONTENT_: &str = include_str!("./installer_bind.sh");
|
||||
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
INSTALLER_CONTENT,
|
||||
"\n",
|
||||
"install_module",
|
||||
"\n",
|
||||
"exit 0",
|
||||
"\n"
|
||||
);
|
||||
const INSTALL_MODULE_SCRIPT_: &str = concatcp!(
|
||||
INSTALLER_CONTENT_,
|
||||
"\n",
|
||||
"install_module",
|
||||
"\n",
|
||||
"exit 0",
|
||||
"\n"
|
||||
);
|
||||
|
||||
fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
let realpath =
|
||||
fs::canonicalize(module_file).with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
let content;
|
||||
|
||||
if !should_use_overlayfs()? {
|
||||
content = INSTALL_MODULE_SCRIPT_.to_string();
|
||||
} else {
|
||||
content = INSTALL_MODULE_SCRIPT.to_string();
|
||||
}
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", &content])
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
)
|
||||
.env("APATCH", "true")
|
||||
.env("APATCH_VER", defs::VERSION_NAME)
|
||||
.env("APATCH_VER_CODE", defs::VERSION_CODE)
|
||||
.env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?))
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
ensure!(result.success(), "Failed to install module script");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// becuase we use something like A-B update
|
||||
// we need to update the module state after the boot_completed
|
||||
// if someone(such as the module) install a module before the boot_completed
|
||||
// then it may cause some problems, just forbid it
|
||||
fn ensure_boot_completed() -> Result<()> {
|
||||
// ensure getprop sys.boot_completed == 1
|
||||
if getprop("sys.boot_completed").as_deref() != Some("1") {
|
||||
bail!("Android is Booting!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_update() -> Result<()> {
|
||||
ensure_file_exists(concatcp!(defs::WORKING_DIR, defs::UPDATE_FILE_NAME))
|
||||
}
|
||||
|
||||
fn mark_module_state(module: &str, flag_file: &str, create_or_delete: bool) -> Result<()> {
|
||||
let module_state_file = Path::new(defs::MODULE_DIR).join(module).join(flag_file);
|
||||
if create_or_delete {
|
||||
ensure_file_exists(module_state_file)
|
||||
} else {
|
||||
if module_state_file.exists() {
|
||||
fs::remove_file(module_state_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
let modules_dir = Path::new(defs::MODULE_DIR);
|
||||
let dir = fs::read_dir(modules_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
warn!("{} is not a directory, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
if active_only && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if active_only && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
warn!("{} is removed, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
f(&path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
foreach_module(true, f)
|
||||
}
|
||||
|
||||
pub fn check_image(img: &str) -> Result<()> {
|
||||
let result = Command::new("e2fsck")
|
||||
.args(["-yf", img])
|
||||
.stdout(Stdio::piped())
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec e2fsck {img}"))?;
|
||||
let code = result.code();
|
||||
// 0 or 1 is ok
|
||||
// 0: no error
|
||||
// 1: file system errors corrected
|
||||
// https://man7.org/linux/man-pages/man8/e2fsck.8.html
|
||||
// ensure!(
|
||||
// code == Some(0) || code == Some(1),
|
||||
// "Failed to check image, e2fsck exit code: {}",
|
||||
// code.unwrap_or(-1)
|
||||
// );
|
||||
info!("e2fsck exit code: {}", code.unwrap_or(-1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_sepolicy_rule() -> Result<()> {
|
||||
foreach_active_module(|path| {
|
||||
let rule_file = path.join("sepolicy.rule");
|
||||
if !rule_file.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("load policy: {}", &rule_file.display());
|
||||
Command::new(assets::MAGISKPOLICY_PATH)
|
||||
.arg("--live")
|
||||
.arg("--apply")
|
||||
.arg(&rule_file)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec {}", rule_file.display()))?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(|| {
|
||||
// ignore the error?
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
}
|
||||
command = command
|
||||
.current_dir(path.as_ref().parent().unwrap())
|
||||
.arg("sh")
|
||||
.arg(path.as_ref())
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env("APATCH", "true")
|
||||
.env("APATCH_VER", defs::VERSION_NAME)
|
||||
.env("APATCH_VER_CODE", defs::VERSION_CODE)
|
||||
.env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?))
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH")?,
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
);
|
||||
|
||||
let result = if wait {
|
||||
command.status().map(|_| ())
|
||||
} else {
|
||||
command.spawn().map(|_| ())
|
||||
};
|
||||
result.map_err(|err| anyhow!("Failed to exec {}: {}", path.as_ref().display(), err))
|
||||
}
|
||||
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
foreach_active_module(|module| {
|
||||
let script_path = module.join(format!("{stage}.sh"));
|
||||
if !script_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
exec_script(&script_path, block)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> {
|
||||
let script_dir = Path::new(defs::ADB_DIR).join(dir);
|
||||
if !script_dir.exists() {
|
||||
info!("{} not exists, skip", script_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dir = fs::read_dir(&script_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if !is_executable(&path) {
|
||||
warn!("{} is not executable, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
exec_script(path, wait)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_system_prop() -> Result<()> {
|
||||
foreach_active_module(|module| {
|
||||
let system_prop = module.join("system.prop");
|
||||
if !system_prop.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
info!("load {} system.prop", module.display());
|
||||
|
||||
// resetprop -n --file system.prop
|
||||
Command::new(assets::RESETPROP_PATH)
|
||||
.arg("-n")
|
||||
.arg("--file")
|
||||
.arg(&system_prop)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec {}", system_prop.display()))?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prune_modules() -> Result<()> {
|
||||
foreach_module(false, |module| {
|
||||
fs::remove_file(module.join(defs::UPDATE_FILE_NAME)).ok();
|
||||
if !module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists() {
|
||||
if let Err(e) = exec_script(uninstaller, true) {
|
||||
warn!("Failed to exec uninstaller: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
let module_path = module.display().to_string();
|
||||
let updated_path = module_path.replace(defs::MODULE_DIR, defs::MODULE_UPDATE_TMP_DIR);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&updated_path) {
|
||||
warn!("Failed to remove {}: {}", updated_path, e);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _install_module(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
|
||||
assets::ensure_binaries().with_context(|| "binary missing")?;
|
||||
|
||||
// first check if workding dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
|
||||
// read the module_id from zip
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {:?}", module_prop);
|
||||
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
|
||||
let modules_dir = Path::new(defs::MODULE_DIR);
|
||||
let modules_update_dir = Path::new(defs::MODULE_UPDATE_TMP_DIR);
|
||||
if !Path::new(modules_dir).exists() {
|
||||
fs::create_dir(modules_dir).expect("Failed to create modules folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(modules_dir, permissions).expect("Failed to set permissions");
|
||||
}
|
||||
|
||||
let module_dir = format!("{}{}", modules_dir.display(), module_id.clone());
|
||||
let _module_update_dir = format!("{}{}", modules_update_dir.display(), module_id.clone());
|
||||
info!("module dir: {}", module_dir);
|
||||
if !Path::new(&module_dir.clone()).exists() {
|
||||
fs::create_dir(&module_dir.clone()).expect("Failed to create module folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(module_dir.clone(), permissions).expect("Failed to set permissions");
|
||||
}
|
||||
// unzip the image and move it to modules_update/<id> dir
|
||||
let file = fs::File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&_module_update_dir)?;
|
||||
|
||||
// set permission and selinux context for $MOD/system
|
||||
let module_system_dir = PathBuf::from(module_dir.clone()).join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&module_system_dir, fs::Permissions::from_mode(0o755))?;
|
||||
restorecon::restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
exec_install_script(zip)?;
|
||||
mark_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
let result = _install_module(zip);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn _uninstall_module(id: &str, update_dir: &str) -> Result<()> {
|
||||
let dir = Path::new(update_dir);
|
||||
ensure!(dir.exists(), "No module installed");
|
||||
|
||||
// iterate the modules_update dir, find the module to be removed
|
||||
let dir = fs::read_dir(dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let module_prop = path.join("module.prop");
|
||||
if !module_prop.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read(module_prop)?;
|
||||
let mut module_id: String = String::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
if k.eq("id") {
|
||||
module_id = v;
|
||||
}
|
||||
},
|
||||
)?;
|
||||
if module_id.eq(id) {
|
||||
let remove_file = path.join(defs::REMOVE_FILE_NAME);
|
||||
fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// santity check
|
||||
let target_module_path = format!("{update_dir}/{id}");
|
||||
let target_module = Path::new(&target_module_path);
|
||||
if target_module.exists() {
|
||||
let remove_file = target_module.join(defs::REMOVE_FILE_NAME);
|
||||
if !remove_file.exists() {
|
||||
fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = mark_module_state(id, defs::REMOVE_FILE_NAME, true);
|
||||
Ok(())
|
||||
}
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
_uninstall_module(id, defs::MODULE_DIR)?;
|
||||
mark_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
let action_script_path = format!("/data/adb/modules/{}/action.sh", id);
|
||||
let _ = exec_script(&action_script_path, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _change_module_state(module_dir: &str, mid: &str, enable: bool) -> Result<()> {
|
||||
let src_module_path = format!("{module_dir}/{mid}");
|
||||
let src_module = Path::new(&src_module_path);
|
||||
ensure!(src_module.exists(), "module: {} not found!", mid);
|
||||
|
||||
let disable_path = src_module.join(defs::DISABLE_FILE_NAME);
|
||||
if enable {
|
||||
if disable_path.exists() {
|
||||
fs::remove_file(&disable_path).with_context(|| {
|
||||
format!("Failed to remove disable file: {}", &disable_path.display())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
ensure_file_exists(disable_path)?;
|
||||
}
|
||||
|
||||
let _ = mark_module_state(mid, defs::DISABLE_FILE_NAME, !enable);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _enable_module(id: &str, update_dir: &Path) -> Result<()> {
|
||||
if let Some(module_dir_str) = update_dir.to_str() {
|
||||
_change_module_state(module_dir_str, id, true)
|
||||
} else {
|
||||
info!("Enable module failed: Invalid path");
|
||||
Err(anyhow::anyhow!("Invalid module directory"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
let update_dir = Path::new(defs::MODULE_DIR);
|
||||
_enable_module(id, update_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _disable_module(id: &str, update_dir: &Path) -> Result<()> {
|
||||
if let Some(module_dir_str) = update_dir.to_str() {
|
||||
_change_module_state(module_dir_str, id, false)
|
||||
} else {
|
||||
info!("Disable module failed: Invalid path");
|
||||
Err(anyhow::anyhow!("Invalid module directory"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
let module_dir = Path::new(defs::MODULE_DIR);
|
||||
_disable_module(id, module_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _disable_all_modules(dir: &str) -> Result<()> {
|
||||
let dir = fs::read_dir(dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let disable_flag = path.join(defs::DISABLE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(disable_flag) {
|
||||
warn!("Failed to disable module: {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
// Skip disabling modules since boot completed
|
||||
if getprop("sys.boot_completed").as_deref() == Some("1") {
|
||||
info!("System boot completed, no need to disable all modules");
|
||||
return Ok(());
|
||||
}
|
||||
mark_update()?;
|
||||
_disable_all_modules(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
// first check enabled modules
|
||||
let dir = fs::read_dir(path);
|
||||
let Ok(dir) = dir else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut modules: Vec<HashMap<String, String>> = Vec::new();
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
info!("path: {}", path.display());
|
||||
let module_prop = path.join("module.prop");
|
||||
if !module_prop.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read(&module_prop);
|
||||
let Ok(content) = content else {
|
||||
warn!("Failed to read file: {}", module_prop.display());
|
||||
continue;
|
||||
};
|
||||
let mut module_prop_map: HashMap<String, String> = HashMap::new();
|
||||
let encoding = encoding_rs::UTF_8;
|
||||
let result =
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding).read_into(|k, v| {
|
||||
module_prop_map.insert(k, v);
|
||||
});
|
||||
|
||||
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
|
||||
match entry.file_name().to_str() {
|
||||
Some(id) => {
|
||||
info!("Use dir name as module id: {}", id);
|
||||
module_prop_map.insert("id".to_owned(), id.to_owned());
|
||||
}
|
||||
_ => {
|
||||
info!("Failed to get module id: {:?}", module_prop);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enabled, update, remove flags
|
||||
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let web = path.join(defs::MODULE_WEB_DIR).exists();
|
||||
let action = path.join(defs::MODULE_ACTION_SH).exists();
|
||||
|
||||
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
|
||||
module_prop_map.insert("update".to_owned(), update.to_string());
|
||||
module_prop_map.insert("remove".to_owned(), remove.to_string());
|
||||
module_prop_map.insert("web".to_owned(), web.to_string());
|
||||
module_prop_map.insert("action".to_owned(), action.to_string());
|
||||
|
||||
if result.is_err() {
|
||||
warn!("Failed to parse module.prop: {}", module_prop.display());
|
||||
continue;
|
||||
}
|
||||
modules.push(module_prop_map);
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
pub fn list_modules() -> Result<()> {
|
||||
let modules = _list_modules(defs::MODULE_DIR);
|
||||
println!("{}", serde_json::to_string_pretty(&modules)?);
|
||||
Ok(())
|
||||
}
|
||||
371
apd/src/mount.rs
Normal file
371
apd/src/mount.rs
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use anyhow::Context;
|
||||
use anyhow::{Ok, Result, anyhow, bail};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[allow(unused_imports)]
|
||||
use retry::delay::NoDelay;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
//use sys_mount::{unmount, FilesystemType, Mount, MountFlags, Unmount, UnmountFlags};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{fd::AsFd, fs::CWD, mount::*};
|
||||
use std::fs::create_dir;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::defs::AP_OVERLAY_SOURCE;
|
||||
use crate::defs::PTS_NAME;
|
||||
use log::{info, warn};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use procfs::process::Process;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct AutoMountExt4 {
|
||||
target: String,
|
||||
auto_umount: bool,
|
||||
}
|
||||
|
||||
impl AutoMountExt4 {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
|
||||
pub fn try_new(source: &str, target: &str, auto_umount: bool) -> Result<Self> {
|
||||
let path = Path::new(source);
|
||||
if !path.exists() {
|
||||
println!("Source path does not exist");
|
||||
} else {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let permissions = metadata.permissions();
|
||||
let mode = permissions.mode();
|
||||
|
||||
if permissions.readonly() {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
println!("File permissions: {:o} (octal)", mode & 0o777);
|
||||
}
|
||||
}
|
||||
|
||||
mount_ext4(source, target)?;
|
||||
Ok(Self {
|
||||
target: target.to_string(),
|
||||
auto_umount,
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn try_new(_src: &str, _mnt: &str, _auto_umount: bool) -> Result<Self> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umount(&self) -> Result<()> {
|
||||
unmount(self.target.as_str(), UnmountFlags::DETACH)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
impl Drop for AutoMountExt4 {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
"AutoMountExt4 drop: {}, auto_umount: {}",
|
||||
self.target, self.auto_umount
|
||||
);
|
||||
if self.auto_umount {
|
||||
let _ = self.umount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_image(src: &str, target: &str, _autodrop: bool) -> Result<()> {
|
||||
mount_ext4(src, target)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_ext4(source: impl AsRef<Path>, target: impl AsRef<Path>) -> Result<()> {
|
||||
let new_loopback = loopdev::LoopControl::open()?.next_free()?;
|
||||
new_loopback.with().attach(source)?;
|
||||
let lo = new_loopback.path().ok_or(anyhow!("no loop"))?;
|
||||
match fsopen("ext4", FsOpenFlags::FSOPEN_CLOEXEC) {
|
||||
Result::Ok(fs) => {
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "source", lo)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
target.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(lo, target.as_ref(), "ext4", MountFlags::empty(), "")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umount_dir(src: impl AsRef<Path>) -> Result<()> {
|
||||
unmount(src.as_ref(), UnmountFlags::empty())
|
||||
.with_context(|| format!("Failed to umount {}", src.as_ref().display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlayfs(
|
||||
lower_dirs: &[String],
|
||||
lowest: &str,
|
||||
upperdir: Option<PathBuf>,
|
||||
workdir: Option<PathBuf>,
|
||||
dest: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let lowerdir_config = lower_dirs
|
||||
.iter()
|
||||
.map(|s| s.as_ref())
|
||||
.chain(std::iter::once(lowest))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":");
|
||||
info!(
|
||||
"mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}",
|
||||
dest.as_ref(),
|
||||
lowerdir_config,
|
||||
upperdir,
|
||||
workdir
|
||||
);
|
||||
|
||||
let upperdir = upperdir
|
||||
.filter(|up| up.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
let workdir = workdir
|
||||
.filter(|wd| wd.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
|
||||
let result = (|| {
|
||||
let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?;
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?;
|
||||
if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) {
|
||||
fsconfig_set_string(fs, "upperdir", upperdir)?;
|
||||
fsconfig_set_string(fs, "workdir", workdir)?;
|
||||
}
|
||||
fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
dest.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!("fsopen mount failed: {:#}, fallback to mount", e);
|
||||
let mut data = format!("lowerdir={lowerdir_config}");
|
||||
if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) {
|
||||
data = format!("{data},upperdir={upperdir},workdir={workdir}");
|
||||
}
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"overlay",
|
||||
MountFlags::empty(),
|
||||
data,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_devpts(dest: impl AsRef<Path>) -> Result<()> {
|
||||
create_dir(dest.as_ref())?;
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"devpts",
|
||||
MountFlags::empty(),
|
||||
"newinstance",
|
||||
)?;
|
||||
mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make devpts private")?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_devpts(_dest: impl AsRef<Path>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_tmpfs(dest: impl AsRef<Path>) -> Result<()> {
|
||||
info!("mount tmpfs on {}", dest.as_ref().display());
|
||||
match fsopen("tmpfs", FsOpenFlags::FSOPEN_CLOEXEC) {
|
||||
Result::Ok(fs) => {
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
dest.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make tmpfs private")?;
|
||||
let pts_dir = format!("{}/{PTS_NAME}", dest.as_ref().display());
|
||||
if let Err(e) = mount_devpts(pts_dir) {
|
||||
warn!("do devpts mount failed: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn bind_mount(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
|
||||
info!(
|
||||
"bind mount {} -> {}",
|
||||
from.as_ref().display(),
|
||||
to.as_ref().display()
|
||||
);
|
||||
match open_tree(
|
||||
CWD,
|
||||
from.as_ref(),
|
||||
OpenTreeFlags::OPEN_TREE_CLOEXEC
|
||||
| OpenTreeFlags::OPEN_TREE_CLONE
|
||||
| OpenTreeFlags::AT_RECURSIVE,
|
||||
) {
|
||||
Result::Ok(tree) => {
|
||||
move_mount(
|
||||
tree.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
to.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(
|
||||
from.as_ref(),
|
||||
to.as_ref(),
|
||||
"",
|
||||
MountFlags::BIND | MountFlags::REC,
|
||||
"",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn mount_overlay_child(
|
||||
mount_point: &str,
|
||||
relative: &String,
|
||||
module_roots: &Vec<String>,
|
||||
stock_root: &String,
|
||||
) -> Result<()> {
|
||||
if !module_roots
|
||||
.iter()
|
||||
.any(|lower| Path::new(&format!("{lower}{relative}")).exists())
|
||||
{
|
||||
return bind_mount(stock_root, mount_point);
|
||||
}
|
||||
if !Path::new(&stock_root).is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut lower_dirs: Vec<String> = vec![];
|
||||
for lower in module_roots {
|
||||
let lower_dir = format!("{lower}{relative}");
|
||||
let path = Path::new(&lower_dir);
|
||||
if path.is_dir() {
|
||||
lower_dirs.push(lower_dir);
|
||||
} else if path.exists() {
|
||||
// stock root has been blocked by this file
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if lower_dirs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// merge modules and stock
|
||||
if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) {
|
||||
warn!("failed: {:#}, fallback to bind mount", e);
|
||||
bind_mount(stock_root, mount_point)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlay(
|
||||
root: &String,
|
||||
module_roots: &Vec<String>,
|
||||
workdir: Option<PathBuf>,
|
||||
upperdir: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
info!("mount overlay for {}", root);
|
||||
std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?;
|
||||
let stock_root = ".";
|
||||
|
||||
// collect child mounts before mounting the root
|
||||
let mounts = Process::myself()?
|
||||
.mountinfo()
|
||||
.with_context(|| "get mountinfo")?;
|
||||
let mut mount_seq = mounts
|
||||
.0
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point)
|
||||
})
|
||||
.map(|m| m.mount_point.to_str())
|
||||
.collect::<Vec<_>>();
|
||||
mount_seq.sort();
|
||||
mount_seq.dedup();
|
||||
|
||||
mount_overlayfs(module_roots, root, upperdir, workdir, root)
|
||||
.with_context(|| "mount overlayfs for root failed")?;
|
||||
for mount_point in mount_seq.iter() {
|
||||
let Some(mount_point) = mount_point else {
|
||||
continue;
|
||||
};
|
||||
let relative = mount_point.replacen(root, "", 1);
|
||||
let stock_root: String = format!("{stock_root}{relative}");
|
||||
if !Path::new(&stock_root).exists() {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) {
|
||||
warn!(
|
||||
"failed to mount overlay for child {}: {:#}, revert",
|
||||
mount_point, e
|
||||
);
|
||||
umount_dir(root).with_context(|| format!("failed to revert {root}"))?;
|
||||
bail!(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_ext4(_src: &str, _target: &str, _autodrop: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn umount_dir(_src: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_overlay(_dest: &String, _lower_dirs: &Vec<String>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
178
apd/src/package.rs
Normal file
178
apd/src/package.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct PackageConfig {
|
||||
pub pkg: String,
|
||||
pub exclude: i32,
|
||||
pub allow: i32,
|
||||
pub uid: i32,
|
||||
pub to_uid: i32,
|
||||
pub sctx: String,
|
||||
}
|
||||
|
||||
pub fn read_ap_package_config() -> Vec<PackageConfig> {
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
let file = match File::open("/data/adb/ap/package_config") {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
warn!("Error opening file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut reader = csv::Reader::from_reader(file);
|
||||
let mut package_configs = Vec::new();
|
||||
let mut success = true;
|
||||
|
||||
for record in reader.deserialize() {
|
||||
match record {
|
||||
Ok(config) => package_configs.push(config),
|
||||
Err(e) => {
|
||||
warn!("Error deserializing record: {}", e);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
return package_configs;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn write_ap_package_config(package_configs: &[PackageConfig]) -> io::Result<()> {
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
let temp_path = "/data/adb/ap/package_config.tmp";
|
||||
let file = match File::create(temp_path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
warn!("Error creating temp file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut writer = csv::Writer::from_writer(file);
|
||||
let mut success = true;
|
||||
|
||||
for config in package_configs {
|
||||
if let Err(e) = writer.serialize(config) {
|
||||
warn!("Error serializing record: {}", e);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = writer.flush() {
|
||||
warn!("Error flushing writer: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::rename(temp_path, "/data/adb/ap/package_config") {
|
||||
warn!("Error renaming temp file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed after max retries",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
File::open(filename).map(|file| io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn synchronize_package_uid() -> io::Result<()> {
|
||||
info!("[synchronize_package_uid] Start synchronizing root list with system packages...");
|
||||
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
match read_lines("/data/system/packages.list") {
|
||||
Ok(lines) => {
|
||||
let lines: Vec<_> = lines.filter_map(|line| line.ok()).collect();
|
||||
|
||||
let mut package_configs = read_ap_package_config();
|
||||
|
||||
let system_packages: Vec<String> = lines
|
||||
.iter()
|
||||
.filter_map(|line| line.split_whitespace().next())
|
||||
.map(|pkg| pkg.to_string())
|
||||
.collect();
|
||||
|
||||
let original_len = package_configs.len();
|
||||
package_configs.retain(|config| system_packages.contains(&config.pkg));
|
||||
let removed_count = original_len - package_configs.len();
|
||||
|
||||
if removed_count > 0 {
|
||||
info!(
|
||||
"Removed {} uninstalled package configurations",
|
||||
removed_count
|
||||
);
|
||||
}
|
||||
|
||||
let mut updated = false;
|
||||
|
||||
for line in &lines {
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
if words.len() >= 2 {
|
||||
let pkg_name = words[0];
|
||||
if let Ok(uid) = words[1].parse::<i32>() {
|
||||
if let Some(config) = package_configs
|
||||
.iter_mut()
|
||||
.find(|config| config.pkg == pkg_name)
|
||||
{
|
||||
if config.uid != uid {
|
||||
info!(
|
||||
"Updating uid for package {}: {} -> {}",
|
||||
pkg_name, config.uid, uid
|
||||
);
|
||||
config.uid = uid;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Error parsing uid: {}", words[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updated || removed_count > 0 {
|
||||
write_ap_package_config(&package_configs)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error reading packages.list: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed after max retries",
|
||||
))
|
||||
}
|
||||
185
apd/src/pty.rs
Normal file
185
apd/src/pty.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
use std::ffi::c_int;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write, stderr, stdin, stdout};
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd};
|
||||
use std::process::exit;
|
||||
use std::ptr::null_mut;
|
||||
use std::thread;
|
||||
|
||||
use crate::defs::PTS_NAME;
|
||||
use crate::utils::get_tmp_path;
|
||||
use anyhow::{Ok, Result, bail};
|
||||
use libc::{
|
||||
__errno, EINTR, SIG_BLOCK, SIG_UNBLOCK, SIGWINCH, TIOCGWINSZ, TIOCSWINSZ, fork,
|
||||
pthread_sigmask, sigaddset, sigemptyset, sigset_t, sigwait, waitpid, winsize,
|
||||
};
|
||||
use rustix::fs::{Mode, OFlags, open};
|
||||
use rustix::io::dup;
|
||||
use rustix::ioctl::{Getter, ReadOpcode, ioctl};
|
||||
use rustix::process::setsid;
|
||||
use rustix::pty::{grantpt, unlockpt};
|
||||
use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout};
|
||||
use rustix::termios::{OptionalActions, Termios, isatty, tcgetattr, tcsetattr};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// https://github.com/topjohnwu/Magisk/blob/5627053b7481618adfdf8fa3569b48275589915b/native/src/core/su/pts.cpp
|
||||
|
||||
fn get_pty_num<F: AsFd>(fd: F) -> Result<u32> {
|
||||
Ok(unsafe {
|
||||
let tiocgptn = Getter::<ReadOpcode<b'T', 0x30, u32>, u32>::new();
|
||||
ioctl(fd, tiocgptn)?
|
||||
})
|
||||
}
|
||||
|
||||
static OLD_STDIN: Mutex<Option<Termios>> = Mutex::new(None);
|
||||
|
||||
fn watch_sigwinch_async(slave: RawFd) {
|
||||
let mut winch = MaybeUninit::<sigset_t>::uninit();
|
||||
unsafe {
|
||||
sigemptyset(winch.as_mut_ptr());
|
||||
sigaddset(winch.as_mut_ptr(), SIGWINCH);
|
||||
pthread_sigmask(SIG_BLOCK, winch.as_mut_ptr(), null_mut());
|
||||
}
|
||||
|
||||
thread::spawn(move || unsafe {
|
||||
let mut winch = MaybeUninit::<sigset_t>::uninit();
|
||||
sigemptyset(winch.as_mut_ptr());
|
||||
sigaddset(winch.as_mut_ptr(), SIGWINCH);
|
||||
pthread_sigmask(SIG_UNBLOCK, winch.as_mut_ptr(), null_mut());
|
||||
let mut sig: c_int = 0;
|
||||
loop {
|
||||
let mut w = MaybeUninit::<winsize>::uninit();
|
||||
if libc::ioctl(1, TIOCGWINSZ, w.as_mut_ptr()) < 0 {
|
||||
continue;
|
||||
}
|
||||
libc::ioctl(slave, TIOCSWINSZ, w.as_mut_ptr());
|
||||
if sigwait(winch.as_mut_ptr(), &mut sig) != 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_stdin_raw() -> rustix::io::Result<()> {
|
||||
let mut termios = tcgetattr(stdin())?;
|
||||
|
||||
let mut guard = OLD_STDIN.lock().unwrap();
|
||||
*guard = Some(termios.clone());
|
||||
drop(guard);
|
||||
|
||||
termios.make_raw();
|
||||
tcsetattr(stdin(), OptionalActions::Flush, &termios)
|
||||
}
|
||||
|
||||
fn restore_stdin() -> Result<()> {
|
||||
let mut guard = OLD_STDIN.lock().unwrap();
|
||||
|
||||
if let Some(original_termios) = guard.take() {
|
||||
tcsetattr(stdin(), OptionalActions::Flush, &original_termios)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pump<R: Read, W: Write>(mut from: R, mut to: W) {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match from.read(&mut buf) {
|
||||
Result::Ok(len) => {
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
if to.write_all(&buf[0..len]).is_err() {
|
||||
return;
|
||||
}
|
||||
if to.flush().is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pump_stdin_async(mut ptmx: File) {
|
||||
let _ = set_stdin_raw();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut stdin = stdin();
|
||||
pump(&mut stdin, &mut ptmx);
|
||||
});
|
||||
}
|
||||
|
||||
fn pump_stdout_blocking(mut ptmx: File) {
|
||||
let mut stdout = stdout();
|
||||
pump(&mut ptmx, &mut stdout);
|
||||
|
||||
let _ = restore_stdin();
|
||||
}
|
||||
|
||||
fn create_transfer(ptmx: OwnedFd) -> Result<()> {
|
||||
let pid = unsafe { fork() };
|
||||
match pid {
|
||||
d if d < 0 => bail!("fork"),
|
||||
0 => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let ptmx_r = ptmx;
|
||||
let ptmx_w = dup(&ptmx_r)?;
|
||||
|
||||
let ptmx_r = File::from(ptmx_r);
|
||||
let ptmx_w = File::from(ptmx_w);
|
||||
|
||||
watch_sigwinch_async(ptmx_w.as_raw_fd());
|
||||
pump_stdin_async(ptmx_r);
|
||||
pump_stdout_blocking(ptmx_w);
|
||||
|
||||
let mut status: c_int = -1;
|
||||
|
||||
unsafe {
|
||||
loop {
|
||||
if waitpid(pid, &mut status, 0) == -1 && *__errno() != EINTR {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
exit(status)
|
||||
}
|
||||
|
||||
pub fn prepare_pty() -> Result<()> {
|
||||
let tty_in = isatty(stdin());
|
||||
let tty_out = isatty(stdout());
|
||||
let tty_err = isatty(stderr());
|
||||
if !tty_in && !tty_out && !tty_err {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut pts_path = format!("{}/{}", get_tmp_path(), PTS_NAME);
|
||||
if !std::path::Path::new(&pts_path).exists() {
|
||||
pts_path = "/dev/pts".to_string();
|
||||
}
|
||||
let ptmx_path = format!("{}/ptmx", pts_path);
|
||||
let ptmx_fd = open(ptmx_path, OFlags::RDWR, Mode::empty())?;
|
||||
grantpt(&ptmx_fd)?;
|
||||
unlockpt(&ptmx_fd)?;
|
||||
let pty_num = get_pty_num(&ptmx_fd)?;
|
||||
create_transfer(ptmx_fd)?;
|
||||
setsid()?;
|
||||
let pty_fd = open(format!("{pts_path}/{pty_num}"), OFlags::RDWR, Mode::empty())?;
|
||||
if tty_in {
|
||||
dup2_stdin(&pty_fd)?;
|
||||
}
|
||||
if tty_out {
|
||||
dup2_stdout(&pty_fd)?;
|
||||
}
|
||||
if tty_err {
|
||||
dup2_stderr(&pty_fd)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
81
apd/src/restorecon.rs
Normal file
81
apd/src/restorecon.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use crate::defs;
|
||||
use anyhow::Result;
|
||||
use jwalk::{Parallelism::Serial, WalkDir};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use anyhow::{Context, Ok};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use extattr::{Flags as XattrFlags, lsetxattr};
|
||||
|
||||
pub const SYSTEM_CON: &str = "u:object_r:system_file:s0";
|
||||
pub const ADB_CON: &str = "u:object_r:adb_data_file:s0";
|
||||
pub const UNLABEL_CON: &str = "u:object_r:unlabeled:s0";
|
||||
|
||||
const SELINUX_XATTR: &str = "security.selinux";
|
||||
|
||||
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
|
||||
format!(
|
||||
"Failed to change SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
|
||||
format!(
|
||||
"Failed to get SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let con = String::from_utf8_lossy(&con);
|
||||
Ok(con.to_string())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
lsetfilecon(path, SYSTEM_CON)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
setsyscon(&path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
if let Result::Ok(con) = lgetfilecon(&path) {
|
||||
if con == UNLABEL_CON || con.is_empty() {
|
||||
lsetfilecon(&path, SYSTEM_CON)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restorecon() -> Result<()> {
|
||||
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
|
||||
restore_syscon_if_unlabeled(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
703
apd/src/sepolicy.rs
Normal file
703
apd/src/sepolicy.rs
Normal file
|
|
@ -0,0 +1,703 @@
|
|||
use anyhow::{Result, bail};
|
||||
use derive_new::new;
|
||||
use nom::{
|
||||
AsChar, IResult, Parser,
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_while, take_while_m_n, take_while1},
|
||||
character::complete::{space0, space1},
|
||||
combinator::map,
|
||||
};
|
||||
use std::{ffi, path::Path, vec};
|
||||
|
||||
type SeObject<'a> = Vec<&'a str>;
|
||||
|
||||
fn is_sepolicy_char(c: char) -> bool {
|
||||
c.is_alphanum() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn parse_single_word(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(is_sepolicy_char).parse(input)
|
||||
}
|
||||
|
||||
fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, (_, words, _)) = (
|
||||
tag("{"),
|
||||
take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()),
|
||||
tag("}"),
|
||||
)
|
||||
.parse(input)?;
|
||||
Ok((input, words.split_whitespace().collect()))
|
||||
}
|
||||
|
||||
fn parse_single_obj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, word) = take_while1(is_sepolicy_char).parse(input)?;
|
||||
Ok((input, vec![word]))
|
||||
}
|
||||
|
||||
fn parse_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, _) = tag("*").parse(input)?;
|
||||
Ok((input, vec!["*"]))
|
||||
}
|
||||
|
||||
// 1. a single sepolicy word
|
||||
// 2. { obj1 obj2 obj3 ...}
|
||||
// 3. *
|
||||
fn parse_seobj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
trait SeObjectParser<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct NormalPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
perm: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct XPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
operation: &'a str,
|
||||
perm_set: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeState<'a> {
|
||||
op: &'a str,
|
||||
stype: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeAttr<'a> {
|
||||
stype: SeObject<'a>,
|
||||
sattr: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Type<'a> {
|
||||
name: &'a str,
|
||||
attrs: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Attr<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeTransition<'a> {
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
object_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeChange<'a> {
|
||||
op: &'a str,
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct GenFsCon<'a> {
|
||||
fs_name: &'a str,
|
||||
partial_path: &'a str,
|
||||
fs_context: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PolicyStatement<'a> {
|
||||
// "allow *source_type *target_type *class *perm_set"
|
||||
// "deny *source_type *target_type *class *perm_set"
|
||||
// "auditallow *source_type *target_type *class *perm_set"
|
||||
// "dontaudit *source_type *target_type *class *perm_set"
|
||||
NormalPerm(NormalPerm<'a>),
|
||||
|
||||
// "allowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "auditallowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "dontauditxperm *source_type *target_type *class operation xperm_set"
|
||||
XPerm(XPerm<'a>),
|
||||
|
||||
// "permissive ^type"
|
||||
// "enforce ^type"
|
||||
TypeState(TypeState<'a>),
|
||||
|
||||
// "type type_name ^(attribute)"
|
||||
Type(Type<'a>),
|
||||
|
||||
// "typeattribute ^type ^attribute"
|
||||
TypeAttr(TypeAttr<'a>),
|
||||
|
||||
// "attribute ^attribute"
|
||||
Attr(Attr<'a>),
|
||||
|
||||
// "type_transition source_type target_type class default_type (object_name)"
|
||||
TypeTransition(TypeTransition<'a>),
|
||||
|
||||
// "type_change source_type target_type class default_type"
|
||||
// "type_member source_type target_type class default_type"
|
||||
TypeChange(TypeChange<'a>),
|
||||
|
||||
// "genfscon fs_name partial_path fs_context"
|
||||
GenFsCon(GenFsCon<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for NormalPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allow"),
|
||||
tag("deny"),
|
||||
tag("auditallow"),
|
||||
tag("dontaudit"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm) = parse_seobj(input)?;
|
||||
Ok((input, NormalPerm::new(op, source, target, class, perm)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for XPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allowxperm"),
|
||||
tag("auditallowxperm"),
|
||||
tag("dontauditxperm"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, operation) = parse_single_word(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm_set) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
XPerm::new(op, source, target, class, operation, perm_set),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeState<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("permissive"), tag("enforce"))).parse(input)?;
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeState::new(op, stype)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Type<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("type")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, name) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((input, Type::new(name, vec!["domain"]))); // default to domain
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attrs) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, Type::new(name, attrs)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeAttr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("typeattribute"), tag("attradd"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeAttr::new(stype, attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Attr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("attribute")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, Attr::new(attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeTransition<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("type_transition"), tag("name_transition"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, None),
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, object) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, Some(object)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeChange<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("type_change"), tag("type_member"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, TypeChange::new(op, source, target, class, default)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for GenFsCon<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (input, _) = tag("genfscon")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, fs) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, path) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, context) = parse_single_word(input)?;
|
||||
Ok((input, GenFsCon::new(fs, path, context)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PolicyStatement<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, statement) = alt((
|
||||
map(NormalPerm::parse, PolicyStatement::NormalPerm),
|
||||
map(XPerm::parse, PolicyStatement::XPerm),
|
||||
map(TypeState::parse, PolicyStatement::TypeState),
|
||||
map(Type::parse, PolicyStatement::Type),
|
||||
map(TypeAttr::parse, PolicyStatement::TypeAttr),
|
||||
map(Attr::parse, PolicyStatement::Attr),
|
||||
map(TypeTransition::parse, PolicyStatement::TypeTransition),
|
||||
map(TypeChange::parse, PolicyStatement::TypeChange),
|
||||
map(GenFsCon::parse, PolicyStatement::GenFsCon),
|
||||
))
|
||||
.parse(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = take_while(|c| c == ';')(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
Ok((input, statement))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result<Vec<PolicyStatement<'a>>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut statements = vec![];
|
||||
|
||||
for line in input.split(['\n', ';']) {
|
||||
let trimmed_line = line.trim();
|
||||
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) {
|
||||
statements.push(statement);
|
||||
} else if strict {
|
||||
bail!("Failed to parse policy statement: {}", line)
|
||||
}
|
||||
}
|
||||
Ok(statements)
|
||||
}
|
||||
|
||||
const SEPOLICY_MAX_LEN: usize = 128;
|
||||
|
||||
const CMD_NORMAL_PERM: u32 = 1;
|
||||
const CMD_XPERM: u32 = 2;
|
||||
const CMD_TYPE_STATE: u32 = 3;
|
||||
const CMD_TYPE: u32 = 4;
|
||||
const CMD_TYPE_ATTR: u32 = 5;
|
||||
const CMD_ATTR: u32 = 6;
|
||||
const CMD_TYPE_TRANSITION: u32 = 7;
|
||||
const CMD_TYPE_CHANGE: u32 = 8;
|
||||
const CMD_GENFSCON: u32 = 9;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum PolicyObject {
|
||||
All, // for "*", stand for all objects, and is NULL in ffi
|
||||
One([u8; SEPOLICY_MAX_LEN]),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PolicyObject {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(s: &str) -> Result<Self> {
|
||||
anyhow::ensure!(s.len() <= SEPOLICY_MAX_LEN, "policy object too long");
|
||||
if s == "*" {
|
||||
return Ok(PolicyObject::All);
|
||||
}
|
||||
let mut buf = [0u8; SEPOLICY_MAX_LEN];
|
||||
buf[..s.len()].copy_from_slice(s.as_bytes());
|
||||
Ok(PolicyObject::One(buf))
|
||||
}
|
||||
}
|
||||
|
||||
/// atomic statement, such as: allow domain1 domain2:file1 read;
|
||||
/// normal statement would be expanded to atomic statement, for example:
|
||||
/// allow domain1 domain2:file1 { read write }; would be expanded to two atomic statement
|
||||
/// allow domain1 domain2:file1 read;allow domain1 domain2:file1 write;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[derive(Debug, new)]
|
||||
struct AtomicStatement {
|
||||
cmd: u32,
|
||||
subcmd: u32,
|
||||
sepol1: PolicyObject,
|
||||
sepol2: PolicyObject,
|
||||
sepol3: PolicyObject,
|
||||
sepol4: PolicyObject,
|
||||
sepol5: PolicyObject,
|
||||
sepol6: PolicyObject,
|
||||
sepol7: PolicyObject,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a NormalPerm<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a NormalPerm<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"allow" => 1,
|
||||
"deny" => 2,
|
||||
"auditallow" => 3,
|
||||
"dontaudit" => 4,
|
||||
_ => 0,
|
||||
};
|
||||
for &s in &perm.source {
|
||||
for &t in &perm.target {
|
||||
for &c in &perm.class {
|
||||
for &p in &perm.perm {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_NORMAL_PERM,
|
||||
subcmd,
|
||||
sepol1: s.try_into()?,
|
||||
sepol2: t.try_into()?,
|
||||
sepol3: c.try_into()?,
|
||||
sepol4: p.try_into()?,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a XPerm<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a XPerm<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"allowxperm" => 1,
|
||||
"auditallowxperm" => 2,
|
||||
"dontauditxperm" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
for &s in &perm.source {
|
||||
for &t in &perm.target {
|
||||
for &c in &perm.class {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_XPERM,
|
||||
subcmd,
|
||||
sepol1: s.try_into()?,
|
||||
sepol2: t.try_into()?,
|
||||
sepol3: c.try_into()?,
|
||||
sepol4: perm.operation.try_into()?,
|
||||
sepol5: perm.perm_set.try_into()?,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeState<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeState<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"permissive" => 1,
|
||||
"enforcing" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
for &t in &perm.stype {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_STATE,
|
||||
subcmd,
|
||||
sepol1: t.try_into()?,
|
||||
sepol2: PolicyObject::None,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Type<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a Type<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
for &attr in &perm.attrs {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE,
|
||||
subcmd: 0,
|
||||
sepol1: perm.name.try_into()?,
|
||||
sepol2: attr.try_into()?,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeAttr<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeAttr<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
for &t in &perm.stype {
|
||||
for &attr in &perm.sattr {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_ATTR,
|
||||
subcmd: 0,
|
||||
sepol1: t.try_into()?,
|
||||
sepol2: attr.try_into()?,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Attr<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a Attr<'a>) -> Result<Self> {
|
||||
let result = vec![AtomicStatement {
|
||||
cmd: CMD_ATTR,
|
||||
subcmd: 0,
|
||||
sepol1: perm.name.try_into()?,
|
||||
sepol2: PolicyObject::None,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
}];
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeTransition<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeTransition<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let obj = match perm.object_name {
|
||||
Some(obj) => obj.try_into()?,
|
||||
None => PolicyObject::None,
|
||||
};
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_TRANSITION,
|
||||
subcmd: 0,
|
||||
sepol1: perm.source.try_into()?,
|
||||
sepol2: perm.target.try_into()?,
|
||||
sepol3: perm.class.try_into()?,
|
||||
sepol4: perm.default_type.try_into()?,
|
||||
sepol5: obj,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeChange<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeChange<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"type_change" => 1,
|
||||
"type_member" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_CHANGE,
|
||||
subcmd,
|
||||
sepol1: perm.source.try_into()?,
|
||||
sepol2: perm.target.try_into()?,
|
||||
sepol3: perm.class.try_into()?,
|
||||
sepol4: perm.default_type.try_into()?,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a GenFsCon<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a GenFsCon<'a>) -> Result<Self> {
|
||||
let result = vec![AtomicStatement {
|
||||
cmd: CMD_GENFSCON,
|
||||
subcmd: 0,
|
||||
sepol1: perm.fs_name.try_into()?,
|
||||
sepol2: perm.partial_path.try_into()?,
|
||||
sepol3: perm.fs_context.try_into()?,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
}];
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PolicyStatement<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &'a PolicyStatement) -> Result<Self> {
|
||||
match value {
|
||||
PolicyStatement::NormalPerm(perm) => perm.try_into(),
|
||||
PolicyStatement::XPerm(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeState(perm) => perm.try_into(),
|
||||
PolicyStatement::Type(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeAttr(perm) => perm.try_into(),
|
||||
PolicyStatement::Attr(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeTransition(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeChange(perm) => perm.try_into(),
|
||||
PolicyStatement::GenFsCon(perm) => perm.try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
/// for C FFI to call kernel interface
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct FfiPolicy {
|
||||
cmd: u32,
|
||||
subcmd: u32,
|
||||
sepol1: *const ffi::c_char,
|
||||
sepol2: *const ffi::c_char,
|
||||
sepol3: *const ffi::c_char,
|
||||
sepol4: *const ffi::c_char,
|
||||
sepol5: *const ffi::c_char,
|
||||
sepol6: *const ffi::c_char,
|
||||
sepol7: *const ffi::c_char,
|
||||
}
|
||||
|
||||
fn to_c_ptr(pol: &PolicyObject) -> *const ffi::c_char {
|
||||
match pol {
|
||||
PolicyObject::None | PolicyObject::All => std::ptr::null(),
|
||||
PolicyObject::One(s) => s.as_ptr().cast::<ffi::c_char>(),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AtomicStatement> for FfiPolicy {
|
||||
fn from(policy: AtomicStatement) -> FfiPolicy {
|
||||
FfiPolicy {
|
||||
cmd: policy.cmd,
|
||||
subcmd: policy.subcmd,
|
||||
sepol1: to_c_ptr(&policy.sepol1),
|
||||
sepol2: to_c_ptr(&policy.sepol2),
|
||||
sepol3: to_c_ptr(&policy.sepol3),
|
||||
sepol4: to_c_ptr(&policy.sepol4),
|
||||
sepol5: to_c_ptr(&policy.sepol5),
|
||||
sepol6: to_c_ptr(&policy.sepol6),
|
||||
sepol7: to_c_ptr(&policy.sepol7),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_rule(policy: &str) -> Result<()> {
|
||||
let path = Path::new(policy);
|
||||
let policy = if path.exists() {
|
||||
std::fs::read_to_string(path)?
|
||||
} else {
|
||||
policy.to_string()
|
||||
};
|
||||
parse_sepolicy(policy.trim(), true)?;
|
||||
Ok(())
|
||||
}
|
||||
476
apd/src/supercall.rs
Normal file
476
apd/src/supercall.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
use crate::package::{read_ap_package_config, synchronize_package_uid};
|
||||
use errno::errno;
|
||||
use libc::{EINVAL, c_int, c_long, c_void, execv, fork, pid_t, setenv, syscall, uid_t, wait};
|
||||
use log::{error, info, warn};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fmt::Write;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use std::{process, ptr};
|
||||
|
||||
const MAJOR: c_long = 0;
|
||||
const MINOR: c_long = 11;
|
||||
const PATCH: c_long = 1;
|
||||
|
||||
const KSTORAGE_EXCLUDE_LIST_GROUP: i32 = 1;
|
||||
|
||||
const __NR_SUPERCALL: c_long = 45;
|
||||
const SUPERCALL_KLOG: c_long = 0x1004;
|
||||
const SUPERCALL_KERNELPATCH_VER: c_long = 0x1008;
|
||||
const SUPERCALL_KERNEL_VER: c_long = 0x1009;
|
||||
const SUPERCALL_SU: c_long = 0x1010;
|
||||
const SUPERCALL_KSTORAGE_WRITE: c_long = 0x1041;
|
||||
const SUPERCALL_SU_GRANT_UID: c_long = 0x1100;
|
||||
const SUPERCALL_SU_REVOKE_UID: c_long = 0x1101;
|
||||
const SUPERCALL_SU_NUMS: c_long = 0x1102;
|
||||
const SUPERCALL_SU_LIST: c_long = 0x1103;
|
||||
const SUPERCALL_SU_RESET_PATH: c_long = 0x1111;
|
||||
const SUPERCALL_SU_GET_SAFEMODE: c_long = 0x1112;
|
||||
|
||||
const SUPERCALL_SCONTEXT_LEN: usize = 0x60;
|
||||
|
||||
#[repr(C)]
|
||||
struct SuProfile {
|
||||
uid: i32,
|
||||
to_uid: i32,
|
||||
scontext: [u8; SUPERCALL_SCONTEXT_LEN],
|
||||
}
|
||||
|
||||
fn ver_and_cmd(cmd: c_long) -> c_long {
|
||||
let version_code: u32 = ((MAJOR << 16) + (MINOR << 8) + PATCH).try_into().unwrap();
|
||||
((version_code as c_long) << 32) | (0x1158 << 16) | (cmd & 0xFFFF)
|
||||
}
|
||||
|
||||
fn sc_su_revoke_uid(key: &CStr, uid: uid_t) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_REVOKE_UID),
|
||||
uid,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_grant_uid(key: &CStr, profile: &SuProfile) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_GRANT_UID),
|
||||
profile,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_kstorage_write(
|
||||
key: &CStr,
|
||||
gid: i32,
|
||||
did: i64,
|
||||
data: *mut c_void,
|
||||
offset: i32,
|
||||
dlen: i32,
|
||||
) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KSTORAGE_WRITE),
|
||||
gid as c_long,
|
||||
did as c_long,
|
||||
data,
|
||||
(((offset as i64) << 32) | (dlen as i64)) as c_long,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_set_ap_mod_exclude(key: &CStr, uid: i64, exclude: i32) -> c_long {
|
||||
sc_kstorage_write(
|
||||
key,
|
||||
KSTORAGE_EXCLUDE_LIST_GROUP,
|
||||
uid,
|
||||
&exclude as *const i32 as *mut c_void,
|
||||
0,
|
||||
size_of::<i32>() as i32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sc_su_get_safemode(key: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
warn!("[sc_su_get_safemode] null superkey, tell apd we are not in safemode!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let key_ptr = key.as_ptr();
|
||||
if key_ptr.is_null() {
|
||||
warn!("[sc_su_get_safemode] superkey pointer is null!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key_ptr,
|
||||
ver_and_cmd(SUPERCALL_SU_GET_SAFEMODE),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su(key: &CStr, profile: &SuProfile) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU),
|
||||
profile,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_reset_path(key: &CStr, path: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() || path.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_RESET_PATH),
|
||||
path.as_ptr(),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_kp_ver(key: &CStr) -> Result<u32, i32> {
|
||||
if key.to_bytes().is_empty() {
|
||||
return Err(-EINVAL);
|
||||
}
|
||||
let ret = unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KERNELPATCH_VER),
|
||||
)
|
||||
};
|
||||
Ok(ret as u32)
|
||||
}
|
||||
|
||||
fn sc_k_ver(key: &CStr) -> Result<u32, i32> {
|
||||
if key.to_bytes().is_empty() {
|
||||
return Err(-EINVAL);
|
||||
}
|
||||
let ret = unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KERNEL_VER),
|
||||
)
|
||||
};
|
||||
Ok(ret as u32)
|
||||
}
|
||||
|
||||
fn sc_klog(key: &CStr, msg: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() || msg.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KLOG),
|
||||
msg.as_ptr(),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_uid_nums(key: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe { syscall(__NR_SUPERCALL, key.as_ptr(), ver_and_cmd(SUPERCALL_SU_NUMS)) as c_long }
|
||||
}
|
||||
|
||||
fn sc_su_allow_uids(key: &CStr, buf: &mut [uid_t]) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
if buf.is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_LIST),
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i32,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file_to_string(path: &str) -> io::Result<String> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn convert_string_to_u8_array(s: &str) -> [u8; SUPERCALL_SCONTEXT_LEN] {
|
||||
let mut u8_array = [0u8; SUPERCALL_SCONTEXT_LEN];
|
||||
let bytes = s.as_bytes();
|
||||
let len = usize::min(SUPERCALL_SCONTEXT_LEN, bytes.len());
|
||||
u8_array[..len].copy_from_slice(&bytes[..len]);
|
||||
u8_array
|
||||
}
|
||||
|
||||
fn convert_superkey(s: &Option<String>) -> Option<CString> {
|
||||
s.as_ref().and_then(|s| CString::new(s.clone()).ok())
|
||||
}
|
||||
|
||||
pub fn refresh_ap_package_list(skey: &CStr, mutex: &Arc<Mutex<()>>) {
|
||||
let _lock = mutex.lock().unwrap();
|
||||
|
||||
let num = sc_su_uid_nums(skey);
|
||||
if num < 0 {
|
||||
error!("[refresh_su_list] Error getting number of UIDs: {}", num);
|
||||
return;
|
||||
}
|
||||
let num = num as usize;
|
||||
let mut uids = vec![0 as uid_t; num];
|
||||
let n = sc_su_allow_uids(skey, &mut uids);
|
||||
if n < 0 {
|
||||
error!("[refresh_su_list] Error getting su list");
|
||||
return;
|
||||
}
|
||||
for uid in &uids {
|
||||
if *uid == 0 || *uid == 2000 {
|
||||
warn!(
|
||||
"[refresh_ap_package_list] Skip revoking critical uid: {}",
|
||||
uid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
info!(
|
||||
"[refresh_ap_package_list] Revoking {} root permission...",
|
||||
uid
|
||||
);
|
||||
let rc = sc_su_revoke_uid(skey, *uid);
|
||||
if rc != 0 {
|
||||
error!("[refresh_ap_package_list] Error revoking UID: {}", rc);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = synchronize_package_uid() {
|
||||
error!("Failed to synchronize package UIDs: {}", e);
|
||||
}
|
||||
|
||||
let package_configs = read_ap_package_config();
|
||||
for config in package_configs {
|
||||
if config.allow == 1 && config.exclude == 0 {
|
||||
let profile = SuProfile {
|
||||
uid: config.uid,
|
||||
to_uid: config.to_uid,
|
||||
scontext: convert_string_to_u8_array(&config.sctx),
|
||||
};
|
||||
let result = sc_su_grant_uid(skey, &profile);
|
||||
info!(
|
||||
"[refresh_ap_package_list] Loading {}: result = {}",
|
||||
config.pkg, result
|
||||
);
|
||||
}
|
||||
if config.allow == 0 && config.exclude == 1 {
|
||||
let result = sc_set_ap_mod_exclude(skey, config.uid as i64, 1);
|
||||
info!(
|
||||
"[refresh_ap_package_list] Loading exclude {}: result = {}",
|
||||
config.pkg, result
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn privilege_apd_profile(superkey: &Option<String>) {
|
||||
let key = convert_superkey(superkey);
|
||||
|
||||
let all_allow_ctx = "u:r:magisk:s0";
|
||||
let profile = SuProfile {
|
||||
uid: process::id().try_into().expect("PID conversion failed"),
|
||||
to_uid: 0,
|
||||
scontext: convert_string_to_u8_array(all_allow_ctx),
|
||||
};
|
||||
if let Some(ref key) = key {
|
||||
let result = sc_su(key, &profile);
|
||||
info!("[privilege_apd_profile] result = {}", result);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_load_package_uid_config(superkey: &Option<String>) {
|
||||
let package_configs = read_ap_package_config();
|
||||
let key = convert_superkey(superkey);
|
||||
|
||||
for config in package_configs {
|
||||
if config.allow == 1 && config.exclude == 0 {
|
||||
match key {
|
||||
Some(ref key) => {
|
||||
let profile = SuProfile {
|
||||
uid: config.uid,
|
||||
to_uid: config.to_uid,
|
||||
scontext: convert_string_to_u8_array(&config.sctx),
|
||||
};
|
||||
let result = sc_su_grant_uid(key, &profile);
|
||||
info!("Processed {}: result = {}", config.pkg, result);
|
||||
}
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping config: {}", config.pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.allow == 0 && config.exclude == 1 {
|
||||
match key {
|
||||
Some(ref key) => {
|
||||
let result = sc_set_ap_mod_exclude(key, config.uid as i64, 1);
|
||||
info!("Processed exclude {}: result = {}", config.pkg, result);
|
||||
}
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping config: {}", config.pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_load_su_path(superkey: &Option<String>) {
|
||||
let su_path_file = "/data/adb/ap/su_path";
|
||||
|
||||
match read_file_to_string(su_path_file) {
|
||||
Ok(su_path) => {
|
||||
let superkey_cstr = convert_superkey(superkey);
|
||||
|
||||
match superkey_cstr {
|
||||
Some(superkey_cstr) => match CString::new(su_path.trim()) {
|
||||
Ok(su_path_cstr) => {
|
||||
let result = sc_su_reset_path(&superkey_cstr, &su_path_cstr);
|
||||
if result == 0 {
|
||||
info!("suPath load successfully");
|
||||
} else {
|
||||
warn!("Failed to load su path, error code: {}", result);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to convert su_path: {}", e);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping...");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to read su_path file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_env_var(key: &str, value: &str) {
|
||||
let key_c = CString::new(key).expect("CString::new failed");
|
||||
let value_c = CString::new(value).expect("CString::new failed");
|
||||
unsafe {
|
||||
setenv(key_c.as_ptr(), value_c.as_ptr(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_kernel(key: &CStr, _fmt: &str, args: std::fmt::Arguments) -> c_long {
|
||||
let mut buf = String::with_capacity(1024);
|
||||
write!(&mut buf, "{}", args).expect("Error formatting string");
|
||||
|
||||
let c_buf = CString::new(buf).expect("CString::new failed");
|
||||
sc_klog(key, &c_buf)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_kernel {
|
||||
($key:expr_2021, $fmt:expr_2021, $($arg:tt)*) => (
|
||||
log_kernel($key, $fmt, std::format_args!($fmt, $($arg)*))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fork_for_result(exec: &str, argv: &[&str], key: &Option<String>) {
|
||||
let mut cmd = String::new();
|
||||
for arg in argv {
|
||||
cmd.push_str(arg);
|
||||
cmd.push(' ');
|
||||
}
|
||||
|
||||
let superkey_cstr = convert_superkey(key);
|
||||
|
||||
match superkey_cstr {
|
||||
Some(superkey_cstr) => {
|
||||
unsafe {
|
||||
let pid: pid_t = fork();
|
||||
if pid < 0 {
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} fork {} error: {}\n",
|
||||
libc::getpid(),
|
||||
exec,
|
||||
-1
|
||||
);
|
||||
} else if pid == 0 {
|
||||
set_env_var("KERNELPATCH", "true");
|
||||
let kpver = format!("{:x}", sc_kp_ver(&superkey_cstr).unwrap_or(0));
|
||||
set_env_var("KERNELPATCH_VERSION", kpver.as_str());
|
||||
let kver = format!("{:x}", sc_k_ver(&superkey_cstr).unwrap_or(0));
|
||||
set_env_var("KERNEL_VERSION", kver.as_str());
|
||||
|
||||
let c_exec = CString::new(exec).expect("CString::new failed");
|
||||
let c_argv: Vec<CString> =
|
||||
argv.iter().map(|&arg| CString::new(arg).unwrap()).collect();
|
||||
let mut c_argv_ptrs: Vec<*const libc::c_char> =
|
||||
c_argv.iter().map(|arg| arg.as_ptr()).collect();
|
||||
c_argv_ptrs.push(ptr::null());
|
||||
|
||||
execv(c_exec.as_ptr(), c_argv_ptrs.as_ptr());
|
||||
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} exec {} error: {}\n",
|
||||
libc::getpid(),
|
||||
cmd,
|
||||
CStr::from_ptr(libc::strerror(errno().0))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
);
|
||||
exit(1); // execv only returns on error
|
||||
} else {
|
||||
let mut status: c_int = 0;
|
||||
wait(&mut status);
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} wait {} status: 0x{}\n",
|
||||
libc::getpid(),
|
||||
cmd,
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!("[fork_for_result] SuperKey convert failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
198
apd/src/utils.rs
Normal file
198
apd/src/utils.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use log::{info, warn};
|
||||
use std::ffi::CString;
|
||||
use std::{
|
||||
fs::{File, OpenOptions, create_dir_all},
|
||||
io::{BufRead, BufReader, ErrorKind::AlreadyExists, Write},
|
||||
path::Path,
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use crate::defs;
|
||||
use std::fs::metadata;
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::supercall::sc_su_get_safemode;
|
||||
|
||||
pub fn ensure_clean_dir(dir: &str) -> Result<()> {
|
||||
let path = Path::new(dir);
|
||||
log::debug!("ensure_clean_dir: {}", path.display());
|
||||
if path.exists() {
|
||||
log::debug!("ensure_clean_dir: {} exists, remove it", path.display());
|
||||
std::fs::remove_dir_all(path)?;
|
||||
}
|
||||
Ok(create_dir_all(path)?)
|
||||
}
|
||||
|
||||
pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
|
||||
match File::options().write(true).create_new(true).open(&file) {
|
||||
Result::Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == AlreadyExists && file.as_ref().is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from(err))
|
||||
.with_context(|| format!("{} is not a regular file", file.as_ref().display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
|
||||
let result = create_dir_all(&dir).map_err(Error::from);
|
||||
if dir.as_ref().is_dir() {
|
||||
result
|
||||
} else if result.is_ok() {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// todo: ensure
|
||||
pub fn ensure_binary<T: AsRef<Path>>(path: T) -> Result<()> {
|
||||
set_permissions(&path, Permissions::from_mode(0o755))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn getprop(prop: &str) -> Option<String> {
|
||||
android_properties::getprop(prop).value()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn getprop(_prop: &str) -> Option<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
pub fn run_command(
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
stdout: Option<Stdio>,
|
||||
) -> Result<std::process::Child> {
|
||||
let mut command_builder = Command::new(command);
|
||||
command_builder.args(args);
|
||||
if let Some(out) = stdout {
|
||||
command_builder.stdout(out);
|
||||
}
|
||||
let child = command_builder.spawn()?;
|
||||
Ok(child)
|
||||
}
|
||||
pub fn is_safe_mode(superkey: Option<String>) -> bool {
|
||||
let safemode = getprop("persist.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some()
|
||||
|| getprop("ro.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some();
|
||||
info!("safemode: {}", safemode);
|
||||
if safemode {
|
||||
return true;
|
||||
}
|
||||
let safemode = superkey
|
||||
.as_ref()
|
||||
.and_then(|key_str| CString::new(key_str.as_str()).ok())
|
||||
.map_or_else(
|
||||
|| {
|
||||
warn!("[is_safe_mode] No valid superkey provided, assuming safemode as false.");
|
||||
false
|
||||
},
|
||||
|cstr| sc_su_get_safemode(&cstr) == 1,
|
||||
);
|
||||
info!("kernel_safemode: {}", safemode);
|
||||
safemode
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn switch_mnt_ns(pid: i32) -> Result<()> {
|
||||
use anyhow::ensure;
|
||||
use std::os::fd::AsRawFd;
|
||||
let path = format!("/proc/{pid}/ns/mnt");
|
||||
let fd = File::open(path)?;
|
||||
let current_dir = std::env::current_dir();
|
||||
let ret = unsafe { libc::setns(fd.as_raw_fd(), libc::CLONE_NEWNS) };
|
||||
if let Result::Ok(current_dir) = current_dir {
|
||||
let _ = std::env::set_current_dir(current_dir);
|
||||
}
|
||||
ensure!(ret == 0, "switch mnt ns failed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_overlayfs_supported() -> Result<bool> {
|
||||
let file =
|
||||
File::open("/proc/filesystems").with_context(|| "Failed to open /proc/filesystems")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let overlay_supported = reader.lines().any(|line| {
|
||||
if let Result::Ok(line) = line {
|
||||
line.contains("overlay")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
Ok(overlay_supported)
|
||||
}
|
||||
|
||||
pub fn should_use_overlayfs() -> Result<bool> {
|
||||
let force_using_overlayfs = Path::new(defs::FORCE_OVERLAYFS_FILE).exists();
|
||||
let overlayfs_supported = is_overlayfs_supported()?;
|
||||
|
||||
Ok(force_using_overlayfs && overlayfs_supported)
|
||||
}
|
||||
|
||||
fn switch_cgroup(grp: &str, pid: u32) {
|
||||
let path = Path::new(grp).join("cgroup.procs");
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let fp = OpenOptions::new().append(true).open(path);
|
||||
if let Result::Ok(mut fp) = fp {
|
||||
let _ = write!(fp, "{pid}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_cgroups() {
|
||||
let pid = std::process::id();
|
||||
switch_cgroup("/acct", pid);
|
||||
switch_cgroup("/dev/cg2_bpf", pid);
|
||||
switch_cgroup("/sys/fs/cgroup", pid);
|
||||
|
||||
if getprop("ro.config.per_app_memcg")
|
||||
.filter(|prop| prop == "false")
|
||||
.is_none()
|
||||
{
|
||||
switch_cgroup("/dev/memcg/apps", pid);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umask(mask: u32) {
|
||||
unsafe { libc::umask(mask) };
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn umask(_mask: u32) {
|
||||
unimplemented!("umask is not supported on this platform")
|
||||
}
|
||||
|
||||
pub fn has_magisk() -> bool {
|
||||
which::which("magisk").is_ok()
|
||||
}
|
||||
pub fn get_tmp_path() -> &'static str {
|
||||
if metadata(defs::TEMP_DIR_LEGACY).is_ok() {
|
||||
return defs::TEMP_DIR_LEGACY;
|
||||
}
|
||||
if metadata(defs::TEMP_DIR).is_ok() {
|
||||
return defs::TEMP_DIR;
|
||||
}
|
||||
""
|
||||
}
|
||||
pub fn get_work_dir() -> String {
|
||||
let tmp_path = get_tmp_path();
|
||||
format!("{}/workdir/", tmp_path)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue