Updated main branch
This commit is contained in:
parent
9a3987bc1e
commit
3f2388004d
350 changed files with 79360 additions and 0 deletions
2
userspace/ksud/.gitignore
vendored
Normal file
2
userspace/ksud/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.cargo/
|
||||
1835
userspace/ksud/Cargo.lock
generated
Normal file
1835
userspace/ksud/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
66
userspace/ksud/Cargo.toml
Normal file
66
userspace/ksud/Cargo.toml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
[package]
|
||||
name = "ksud"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
notify = "8.2"
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
const_format = "0.2"
|
||||
zip = { version = "6", features = [
|
||||
"deflate",
|
||||
"deflate64",
|
||||
"time",
|
||||
"lzma",
|
||||
"xz",
|
||||
], default-features = false }
|
||||
zip-extensions = { version = "0.8", features = [
|
||||
"deflate",
|
||||
"lzma",
|
||||
"xz",
|
||||
], default-features = false }
|
||||
java-properties = { git = "https://github.com/Kernel-SU/java-properties.git", branch = "master", default-features = false }
|
||||
log = "0.4"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
serde_json = "1"
|
||||
encoding_rs = "0.8"
|
||||
humansize = "2"
|
||||
libc = "0.2"
|
||||
extattr = "1"
|
||||
jwalk = "0.8"
|
||||
is_executable = "1"
|
||||
nom = "8"
|
||||
derive-new = "0.7"
|
||||
rust-embed = { version = "8", features = [
|
||||
"debug-embed",
|
||||
"compression", # must clean build after updating binaries
|
||||
] }
|
||||
which = "8"
|
||||
getopts = "0.2"
|
||||
sha256 = "1"
|
||||
sha1 = "0.10"
|
||||
tempfile = "3"
|
||||
chrono = "0.4"
|
||||
regex-lite = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies]
|
||||
rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = [
|
||||
"all-apis",
|
||||
] }
|
||||
# some android specific dependencies which compiles under unix are also listed here for convenience of coding
|
||||
android-properties = { version = "0.2", features = ["bionic-deprecated"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = { version = "0.15", default-features = false }
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = false
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
opt-level = 3
|
||||
strip = true
|
||||
split-debuginfo = "unpacked"
|
||||
1
userspace/ksud/bin/.gitignore
vendored
Normal file
1
userspace/ksud/bin/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
**/*.ko
|
||||
BIN
userspace/ksud/bin/aarch64/bootctl
Normal file
BIN
userspace/ksud/bin/aarch64/bootctl
Normal file
Binary file not shown.
BIN
userspace/ksud/bin/aarch64/busybox
Executable file
BIN
userspace/ksud/bin/aarch64/busybox
Executable file
Binary file not shown.
BIN
userspace/ksud/bin/aarch64/ksuinit
Executable file
BIN
userspace/ksud/bin/aarch64/ksuinit
Executable file
Binary file not shown.
BIN
userspace/ksud/bin/aarch64/resetprop
Normal file
BIN
userspace/ksud/bin/aarch64/resetprop
Normal file
Binary file not shown.
BIN
userspace/ksud/bin/arm/busybox
Normal file
BIN
userspace/ksud/bin/arm/busybox
Normal file
Binary file not shown.
BIN
userspace/ksud/bin/arm/resetprop
Executable file
BIN
userspace/ksud/bin/arm/resetprop
Executable file
Binary file not shown.
BIN
userspace/ksud/bin/x86_64/busybox
Executable file
BIN
userspace/ksud/bin/x86_64/busybox
Executable file
Binary file not shown.
BIN
userspace/ksud/bin/x86_64/ksuinit
Executable file
BIN
userspace/ksud/bin/x86_64/ksuinit
Executable file
Binary file not shown.
BIN
userspace/ksud/bin/x86_64/resetprop
Normal file
BIN
userspace/ksud/bin/x86_64/resetprop
Normal file
Binary file not shown.
47
userspace/ksud/build.rs
Normal file
47
userspace/ksud/build.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use std::{env, fs::File, io::Write, path::Path, process::Command};
|
||||
|
||||
fn get_git_version() -> Result<(u32, String), std::io::Error> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-list", "--count", "HEAD"])
|
||||
.output()?;
|
||||
|
||||
let output = output.stdout;
|
||||
let version_code = String::from_utf8(output).expect("Failed to read git count stdout");
|
||||
let version_code: u32 = version_code
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| std::io::Error::other("Failed to parse git count"))?;
|
||||
let version_code = 40000 - 2815 + version_code; // For historical reasons
|
||||
|
||||
let version_name = String::from_utf8(
|
||||
Command::new("git")
|
||||
.args(["describe", "--tags", "--always"])
|
||||
.output()?
|
||||
.stdout,
|
||||
)
|
||||
.map_err(|_| std::io::Error::other("Failed to parse git count"))?;
|
||||
let version_name = version_name.trim_start_matches('v').to_string();
|
||||
Ok((version_code, version_name))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (code, name) = match get_git_version() {
|
||||
Ok((code, name)) => (code, name),
|
||||
Err(_) => {
|
||||
// show warning if git is not installed
|
||||
println!("cargo:warning=Failed to get git version, using 0.0.0");
|
||||
(0, "0.0.0".to_string())
|
||||
}
|
||||
};
|
||||
let out_dir = env::var("OUT_DIR").expect("Failed to get $OUT_DIR");
|
||||
let out_dir = Path::new(&out_dir);
|
||||
File::create(Path::new(out_dir).join("VERSION_CODE"))
|
||||
.expect("Failed to create VERSION_CODE")
|
||||
.write_all(code.to_string().as_bytes())
|
||||
.expect("Failed to write VERSION_CODE");
|
||||
|
||||
File::create(Path::new(out_dir).join("VERSION_NAME"))
|
||||
.expect("Failed to create VERSION_NAME")
|
||||
.write_all(name.trim().as_bytes())
|
||||
.expect("Failed to write VERSION_NAME");
|
||||
}
|
||||
115
userspace/ksud/src/apk_sign.rs
Normal file
115
userspace/ksud/src/apk_sign.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use anyhow::{Result, ensure};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub fn get_apk_signature(apk: &str) -> Result<(u32, String)> {
|
||||
let mut buffer = [0u8; 0x10];
|
||||
let mut size4 = [0u8; 4];
|
||||
let mut size8 = [0u8; 8];
|
||||
let mut size_of_block = [0u8; 8];
|
||||
|
||||
let mut f = std::fs::File::open(apk)?;
|
||||
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let mut n = [0u8; 2];
|
||||
f.seek(SeekFrom::End(-i - 2))?;
|
||||
f.read_exact(&mut n)?;
|
||||
|
||||
let n = u16::from_le_bytes(n);
|
||||
if i64::from(n) == i {
|
||||
f.seek(SeekFrom::Current(-22))?;
|
||||
f.read_exact(&mut size4)?;
|
||||
|
||||
if u32::from_le_bytes(size4) ^ 0xcafe_babe_u32 == 0xccfb_f1ee_u32 {
|
||||
if i > 0 {
|
||||
println!("warning: comment length is {i}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(n != 0xffff, "not a zip file");
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
f.seek(SeekFrom::Current(12))?;
|
||||
// offset
|
||||
f.read_exact(&mut size4)?;
|
||||
f.seek(SeekFrom::Start(u64::from(u32::from_le_bytes(size4)) - 0x18))?;
|
||||
|
||||
f.read_exact(&mut size8)?;
|
||||
f.read_exact(&mut buffer)?;
|
||||
|
||||
ensure!(&buffer == b"APK Sig Block 42", "Can not found sig block");
|
||||
|
||||
let pos = u64::from(u32::from_le_bytes(size4)) - (u64::from_le_bytes(size8) + 0x8);
|
||||
f.seek(SeekFrom::Start(pos))?;
|
||||
f.read_exact(&mut size_of_block)?;
|
||||
|
||||
ensure!(size_of_block == size8, "not a signed apk");
|
||||
|
||||
let mut v2_signing: Option<(u32, String)> = None;
|
||||
let mut v3_signing_exist = false;
|
||||
let mut v3_1_signing_exist = false;
|
||||
|
||||
loop {
|
||||
let mut id = [0u8; 4];
|
||||
let mut offset = 4u32;
|
||||
|
||||
f.read_exact(&mut size8)?; // sequence length
|
||||
if size8 == size_of_block {
|
||||
break;
|
||||
}
|
||||
|
||||
f.read_exact(&mut id)?; // id
|
||||
|
||||
let id = u32::from_le_bytes(id);
|
||||
if id == 0x7109_871a_u32 {
|
||||
v2_signing = Some(calc_cert_sha256(&mut f, &mut size4, &mut offset)?);
|
||||
} else if id == 0xf053_68c0_u32 {
|
||||
// v3 signature scheme
|
||||
v3_signing_exist = true;
|
||||
} else if id == 0x1b93_ad61_u32 {
|
||||
// v3.1 signature scheme: credits to vvb2060
|
||||
v3_1_signing_exist = true;
|
||||
}
|
||||
|
||||
f.seek(SeekFrom::Current(
|
||||
i64::from_le_bytes(size8) - i64::from(offset),
|
||||
))?;
|
||||
}
|
||||
|
||||
if v3_signing_exist || v3_1_signing_exist {
|
||||
return Err(anyhow::anyhow!("Unexpected v3 signature found!",));
|
||||
}
|
||||
|
||||
v2_signing.ok_or(anyhow::anyhow!("No signature found!"))
|
||||
}
|
||||
|
||||
fn calc_cert_sha256(
|
||||
f: &mut std::fs::File,
|
||||
size4: &mut [u8; 4],
|
||||
offset: &mut u32,
|
||||
) -> Result<(u32, String)> {
|
||||
f.read_exact(size4)?; // signer-sequence length
|
||||
f.read_exact(size4)?; // signer length
|
||||
f.read_exact(size4)?; // signed data length
|
||||
*offset += 0x4 * 3;
|
||||
|
||||
f.read_exact(size4)?; // digests-sequence length
|
||||
let pos = u32::from_le_bytes(*size4); // skip digests
|
||||
f.seek(SeekFrom::Current(i64::from(pos)))?;
|
||||
*offset += 0x4 + pos;
|
||||
|
||||
f.read_exact(size4)?; // certificates length
|
||||
f.read_exact(size4)?; // certificate length
|
||||
*offset += 0x4 * 2;
|
||||
|
||||
let cert_len = u32::from_le_bytes(*size4);
|
||||
let mut cert: Vec<u8> = vec![0; cert_len as usize];
|
||||
f.read_exact(&mut cert)?;
|
||||
*offset += cert_len;
|
||||
|
||||
Ok((cert_len, sha256::digest(&cert)))
|
||||
}
|
||||
55
userspace/ksud/src/assets.rs
Normal file
55
userspace/ksud/src/assets.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use anyhow::Result;
|
||||
use const_format::concatcp;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::path::Path;
|
||||
|
||||
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 BOOTCTL_PATH: &str = concatcp!(BINARY_DIR, "bootctl");
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_os = "android"))]
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "bin/x86_64"]
|
||||
struct Asset;
|
||||
|
||||
// IF NOT x86_64 ANDROID, ie. macos, linux, windows, always use aarch64
|
||||
#[cfg(all(target_arch = "aarch64", target_os = "android"))]
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "bin/aarch64"]
|
||||
struct Asset;
|
||||
|
||||
#[cfg(all(target_arch = "arm", target_os = "android"))]
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "bin/arm"]
|
||||
struct Asset;
|
||||
|
||||
pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> {
|
||||
for file in Asset::iter() {
|
||||
if file == "ksuinit" || file.ends_with(".ko") {
|
||||
// don't extract ksuinit and kernel modules
|
||||
continue;
|
||||
}
|
||||
let asset = Asset::get(&file).ok_or(anyhow::anyhow!("asset not found: {}", file))?;
|
||||
utils::ensure_binary(format!("{BINARY_DIR}{file}"), &asset.data, ignore_if_exist)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_assets_to_file(name: &str, dst: impl AsRef<Path>) -> Result<()> {
|
||||
let asset = Asset::get(name).ok_or(anyhow::anyhow!("asset not found: {}", name))?;
|
||||
std::fs::write(dst, asset.data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_supported_kmi() -> Result<Vec<String>> {
|
||||
let mut list = Vec::new();
|
||||
for file in Asset::iter() {
|
||||
// kmi_name = "xxx_kernelsu.ko"
|
||||
if let Some(kmi) = file.strip_suffix("_kernelsu.ko") {
|
||||
list.push(kmi.to_string());
|
||||
}
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
10
userspace/ksud/src/banner
Normal file
10
userspace/ksud/src/banner
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
____ _ _ ____ _ _
|
||||
/ ___| _ _| | _(_) ___|| | | |
|
||||
\___ \| | | | |/ / \___ \| | | |
|
||||
___) | |_| | <| |___) | |_| |
|
||||
|____/ \__,_|_|\_\_|____/ \___/
|
||||
_ _ _ _
|
||||
| | | | | |_ _ __ __ _
|
||||
| | | | | __| '__/ _\ |
|
||||
| |_| | | |_| | | (_| |
|
||||
\___/|_|\__|_| \__,_|
|
||||
791
userspace/ksud/src/boot_patch.rs
Normal file
791
userspace/ksud/src/boot_patch.rs
Normal file
|
|
@ -0,0 +1,791 @@
|
|||
#[cfg(unix)]
|
||||
use std::{
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use regex_lite::Regex;
|
||||
use which::which;
|
||||
|
||||
use crate::{
|
||||
assets,
|
||||
defs::{self, BACKUP_FILENAME, KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX},
|
||||
utils,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn ensure_gki_kernel() -> Result<()> {
|
||||
let version = get_kernel_version()?;
|
||||
let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5;
|
||||
ensure!(is_gki, "only support GKI kernel");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn get_kernel_version() -> Result<(i32, i32, i32)> {
|
||||
let uname = rustix::system::uname();
|
||||
let version = uname.release().to_string_lossy();
|
||||
let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)")?;
|
||||
if let Some(captures) = re.captures(&version) {
|
||||
let major = captures
|
||||
.get(1)
|
||||
.and_then(|m| m.as_str().parse::<i32>().ok())
|
||||
.ok_or_else(|| anyhow!("Major version parse error"))?;
|
||||
let minor = captures
|
||||
.get(2)
|
||||
.and_then(|m| m.as_str().parse::<i32>().ok())
|
||||
.ok_or_else(|| anyhow!("Minor version parse error"))?;
|
||||
let patch = captures
|
||||
.get(3)
|
||||
.and_then(|m| m.as_str().parse::<i32>().ok())
|
||||
.ok_or_else(|| anyhow!("Patch version parse error"))?;
|
||||
Ok((major, minor, patch))
|
||||
} else {
|
||||
Err(anyhow!("Invalid kernel version string"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn parse_kmi(version: &str) -> Result<String> {
|
||||
let re = Regex::new(r"(.* )?(\d+\.\d+)(\S+)?(android\d+)(.*)")?;
|
||||
let cap = re
|
||||
.captures(version)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get KMI from boot/modules"))?;
|
||||
let android_version = cap.get(4).map_or("", |m| m.as_str());
|
||||
let kernel_version = cap.get(2).map_or("", |m| m.as_str());
|
||||
Ok(format!("{android_version}-{kernel_version}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn parse_kmi_from_uname() -> Result<String> {
|
||||
let uname = rustix::system::uname();
|
||||
let version = uname.release().to_string_lossy();
|
||||
parse_kmi(&version)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn parse_kmi_from_modules() -> Result<String> {
|
||||
use std::io::BufRead;
|
||||
// find a *.ko in /vendor/lib/modules
|
||||
let modfile = std::fs::read_dir("/vendor/lib/modules")?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "ko"))
|
||||
.map(|entry| entry.path())
|
||||
.ok_or_else(|| anyhow!("No kernel module found"))?;
|
||||
let output = Command::new("modinfo").arg(modfile).output()?;
|
||||
for line in output.stdout.lines().map_while(Result::ok) {
|
||||
if line.starts_with("vermagic") {
|
||||
return parse_kmi(&line);
|
||||
}
|
||||
}
|
||||
anyhow::bail!("Parse KMI from modules failed")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_current_kmi() -> Result<String> {
|
||||
parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn get_current_kmi() -> Result<String> {
|
||||
bail!("Unsupported platform")
|
||||
}
|
||||
|
||||
fn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result<String> {
|
||||
use std::fs::{File, copy};
|
||||
use std::io::{BufReader, Read};
|
||||
let kernel_path = workdir.join("kernel");
|
||||
copy(kernel, &kernel_path).context("Failed to copy kernel")?;
|
||||
|
||||
let file = File::open(&kernel_path).context("Failed to open kernel file")?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = Vec::new();
|
||||
reader
|
||||
.read_to_end(&mut buffer)
|
||||
.context("Failed to read kernel file")?;
|
||||
|
||||
let printable_strings: Vec<&str> = buffer
|
||||
.split(|&b| b == 0)
|
||||
.filter_map(|slice| std::str::from_utf8(slice).ok())
|
||||
.filter(|s| s.chars().all(|c| c.is_ascii_graphic() || c == ' '))
|
||||
.collect();
|
||||
|
||||
let re =
|
||||
Regex::new(r"(?:.* )?(\d+\.\d+)(?:\S+)?(android\d+)").context("Failed to compile regex")?;
|
||||
for s in printable_strings {
|
||||
if let Some(caps) = re.captures(s)
|
||||
&& let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2))
|
||||
{
|
||||
let kmi = format!("{}-{}", android_version.as_str(), kernel_version.as_str());
|
||||
return Ok(kmi);
|
||||
}
|
||||
}
|
||||
println!("- Failed to get KMI version");
|
||||
bail!("Try to choose LKM manually")
|
||||
}
|
||||
|
||||
fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Result<String> {
|
||||
let image_path = workdir.join("image");
|
||||
|
||||
std::fs::copy(image, &image_path).context("Failed to copy image")?;
|
||||
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("unpack")
|
||||
.arg(&image_path)
|
||||
.status()
|
||||
.context("Failed to execute magiskboot command")?;
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"magiskboot unpack failed with status: {:?}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
parse_kmi_from_kernel(&image_path, workdir)
|
||||
}
|
||||
|
||||
fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("cpio")
|
||||
.arg(cpio_path)
|
||||
.arg(cmd)
|
||||
.status()?;
|
||||
ensure!(status.success(), "magiskboot cpio {} failed", cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("cpio")
|
||||
.arg(cpio_path)
|
||||
.arg("test")
|
||||
.status()?;
|
||||
// 0: stock, 1: magisk
|
||||
Ok(status.code() == Some(1))
|
||||
}
|
||||
|
||||
fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("cpio")
|
||||
.arg(cpio_path)
|
||||
.arg("exists kernelsu.ko")
|
||||
.status()?;
|
||||
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
fn dd<P: AsRef<Path>, Q: AsRef<Path>>(ifile: P, ofile: Q) -> Result<()> {
|
||||
let status = Command::new("dd")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg(format!("if={}", ifile.as_ref().display()))
|
||||
.arg(format!("of={}", ofile.as_ref().display()))
|
||||
.status()?;
|
||||
ensure!(
|
||||
status.success(),
|
||||
"dd if={:?} of={:?} failed",
|
||||
ifile.as_ref(),
|
||||
ofile.as_ref()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore(
|
||||
image: Option<PathBuf>,
|
||||
magiskboot_path: Option<PathBuf>,
|
||||
flash: bool,
|
||||
) -> Result<()> {
|
||||
let tmpdir = tempfile::Builder::new()
|
||||
.prefix("KernelSU")
|
||||
.tempdir()
|
||||
.context("create temp dir failed")?;
|
||||
let workdir = tmpdir.path();
|
||||
let magiskboot = find_magiskboot(magiskboot_path, workdir)?;
|
||||
|
||||
let kmi = get_current_kmi().unwrap_or_else(|_| String::from(""));
|
||||
|
||||
let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?;
|
||||
|
||||
println!("- Unpacking boot image");
|
||||
let status = Command::new(&magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("unpack")
|
||||
.arg(bootimage.display().to_string())
|
||||
.status()?;
|
||||
ensure!(status.success(), "magiskboot unpack failed");
|
||||
|
||||
let mut ramdisk = workdir.join("ramdisk.cpio");
|
||||
if !ramdisk.exists() {
|
||||
ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio")
|
||||
}
|
||||
if !ramdisk.exists() {
|
||||
ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
|
||||
}
|
||||
if !ramdisk.exists() {
|
||||
bail!("No compatible ramdisk found.")
|
||||
}
|
||||
let ramdisk = ramdisk.as_path();
|
||||
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;
|
||||
ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU");
|
||||
|
||||
let mut new_boot = None;
|
||||
let mut from_backup = false;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if do_cpio_cmd(
|
||||
&magiskboot,
|
||||
workdir,
|
||||
ramdisk,
|
||||
&format!("exists {BACKUP_FILENAME}"),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
do_cpio_cmd(
|
||||
&magiskboot,
|
||||
workdir,
|
||||
ramdisk,
|
||||
&format!("extract {BACKUP_FILENAME} {BACKUP_FILENAME}"),
|
||||
)?;
|
||||
let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?;
|
||||
let sha = String::from_utf8(sha)?;
|
||||
let sha = sha.trim();
|
||||
let backup_path =
|
||||
PathBuf::from(KSU_BACKUP_DIR).join(format!("{KSU_BACKUP_FILE_PREFIX}{sha}"));
|
||||
if backup_path.is_file() {
|
||||
new_boot = Some(backup_path);
|
||||
from_backup = true;
|
||||
} else {
|
||||
println!("- Warning: no backup {backup_path:?} found!");
|
||||
}
|
||||
|
||||
if let Err(e) = clean_backup(sha) {
|
||||
println!("- Warning: Cleanup backup image failed: {e}");
|
||||
}
|
||||
} else {
|
||||
println!("- Backup info is absent!");
|
||||
}
|
||||
|
||||
if new_boot.is_none() {
|
||||
// remove kernelsu.ko
|
||||
do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm kernelsu.ko")?;
|
||||
|
||||
// if init.real exists, restore it
|
||||
let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init.real").is_ok();
|
||||
if status {
|
||||
do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init.real init")?;
|
||||
}
|
||||
|
||||
println!("- Repacking boot image");
|
||||
let status = Command::new(&magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("repack")
|
||||
.arg(&bootimage)
|
||||
.status()?;
|
||||
ensure!(status.success(), "magiskboot repack failed");
|
||||
new_boot = Some(workdir.join("new-boot.img"));
|
||||
}
|
||||
|
||||
let new_boot = new_boot.unwrap();
|
||||
|
||||
if image.is_some() {
|
||||
// if image is specified, write to output file
|
||||
let output_dir = std::env::current_dir()?;
|
||||
let now = chrono::Utc::now();
|
||||
let output_image = output_dir.join(format!(
|
||||
"kernelsu_restore_{}.img",
|
||||
now.format("%Y%m%d_%H%M%S")
|
||||
));
|
||||
|
||||
if from_backup || std::fs::rename(&new_boot, &output_image).is_err() {
|
||||
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
|
||||
}
|
||||
println!("- Output file is written to");
|
||||
println!("- {}", output_image.display().to_string().trim_matches('"'));
|
||||
}
|
||||
if flash {
|
||||
if from_backup {
|
||||
println!("- Flashing new boot image from {}", new_boot.display());
|
||||
} else {
|
||||
println!("- Flashing new boot image");
|
||||
}
|
||||
flash_boot(&bootdevice, new_boot)?;
|
||||
}
|
||||
println!("- Done!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn patch(
|
||||
image: Option<PathBuf>,
|
||||
kernel: Option<PathBuf>,
|
||||
kmod: Option<PathBuf>,
|
||||
init: Option<PathBuf>,
|
||||
ota: bool,
|
||||
flash: bool,
|
||||
out: Option<PathBuf>,
|
||||
magiskboot: Option<PathBuf>,
|
||||
kmi: Option<String>,
|
||||
partition: Option<String>,
|
||||
) -> Result<()> {
|
||||
let result = do_patch(
|
||||
image, kernel, kmod, init, ota, flash, out, magiskboot, kmi, partition,
|
||||
);
|
||||
if let Err(ref e) = result {
|
||||
println!("- Install Error: {e}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn do_patch(
|
||||
image: Option<PathBuf>,
|
||||
kernel: Option<PathBuf>,
|
||||
kmod: Option<PathBuf>,
|
||||
init: Option<PathBuf>,
|
||||
ota: bool,
|
||||
flash: bool,
|
||||
out: Option<PathBuf>,
|
||||
magiskboot_path: Option<PathBuf>,
|
||||
kmi: Option<String>,
|
||||
partition: Option<String>,
|
||||
) -> Result<()> {
|
||||
println!(include_str!("banner"));
|
||||
|
||||
let patch_file = image.is_some();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !patch_file {
|
||||
ensure_gki_kernel()?;
|
||||
}
|
||||
|
||||
let is_replace_kernel = kernel.is_some();
|
||||
|
||||
if is_replace_kernel {
|
||||
ensure!(
|
||||
init.is_none() && kmod.is_none(),
|
||||
"init and module must not be specified."
|
||||
);
|
||||
}
|
||||
|
||||
let tmpdir = tempfile::Builder::new()
|
||||
.prefix("KernelSU")
|
||||
.tempdir()
|
||||
.context("create temp dir failed")?;
|
||||
let workdir = tmpdir.path();
|
||||
|
||||
// extract magiskboot
|
||||
let magiskboot = find_magiskboot(magiskboot_path, workdir)?;
|
||||
|
||||
let kmi = if let Some(kmi) = kmi {
|
||||
kmi
|
||||
} else {
|
||||
match get_current_kmi() {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
println!("- {e}");
|
||||
if let Some(image_path) = &image {
|
||||
println!(
|
||||
"- Trying to auto detect KMI version for {}",
|
||||
image_path.to_str().unwrap()
|
||||
);
|
||||
parse_kmi_from_boot(&magiskboot, image_path, tmpdir.path())?
|
||||
} else if let Some(kernel_path) = &kernel {
|
||||
println!(
|
||||
"- Trying to auto detect KMI version for {}",
|
||||
kernel_path.to_str().unwrap()
|
||||
);
|
||||
parse_kmi_from_kernel(kernel_path, tmpdir.path())?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (bootimage, bootdevice) =
|
||||
find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?;
|
||||
|
||||
let bootimage = bootimage.as_path();
|
||||
|
||||
// try extract magiskboot/bootctl
|
||||
let _ = assets::ensure_binaries(false);
|
||||
|
||||
if let Some(kernel) = kernel {
|
||||
std::fs::copy(kernel, workdir.join("kernel")).context("copy kernel from failed")?;
|
||||
}
|
||||
|
||||
println!("- Preparing assets");
|
||||
|
||||
let kmod_file = workdir.join("kernelsu.ko");
|
||||
if let Some(kmod) = kmod {
|
||||
std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?;
|
||||
} else {
|
||||
// If kmod is not specified, extract from assets
|
||||
println!("- KMI: {kmi}");
|
||||
let name = format!("{kmi}_kernelsu.ko");
|
||||
assets::copy_assets_to_file(&name, kmod_file)
|
||||
.with_context(|| format!("Failed to copy {name}"))?;
|
||||
};
|
||||
|
||||
let init_file = workdir.join("init");
|
||||
if let Some(init) = init {
|
||||
std::fs::copy(init, init_file).context("copy init failed")?;
|
||||
} else {
|
||||
assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?;
|
||||
}
|
||||
|
||||
println!("- Unpacking boot image");
|
||||
let status = Command::new(&magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("unpack")
|
||||
.arg(bootimage)
|
||||
.status()?;
|
||||
ensure!(status.success(), "magiskboot unpack failed");
|
||||
|
||||
let mut ramdisk = workdir.join("ramdisk.cpio");
|
||||
if !ramdisk.exists() {
|
||||
ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio")
|
||||
}
|
||||
if !ramdisk.exists() {
|
||||
ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
|
||||
}
|
||||
if !ramdisk.exists() {
|
||||
println!("- No ramdisk, create by default");
|
||||
ramdisk = "ramdisk.cpio".into();
|
||||
}
|
||||
let ramdisk = ramdisk.as_path();
|
||||
let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?;
|
||||
ensure!(!is_magisk_patched, "Cannot work with Magisk patched image");
|
||||
|
||||
println!("- Adding KernelSU LKM");
|
||||
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;
|
||||
|
||||
let mut need_backup = false;
|
||||
if !is_kernelsu_patched {
|
||||
// kernelsu.ko is not exist, backup init if necessary
|
||||
let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init");
|
||||
if status.is_ok() {
|
||||
do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init init.real")?;
|
||||
}
|
||||
need_backup = flash;
|
||||
}
|
||||
|
||||
do_cpio_cmd(&magiskboot, workdir, ramdisk, "add 0755 init init")?;
|
||||
do_cpio_cmd(
|
||||
&magiskboot,
|
||||
workdir,
|
||||
ramdisk,
|
||||
"add 0755 kernelsu.ko kernelsu.ko",
|
||||
)?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if need_backup && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage) {
|
||||
println!("- Backup stock image failed: {e}");
|
||||
}
|
||||
|
||||
println!("- Repacking boot image");
|
||||
// magiskboot repack boot.img
|
||||
let status = Command::new(&magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("repack")
|
||||
.arg(bootimage)
|
||||
.status()?;
|
||||
ensure!(status.success(), "magiskboot repack failed");
|
||||
let new_boot = workdir.join("new-boot.img");
|
||||
|
||||
if patch_file {
|
||||
// if image is specified, write to output file
|
||||
let output_dir = out.unwrap_or(std::env::current_dir()?);
|
||||
let now = chrono::Utc::now();
|
||||
let output_image = output_dir.join(format!(
|
||||
"kernelsu_patched_{}.img",
|
||||
now.format("%Y%m%d_%H%M%S")
|
||||
));
|
||||
|
||||
if std::fs::rename(&new_boot, &output_image).is_err() {
|
||||
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
|
||||
}
|
||||
println!("- Output file is written to");
|
||||
println!("- {}", output_image.display().to_string().trim_matches('"'));
|
||||
}
|
||||
|
||||
if flash {
|
||||
println!("- Flashing new boot image");
|
||||
flash_boot(&bootdevice, new_boot)?;
|
||||
|
||||
if ota {
|
||||
post_ota()?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("- Done!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn calculate_sha1(file_path: impl AsRef<Path>) -> Result<String> {
|
||||
use sha1::Digest;
|
||||
use std::io::Read;
|
||||
let mut file = std::fs::File::open(file_path.as_ref())?;
|
||||
let mut hasher = sha1::Sha1::new();
|
||||
let mut buffer = [0; 1024];
|
||||
|
||||
loop {
|
||||
let n = file.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
|
||||
let result = hasher.finalize();
|
||||
Ok(format!("{result:x}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn do_backup(magiskboot: &Path, workdir: &Path, cpio_path: &Path, image: &Path) -> Result<()> {
|
||||
let sha1 = calculate_sha1(image)?;
|
||||
let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");
|
||||
|
||||
println!("- Backup stock boot image");
|
||||
// magiskboot cpio ramdisk.cpio 'add 0755 $BACKUP_FILENAME'
|
||||
let target = format!("{KSU_BACKUP_DIR}{filename}");
|
||||
std::fs::copy(image, &target).with_context(|| format!("backup to {target}"))?;
|
||||
std::fs::write(workdir.join(BACKUP_FILENAME), sha1.as_bytes()).context("write sha1")?;
|
||||
do_cpio_cmd(
|
||||
magiskboot,
|
||||
workdir,
|
||||
cpio_path,
|
||||
&format!("add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}"),
|
||||
)?;
|
||||
println!("- Stock image has been backup to");
|
||||
println!("- {target}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn clean_backup(sha1: &str) -> Result<()> {
|
||||
println!("- Clean up backup");
|
||||
let backup_name = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");
|
||||
let dir = std::fs::read_dir(defs::KSU_BACKUP_DIR)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = path.file_name() {
|
||||
let name = name.to_string_lossy().to_string();
|
||||
if name != backup_name
|
||||
&& name.starts_with(KSU_BACKUP_FILE_PREFIX)
|
||||
&& std::fs::remove_file(path).is_ok()
|
||||
{
|
||||
println!("- removed {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flash_boot(bootdevice: &Option<String>, new_boot: PathBuf) -> Result<()> {
|
||||
let Some(bootdevice) = bootdevice else {
|
||||
bail!("boot device not found")
|
||||
};
|
||||
let status = Command::new("blockdev")
|
||||
.arg("--setrw")
|
||||
.arg(bootdevice)
|
||||
.status()?;
|
||||
ensure!(status.success(), "set boot device rw failed");
|
||||
dd(new_boot, bootdevice).context("flash boot failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_magiskboot(magiskboot_path: Option<PathBuf>, workdir: &Path) -> Result<PathBuf> {
|
||||
let magiskboot = {
|
||||
if which("magiskboot").is_ok() {
|
||||
let _ = assets::ensure_binaries(true);
|
||||
"magiskboot".into()
|
||||
} else {
|
||||
// magiskboot is not in $PATH, use builtin or specified one
|
||||
let magiskboot = if let Some(magiskboot_path) = magiskboot_path {
|
||||
std::fs::canonicalize(magiskboot_path)?
|
||||
} else {
|
||||
let magiskboot_path = workdir.join("magiskboot");
|
||||
assets::copy_assets_to_file("magiskboot", &magiskboot_path)
|
||||
.context("copy magiskboot failed")?;
|
||||
magiskboot_path
|
||||
};
|
||||
ensure!(magiskboot.exists(), "{magiskboot:?} is not exist");
|
||||
#[cfg(unix)]
|
||||
let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755));
|
||||
magiskboot
|
||||
}
|
||||
};
|
||||
Ok(magiskboot)
|
||||
}
|
||||
|
||||
fn find_boot_image(
|
||||
image: &Option<PathBuf>,
|
||||
kmi: &str,
|
||||
ota: bool,
|
||||
is_replace_kernel: bool,
|
||||
workdir: &Path,
|
||||
partition: &Option<String>,
|
||||
) -> Result<(PathBuf, Option<String>)> {
|
||||
let bootimage;
|
||||
let mut bootdevice = None;
|
||||
if let Some(ref image) = *image {
|
||||
ensure!(image.exists(), "boot image not found");
|
||||
bootimage = std::fs::canonicalize(image)?;
|
||||
} else {
|
||||
if cfg!(not(target_os = "android")) {
|
||||
println!("- Current OS is not android, refusing auto bootimage/bootdevice detection");
|
||||
bail!("Please specify a boot image");
|
||||
}
|
||||
|
||||
let slot_suffix = get_slot_suffix(ota);
|
||||
let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition);
|
||||
let boot_partition = format!("/dev/block/by-name/{boot_partition_name}{slot_suffix}");
|
||||
|
||||
println!("- Bootdevice: {boot_partition}");
|
||||
let tmp_boot_path = workdir.join("boot.img");
|
||||
|
||||
dd(&boot_partition, &tmp_boot_path)?;
|
||||
|
||||
ensure!(tmp_boot_path.exists(), "boot image not found");
|
||||
|
||||
bootimage = tmp_boot_path;
|
||||
bootdevice = Some(boot_partition);
|
||||
};
|
||||
Ok((bootimage, bootdevice))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn choose_boot_partition(
|
||||
kmi: &str,
|
||||
is_replace_kernel: bool,
|
||||
partition: &Option<String>,
|
||||
) -> String {
|
||||
let slot_suffix = get_slot_suffix(false);
|
||||
let skip_init_boot = kmi.starts_with("android12-");
|
||||
let init_boot_exist = Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists();
|
||||
|
||||
// if specific partition is specified, use it
|
||||
if let Some(part) = partition {
|
||||
return match part.as_str() {
|
||||
"boot" | "init_boot" | "vendor_boot" => part.clone(),
|
||||
_ => "boot".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// if init_boot exists and not skipping it, use it
|
||||
if !is_replace_kernel && init_boot_exist && !skip_init_boot {
|
||||
return "init_boot".to_string();
|
||||
}
|
||||
|
||||
"boot".to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn choose_boot_partition(
|
||||
_kmi: &str,
|
||||
_is_replace_kernel: bool,
|
||||
_partition: &Option<String>,
|
||||
) -> String {
|
||||
"boot".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_slot_suffix(ota: bool) -> String {
|
||||
let mut slot_suffix = utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from(""));
|
||||
if !slot_suffix.is_empty() && ota {
|
||||
if slot_suffix == "_a" {
|
||||
slot_suffix = "_b".to_string()
|
||||
} else {
|
||||
slot_suffix = "_a".to_string()
|
||||
}
|
||||
}
|
||||
slot_suffix
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn get_slot_suffix(_ota: bool) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn list_available_partitions() -> Vec<String> {
|
||||
let slot_suffix = get_slot_suffix(false);
|
||||
let candidates = vec!["boot", "init_boot", "vendor_boot"];
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|name| Path::new(&format!("/dev/block/by-name/{}{}", name, slot_suffix)).exists())
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn list_available_partitions() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn post_ota() -> Result<()> {
|
||||
use crate::defs::ADB_DIR;
|
||||
use assets::BOOTCTL_PATH;
|
||||
let status = Command::new(BOOTCTL_PATH).arg("hal-info").status()?;
|
||||
if !status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_slot = Command::new(BOOTCTL_PATH)
|
||||
.arg("get-current-slot")
|
||||
.output()?
|
||||
.stdout;
|
||||
let current_slot = String::from_utf8(current_slot)?;
|
||||
let current_slot = current_slot.trim();
|
||||
let target_slot = if current_slot == "0" { 1 } else { 0 };
|
||||
|
||||
Command::new(BOOTCTL_PATH)
|
||||
.arg(format!("set-active-boot-slot {target_slot}"))
|
||||
.status()?;
|
||||
|
||||
let post_fs_data = std::path::Path::new(ADB_DIR).join("post-fs-data.d");
|
||||
utils::ensure_dir_exists(&post_fs_data)?;
|
||||
let post_ota_sh = post_fs_data.join("post_ota.sh");
|
||||
|
||||
let sh_content = format!(
|
||||
r###"
|
||||
{BOOTCTL_PATH} mark-boot-successful
|
||||
rm -f {BOOTCTL_PATH}
|
||||
rm -f /data/adb/post-fs-data.d/post_ota.sh
|
||||
"###
|
||||
);
|
||||
|
||||
std::fs::write(&post_ota_sh, sh_content)?;
|
||||
#[cfg(unix)]
|
||||
std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
771
userspace/ksud/src/cli.rs
Normal file
771
userspace/ksud/src/cli.rs
Normal file
|
|
@ -0,0 +1,771 @@
|
|||
use anyhow::{Ok, Result};
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils};
|
||||
|
||||
/// KernelSU userspace cli
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = defs::VERSION_NAME, about, long_about = None)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Manage KernelSU modules
|
||||
Module {
|
||||
#[command(subcommand)]
|
||||
command: Module,
|
||||
},
|
||||
|
||||
/// Trigger `post-fs-data` event
|
||||
PostFsData,
|
||||
|
||||
/// Trigger `service` event
|
||||
Services,
|
||||
|
||||
/// Trigger `boot-complete` event
|
||||
BootCompleted,
|
||||
|
||||
/// Install KernelSU userspace component to system
|
||||
Install {
|
||||
#[arg(long, default_value = None)]
|
||||
magiskboot: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Uninstall KernelSU modules and itself(LKM Only)
|
||||
Uninstall {
|
||||
/// magiskboot path, if not specified, will search from $PATH
|
||||
#[arg(long, default_value = None)]
|
||||
magiskboot: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// SELinux policy Patch tool
|
||||
Sepolicy {
|
||||
#[command(subcommand)]
|
||||
command: Sepolicy,
|
||||
},
|
||||
|
||||
/// Manage App Profiles
|
||||
Profile {
|
||||
#[command(subcommand)]
|
||||
command: Profile,
|
||||
},
|
||||
|
||||
/// Manage kernel features
|
||||
Feature {
|
||||
#[command(subcommand)]
|
||||
command: Feature,
|
||||
},
|
||||
|
||||
/// Patch boot or init_boot images to apply KernelSU
|
||||
BootPatch {
|
||||
/// boot image path, if not specified, will try to find the boot image automatically
|
||||
#[arg(short, long)]
|
||||
boot: Option<PathBuf>,
|
||||
|
||||
/// kernel image path to replace
|
||||
#[arg(short, long)]
|
||||
kernel: Option<PathBuf>,
|
||||
|
||||
/// LKM module path to replace, if not specified, will use the builtin one
|
||||
#[arg(short, long)]
|
||||
module: Option<PathBuf>,
|
||||
|
||||
/// init to be replaced
|
||||
#[arg(short, long, requires("module"))]
|
||||
init: Option<PathBuf>,
|
||||
|
||||
/// will use another slot when boot image is not specified
|
||||
#[arg(short = 'u', long, default_value = "false")]
|
||||
ota: bool,
|
||||
|
||||
/// Flash it to boot partition after patch
|
||||
#[arg(short, long, default_value = "false")]
|
||||
flash: bool,
|
||||
|
||||
/// output path, if not specified, will use current directory
|
||||
#[arg(short, long, default_value = None)]
|
||||
out: Option<PathBuf>,
|
||||
|
||||
/// magiskboot path, if not specified, will search from $PATH
|
||||
#[arg(long, default_value = None)]
|
||||
magiskboot: Option<PathBuf>,
|
||||
|
||||
/// KMI version, if specified, will use the specified KMI
|
||||
#[arg(long, default_value = None)]
|
||||
kmi: Option<String>,
|
||||
|
||||
/// target partition override (init_boot | boot | vendor_boot)
|
||||
#[arg(long, default_value = None)]
|
||||
partition: Option<String>,
|
||||
},
|
||||
|
||||
/// Restore boot or init_boot images patched by KernelSU
|
||||
BootRestore {
|
||||
/// boot image path, if not specified, will try to find the boot image automatically
|
||||
#[arg(short, long)]
|
||||
boot: Option<PathBuf>,
|
||||
|
||||
/// Flash it to boot partition after patch
|
||||
#[arg(short, long, default_value = "false")]
|
||||
flash: bool,
|
||||
|
||||
/// magiskboot path, if not specified, will search from $PATH
|
||||
#[arg(long, default_value = None)]
|
||||
magiskboot: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Show boot information
|
||||
BootInfo {
|
||||
#[command(subcommand)]
|
||||
command: BootInfo,
|
||||
},
|
||||
|
||||
/// KPM module manager
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
Kpm {
|
||||
#[command(subcommand)]
|
||||
command: kpm_cmd::Kpm,
|
||||
},
|
||||
|
||||
/// Manage kernel umount paths
|
||||
Umount {
|
||||
#[command(subcommand)]
|
||||
command: Umount,
|
||||
},
|
||||
|
||||
/// For developers
|
||||
Debug {
|
||||
#[command(subcommand)]
|
||||
command: Debug,
|
||||
},
|
||||
/// Kernel interface
|
||||
Kernel {
|
||||
#[command(subcommand)]
|
||||
command: Kernel,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum BootInfo {
|
||||
/// show current kmi version
|
||||
CurrentKmi,
|
||||
|
||||
/// show supported kmi versions
|
||||
SupportedKmis,
|
||||
|
||||
/// check if device is A/B capable
|
||||
IsAbDevice,
|
||||
|
||||
/// show auto-selected boot partition name
|
||||
DefaultPartition,
|
||||
|
||||
/// list available partitions for current or OTA toggled slot
|
||||
AvailablePartitions,
|
||||
|
||||
/// show slot suffix for current or OTA toggled slot
|
||||
SlotSuffix {
|
||||
/// toggle to another slot
|
||||
#[arg(short = 'u', long, default_value = "false")]
|
||||
ota: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Debug {
|
||||
/// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled.
|
||||
SetManager {
|
||||
/// manager package name
|
||||
#[arg(default_value_t = String::from("com.sukisu.ultra"))]
|
||||
apk: String,
|
||||
},
|
||||
|
||||
/// Get apk size and hash
|
||||
GetSign {
|
||||
/// apk path
|
||||
apk: String,
|
||||
},
|
||||
|
||||
/// Root Shell
|
||||
Su {
|
||||
/// switch to gloabl mount namespace
|
||||
#[arg(short, long, default_value = "false")]
|
||||
global_mnt: bool,
|
||||
},
|
||||
|
||||
/// Get kernel version
|
||||
Version,
|
||||
|
||||
/// For testing
|
||||
Test,
|
||||
|
||||
/// Process mark management
|
||||
Mark {
|
||||
#[command(subcommand)]
|
||||
command: MarkCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum MarkCommand {
|
||||
/// Get mark status for a process (or all)
|
||||
Get {
|
||||
/// target pid (0 for total count)
|
||||
#[arg(default_value = "0")]
|
||||
pid: i32,
|
||||
},
|
||||
|
||||
/// Mark a process
|
||||
Mark {
|
||||
/// target pid (0 for all processes)
|
||||
#[arg(default_value = "0")]
|
||||
pid: i32,
|
||||
},
|
||||
|
||||
/// Unmark a process
|
||||
Unmark {
|
||||
/// target pid (0 for all processes)
|
||||
#[arg(default_value = "0")]
|
||||
pid: i32,
|
||||
},
|
||||
|
||||
/// Refresh mark for all running processes
|
||||
Refresh,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Sepolicy {
|
||||
/// Patch sepolicy
|
||||
Patch {
|
||||
/// sepolicy statements
|
||||
sepolicy: String,
|
||||
},
|
||||
|
||||
/// Apply sepolicy from file
|
||||
Apply {
|
||||
/// sepolicy file path
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// Check if sepolicy statement is supported/valid
|
||||
Check {
|
||||
/// sepolicy statements
|
||||
sepolicy: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Module {
|
||||
/// Install module <ZIP>
|
||||
Install {
|
||||
/// module zip file path
|
||||
zip: String,
|
||||
},
|
||||
|
||||
/// Undo module uninstall mark <id>
|
||||
UndoUninstall {
|
||||
/// module id
|
||||
id: 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,
|
||||
|
||||
/// manage module configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
command: ModuleConfigCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum ModuleConfigCmd {
|
||||
/// Get a config value
|
||||
Get {
|
||||
/// config key
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Set a config value
|
||||
Set {
|
||||
/// config key
|
||||
key: String,
|
||||
/// config value
|
||||
value: String,
|
||||
/// use temporary config (cleared on reboot)
|
||||
#[arg(short, long)]
|
||||
temp: bool,
|
||||
},
|
||||
|
||||
/// List all config entries
|
||||
List,
|
||||
|
||||
/// Delete a config entry
|
||||
Delete {
|
||||
/// config key
|
||||
key: String,
|
||||
/// delete from temporary config
|
||||
#[arg(short, long)]
|
||||
temp: bool,
|
||||
},
|
||||
|
||||
/// Clear all config entries
|
||||
Clear {
|
||||
/// clear temporary config
|
||||
#[arg(short, long)]
|
||||
temp: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Profile {
|
||||
/// get root profile's selinux policy of <package-name>
|
||||
GetSepolicy {
|
||||
/// package name
|
||||
package: String,
|
||||
},
|
||||
|
||||
/// set root profile's selinux policy of <package-name> to <profile>
|
||||
SetSepolicy {
|
||||
/// package name
|
||||
package: String,
|
||||
/// policy statements
|
||||
policy: String,
|
||||
},
|
||||
|
||||
/// get template of <id>
|
||||
GetTemplate {
|
||||
/// template id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// set template of <id> to <template string>
|
||||
SetTemplate {
|
||||
/// template id
|
||||
id: String,
|
||||
/// template string
|
||||
template: String,
|
||||
},
|
||||
|
||||
/// delete template of <id>
|
||||
DeleteTemplate {
|
||||
/// template id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// list all templates
|
||||
ListTemplates,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Feature {
|
||||
/// Get feature value and support status
|
||||
Get {
|
||||
/// Feature ID or name (su_compat, kernel_umount)
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Set feature value
|
||||
Set {
|
||||
/// Feature ID or name
|
||||
id: String,
|
||||
/// Feature value (0=disable, 1=enable)
|
||||
value: u64,
|
||||
},
|
||||
|
||||
/// List all available features
|
||||
List,
|
||||
|
||||
/// Check feature status (supported/unsupported/managed)
|
||||
Check {
|
||||
/// Feature ID or name (su_compat, kernel_umount)
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Load configuration from file and apply to kernel
|
||||
Load,
|
||||
|
||||
/// Save current kernel feature states to file
|
||||
Save,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Kernel {
|
||||
/// Nuke ext4 sysfs
|
||||
NukeExt4Sysfs {
|
||||
/// mount point
|
||||
mnt: String,
|
||||
},
|
||||
/// Manage umount list
|
||||
Umount {
|
||||
#[command(subcommand)]
|
||||
command: UmountOp,
|
||||
},
|
||||
/// Notify that module is mounted
|
||||
NotifyModuleMounted,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum UmountOp {
|
||||
/// Add mount point to umount list
|
||||
Add {
|
||||
/// mount point path
|
||||
mnt: String,
|
||||
/// umount flags (default: 0, MNT_DETACH: 2)
|
||||
#[arg(short, long, default_value = "0")]
|
||||
flags: u32,
|
||||
},
|
||||
/// Delete mount point from umount list
|
||||
Del {
|
||||
/// mount point path
|
||||
mnt: String,
|
||||
},
|
||||
/// Wipe all entries from umount list
|
||||
Wipe,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod kpm_cmd {
|
||||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Kpm {
|
||||
/// Load a KPM module: load <path> [args]
|
||||
Load { path: PathBuf, args: Option<String> },
|
||||
/// Unload a KPM module: unload <name>
|
||||
Unload { name: String },
|
||||
/// Get number of loaded modules
|
||||
Num,
|
||||
/// List loaded KPM modules
|
||||
List,
|
||||
/// Get info of a KPM module: info <name>
|
||||
Info { name: String },
|
||||
/// Send control command to a KPM module: control <name> <args>
|
||||
Control { name: String, args: String },
|
||||
/// Print KPM Loader version
|
||||
Version,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Umount {
|
||||
/// Add custom umount path
|
||||
Add {
|
||||
/// Mount path to add
|
||||
path: String,
|
||||
|
||||
/// Check mount type (overlay)
|
||||
#[arg(long, default_value = "false")]
|
||||
|
||||
/// Umount flags (0 or 8 for MNT_DETACH)
|
||||
#[arg(long, default_value = "-1")]
|
||||
flags: i32,
|
||||
},
|
||||
|
||||
/// Remove custom umount path
|
||||
Remove {
|
||||
/// Mount path to remove
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// List all umount paths
|
||||
List,
|
||||
|
||||
/// Clear all recorded umount paths
|
||||
ClearCustom,
|
||||
|
||||
/// Save configuration to file
|
||||
Save,
|
||||
|
||||
/// Load and apply configuration from file
|
||||
Load,
|
||||
|
||||
/// Apply current configuration to kernel
|
||||
Apply,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
#[cfg(target_os = "android")]
|
||||
android_logger::init_once(
|
||||
Config::default()
|
||||
.with_max_level(LevelFilter::Trace) // limit log level
|
||||
.with_tag("KernelSU"), // logs will show under mytag tag
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
env_logger::init();
|
||||
|
||||
// the kernel executes su with argv[0] = "su" and replace it with us
|
||||
let arg0 = std::env::args().next().unwrap_or_default();
|
||||
if arg0 == "su" || arg0 == "/system/bin/su" {
|
||||
return crate::su::root_shell();
|
||||
}
|
||||
|
||||
let cli = Args::parse();
|
||||
|
||||
log::info!("command: {:?}", cli.command);
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::PostFsData => init_event::on_post_data_fs(),
|
||||
Commands::BootCompleted => init_event::on_boot_completed(),
|
||||
|
||||
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::UndoUninstall { id } => module::undo_uninstall_module(&id),
|
||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Enable { id } => module::enable_module(&id),
|
||||
Module::Disable { id } => module::disable_module(&id),
|
||||
Module::Action { id } => module::run_action(&id),
|
||||
Module::List => module::list_modules(),
|
||||
Module::Config { command } => {
|
||||
// Get module ID from environment variable
|
||||
let module_id = std::env::var("KSU_MODULE").map_err(|_| {
|
||||
anyhow::anyhow!("This command must be run in the context of a module")
|
||||
})?;
|
||||
|
||||
use crate::module_config;
|
||||
match command {
|
||||
ModuleConfigCmd::Get { key } => {
|
||||
// Use merge_configs to respect priority (temp overrides persist)
|
||||
let config = module_config::merge_configs(&module_id)?;
|
||||
match config.get(&key) {
|
||||
Some(value) => {
|
||||
println!("{}", value);
|
||||
Ok(())
|
||||
}
|
||||
None => anyhow::bail!("Key '{}' not found", key),
|
||||
}
|
||||
}
|
||||
ModuleConfigCmd::Set { key, value, temp } => {
|
||||
// Validate input at CLI layer for better user experience
|
||||
module_config::validate_config_key(&key)?;
|
||||
module_config::validate_config_value(&value)?;
|
||||
|
||||
let config_type = if temp {
|
||||
module_config::ConfigType::Temp
|
||||
} else {
|
||||
module_config::ConfigType::Persist
|
||||
};
|
||||
module_config::set_config_value(&module_id, &key, &value, config_type)
|
||||
}
|
||||
ModuleConfigCmd::List => {
|
||||
let config = module_config::merge_configs(&module_id)?;
|
||||
if config.is_empty() {
|
||||
println!("No config entries found");
|
||||
} else {
|
||||
for (key, value) in config {
|
||||
println!("{}={}", key, value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ModuleConfigCmd::Delete { key, temp } => {
|
||||
let config_type = if temp {
|
||||
module_config::ConfigType::Temp
|
||||
} else {
|
||||
module_config::ConfigType::Persist
|
||||
};
|
||||
module_config::delete_config_value(&module_id, &key, config_type)
|
||||
}
|
||||
ModuleConfigCmd::Clear { temp } => {
|
||||
let config_type = if temp {
|
||||
module_config::ConfigType::Temp
|
||||
} else {
|
||||
module_config::ConfigType::Persist
|
||||
};
|
||||
module_config::clear_config(&module_id, config_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Install { magiskboot } => utils::install(magiskboot),
|
||||
Commands::Uninstall { magiskboot } => utils::uninstall(magiskboot),
|
||||
Commands::Sepolicy { command } => match command {
|
||||
Sepolicy::Patch { sepolicy } => crate::sepolicy::live_patch(&sepolicy),
|
||||
Sepolicy::Apply { file } => crate::sepolicy::apply_file(file),
|
||||
Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy),
|
||||
},
|
||||
Commands::Services => init_event::on_services(),
|
||||
Commands::Profile { command } => match command {
|
||||
Profile::GetSepolicy { package } => crate::profile::get_sepolicy(package),
|
||||
Profile::SetSepolicy { package, policy } => {
|
||||
crate::profile::set_sepolicy(package, policy)
|
||||
}
|
||||
Profile::GetTemplate { id } => crate::profile::get_template(id),
|
||||
Profile::SetTemplate { id, template } => crate::profile::set_template(id, template),
|
||||
Profile::DeleteTemplate { id } => crate::profile::delete_template(id),
|
||||
Profile::ListTemplates => crate::profile::list_templates(),
|
||||
},
|
||||
|
||||
Commands::Feature { command } => match command {
|
||||
Feature::Get { id } => crate::feature::get_feature(id),
|
||||
Feature::Set { id, value } => crate::feature::set_feature(id, value),
|
||||
Feature::List => crate::feature::list_features(),
|
||||
Feature::Check { id } => crate::feature::check_feature(id),
|
||||
Feature::Load => crate::feature::load_config_and_apply(),
|
||||
Feature::Save => crate::feature::save_config(),
|
||||
},
|
||||
|
||||
Commands::Debug { command } => match command {
|
||||
Debug::SetManager { apk } => debug::set_manager(&apk),
|
||||
Debug::GetSign { apk } => {
|
||||
let sign = apk_sign::get_apk_signature(&apk)?;
|
||||
println!("size: {:#x}, hash: {}", sign.0, sign.1);
|
||||
Ok(())
|
||||
}
|
||||
Debug::Version => {
|
||||
println!("Kernel Version: {}", ksucalls::get_version());
|
||||
Ok(())
|
||||
}
|
||||
Debug::Su { global_mnt } => crate::su::grant_root(global_mnt),
|
||||
Debug::Test => assets::ensure_binaries(false),
|
||||
Debug::Mark { command } => match command {
|
||||
MarkCommand::Get { pid } => debug::mark_get(pid),
|
||||
MarkCommand::Mark { pid } => debug::mark_set(pid),
|
||||
MarkCommand::Unmark { pid } => debug::mark_unset(pid),
|
||||
MarkCommand::Refresh => debug::mark_refresh(),
|
||||
},
|
||||
},
|
||||
|
||||
Commands::BootPatch {
|
||||
boot,
|
||||
init,
|
||||
kernel,
|
||||
module,
|
||||
ota,
|
||||
flash,
|
||||
out,
|
||||
magiskboot,
|
||||
kmi,
|
||||
partition,
|
||||
} => crate::boot_patch::patch(
|
||||
boot, kernel, module, init, ota, flash, out, magiskboot, kmi, partition,
|
||||
),
|
||||
|
||||
Commands::BootInfo { command } => match command {
|
||||
BootInfo::CurrentKmi => {
|
||||
let kmi = crate::boot_patch::get_current_kmi()?;
|
||||
println!("{kmi}");
|
||||
// return here to avoid printing the error message
|
||||
return Ok(());
|
||||
}
|
||||
BootInfo::SupportedKmis => {
|
||||
let kmi = crate::assets::list_supported_kmi()?;
|
||||
kmi.iter().for_each(|kmi| println!("{kmi}"));
|
||||
return Ok(());
|
||||
}
|
||||
BootInfo::IsAbDevice => {
|
||||
let val = crate::utils::getprop("ro.build.ab_update")
|
||||
.unwrap_or_else(|| String::from("false"));
|
||||
let is_ab = val.trim().to_lowercase() == "true";
|
||||
println!("{}", if is_ab { "true" } else { "false" });
|
||||
return Ok(());
|
||||
}
|
||||
BootInfo::DefaultPartition => {
|
||||
let kmi = crate::boot_patch::get_current_kmi().unwrap_or_else(|_| String::from(""));
|
||||
let name = crate::boot_patch::choose_boot_partition(&kmi, false, &None);
|
||||
println!("{name}");
|
||||
return Ok(());
|
||||
}
|
||||
BootInfo::SlotSuffix { ota } => {
|
||||
let suffix = crate::boot_patch::get_slot_suffix(ota);
|
||||
println!("{suffix}");
|
||||
return Ok(());
|
||||
}
|
||||
BootInfo::AvailablePartitions => {
|
||||
let parts = crate::boot_patch::list_available_partitions();
|
||||
parts.iter().for_each(|p| println!("{p}"));
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Commands::BootRestore {
|
||||
boot,
|
||||
magiskboot,
|
||||
flash,
|
||||
} => crate::boot_patch::restore(boot, magiskboot, flash),
|
||||
Commands::Kernel { command } => match command {
|
||||
Kernel::NukeExt4Sysfs { mnt } => ksucalls::nuke_ext4_sysfs(&mnt),
|
||||
Kernel::Umount { command } => match command {
|
||||
UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags),
|
||||
UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt),
|
||||
UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into),
|
||||
},
|
||||
Kernel::NotifyModuleMounted => {
|
||||
ksucalls::report_module_mounted();
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
Commands::Kpm { command } => {
|
||||
use crate::cli::kpm_cmd::Kpm;
|
||||
match command {
|
||||
Kpm::Load { path, args } => {
|
||||
crate::kpm::kpm_load(path.to_str().unwrap(), args.as_deref())
|
||||
}
|
||||
Kpm::Unload { name } => crate::kpm::kpm_unload(&name),
|
||||
Kpm::Num => crate::kpm::kpm_num().map(|_| ()),
|
||||
Kpm::List => crate::kpm::kpm_list(),
|
||||
Kpm::Info { name } => crate::kpm::kpm_info(&name),
|
||||
Kpm::Control { name, args } => {
|
||||
let ret = crate::kpm::kpm_control(&name, &args)?;
|
||||
println!("{}", ret);
|
||||
Ok(())
|
||||
}
|
||||
Kpm::Version => crate::kpm::kpm_version_loader(),
|
||||
}
|
||||
}
|
||||
Commands::Umount { command } => match command {
|
||||
Umount::Add { path, flags } => crate::umount_manager::add_umount_path(&path, flags),
|
||||
Umount::Remove { path } => crate::umount_manager::remove_umount_path(&path),
|
||||
Umount::List => crate::umount_manager::list_umount_paths(),
|
||||
Umount::ClearCustom => crate::umount_manager::clear_custom_paths(),
|
||||
Umount::Save => crate::umount_manager::save_umount_config(),
|
||||
Umount::Load => crate::umount_manager::load_and_apply_config(),
|
||||
Umount::Apply => crate::umount_manager::apply_config_to_kernel(),
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
log::error!("Error: {e:?}");
|
||||
}
|
||||
result
|
||||
}
|
||||
98
userspace/ksud/src/debug.rs
Normal file
98
userspace/ksud/src/debug.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use anyhow::{Context, Ok, Result, bail, ensure};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::ksucalls;
|
||||
|
||||
const KERNEL_PARAM_PATH: &str = "/sys/module/kernelsu";
|
||||
|
||||
fn read_u32(path: &PathBuf) -> Result<u32> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let content = content.trim();
|
||||
let content = content.parse::<u32>()?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn set_kernel_param(uid: u32) -> Result<()> {
|
||||
let kernel_param_path = Path::new(KERNEL_PARAM_PATH).join("parameters");
|
||||
|
||||
let ksu_debug_manager_uid = kernel_param_path.join("ksu_debug_manager_uid");
|
||||
let before_uid = read_u32(&ksu_debug_manager_uid)?;
|
||||
std::fs::write(&ksu_debug_manager_uid, uid.to_string())?;
|
||||
let after_uid = read_u32(&ksu_debug_manager_uid)?;
|
||||
|
||||
println!("set manager uid: {before_uid} -> {after_uid}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn get_pkg_uid(pkg: &str) -> Result<u32> {
|
||||
// stat /data/data/<pkg>
|
||||
let uid = rustix::fs::stat(format!("/data/data/{pkg}"))
|
||||
.with_context(|| format!("stat /data/data/{pkg}"))?
|
||||
.st_uid;
|
||||
Ok(uid)
|
||||
}
|
||||
|
||||
pub fn set_manager(pkg: &str) -> Result<()> {
|
||||
ensure!(
|
||||
Path::new(KERNEL_PARAM_PATH).exists(),
|
||||
"CONFIG_KSU_DEBUG is not enabled"
|
||||
);
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
let uid = get_pkg_uid(pkg)?;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let uid = 0;
|
||||
set_kernel_param(uid)?;
|
||||
// force-stop it
|
||||
let _ = Command::new("am").args(["force-stop", pkg]).status();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get mark status for a process
|
||||
pub fn mark_get(pid: i32) -> Result<()> {
|
||||
let result = ksucalls::mark_get(pid)?;
|
||||
if pid == 0 {
|
||||
bail!("Please specify a pid to get its mark status");
|
||||
} else {
|
||||
println!(
|
||||
"Process {} mark status: {}",
|
||||
pid,
|
||||
if result != 0 { "marked" } else { "unmarked" }
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a process
|
||||
pub fn mark_set(pid: i32) -> Result<()> {
|
||||
ksucalls::mark_set(pid)?;
|
||||
if pid == 0 {
|
||||
println!("All processes marked successfully");
|
||||
} else {
|
||||
println!("Process {} marked successfully", pid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmark a process
|
||||
pub fn mark_unset(pid: i32) -> Result<()> {
|
||||
ksucalls::mark_unset(pid)?;
|
||||
if pid == 0 {
|
||||
println!("All processes unmarked successfully");
|
||||
} else {
|
||||
println!("Process {} unmarked successfully", pid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refresh mark for all running processes
|
||||
pub fn mark_refresh() -> Result<()> {
|
||||
ksucalls::mark_refresh()?;
|
||||
println!("Refreshed mark for all running processes");
|
||||
Ok(())
|
||||
}
|
||||
44
userspace/ksud/src/defs.rs
Normal file
44
userspace/ksud/src/defs.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use const_format::concatcp;
|
||||
|
||||
pub const ADB_DIR: &str = "/data/adb/";
|
||||
pub const WORKING_DIR: &str = concatcp!(ADB_DIR, "ksu/");
|
||||
pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, "bin/");
|
||||
pub const LOG_DIR: &str = concatcp!(WORKING_DIR, "log/");
|
||||
|
||||
pub const PROFILE_DIR: &str = concatcp!(WORKING_DIR, "profile/");
|
||||
pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/");
|
||||
pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/");
|
||||
|
||||
pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc");
|
||||
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud");
|
||||
pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud");
|
||||
|
||||
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
|
||||
pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/");
|
||||
|
||||
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";
|
||||
|
||||
// Module config system
|
||||
pub const MODULE_CONFIG_DIR: &str = concatcp!(WORKING_DIR, "module_configs/");
|
||||
pub const PERSIST_CONFIG_NAME: &str = "persist.config";
|
||||
pub const TEMP_CONFIG_NAME: &str = "tmp.config";
|
||||
|
||||
// Metamodule support
|
||||
pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh";
|
||||
pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh";
|
||||
pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh";
|
||||
|
||||
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"));
|
||||
|
||||
pub const KSU_BACKUP_DIR: &str = WORKING_DIR;
|
||||
pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";
|
||||
pub const BACKUP_FILENAME: &str = "stock_image.sha1";
|
||||
454
userspace/ksud/src/feature.rs
Normal file
454
userspace/ksud/src/feature.rs
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use const_format::concatcp;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::defs;
|
||||
|
||||
const FEATURE_CONFIG_PATH: &str = concatcp!(defs::WORKING_DIR, ".feature_config");
|
||||
const FEATURE_MAGIC: u32 = 0x7f4b5355;
|
||||
const FEATURE_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum FeatureId {
|
||||
SuCompat = 0,
|
||||
KernelUmount = 1,
|
||||
EnhancedSecurity = 2,
|
||||
SuLog = 3,
|
||||
}
|
||||
|
||||
impl FeatureId {
|
||||
pub fn from_u32(id: u32) -> Option<Self> {
|
||||
match id {
|
||||
0 => Some(FeatureId::SuCompat),
|
||||
1 => Some(FeatureId::KernelUmount),
|
||||
2 => Some(FeatureId::EnhancedSecurity),
|
||||
3 => Some(FeatureId::SuLog),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
FeatureId::SuCompat => "su_compat",
|
||||
FeatureId::KernelUmount => "kernel_umount",
|
||||
FeatureId::EnhancedSecurity => "enhanced_security",
|
||||
FeatureId::SuLog => "sulog",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &'static str {
|
||||
match self {
|
||||
FeatureId::SuCompat => {
|
||||
"SU Compatibility Mode - allows authorized apps to gain root via traditional 'su' command"
|
||||
}
|
||||
FeatureId::KernelUmount => {
|
||||
"Kernel Umount - controls whether kernel automatically unmounts modules when not needed"
|
||||
}
|
||||
FeatureId::EnhancedSecurity => {
|
||||
"Enhanced Security - disable non‑KSU root elevation and unauthorized UID downgrades"
|
||||
}
|
||||
FeatureId::SuLog => {
|
||||
"SU Log - enables logging of SU command usage to kernel log for auditing purposes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_feature_id(name: &str) -> Result<FeatureId> {
|
||||
match name {
|
||||
"su_compat" | "0" => Ok(FeatureId::SuCompat),
|
||||
"kernel_umount" | "1" => Ok(FeatureId::KernelUmount),
|
||||
"enhanced_security" | "2" => Ok(FeatureId::EnhancedSecurity),
|
||||
"sulog" | "3" => Ok(FeatureId::SuLog),
|
||||
_ => bail!("Unknown feature: {}", name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_binary_config() -> Result<HashMap<u32, u64>> {
|
||||
let path = Path::new(FEATURE_CONFIG_PATH);
|
||||
if !path.exists() {
|
||||
log::info!("Feature config not found, using defaults");
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let mut file = File::open(path).with_context(|| "Failed to open feature config")?;
|
||||
|
||||
let mut magic_buf = [0u8; 4];
|
||||
file.read_exact(&mut magic_buf)
|
||||
.with_context(|| "Failed to read magic")?;
|
||||
let magic = u32::from_le_bytes(magic_buf);
|
||||
|
||||
if magic != FEATURE_MAGIC {
|
||||
bail!(
|
||||
"Invalid feature config magic: expected 0x{:08x}, got 0x{:08x}",
|
||||
FEATURE_MAGIC,
|
||||
magic
|
||||
);
|
||||
}
|
||||
|
||||
let mut version_buf = [0u8; 4];
|
||||
file.read_exact(&mut version_buf)
|
||||
.with_context(|| "Failed to read version")?;
|
||||
let version = u32::from_le_bytes(version_buf);
|
||||
|
||||
if version != FEATURE_VERSION {
|
||||
log::warn!(
|
||||
"Feature config version mismatch: expected {}, got {}",
|
||||
FEATURE_VERSION,
|
||||
version
|
||||
);
|
||||
}
|
||||
|
||||
let mut count_buf = [0u8; 4];
|
||||
file.read_exact(&mut count_buf)
|
||||
.with_context(|| "Failed to read count")?;
|
||||
let count = u32::from_le_bytes(count_buf);
|
||||
|
||||
let mut features = HashMap::new();
|
||||
for _ in 0..count {
|
||||
let mut id_buf = [0u8; 4];
|
||||
let mut value_buf = [0u8; 8];
|
||||
|
||||
file.read_exact(&mut id_buf)
|
||||
.with_context(|| "Failed to read feature id")?;
|
||||
file.read_exact(&mut value_buf)
|
||||
.with_context(|| "Failed to read feature value")?;
|
||||
|
||||
let id = u32::from_le_bytes(id_buf);
|
||||
let value = u64::from_le_bytes(value_buf);
|
||||
|
||||
features.insert(id, value);
|
||||
}
|
||||
|
||||
log::info!("Loaded {} features from config", features.len());
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
pub fn save_binary_config(features: &HashMap<u32, u64>) -> Result<()> {
|
||||
crate::utils::ensure_dir_exists(Path::new(defs::WORKING_DIR))?;
|
||||
|
||||
let path = Path::new(FEATURE_CONFIG_PATH);
|
||||
let mut file = File::create(path).with_context(|| "Failed to create feature config")?;
|
||||
|
||||
file.write_all(&FEATURE_MAGIC.to_le_bytes())
|
||||
.with_context(|| "Failed to write magic")?;
|
||||
|
||||
file.write_all(&FEATURE_VERSION.to_le_bytes())
|
||||
.with_context(|| "Failed to write version")?;
|
||||
|
||||
let count = features.len() as u32;
|
||||
file.write_all(&count.to_le_bytes())
|
||||
.with_context(|| "Failed to write count")?;
|
||||
|
||||
for (&id, &value) in features.iter() {
|
||||
file.write_all(&id.to_le_bytes())
|
||||
.with_context(|| format!("Failed to write feature id {}", id))?;
|
||||
file.write_all(&value.to_le_bytes())
|
||||
.with_context(|| format!("Failed to write feature value for id {}", id))?;
|
||||
}
|
||||
|
||||
file.sync_all()
|
||||
.with_context(|| "Failed to sync feature config")?;
|
||||
|
||||
log::info!("Saved {} features to config", features.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_config(features: &HashMap<u32, u64>) -> Result<()> {
|
||||
log::info!("Applying feature configuration to kernel...");
|
||||
|
||||
let mut applied = 0;
|
||||
for (&id, &value) in features.iter() {
|
||||
match crate::ksucalls::set_feature(id, value) {
|
||||
Ok(_) => {
|
||||
if let Some(feature_id) = FeatureId::from_u32(id) {
|
||||
log::info!("Set feature {} to {}", feature_id.name(), value);
|
||||
} else {
|
||||
log::info!("Set feature {} to {}", id, value);
|
||||
}
|
||||
applied += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to set feature {}: {}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Applied {} features successfully", applied);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_feature(id: String) -> Result<()> {
|
||||
let feature_id = parse_feature_id(&id)?;
|
||||
let (value, supported) = crate::ksucalls::get_feature(feature_id as u32)
|
||||
.with_context(|| format!("Failed to get feature {}", id))?;
|
||||
|
||||
if !supported {
|
||||
println!("Feature '{}' is not supported by kernel", id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Feature: {} ({})", feature_id.name(), feature_id as u32);
|
||||
println!("Description: {}", feature_id.description());
|
||||
println!("Value: {}", value);
|
||||
println!(
|
||||
"Status: {}",
|
||||
if value != 0 { "enabled" } else { "disabled" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_feature(id: String, value: u64) -> Result<()> {
|
||||
let feature_id = parse_feature_id(&id)?;
|
||||
|
||||
// Check if this feature is managed by any module
|
||||
if let Ok(managed_features_map) = crate::module::get_managed_features() {
|
||||
// Find which modules manage this feature
|
||||
let managing_modules: Vec<&String> = managed_features_map
|
||||
.iter()
|
||||
.filter(|(_, features)| features.iter().any(|f| f == feature_id.name()))
|
||||
.map(|(module_id, _)| module_id)
|
||||
.collect();
|
||||
|
||||
if !managing_modules.is_empty() {
|
||||
// Feature is managed, check if caller is an authorized module
|
||||
let caller_module = std::env::var("KSU_MODULE").unwrap_or_default();
|
||||
|
||||
if caller_module.is_empty() || !managing_modules.contains(&&caller_module) {
|
||||
bail!(
|
||||
"Feature '{}' is managed by module(s): {}. Direct modification is not allowed.",
|
||||
feature_id.name(),
|
||||
managing_modules
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Module '{}' is setting managed feature '{}'",
|
||||
caller_module,
|
||||
feature_id.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
crate::ksucalls::set_feature(feature_id as u32, value)
|
||||
.with_context(|| format!("Failed to set feature {} to {}", id, value))?;
|
||||
|
||||
println!(
|
||||
"Feature '{}' set to {} ({})",
|
||||
feature_id.name(),
|
||||
value,
|
||||
if value != 0 { "enabled" } else { "disabled" }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_features() -> Result<()> {
|
||||
println!("Available Features:");
|
||||
println!("{}", "=".repeat(80));
|
||||
|
||||
// Get managed features from modules
|
||||
let managed_features_map = crate::module::get_managed_features().unwrap_or_default();
|
||||
|
||||
// Build a reverse map: feature_name -> Vec<module_id>
|
||||
let mut feature_to_modules: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for (module_id, feature_list) in managed_features_map.iter() {
|
||||
for feature_name in feature_list {
|
||||
feature_to_modules
|
||||
.entry(feature_name.clone())
|
||||
.or_default()
|
||||
.push(module_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let all_features = [
|
||||
FeatureId::SuCompat,
|
||||
FeatureId::KernelUmount,
|
||||
FeatureId::EnhancedSecurity,
|
||||
FeatureId::SuLog,
|
||||
];
|
||||
|
||||
for feature_id in all_features.iter() {
|
||||
let id = *feature_id as u32;
|
||||
let (value, supported) = crate::ksucalls::get_feature(id).unwrap_or((0, false));
|
||||
|
||||
let status = if !supported {
|
||||
"NOT_SUPPORTED".to_string()
|
||||
} else if value != 0 {
|
||||
format!("ENABLED ({})", value)
|
||||
} else {
|
||||
"DISABLED".to_string()
|
||||
};
|
||||
|
||||
let managed_by = feature_to_modules.get(feature_id.name());
|
||||
let managed_mark = if managed_by.is_some() {
|
||||
" [MODULE_MANAGED]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
println!(
|
||||
"[{}] {} (ID={}){}",
|
||||
status,
|
||||
feature_id.name(),
|
||||
id,
|
||||
managed_mark
|
||||
);
|
||||
println!(" {}", feature_id.description());
|
||||
|
||||
if let Some(modules) = managed_by {
|
||||
println!(
|
||||
" ⚠️ Managed by module(s): {} (forced to 0 on initialization)",
|
||||
modules.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_config_and_apply() -> Result<()> {
|
||||
let features = load_binary_config()?;
|
||||
|
||||
if features.is_empty() {
|
||||
println!("No features found in config file");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
apply_config(&features)?;
|
||||
println!("Feature configuration loaded and applied");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_config() -> Result<()> {
|
||||
let mut features = HashMap::new();
|
||||
|
||||
let all_features = [
|
||||
FeatureId::SuCompat,
|
||||
FeatureId::KernelUmount,
|
||||
FeatureId::EnhancedSecurity,
|
||||
FeatureId::SuLog,
|
||||
];
|
||||
|
||||
for feature_id in all_features.iter() {
|
||||
let id = *feature_id as u32;
|
||||
if let Ok((value, supported)) = crate::ksucalls::get_feature(id)
|
||||
&& supported
|
||||
{
|
||||
features.insert(id, value);
|
||||
log::info!("Saved feature {} = {}", feature_id.name(), value);
|
||||
}
|
||||
}
|
||||
|
||||
save_binary_config(&features)?;
|
||||
println!(
|
||||
"Current feature states saved to config file ({} features)",
|
||||
features.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_feature(id: String) -> Result<()> {
|
||||
let feature_id = parse_feature_id(&id)?;
|
||||
|
||||
// Check if this feature is managed by any module
|
||||
let managed_features_map = crate::module::get_managed_features().unwrap_or_default();
|
||||
let is_managed = managed_features_map
|
||||
.values()
|
||||
.any(|features| features.iter().any(|f| f == feature_id.name()));
|
||||
|
||||
if is_managed {
|
||||
println!("managed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the feature is supported by kernel
|
||||
let (_value, supported) = crate::ksucalls::get_feature(feature_id as u32)
|
||||
.with_context(|| format!("Failed to get feature {}", id))?;
|
||||
|
||||
if supported {
|
||||
println!("supported");
|
||||
} else {
|
||||
println!("unsupported");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_features() -> Result<()> {
|
||||
log::info!("Initializing features from config...");
|
||||
|
||||
let mut features = load_binary_config()?;
|
||||
|
||||
// Get managed features from active modules and skip them during init
|
||||
if let Ok(managed_features_map) = crate::module::get_managed_features() {
|
||||
if !managed_features_map.is_empty() {
|
||||
log::info!(
|
||||
"Found {} modules managing features",
|
||||
managed_features_map.len()
|
||||
);
|
||||
|
||||
// Build a set of all managed feature IDs to skip
|
||||
for (module_id, feature_list) in managed_features_map.iter() {
|
||||
log::info!(
|
||||
"Module '{}' manages {} feature(s)",
|
||||
module_id,
|
||||
feature_list.len()
|
||||
);
|
||||
for feature_name in feature_list {
|
||||
if let Ok(feature_id) = parse_feature_id(feature_name) {
|
||||
let feature_id_u32 = feature_id as u32;
|
||||
// Remove managed features from config, let modules control them
|
||||
if features.remove(&feature_id_u32).is_some() {
|
||||
log::info!(
|
||||
" - Skipping managed feature '{}' (controlled by module: {})",
|
||||
feature_name,
|
||||
module_id
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
" - Feature '{}' is managed by module '{}', skipping",
|
||||
feature_name,
|
||||
module_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
" - Unknown managed feature '{}' from module '{}', ignoring",
|
||||
feature_name,
|
||||
module_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Failed to get managed features from modules, continuing with normal initialization"
|
||||
);
|
||||
}
|
||||
|
||||
if features.is_empty() {
|
||||
log::info!("No features to apply, skipping initialization");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
apply_config(&features)?;
|
||||
|
||||
// Save the configuration (excluding managed features)
|
||||
save_binary_config(&features)?;
|
||||
log::info!("Saved feature configuration to file");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
214
userspace/ksud/src/init_event.rs
Normal file
214
userspace/ksud/src/init_event.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#[cfg(target_arch = "aarch64")]
|
||||
use crate::kpm;
|
||||
use crate::module::{handle_updated_modules, prune_modules};
|
||||
use crate::utils::is_safe_mode;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule, restorecon,
|
||||
utils::{self},
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn on_post_data_fs() -> Result<()> {
|
||||
ksucalls::report_post_fs_data();
|
||||
|
||||
utils::umask(0);
|
||||
|
||||
// Clear all temporary module configs early
|
||||
if let Err(e) = crate::module_config::clear_all_temp_configs() {
|
||||
warn!("clear temp configs failed: {e}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let _ = catch_bootlog("logcat", vec!["logcat"]);
|
||||
#[cfg(unix)]
|
||||
let _ = catch_bootlog("dmesg", vec!["dmesg", "-w"]);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip post-fs-data!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let safe_mode = crate::utils::is_safe_mode();
|
||||
|
||||
if safe_mode {
|
||||
// we should still ensure module directory exists in safe mode
|
||||
// because we may need to operate the module dir in safe mode
|
||||
warn!("safe mode, skip common post-fs-data.d scripts");
|
||||
} else {
|
||||
// Then exec common post-fs-data scripts
|
||||
if let Err(e) = crate::module::exec_common_scripts("post-fs-data.d", true) {
|
||||
warn!("exec common post-fs-data scripts failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let module_dir = defs::MODULE_DIR;
|
||||
|
||||
assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?;
|
||||
|
||||
// Start UID scanner daemon with highest priority
|
||||
crate::uid_scanner::start_uid_scanner_daemon()?;
|
||||
|
||||
if is_safe_mode() {
|
||||
warn!("safe mode, skip load feature config");
|
||||
} else if let Err(e) = crate::umount_manager::load_and_apply_config() {
|
||||
warn!("Failed to load umount config: {e}");
|
||||
}
|
||||
|
||||
// if we are in safe mode, we should disable all modules
|
||||
if safe_mode {
|
||||
warn!("safe mode, skip post-fs-data scripts and disable all modules!");
|
||||
if let Err(e) = crate::module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {e}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = handle_updated_modules() {
|
||||
warn!("handle updated modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = prune_modules() {
|
||||
warn!("prune modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = restorecon::restorecon() {
|
||||
warn!("restorecon failed: {e}");
|
||||
}
|
||||
|
||||
// load sepolicy.rule
|
||||
if crate::module::load_sepolicy_rule().is_err() {
|
||||
warn!("load sepolicy.rule failed");
|
||||
}
|
||||
|
||||
if let Err(e) = crate::profile::apply_sepolies() {
|
||||
warn!("apply root profile sepolicy failed: {e}");
|
||||
}
|
||||
|
||||
// load feature config
|
||||
if is_safe_mode() {
|
||||
warn!("safe mode, skip load feature config");
|
||||
} else if let Err(e) = crate::feature::init_features() {
|
||||
warn!("init features failed: {e}");
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
if let Err(e) = kpm::start_kpm_watcher() {
|
||||
warn!("KPM: Failed to start KPM watcher: {e}");
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
if let Err(e) = kpm::load_kpm_modules() {
|
||||
warn!("KPM: Failed to load KPM modules: {e}");
|
||||
}
|
||||
|
||||
// execute metamodule post-fs-data script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec metamodule post-fs-data script failed: {e}");
|
||||
}
|
||||
|
||||
// exec modules post-fs-data scripts
|
||||
// TODO: Add timeout
|
||||
if let Err(e) = crate::module::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec post-fs-data scripts failed: {e}");
|
||||
}
|
||||
|
||||
// load system.prop
|
||||
if let Err(e) = crate::module::load_system_prop() {
|
||||
warn!("load system.prop failed: {e}");
|
||||
}
|
||||
|
||||
// execute metamodule mount script
|
||||
if let Err(e) = metamodule::exec_mount_script(module_dir) {
|
||||
warn!("execute metamodule mount failed: {e}");
|
||||
}
|
||||
|
||||
run_stage("post-mount", true);
|
||||
|
||||
std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_stage(stage: &str, block: bool) {
|
||||
utils::umask(0);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip {stage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::utils::is_safe_mode() {
|
||||
warn!("safe mode, skip {stage} scripts");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) {
|
||||
warn!("Failed to exec common {stage} scripts: {e}");
|
||||
}
|
||||
|
||||
// execute metamodule stage script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec metamodule {stage} script: {e}");
|
||||
}
|
||||
|
||||
// execute regular modules stage scripts
|
||||
if let Err(e) = crate::module::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec {stage} scripts: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_services() -> Result<()> {
|
||||
info!("on_services triggered!");
|
||||
run_stage("service", false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_boot_completed() -> Result<()> {
|
||||
ksucalls::report_boot_complete();
|
||||
info!("on_boot_completed triggered!");
|
||||
|
||||
run_stage("boot-completed", false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn catch_bootlog(logname: &str, command: Vec<&str>) -> Result<()> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
let logdir = Path::new(defs::LOG_DIR);
|
||||
utils::ensure_dir_exists(logdir)?;
|
||||
let bootlog = logdir.join(format!("{logname}.log"));
|
||||
let oldbootlog = logdir.join(format!("{logname}.old.log"));
|
||||
|
||||
if bootlog.exists() {
|
||||
std::fs::rename(&bootlog, oldbootlog)?;
|
||||
}
|
||||
|
||||
let bootlog = std::fs::File::create(bootlog)?;
|
||||
|
||||
let mut args = vec!["-s", "9", "30s"];
|
||||
args.extend_from_slice(&command);
|
||||
// timeout -s 9 30s logcat > boot.log
|
||||
let result = unsafe {
|
||||
std::process::Command::new("timeout")
|
||||
.process_group(0)
|
||||
.pre_exec(|| {
|
||||
utils::switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
.args(args)
|
||||
.stdout(Stdio::from(bootlog))
|
||||
.spawn()
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!("Failed to start logcat: {e:#}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
479
userspace/ksud/src/installer.sh
Normal file
479
userspace/ksud/src/installer.sh
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# KernelSU 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/ksud 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 `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
|
||||
#################
|
||||
|
||||
check_managed_features() {
|
||||
local PROP_FILE=$1
|
||||
local MANAGED_FEATURES=$(grep_prop managedFeatures "$PROP_FILE")
|
||||
|
||||
[ -z "$MANAGED_FEATURES" ] && return 0
|
||||
|
||||
ui_print "- Checking managed features: $MANAGED_FEATURES"
|
||||
|
||||
# Split features by comma
|
||||
echo "$MANAGED_FEATURES" | tr ',' '\n' | while read -r feature; do
|
||||
# Trim whitespace
|
||||
feature=$(echo "$feature" | xargs)
|
||||
[ -z "$feature" ] && continue
|
||||
|
||||
# Check feature status using ksud
|
||||
local status=$(/data/adb/ksud feature check "$feature" 2>/dev/null)
|
||||
|
||||
case "$status" in
|
||||
"unsupported")
|
||||
ui_print "! WARNING: Feature '$feature' is NOT SUPPORTED by kernel"
|
||||
ui_print "! This module may not work correctly!"
|
||||
;;
|
||||
"managed")
|
||||
ui_print "! WARNING: Feature '$feature' is already MANAGED by another module"
|
||||
ui_print "! Feature conflicts may occur!"
|
||||
;;
|
||||
"supported")
|
||||
ui_print "- Feature '$feature' is supported and available"
|
||||
;;
|
||||
*)
|
||||
ui_print "! WARNING: Unable to check feature '$feature' status"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
# if /system/vendor is a normal directory, no special handling is needed.
|
||||
if [ ! -e $MODPATH/system/$1 ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
# we move the folder to / only if it is a native folder that is not a symlink
|
||||
if [ -d "/$1" ] && [ ! -L "/$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
|
||||
|
||||
# Check managed features
|
||||
check_managed_features $TMPDIR/module.prop
|
||||
|
||||
# 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 KernelSU"
|
||||
|
||||
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
|
||||
handle_partition odm
|
||||
|
||||
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=25.2
|
||||
export MAGISK_VER_CODE=25200
|
||||
331
userspace/ksud/src/kpm.rs
Normal file
331
userspace/ksud/src/kpm.rs
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
use std::{
|
||||
ffi::{CStr, CString, OsStr},
|
||||
fs,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
|
||||
use crate::ksucalls::ksuctl;
|
||||
|
||||
pub const KPM_DIR: &str = "/data/adb/kpm";
|
||||
|
||||
const KPM_LOAD: u64 = 1;
|
||||
const KPM_UNLOAD: u64 = 2;
|
||||
const KPM_NUM: u64 = 3;
|
||||
const KPM_LIST: u64 = 4;
|
||||
const KPM_INFO: u64 = 5;
|
||||
const KPM_CONTROL: u64 = 6;
|
||||
const KPM_VERSION: u64 = 7;
|
||||
|
||||
const KSU_IOCTL_KPM: u32 = 0xc0004bc8; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0)
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct KsuKpmCmd {
|
||||
pub control_code: u64,
|
||||
pub arg1: u64,
|
||||
pub arg2: u64,
|
||||
pub result_code: u64,
|
||||
}
|
||||
|
||||
fn kpm_ioctl(cmd: &mut KsuKpmCmd) -> std::io::Result<()> {
|
||||
ksuctl(KSU_IOCTL_KPM, cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert raw kernel return code to `Result`.
|
||||
fn check_ret(rc: i32) -> Result<i32> {
|
||||
if rc < 0 {
|
||||
bail!("KPM error: {}", std::io::Error::from_raw_os_error(-rc));
|
||||
}
|
||||
Ok(rc)
|
||||
}
|
||||
|
||||
/// Load a `.kpm` into kernel space.
|
||||
pub fn kpm_load<P>(path: P, args: Option<&str>) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path_c = CString::new(path.as_ref().to_string_lossy().to_string())?;
|
||||
let args_c = args.map(CString::new).transpose()?;
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_LOAD,
|
||||
arg1: path_c.as_ptr() as u64,
|
||||
arg2: args_c.as_ref().map_or(0, |s| s.as_ptr() as u64),
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
println!("Success");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unload by module name.
|
||||
pub fn kpm_unload(name: &str) -> Result<()> {
|
||||
let name_c = CString::new(name)?;
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_UNLOAD,
|
||||
arg1: name_c.as_ptr() as u64,
|
||||
arg2: 0,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return loaded module count.
|
||||
pub fn kpm_num() -> Result<i32> {
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_NUM,
|
||||
arg1: 0,
|
||||
arg2: 0,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
let n = check_ret(result)?;
|
||||
println!("{n}");
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Print name list of loaded modules.
|
||||
pub fn kpm_list() -> Result<()> {
|
||||
let mut buf = vec![0u8; 1024];
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_LIST,
|
||||
arg1: buf.as_mut_ptr() as u64,
|
||||
arg2: buf.len() as u64,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
print!("{}", buf2str(&buf));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print single module info.
|
||||
pub fn kpm_info(name: &str) -> Result<()> {
|
||||
let name_c = CString::new(name)?;
|
||||
let mut buf = vec![0u8; 256];
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_INFO,
|
||||
arg1: name_c.as_ptr() as u64,
|
||||
arg2: buf.as_mut_ptr() as u64,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
println!("{}", buf2str(&buf));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send control string to a module; returns kernel answer.
|
||||
pub fn kpm_control(name: &str, args: &str) -> Result<i32> {
|
||||
let name_c = CString::new(name)?;
|
||||
let args_c = CString::new(args)?;
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_CONTROL,
|
||||
arg1: name_c.as_ptr() as u64,
|
||||
arg2: args_c.as_ptr() as u64,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)
|
||||
}
|
||||
|
||||
/// Print loader version string.
|
||||
pub fn kpm_version_loader() -> Result<()> {
|
||||
let mut buf = vec![0u8; 1024];
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_VERSION,
|
||||
arg1: buf.as_mut_ptr() as u64,
|
||||
arg2: buf.len() as u64,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
print!("{}", buf2str(&buf));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate loader version; empty or "Error*" => fail.
|
||||
pub fn check_kpm_version() -> Result<String> {
|
||||
let mut buf = vec![0u8; 1024];
|
||||
|
||||
let mut result: i32 = -1;
|
||||
let mut cmd = KsuKpmCmd {
|
||||
control_code: KPM_VERSION,
|
||||
arg1: buf.as_mut_ptr() as u64,
|
||||
arg2: buf.len() as u64,
|
||||
result_code: &mut result as *mut i32 as u64,
|
||||
};
|
||||
|
||||
kpm_ioctl(&mut cmd)?;
|
||||
check_ret(result)?;
|
||||
let ver = buf2str(&buf);
|
||||
if ver.is_empty() {
|
||||
bail!("KPM: invalid version response: {ver}");
|
||||
}
|
||||
log::info!("KPM: version check ok: {ver}");
|
||||
Ok(ver)
|
||||
}
|
||||
|
||||
/// Create `/data/adb/kpm` with 0o777 if missing.
|
||||
pub fn ensure_kpm_dir() -> Result<()> {
|
||||
let _ = fs::create_dir_all(KPM_DIR);
|
||||
let meta = fs::metadata(KPM_DIR)?;
|
||||
|
||||
if meta.permissions().mode() != 0o777 {
|
||||
fs::set_permissions(KPM_DIR, fs::Permissions::from_mode(0o777))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start file watcher for hot-(un)load.
|
||||
pub fn start_kpm_watcher() -> Result<()> {
|
||||
check_kpm_version()?; // bails if loader too old
|
||||
ensure_kpm_dir()?;
|
||||
|
||||
if crate::utils::is_safe_mode() {
|
||||
log::warn!("KPM: safe-mode – removing all modules");
|
||||
remove_all_kpms()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut watcher = notify::recommended_watcher(|res: Result<_, _>| match res {
|
||||
Ok(evt) => handle_kpm_event(evt),
|
||||
Err(e) => log::error!("KPM: watcher error: {e}"),
|
||||
})?;
|
||||
watcher.watch(Path::new(KPM_DIR), RecursiveMode::NonRecursive)?;
|
||||
log::info!("KPM: watcher active on {KPM_DIR}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_kpm_event(evt: notify::Event) {
|
||||
if let notify::EventKind::Create(_) = evt.kind {
|
||||
for p in evt.paths {
|
||||
if let Some(ex) = p.extension()
|
||||
&& ex == OsStr::new("kpm")
|
||||
&& kpm_load(&p, None).is_err()
|
||||
{
|
||||
log::warn!("KPM: failed to load {}", p.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Locate `/data/adb/kpm/<name>.kpm`.
|
||||
fn find_kpm_file(name: &str) -> Result<Option<PathBuf>> {
|
||||
let dir = Path::new(KPM_DIR);
|
||||
|
||||
if !dir.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let p = entry?.path();
|
||||
if let Some(ex) = p.extension()
|
||||
&& ex == OsStr::new("kpm")
|
||||
&& let Some(fs) = p.file_stem()
|
||||
&& fs == OsStr::new(name)
|
||||
{
|
||||
return Ok(Some(p));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Remove every `.kpm` file and unload it.
|
||||
pub fn remove_all_kpms() -> Result<()> {
|
||||
let dir = Path::new(KPM_DIR);
|
||||
if !dir.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let p = entry?.path();
|
||||
if let Some(ex) = p.extension()
|
||||
&& ex == OsStr::new("kpm")
|
||||
&& let Some(name) = p.file_stem().and_then(|s| s.to_str())
|
||||
&& let Err(e) = (|| -> Result<()> {
|
||||
kpm_unload(name)?;
|
||||
|
||||
if let Some(p) = find_kpm_file(name)? {
|
||||
if let Err(e) = fs::remove_file(&p) {
|
||||
log::warn!("KPM: delete {} failed: {e}", p.display());
|
||||
return Err(e.into());
|
||||
}
|
||||
log::info!("KPM: deleted {}", p.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})()
|
||||
{
|
||||
log::error!("KPM: unload {name} failed: {e}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bulk-load existing `.kpm`s at boot.
|
||||
pub fn load_kpm_modules() -> Result<()> {
|
||||
check_kpm_version()?;
|
||||
ensure_kpm_dir()?;
|
||||
|
||||
let dir = Path::new(KPM_DIR);
|
||||
if !dir.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut ok, mut ng) = (0, 0);
|
||||
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let p = entry?.path();
|
||||
if let Some(ex) = p.extension()
|
||||
&& ex == OsStr::new("kpm")
|
||||
{
|
||||
match kpm_load(&p, None) {
|
||||
Ok(_) => ok += 1,
|
||||
Err(e) => {
|
||||
log::warn!("KPM: load {} failed: {e}", p.display());
|
||||
ng += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("KPM: bulk-load done – ok: {ok}, failed: {ng}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert zero-padded kernel buffer to owned String.
|
||||
fn buf2str(buf: &[u8]) -> String {
|
||||
// SAFETY: buffer is always NUL-terminated by kernel.
|
||||
unsafe {
|
||||
CStr::from_ptr(buf.as_ptr().cast())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
361
userspace/ksud/src/ksucalls.rs
Normal file
361
userspace/ksud/src/ksucalls.rs
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
use std::fs;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// Event constants
|
||||
const EVENT_POST_FS_DATA: u32 = 1;
|
||||
const EVENT_BOOT_COMPLETED: u32 = 2;
|
||||
const EVENT_MODULE_MOUNTED: u32 = 3;
|
||||
|
||||
const KSU_IOCTL_GRANT_ROOT: u32 = 0x00004b01; // _IOC(_IOC_NONE, 'K', 1, 0)
|
||||
const KSU_IOCTL_GET_INFO: u32 = 0x80004b02; // _IOC(_IOC_READ, 'K', 2, 0)
|
||||
const KSU_IOCTL_REPORT_EVENT: u32 = 0x40004b03; // _IOC(_IOC_WRITE, 'K', 3, 0)
|
||||
const KSU_IOCTL_SET_SEPOLICY: u32 = 0xc0004b04; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0)
|
||||
const KSU_IOCTL_CHECK_SAFEMODE: u32 = 0x80004b05; // _IOC(_IOC_READ, 'K', 5, 0)
|
||||
const KSU_IOCTL_GET_FEATURE: u32 = 0xc0004b0d; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0)
|
||||
const KSU_IOCTL_SET_FEATURE: u32 = 0x40004b0e; // _IOC(_IOC_WRITE, 'K', 14, 0)
|
||||
const KSU_IOCTL_GET_WRAPPER_FD: u32 = 0x40004b0f; // _IOC(_IOC_WRITE, 'K', 15, 0)
|
||||
const KSU_IOCTL_MANAGE_MARK: u32 = 0xc0004b10; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0)
|
||||
const KSU_IOCTL_NUKE_EXT4_SYSFS: u32 = 0x40004b11; // _IOC(_IOC_WRITE, 'K', 17, 0)
|
||||
const KSU_IOCTL_ADD_TRY_UMOUNT: u32 = 0x40004b12; // _IOC(_IOC_WRITE, 'K', 18, 0)
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct GetInfoCmd {
|
||||
version: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct ReportEventCmd {
|
||||
event: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct SetSepolicyCmd {
|
||||
pub cmd: u64,
|
||||
pub arg: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct CheckSafemodeCmd {
|
||||
in_safe_mode: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct GetFeatureCmd {
|
||||
feature_id: u32,
|
||||
value: u64,
|
||||
supported: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct SetFeatureCmd {
|
||||
feature_id: u32,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct GetWrapperFdCmd {
|
||||
fd: i32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct ManageMarkCmd {
|
||||
operation: u32,
|
||||
pid: i32,
|
||||
result: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct NukeExt4SysfsCmd {
|
||||
pub arg: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct AddTryUmountCmd {
|
||||
arg: u64, // char ptr, this is the mountpoint
|
||||
flags: u32, // this is the flag we use for it
|
||||
mode: u8, // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry
|
||||
}
|
||||
|
||||
// Mark operation constants
|
||||
const KSU_MARK_GET: u32 = 1;
|
||||
const KSU_MARK_MARK: u32 = 2;
|
||||
const KSU_MARK_UNMARK: u32 = 3;
|
||||
const KSU_MARK_REFRESH: u32 = 4;
|
||||
|
||||
// Umount operation constants
|
||||
const KSU_UMOUNT_WIPE: u8 = 0;
|
||||
const KSU_UMOUNT_ADD: u8 = 1;
|
||||
const KSU_UMOUNT_DEL: u8 = 2;
|
||||
|
||||
// Global driver fd cache
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
static DRIVER_FD: OnceLock<RawFd> = OnceLock::new();
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
static INFO_CACHE: OnceLock<GetInfoCmd> = OnceLock::new();
|
||||
|
||||
const KSU_INSTALL_MAGIC1: u32 = 0xDEADBEEF;
|
||||
const KSU_INSTALL_MAGIC2: u32 = 0xCAFEBABE;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn scan_driver_fd() -> Option<RawFd> {
|
||||
let fd_dir = fs::read_dir("/proc/self/fd").ok()?;
|
||||
|
||||
for entry in fd_dir.flatten() {
|
||||
if let Ok(fd_num) = entry.file_name().to_string_lossy().parse::<i32>() {
|
||||
let link_path = format!("/proc/self/fd/{}", fd_num);
|
||||
if let Ok(target) = fs::read_link(&link_path) {
|
||||
let target_str = target.to_string_lossy();
|
||||
if target_str.contains("[ksu_driver]") {
|
||||
return Some(fd_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Get cached driver fd
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn init_driver_fd() -> Option<RawFd> {
|
||||
let fd = scan_driver_fd();
|
||||
if fd.is_none() {
|
||||
let mut fd = -1;
|
||||
unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_reboot,
|
||||
KSU_INSTALL_MAGIC1,
|
||||
KSU_INSTALL_MAGIC2,
|
||||
0,
|
||||
&mut fd,
|
||||
);
|
||||
};
|
||||
if fd >= 0 { Some(fd) } else { None }
|
||||
} else {
|
||||
fd
|
||||
}
|
||||
}
|
||||
|
||||
// ioctl wrapper using libc
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn ksuctl<T>(request: u32, arg: *mut T) -> std::io::Result<i32> {
|
||||
use std::io;
|
||||
|
||||
let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1));
|
||||
unsafe {
|
||||
#[cfg(not(target_env = "gnu"))]
|
||||
let ret = libc::ioctl(fd as libc::c_int, request as i32, arg);
|
||||
#[cfg(target_env = "gnu")]
|
||||
let ret = libc::ioctl(fd as libc::c_int, request as u64, arg);
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
fn ksuctl<T>(_request: u32, _arg: *mut T) -> std::io::Result<i32> {
|
||||
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
|
||||
}
|
||||
|
||||
// API implementations
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn get_info() -> GetInfoCmd {
|
||||
*INFO_CACHE.get_or_init(|| {
|
||||
let mut cmd = GetInfoCmd {
|
||||
version: 0,
|
||||
flags: 0,
|
||||
};
|
||||
let _ = ksuctl(KSU_IOCTL_GET_INFO, &mut cmd as *mut _);
|
||||
cmd
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn get_version() -> i32 {
|
||||
get_info().version as i32
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn get_version() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn grant_root() -> std::io::Result<()> {
|
||||
ksuctl(KSU_IOCTL_GRANT_ROOT, std::ptr::null_mut::<u8>())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn grant_root() -> std::io::Result<()> {
|
||||
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn report_event(event: u32) {
|
||||
let mut cmd = ReportEventCmd { event };
|
||||
let _ = ksuctl(KSU_IOCTL_REPORT_EVENT, &mut cmd as *mut _);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
fn report_event(_event: u32) {}
|
||||
|
||||
pub fn report_post_fs_data() {
|
||||
report_event(EVENT_POST_FS_DATA);
|
||||
}
|
||||
|
||||
pub fn report_boot_complete() {
|
||||
report_event(EVENT_BOOT_COMPLETED);
|
||||
}
|
||||
|
||||
pub fn report_module_mounted() {
|
||||
report_event(EVENT_MODULE_MOUNTED);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn check_kernel_safemode() -> bool {
|
||||
let mut cmd = CheckSafemodeCmd { in_safe_mode: 0 };
|
||||
let _ = ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &mut cmd as *mut _);
|
||||
cmd.in_safe_mode != 0
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn check_kernel_safemode() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn set_sepolicy(cmd: &SetSepolicyCmd) -> std::io::Result<()> {
|
||||
let mut ioctl_cmd = *cmd;
|
||||
ksuctl(KSU_IOCTL_SET_SEPOLICY, &mut ioctl_cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get feature value and support status from kernel
|
||||
/// Returns (value, supported)
|
||||
pub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> {
|
||||
let mut cmd = GetFeatureCmd {
|
||||
feature_id,
|
||||
value: 0,
|
||||
supported: 0,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_GET_FEATURE, &mut cmd as *mut _)?;
|
||||
Ok((cmd.value, cmd.supported != 0))
|
||||
}
|
||||
|
||||
/// Set feature value in kernel
|
||||
pub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> {
|
||||
let mut cmd = SetFeatureCmd { feature_id, value };
|
||||
ksuctl(KSU_IOCTL_SET_FEATURE, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result<RawFd> {
|
||||
let mut cmd = GetWrapperFdCmd { fd, flags: 0 };
|
||||
let result = ksuctl(KSU_IOCTL_GET_WRAPPER_FD, &mut cmd as *mut _)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get mark status for a process (pid=0 returns total marked count)
|
||||
pub fn mark_get(pid: i32) -> std::io::Result<u32> {
|
||||
let mut cmd = ManageMarkCmd {
|
||||
operation: KSU_MARK_GET,
|
||||
pid,
|
||||
result: 0,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
|
||||
Ok(cmd.result)
|
||||
}
|
||||
|
||||
/// Mark a process (pid=0 marks all processes)
|
||||
pub fn mark_set(pid: i32) -> std::io::Result<()> {
|
||||
let mut cmd = ManageMarkCmd {
|
||||
operation: KSU_MARK_MARK,
|
||||
pid,
|
||||
result: 0,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmark a process (pid=0 unmarks all processes)
|
||||
pub fn mark_unset(pid: i32) -> std::io::Result<()> {
|
||||
let mut cmd = ManageMarkCmd {
|
||||
operation: KSU_MARK_UNMARK,
|
||||
pid,
|
||||
result: 0,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Refresh mark for all running processes
|
||||
pub fn mark_refresh() -> std::io::Result<()> {
|
||||
let mut cmd = ManageMarkCmd {
|
||||
operation: KSU_MARK_REFRESH,
|
||||
pid: 0,
|
||||
result: 0,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> {
|
||||
let c_mnt = std::ffi::CString::new(mnt)?;
|
||||
let mut ioctl_cmd = NukeExt4SysfsCmd {
|
||||
arg: c_mnt.as_ptr() as u64,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_NUKE_EXT4_SYSFS, &mut ioctl_cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wipe all entries from umount list
|
||||
pub fn umount_list_wipe() -> std::io::Result<()> {
|
||||
let mut cmd = AddTryUmountCmd {
|
||||
arg: 0,
|
||||
flags: 0,
|
||||
mode: KSU_UMOUNT_WIPE,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add mount point to umount list
|
||||
pub fn umount_list_add(path: &str, flags: u32) -> anyhow::Result<()> {
|
||||
let c_path = std::ffi::CString::new(path)?;
|
||||
let mut cmd = AddTryUmountCmd {
|
||||
arg: c_path.as_ptr() as u64,
|
||||
flags,
|
||||
mode: KSU_UMOUNT_ADD,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete mount point from umount list
|
||||
pub fn umount_list_del(path: &str) -> anyhow::Result<()> {
|
||||
let c_path = std::ffi::CString::new(path)?;
|
||||
let mut cmd = AddTryUmountCmd {
|
||||
arg: c_path.as_ptr() as u64,
|
||||
flags: 0,
|
||||
mode: KSU_UMOUNT_DEL,
|
||||
};
|
||||
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
26
userspace/ksud/src/main.rs
Normal file
26
userspace/ksud/src/main.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
mod apk_sign;
|
||||
mod assets;
|
||||
mod boot_patch;
|
||||
mod cli;
|
||||
mod debug;
|
||||
mod defs;
|
||||
mod feature;
|
||||
mod init_event;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod kpm;
|
||||
mod ksucalls;
|
||||
mod metamodule;
|
||||
mod module;
|
||||
mod module_config;
|
||||
mod profile;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod su;
|
||||
#[cfg(target_os = "android")]
|
||||
mod uid_scanner;
|
||||
mod umount_manager;
|
||||
mod utils;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
cli::run()
|
||||
}
|
||||
287
userspace/ksud/src/metamodule.rs
Normal file
287
userspace/ksud/src/metamodule.rs
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
//! Metamodule management
|
||||
//!
|
||||
//! This module handles all metamodule-related functionality.
|
||||
//! Metamodules are special modules that manage how regular modules are mounted
|
||||
//! and provide hooks for module installation/uninstallation.
|
||||
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use log::{info, warn};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::module::ModuleType::All;
|
||||
use crate::{assets, defs};
|
||||
|
||||
/// Determine whether the provided module properties mark it as a metamodule
|
||||
pub fn is_metamodule(props: &HashMap<String, String>) -> bool {
|
||||
props
|
||||
.get("metamodule")
|
||||
.map(|s| {
|
||||
let trimmed = s.trim();
|
||||
trimmed == "1" || trimmed.eq_ignore_ascii_case("true")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get metamodule path if it exists
|
||||
/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule
|
||||
pub fn get_metamodule_path() -> Option<PathBuf> {
|
||||
let path = Path::new(defs::METAMODULE_DIR);
|
||||
|
||||
// Check if symlink exists and resolve it
|
||||
if path.is_symlink()
|
||||
&& let Ok(target) = std::fs::read_link(path)
|
||||
{
|
||||
// If target is relative, resolve it
|
||||
let resolved = if target.is_absolute() {
|
||||
target
|
||||
} else {
|
||||
path.parent()?.join(target)
|
||||
};
|
||||
|
||||
if resolved.exists() && resolved.is_dir() {
|
||||
return Some(resolved);
|
||||
} else {
|
||||
warn!(
|
||||
"Metamodule symlink points to non-existent path: {:?}",
|
||||
resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search for metamodule=1 in modules directory
|
||||
let mut result = None;
|
||||
let _ = crate::module::foreach_module(All, |module_path| {
|
||||
if let Ok(props) = crate::module::read_module_prop(module_path)
|
||||
&& is_metamodule(&props)
|
||||
{
|
||||
info!("Found metamodule in modules directory: {:?}", module_path);
|
||||
result = Some(module_path.to_path_buf());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if metamodule exists
|
||||
pub fn has_metamodule() -> bool {
|
||||
get_metamodule_path().is_some()
|
||||
}
|
||||
|
||||
/// Check if it's safe to install a regular module
|
||||
/// Returns Ok(()) if safe, Err(is_disabled) if blocked
|
||||
/// - Err(true) means metamodule is disabled
|
||||
/// - Err(false) means metamodule is in other unstable state
|
||||
pub fn check_install_safety() -> Result<(), bool> {
|
||||
// No metamodule → safe
|
||||
let Some(metamodule_path) = get_metamodule_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// No metainstall.sh → safe (uses default installer)
|
||||
// The staged update directory may contain the latest scripts, so check both locations
|
||||
let has_metainstall = metamodule_path
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
|| metamodule_path.file_name().is_some_and(|module_id| {
|
||||
Path::new(defs::MODULE_UPDATE_DIR)
|
||||
.join(module_id)
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
});
|
||||
if !has_metainstall {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check for marker files
|
||||
let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
|
||||
// Stable state (no markers) → safe
|
||||
if !has_update && !has_remove && !has_disable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Return true if disabled, false for other unstable states
|
||||
Err(has_disable && !has_update && !has_remove)
|
||||
}
|
||||
|
||||
/// Create or update the metamodule symlink
|
||||
/// Points /data/adb/metamodule -> /data/adb/modules/{module_id}
|
||||
pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> {
|
||||
// METAMODULE_DIR might have trailing slash, so we need to trim it
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
info!(
|
||||
"Creating metamodule symlink: {:?} -> {:?}",
|
||||
symlink_path, module_path
|
||||
);
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if symlink_path.exists() || symlink_path.is_symlink() {
|
||||
info!("Removing old metamodule symlink/path");
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?;
|
||||
} else {
|
||||
// Could be a directory, remove it
|
||||
std::fs::remove_dir_all(symlink_path)
|
||||
.with_context(|| "Failed to remove old directory")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(module_path, symlink_path)
|
||||
.with_context(|| format!("Failed to create symlink to {:?}", module_path))?;
|
||||
|
||||
info!("Metamodule symlink created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the metamodule symlink
|
||||
pub(crate) fn remove_symlink() -> Result<()> {
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path)
|
||||
.with_context(|| "Failed to remove metamodule symlink")?;
|
||||
info!("Metamodule symlink removed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the install script content, using metainstall.sh from metamodule if available
|
||||
/// Returns the script content to be executed
|
||||
pub(crate) fn get_install_script(
|
||||
is_metamodule: bool,
|
||||
installer_content: &str,
|
||||
install_module_script: &str,
|
||||
) -> Result<String> {
|
||||
// Check if there's a metamodule with metainstall.sh
|
||||
// Only apply this logic for regular modules (not when installing metamodule itself)
|
||||
let install_script = if !is_metamodule {
|
||||
if let Some(metamodule_path) = get_metamodule_path() {
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, using default installer");
|
||||
install_module_script.to_string()
|
||||
} else {
|
||||
let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT);
|
||||
|
||||
if metainstall_path.exists() {
|
||||
info!("Using metainstall.sh from metamodule");
|
||||
let metamodule_content = std::fs::read_to_string(&metainstall_path)
|
||||
.with_context(|| "Failed to read metamodule metainstall.sh")?;
|
||||
format!("{}\n{}\nexit 0\n", installer_content, metamodule_content)
|
||||
} else {
|
||||
info!("Metamodule exists but has no metainstall.sh, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No metamodule found, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
} else {
|
||||
info!("Installing metamodule, using default installer");
|
||||
install_module_script.to_string()
|
||||
};
|
||||
|
||||
Ok(install_script)
|
||||
}
|
||||
|
||||
/// Check if metamodule script exists and is ready to execute
|
||||
/// Returns None if metamodule doesn't exist, is disabled, or script is missing
|
||||
/// Returns Some(script_path) if script is ready to execute
|
||||
fn check_metamodule_script(script_name: &str) -> Option<PathBuf> {
|
||||
// Check if metamodule exists
|
||||
let metamodule_path = get_metamodule_path()?;
|
||||
|
||||
// Check if metamodule is disabled
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, skipping {}", script_name);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if script exists
|
||||
let script_path = metamodule_path.join(script_name);
|
||||
if !script_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(script_path)
|
||||
}
|
||||
|
||||
/// Execute metamodule's metauninstall.sh for a specific module
|
||||
pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> {
|
||||
let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(
|
||||
"Executing metamodule metauninstall.sh for module: {}",
|
||||
module_id
|
||||
);
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", metauninstall_path.to_str().unwrap()])
|
||||
.current_dir(metauninstall_path.parent().unwrap())
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_ID", module_id)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule metauninstall.sh failed for module {}: {:?}",
|
||||
module_id,
|
||||
result
|
||||
);
|
||||
|
||||
info!(
|
||||
"Metamodule metauninstall.sh executed successfully for {}",
|
||||
module_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule mount script
|
||||
pub fn exec_mount_script(module_dir: &str) -> Result<()> {
|
||||
let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing mount script for metamodule");
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", mount_script.to_str().unwrap()])
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_DIR", module_dir)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule mount script failed with status: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
info!("Metamodule mount script executed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule script for a specific stage
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing metamodule {}.sh", stage);
|
||||
crate::module::exec_script(&script_path, block)?;
|
||||
info!("Metamodule {}.sh executed successfully", stage);
|
||||
Ok(())
|
||||
}
|
||||
855
userspace/ksud/src/module.rs
Normal file
855
userspace/ksud/src/module.rs
Normal file
|
|
@ -0,0 +1,855 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule,
|
||||
restorecon::{restore_syscon, setsyscon},
|
||||
sepolicy,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use const_format::concatcp;
|
||||
use is_executable::is_executable;
|
||||
use java_properties::PropertiesIter;
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use std::fs::{copy, rename};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, canonicalize, remove_dir_all, set_permissions},
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::FromStr,
|
||||
};
|
||||
use zip_extensions::zip_extract_file_to_memory;
|
||||
|
||||
use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME};
|
||||
use crate::module::ModuleType::{Active, All};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
|
||||
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
|
||||
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
INSTALLER_CONTENT,
|
||||
"\n",
|
||||
"install_module",
|
||||
"\n",
|
||||
"exit 0",
|
||||
"\n"
|
||||
);
|
||||
|
||||
/// Validate module_id format and security
|
||||
/// Module ID must match: ^[a-zA-Z][a-zA-Z0-9._-]+$
|
||||
/// - Must start with a letter (a-zA-Z)
|
||||
/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters
|
||||
/// - Minimum length: 2 characters
|
||||
pub fn validate_module_id(module_id: &str) -> Result<()> {
|
||||
if module_id.is_empty() {
|
||||
bail!("Module ID cannot be empty");
|
||||
}
|
||||
|
||||
if module_id.len() < 2 {
|
||||
bail!("Module ID too short: must be at least 2 characters");
|
||||
}
|
||||
|
||||
if module_id.len() > 64 {
|
||||
bail!(
|
||||
"Module ID too long: {} characters (max: 64)",
|
||||
module_id.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Check first character: must be a letter
|
||||
let first_char = module_id.chars().next().unwrap();
|
||||
if !first_char.is_ascii_alphabetic() {
|
||||
bail!(
|
||||
"Module ID must start with a letter (a-zA-Z), got: '{}'",
|
||||
first_char
|
||||
);
|
||||
}
|
||||
|
||||
// Check remaining characters: alphanumeric, dot, underscore, or hyphen
|
||||
for (i, ch) in module_id.chars().enumerate() {
|
||||
if i == 0 {
|
||||
continue; // Already checked
|
||||
}
|
||||
|
||||
if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '_' && ch != '-' {
|
||||
bail!(
|
||||
"Module ID contains invalid character '{}' at position {}. Only letters, digits, '.', '_', and '-' are allowed",
|
||||
ch,
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional security checks
|
||||
if module_id.contains("..") {
|
||||
bail!("Module ID cannot contain '..' sequence");
|
||||
}
|
||||
|
||||
if module_id == "." || module_id == ".." {
|
||||
bail!("Module ID cannot be '.' or '..'");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get common environment variables for script execution
|
||||
pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
("ASH_STANDALONE", "1".to_string()),
|
||||
("KSU", "true".to_string()),
|
||||
("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()),
|
||||
("KSU_VER_CODE", defs::VERSION_CODE.to_string()),
|
||||
("KSU_VER", defs::VERSION_NAME.to_string()),
|
||||
(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap_or_default(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
// Get install script from metamodule module
|
||||
let install_script =
|
||||
metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", &install_script])
|
||||
.envs(get_common_script_envs())
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
ensure!(result.success(), "Failed to install module script");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check if Android boot is completed before installing modules
|
||||
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(())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub(crate) enum ModuleType {
|
||||
All,
|
||||
Active,
|
||||
Updated,
|
||||
}
|
||||
|
||||
pub(crate) fn foreach_module(
|
||||
module_type: ModuleType,
|
||||
mut f: impl FnMut(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let modules_dir = Path::new(match module_type {
|
||||
ModuleType::Updated => MODULE_UPDATE_DIR,
|
||||
_ => defs::MODULE_DIR,
|
||||
});
|
||||
let dir = std::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 module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if module_type == Active && 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(Active, f)
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if sepolicy::apply_file(&rule_file).is_err() {
|
||||
warn!("Failed to load sepolicy.rule for {}", &rule_file.display());
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
// Extract module_id from path if it matches /data/adb/modules/{id}/...
|
||||
let module_id = path
|
||||
.as_ref()
|
||||
.strip_prefix(defs::MODULE_DIR)
|
||||
.ok()
|
||||
.and_then(|p| p.components().next())
|
||||
.and_then(|c| c.as_os_str().to_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Validate and log module_id extraction
|
||||
let validated_module_id = module_id
|
||||
.as_ref()
|
||||
.and_then(|id| match validate_module_id(id) {
|
||||
Ok(_) => {
|
||||
debug!("Module ID extracted from script path: '{}'", id);
|
||||
Some(id.as_str())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Invalid module ID '{}' extracted from script path '{}': {}",
|
||||
id,
|
||||
path.as_ref().display(),
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if module_id.is_none() {
|
||||
debug!(
|
||||
"Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.",
|
||||
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())
|
||||
.envs(get_common_script_envs());
|
||||
|
||||
// Set KSU_MODULE environment variable if module_id was validated successfully
|
||||
if let Some(id) = validated_module_id {
|
||||
command = command.env("KSU_MODULE", id);
|
||||
}
|
||||
|
||||
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<()> {
|
||||
let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok());
|
||||
|
||||
foreach_active_module(|module| {
|
||||
if metamodule_dir.as_ref().is_some_and(|meta_dir| {
|
||||
canonicalize(module)
|
||||
.map(|resolved| resolved == *meta_dir)
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 = std::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(All, |module| {
|
||||
if !module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
// Execute metamodule's metauninstall.sh first
|
||||
let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
// Check if this is a metamodule
|
||||
let is_metamodule = read_module_prop(module)
|
||||
.map(|props| metamodule::is_metamodule(&props))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Removing metamodule symlink");
|
||||
if let Err(e) = metamodule::remove_symlink() {
|
||||
warn!("Failed to remove metamodule symlink: {}", e);
|
||||
}
|
||||
} else if let Err(e) = metamodule::exec_metauninstall_script(module_id) {
|
||||
warn!(
|
||||
"Failed to exec metamodule uninstall for {}: {}",
|
||||
module_id, e
|
||||
);
|
||||
}
|
||||
|
||||
// Then execute module's own uninstall.sh
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists()
|
||||
&& let Err(e) = exec_script(uninstaller, true)
|
||||
{
|
||||
warn!("Failed to exec uninstaller: {e}");
|
||||
}
|
||||
|
||||
// Clear module configs before removing module directory
|
||||
if let Err(e) = crate::module_config::clear_module_configs(module_id) {
|
||||
warn!("Failed to clear configs for {}: {}", module_id, e);
|
||||
}
|
||||
|
||||
// Finally remove the module directory
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// collect remaining modules, if none, clean up metamodule record
|
||||
let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().join("module.prop").exists())
|
||||
.collect();
|
||||
|
||||
if remaining_modules.is_empty() {
|
||||
info!("no remaining modules.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_updated_modules() -> Result<()> {
|
||||
let modules_root = Path::new(MODULE_DIR);
|
||||
foreach_module(ModuleType::Updated, |updated_module| {
|
||||
if !updated_module.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(name) = updated_module.file_name() {
|
||||
let module_dir = modules_root.join(name);
|
||||
let mut disabled = false;
|
||||
let mut removed = false;
|
||||
if module_dir.exists() {
|
||||
// If the old module is disabled, we need to also disable the new one
|
||||
disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists();
|
||||
removed = module_dir.join(defs::REMOVE_FILE_NAME).exists();
|
||||
remove_dir_all(&module_dir)?;
|
||||
}
|
||||
rename(updated_module, &module_dir)?;
|
||||
if removed {
|
||||
let path = module_dir.join(defs::REMOVE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
} else if disabled {
|
||||
let path = module_dir.join(defs::DISABLE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _install_module(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
|
||||
// first check if working 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, if failed it will return early.
|
||||
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 module_id = module_id.trim();
|
||||
|
||||
// Validate module_id format
|
||||
validate_module_id(module_id)
|
||||
.with_context(|| format!("Invalid module ID in module.prop: '{}'", module_id))?;
|
||||
|
||||
// Check if this module is a metamodule
|
||||
let is_metamodule = metamodule::is_metamodule(&module_prop);
|
||||
|
||||
// Check if it's safe to install regular module
|
||||
if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() {
|
||||
println!("\n❌ Installation Blocked");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule with custom installer is active");
|
||||
println!("│");
|
||||
if is_disabled {
|
||||
println!("│ Current state: Disabled");
|
||||
println!("│ Action required: Re-enable or uninstall it, then reboot");
|
||||
} else {
|
||||
println!("│ Current state: Pending changes");
|
||||
println!("│ Action required: Reboot to apply changes first");
|
||||
}
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Metamodule installation blocked");
|
||||
}
|
||||
|
||||
// All modules (including metamodules) are installed to MODULE_UPDATE_DIR
|
||||
let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Installing metamodule: {}", module_id);
|
||||
|
||||
// Check if there's already a metamodule installed
|
||||
if metamodule::has_metamodule()
|
||||
&& let Some(existing_path) = metamodule::get_metamodule_path()
|
||||
{
|
||||
let existing_id = read_module_prop(&existing_path)
|
||||
.ok()
|
||||
.and_then(|m| m.get("id").cloned())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
if existing_id != module_id {
|
||||
println!("\n❌ Installation Failed");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule is already installed");
|
||||
println!("│ Current metamodule: {}", existing_id);
|
||||
println!("│");
|
||||
println!("│ Only one metamodule can be active at a time.");
|
||||
println!("│");
|
||||
println!("│ To install this metamodule:");
|
||||
println!("│ 1. Uninstall the current metamodule");
|
||||
println!("│ 2. Reboot your device");
|
||||
println!("│ 3. Install the new metamodule");
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Cannot install multiple metamodules");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// Ensure module directory exists and set SELinux context
|
||||
ensure_dir_exists(defs::MODULE_UPDATE_DIR)?;
|
||||
setsyscon(defs::MODULE_UPDATE_DIR)?;
|
||||
|
||||
// Prepare target directory
|
||||
println!("- Installing to {}", updated_dir.display());
|
||||
ensure_clean_dir(&updated_dir)?;
|
||||
info!("target dir: {}", updated_dir.display());
|
||||
|
||||
// Extract zip to target directory
|
||||
println!("- Extracting module files");
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&updated_dir)?;
|
||||
|
||||
// Set permission and selinux context for $MOD/system
|
||||
let module_system_dir = updated_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
// Execute install script
|
||||
println!("- Running module installer");
|
||||
exec_install_script(zip, is_metamodule)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
updated_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
// Create symlink for metamodule
|
||||
if is_metamodule {
|
||||
println!("- Creating metamodule symlink");
|
||||
metamodule::ensure_symlink(&module_dir)?;
|
||||
}
|
||||
|
||||
println!("- Module installed successfully!");
|
||||
info!("Module {} installed successfully!", module_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
let result = _install_module(zip);
|
||||
if let Err(ref e) = result {
|
||||
println!("- Error: {e}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn undo_uninstall_module(id: &str) -> Result<()> {
|
||||
validate_module_id(id)?;
|
||||
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Remove the remove mark
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
if remove_file.exists() {
|
||||
std::fs::remove_file(&remove_file)
|
||||
.with_context(|| format!("Failed to delete remove file for module '{}'", id))?;
|
||||
info!("Removed the remove mark for module {}", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
validate_module_id(id)?;
|
||||
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Mark for removal
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
File::create(remove_file).with_context(|| "Failed to create remove file")?;
|
||||
|
||||
info!("Module {} marked for removal", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
validate_module_id(id)?;
|
||||
|
||||
let action_script_path = format!("/data/adb/modules/{id}/action.sh");
|
||||
exec_script(&action_script_path, true)
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
validate_module_id(id)?;
|
||||
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
if disable_path.exists() {
|
||||
std::fs::remove_file(&disable_path).with_context(|| {
|
||||
format!("Failed to remove disable file: {}", disable_path.display())
|
||||
})?;
|
||||
info!("Module {} enabled", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
ensure_file_exists(disable_path)?;
|
||||
|
||||
info!("Module {} disabled", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
mark_all_modules(defs::DISABLE_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn uninstall_all_modules() -> Result<()> {
|
||||
info!("Uninstalling all modules");
|
||||
mark_all_modules(defs::REMOVE_FILE_NAME)
|
||||
}
|
||||
|
||||
fn mark_all_modules(flag_file: &str) -> Result<()> {
|
||||
// we assume the module dir is already mounted
|
||||
let dir = std::fs::read_dir(defs::MODULE_DIR)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let flag = path.join(flag_file);
|
||||
if let Err(e) = ensure_file_exists(flag) {
|
||||
warn!("Failed to mark module: {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read module.prop from the given module path and return as a HashMap
|
||||
pub fn read_module_prop(module_path: &Path) -> Result<HashMap<String, String>> {
|
||||
let module_prop = module_path.join("module.prop");
|
||||
ensure!(
|
||||
module_prop.exists(),
|
||||
"module.prop not found in {}",
|
||||
module_path.display()
|
||||
);
|
||||
|
||||
let content = std::fs::read(&module_prop)
|
||||
.with_context(|| format!("Failed to read module.prop: {}", module_prop.display()))?;
|
||||
|
||||
let mut prop_map: HashMap<String, String> = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8)
|
||||
.read_into(|k, v| {
|
||||
prop_map.insert(k, v);
|
||||
})
|
||||
.with_context(|| format!("Failed to parse module.prop: {}", module_prop.display()))?;
|
||||
|
||||
Ok(prop_map)
|
||||
}
|
||||
|
||||
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
// Load all module configs once to minimize I/O overhead
|
||||
let all_configs = match crate::module_config::get_all_module_configs() {
|
||||
Ok(configs) => configs,
|
||||
Err(e) => {
|
||||
warn!("Failed to load module configs: {}", e);
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
// first check enabled modules
|
||||
let dir = std::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());
|
||||
|
||||
if !path.join("module.prop").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut module_prop_map = match read_module_prop(&path) {
|
||||
Ok(prop) => prop,
|
||||
Err(e) => {
|
||||
warn!("Failed to read module.prop for {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// If id is missing or empty, use directory name as fallback
|
||||
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 from dir name");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enabled, update, remove, web, action 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();
|
||||
let need_mount = path.join("system").exists() && !path.join("skip_mount").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());
|
||||
module_prop_map.insert("mount".to_owned(), need_mount.to_string());
|
||||
|
||||
// Apply module config overrides and extract managed features
|
||||
if let Some(module_id) = module_prop_map.get("id")
|
||||
&& let Some(config) = all_configs.get(module_id.as_str())
|
||||
{
|
||||
// Apply override.description
|
||||
if let Some(desc) = config.get("override.description") {
|
||||
module_prop_map.insert("description".to_owned(), desc.clone());
|
||||
}
|
||||
|
||||
// Extract managed features from manage.* config entries
|
||||
let managed_features: Vec<String> = config
|
||||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
if k.starts_with("manage.") && crate::module_config::parse_bool_config(v) {
|
||||
k.strip_prefix("manage.").map(|f| f.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !managed_features.is_empty() {
|
||||
module_prop_map.insert("managedFeatures".to_owned(), managed_features.join(","));
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Get all managed features from active modules
|
||||
/// Modules declare managed features via config system (manage.<feature>=true)
|
||||
/// Returns: HashMap<ModuleId, Vec<ManagedFeature>>
|
||||
pub fn get_managed_features() -> Result<HashMap<String, Vec<String>>> {
|
||||
let mut managed_features_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
foreach_active_module(|module_path| {
|
||||
// Get module ID
|
||||
let module_id = match module_path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
warn!(
|
||||
"Failed to get module id from path: {}",
|
||||
module_path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Read module config
|
||||
let config = match crate::module_config::merge_configs(module_id) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Failed to merge configs for module '{}': {}", module_id, e);
|
||||
return Ok(()); // Skip this module
|
||||
}
|
||||
};
|
||||
|
||||
// Extract manage.* config entries
|
||||
let mut feature_list = Vec::new();
|
||||
for (key, value) in config.iter() {
|
||||
if key.starts_with("manage.") {
|
||||
// Parse feature name
|
||||
if let Some(feature_name) = key.strip_prefix("manage.")
|
||||
&& crate::module_config::parse_bool_config(value)
|
||||
{
|
||||
info!("Module {} manages feature: {}", module_id, feature_name);
|
||||
feature_list.push(feature_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !feature_list.is_empty() {
|
||||
managed_features_map.insert(module_id.to_string(), feature_list);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(managed_features_map)
|
||||
}
|
||||
474
userspace/ksud/src/module_config.rs
Normal file
474
userspace/ksud/src/module_config.rs
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use log::{debug, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::defs;
|
||||
use crate::utils::ensure_dir_exists;
|
||||
|
||||
const MODULE_CONFIG_MAGIC: u32 = 0x4b53554d; // "KSUM"
|
||||
const MODULE_CONFIG_VERSION: u32 = 1;
|
||||
|
||||
// Validation limits
|
||||
pub const MAX_CONFIG_KEY_LEN: usize = 256;
|
||||
pub const MAX_CONFIG_VALUE_LEN: usize = 256;
|
||||
pub const MAX_CONFIG_COUNT: usize = 32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConfigType {
|
||||
Persist,
|
||||
Temp,
|
||||
}
|
||||
|
||||
impl ConfigType {
|
||||
fn filename(&self) -> &'static str {
|
||||
match self {
|
||||
ConfigType::Persist => defs::PERSIST_CONFIG_NAME,
|
||||
ConfigType::Temp => defs::TEMP_CONFIG_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a boolean config value
|
||||
/// Accepts "true", "1" (case-insensitive) as true, everything else as false
|
||||
pub fn parse_bool_config(value: &str) -> bool {
|
||||
let trimmed = value.trim();
|
||||
trimmed.eq_ignore_ascii_case("true") || trimmed == "1"
|
||||
}
|
||||
|
||||
/// Validate config key
|
||||
/// Rejects keys with control characters, newlines, or excessive length
|
||||
pub fn validate_config_key(key: &str) -> Result<()> {
|
||||
if key.is_empty() {
|
||||
bail!("Config key cannot be empty");
|
||||
}
|
||||
|
||||
if key.len() > MAX_CONFIG_KEY_LEN {
|
||||
bail!(
|
||||
"Config key too long: {} bytes (max: {})",
|
||||
key.len(),
|
||||
MAX_CONFIG_KEY_LEN
|
||||
);
|
||||
}
|
||||
|
||||
// Check for control characters and newlines
|
||||
for ch in key.chars() {
|
||||
if ch.is_control() {
|
||||
bail!(
|
||||
"Config key contains control character: {:?} (U+{:04X})",
|
||||
ch,
|
||||
ch as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reject keys with path separators to prevent path traversal
|
||||
if key.contains('/') || key.contains('\\') {
|
||||
bail!("Config key cannot contain path separators");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate config value
|
||||
/// Rejects values with control characters (except tab), newlines, or excessive length
|
||||
pub fn validate_config_value(value: &str) -> Result<()> {
|
||||
if value.len() > MAX_CONFIG_VALUE_LEN {
|
||||
bail!(
|
||||
"Config value too long: {} bytes (max: {})",
|
||||
value.len(),
|
||||
MAX_CONFIG_VALUE_LEN
|
||||
);
|
||||
}
|
||||
|
||||
// Check for control characters (allow tab but reject newlines and others)
|
||||
for ch in value.chars() {
|
||||
if ch.is_control() && ch != '\t' {
|
||||
bail!(
|
||||
"Config value contains invalid control character: {:?} (U+{:04X})",
|
||||
ch,
|
||||
ch as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate config count
|
||||
fn validate_config_count(config: &HashMap<String, String>) -> Result<()> {
|
||||
if config.len() > MAX_CONFIG_COUNT {
|
||||
bail!(
|
||||
"Too many config entries: {} (max: {})",
|
||||
config.len(),
|
||||
MAX_CONFIG_COUNT
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the config directory path for a module
|
||||
fn get_config_dir(module_id: &str) -> PathBuf {
|
||||
Path::new(defs::MODULE_CONFIG_DIR).join(module_id)
|
||||
}
|
||||
|
||||
/// Get the config file path for a module
|
||||
fn get_config_path(module_id: &str, config_type: ConfigType) -> PathBuf {
|
||||
get_config_dir(module_id).join(config_type.filename())
|
||||
}
|
||||
|
||||
/// Ensure the config directory exists
|
||||
fn ensure_config_dir(module_id: &str) -> Result<PathBuf> {
|
||||
let dir = get_config_dir(module_id);
|
||||
ensure_dir_exists(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Load config from binary file
|
||||
pub fn load_config(module_id: &str, config_type: ConfigType) -> Result<HashMap<String, String>> {
|
||||
crate::module::validate_module_id(module_id)?;
|
||||
|
||||
let config_path = get_config_path(module_id, config_type);
|
||||
|
||||
if !config_path.exists() {
|
||||
debug!("Config file not found: {:?}", config_path);
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let mut file = File::open(&config_path)
|
||||
.with_context(|| format!("Failed to open config file: {:?}", config_path))?;
|
||||
|
||||
// Read magic
|
||||
let mut magic_buf = [0u8; 4];
|
||||
file.read_exact(&mut magic_buf)
|
||||
.with_context(|| "Failed to read magic")?;
|
||||
let magic = u32::from_le_bytes(magic_buf);
|
||||
|
||||
if magic != MODULE_CONFIG_MAGIC {
|
||||
bail!(
|
||||
"Invalid config magic: expected 0x{:08x}, got 0x{:08x}",
|
||||
MODULE_CONFIG_MAGIC,
|
||||
magic
|
||||
);
|
||||
}
|
||||
|
||||
// Read version
|
||||
let mut version_buf = [0u8; 4];
|
||||
file.read_exact(&mut version_buf)
|
||||
.with_context(|| "Failed to read version")?;
|
||||
let version = u32::from_le_bytes(version_buf);
|
||||
|
||||
if version != MODULE_CONFIG_VERSION {
|
||||
bail!(
|
||||
"Unsupported config version: expected {}, got {}",
|
||||
MODULE_CONFIG_VERSION,
|
||||
version
|
||||
);
|
||||
}
|
||||
|
||||
// Read count
|
||||
let mut count_buf = [0u8; 4];
|
||||
file.read_exact(&mut count_buf)
|
||||
.with_context(|| "Failed to read count")?;
|
||||
let count = u32::from_le_bytes(count_buf);
|
||||
|
||||
// Read entries
|
||||
let mut config = HashMap::new();
|
||||
for i in 0..count {
|
||||
// Read key length
|
||||
let mut key_len_buf = [0u8; 4];
|
||||
file.read_exact(&mut key_len_buf)
|
||||
.with_context(|| format!("Failed to read key length for entry {}", i))?;
|
||||
let key_len = u32::from_le_bytes(key_len_buf) as usize;
|
||||
|
||||
// Read key data
|
||||
let mut key_buf = vec![0u8; key_len];
|
||||
file.read_exact(&mut key_buf)
|
||||
.with_context(|| format!("Failed to read key data for entry {}", i))?;
|
||||
let key = String::from_utf8(key_buf)
|
||||
.with_context(|| format!("Invalid UTF-8 in key for entry {}", i))?;
|
||||
|
||||
// Read value length
|
||||
let mut value_len_buf = [0u8; 4];
|
||||
file.read_exact(&mut value_len_buf)
|
||||
.with_context(|| format!("Failed to read value length for entry {}", i))?;
|
||||
let value_len = u32::from_le_bytes(value_len_buf) as usize;
|
||||
|
||||
// Read value data
|
||||
let mut value_buf = vec![0u8; value_len];
|
||||
file.read_exact(&mut value_buf)
|
||||
.with_context(|| format!("Failed to read value data for entry {}", i))?;
|
||||
let value = String::from_utf8(value_buf)
|
||||
.with_context(|| format!("Invalid UTF-8 in value for entry {}", i))?;
|
||||
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
debug!("Loaded {} entries from {:?}", config.len(), config_path);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save config to binary file
|
||||
pub fn save_config(
|
||||
module_id: &str,
|
||||
config_type: ConfigType,
|
||||
config: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
crate::module::validate_module_id(module_id)?;
|
||||
|
||||
// Validate config count
|
||||
validate_config_count(config)?;
|
||||
|
||||
// Validate all keys and values
|
||||
for (key, value) in config {
|
||||
validate_config_key(key).with_context(|| format!("Invalid config key: '{}'", key))?;
|
||||
validate_config_value(value)
|
||||
.with_context(|| format!("Invalid config value for key '{}'", key))?;
|
||||
}
|
||||
|
||||
ensure_config_dir(module_id)?;
|
||||
|
||||
let config_path = get_config_path(module_id, config_type);
|
||||
let temp_path = config_path.with_extension("tmp");
|
||||
|
||||
// Write to temporary file first
|
||||
let mut file = File::create(&temp_path)
|
||||
.with_context(|| format!("Failed to create temp config file: {:?}", temp_path))?;
|
||||
|
||||
// Write magic
|
||||
file.write_all(&MODULE_CONFIG_MAGIC.to_le_bytes())
|
||||
.with_context(|| "Failed to write magic")?;
|
||||
|
||||
// Write version
|
||||
file.write_all(&MODULE_CONFIG_VERSION.to_le_bytes())
|
||||
.with_context(|| "Failed to write version")?;
|
||||
|
||||
// Write count
|
||||
let count = config.len() as u32;
|
||||
file.write_all(&count.to_le_bytes())
|
||||
.with_context(|| "Failed to write count")?;
|
||||
|
||||
// Write entries
|
||||
for (key, value) in config {
|
||||
// Write key length
|
||||
let key_bytes = key.as_bytes();
|
||||
let key_len = key_bytes.len() as u32;
|
||||
file.write_all(&key_len.to_le_bytes())
|
||||
.with_context(|| format!("Failed to write key length for '{}'", key))?;
|
||||
|
||||
// Write key data
|
||||
file.write_all(key_bytes)
|
||||
.with_context(|| format!("Failed to write key data for '{}'", key))?;
|
||||
|
||||
// Write value length
|
||||
let value_bytes = value.as_bytes();
|
||||
let value_len = value_bytes.len() as u32;
|
||||
file.write_all(&value_len.to_le_bytes())
|
||||
.with_context(|| format!("Failed to write value length for '{}'", key))?;
|
||||
|
||||
// Write value data
|
||||
file.write_all(value_bytes)
|
||||
.with_context(|| format!("Failed to write value data for '{}'", key))?;
|
||||
}
|
||||
|
||||
file.sync_all()
|
||||
.with_context(|| "Failed to sync config file")?;
|
||||
|
||||
// Atomic rename
|
||||
fs::rename(&temp_path, &config_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to rename config file: {:?} -> {:?}",
|
||||
temp_path, config_path
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!("Saved {} entries to {:?}", config.len(), config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a single config value
|
||||
#[allow(dead_code)]
|
||||
pub fn get_config_value(
|
||||
module_id: &str,
|
||||
key: &str,
|
||||
config_type: ConfigType,
|
||||
) -> Result<Option<String>> {
|
||||
let config = load_config(module_id, config_type)?;
|
||||
Ok(config.get(key).cloned())
|
||||
}
|
||||
|
||||
/// Set a single config value
|
||||
pub fn set_config_value(
|
||||
module_id: &str,
|
||||
key: &str,
|
||||
value: &str,
|
||||
config_type: ConfigType,
|
||||
) -> Result<()> {
|
||||
// Validate input early for better error messages
|
||||
validate_config_key(key)?;
|
||||
validate_config_value(value)?;
|
||||
|
||||
let mut config = load_config(module_id, config_type)?;
|
||||
config.insert(key.to_string(), value.to_string());
|
||||
|
||||
// Note: save_config will also validate, but this provides earlier feedback
|
||||
save_config(module_id, config_type, &config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a single config value
|
||||
pub fn delete_config_value(module_id: &str, key: &str, config_type: ConfigType) -> Result<()> {
|
||||
let mut config = load_config(module_id, config_type)?;
|
||||
|
||||
if config.remove(key).is_none() {
|
||||
bail!("Key '{}' not found in config", key);
|
||||
}
|
||||
|
||||
save_config(module_id, config_type, &config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all config values
|
||||
pub fn clear_config(module_id: &str, config_type: ConfigType) -> Result<()> {
|
||||
let config_path = get_config_path(module_id, config_type);
|
||||
|
||||
if config_path.exists() {
|
||||
fs::remove_file(&config_path)
|
||||
.with_context(|| format!("Failed to remove config file: {:?}", config_path))?;
|
||||
debug!("Cleared config: {:?}", config_path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge persist and temp configs (temp takes priority)
|
||||
pub fn merge_configs(module_id: &str) -> Result<HashMap<String, String>> {
|
||||
crate::module::validate_module_id(module_id)?;
|
||||
|
||||
let mut merged = match load_config(module_id, ConfigType::Persist) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to load persist config for module '{}': {}",
|
||||
module_id, e
|
||||
);
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
let temp = match load_config(module_id, ConfigType::Temp) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to load temp config for module '{}': {}",
|
||||
module_id, e
|
||||
);
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Temp config overrides persist config
|
||||
for (key, value) in temp {
|
||||
merged.insert(key, value);
|
||||
}
|
||||
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
/// Get all module configs (for iteration)
|
||||
/// Loads all configs in a single pass to minimize I/O overhead
|
||||
pub fn get_all_module_configs() -> Result<HashMap<String, HashMap<String, String>>> {
|
||||
let config_root = Path::new(defs::MODULE_CONFIG_DIR);
|
||||
|
||||
if !config_root.exists() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let mut all_configs = HashMap::new();
|
||||
|
||||
for entry in fs::read_dir(config_root)
|
||||
.with_context(|| format!("Failed to read config directory: {:?}", config_root))?
|
||||
{
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(module_id) = path.file_name().and_then(|n| n.to_str()) {
|
||||
match merge_configs(module_id) {
|
||||
Ok(config) => {
|
||||
if !config.is_empty() {
|
||||
all_configs.insert(module_id.to_string(), config);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to load config for module '{}': {}", module_id, e);
|
||||
// Continue processing other modules
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_configs)
|
||||
}
|
||||
|
||||
/// Clear all temporary configs (called during post-fs-data)
|
||||
pub fn clear_all_temp_configs() -> Result<()> {
|
||||
let config_root = Path::new(defs::MODULE_CONFIG_DIR);
|
||||
|
||||
if !config_root.exists() {
|
||||
debug!("Config directory does not exist, nothing to clear");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cleared_count = 0;
|
||||
|
||||
for entry in fs::read_dir(config_root)
|
||||
.with_context(|| format!("Failed to read config directory: {:?}", config_root))?
|
||||
{
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let temp_config = path.join(defs::TEMP_CONFIG_NAME);
|
||||
if temp_config.exists() {
|
||||
match fs::remove_file(&temp_config) {
|
||||
Ok(_) => {
|
||||
debug!("Cleared temp config: {:?}", temp_config);
|
||||
cleared_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to clear temp config {:?}: {}", temp_config, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cleared_count > 0 {
|
||||
debug!("Cleared {} temp config file(s)", cleared_count);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all configs for a module (called during uninstall)
|
||||
pub fn clear_module_configs(module_id: &str) -> Result<()> {
|
||||
crate::module::validate_module_id(module_id)?;
|
||||
|
||||
let config_dir = get_config_dir(module_id);
|
||||
|
||||
if config_dir.exists() {
|
||||
fs::remove_dir_all(&config_dir)
|
||||
.with_context(|| format!("Failed to remove config directory: {:?}", config_dir))?;
|
||||
debug!("Cleared all configs for module: {}", module_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
80
userspace/ksud/src/profile.rs
Normal file
80
userspace/ksud/src/profile.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::{defs, sepolicy, utils::ensure_dir_exists};
|
||||
|
||||
pub fn set_sepolicy(pkg: String, policy: String) -> Result<()> {
|
||||
ensure_dir_exists(defs::PROFILE_SELINUX_DIR)?;
|
||||
let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);
|
||||
std::fs::write(&policy_file, policy)?;
|
||||
sepolicy::apply_file(&policy_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_sepolicy(pkg: String) -> Result<()> {
|
||||
let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);
|
||||
let policy = std::fs::read_to_string(policy_file)?;
|
||||
println!("{policy}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ksud doesn't guarteen the correctness of template, it just save
|
||||
pub fn set_template(id: String, template: String) -> Result<()> {
|
||||
ensure_dir_exists(defs::PROFILE_TEMPLATE_DIR)?;
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
std::fs::write(template_file, template)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_template(id: String) -> Result<()> {
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
let template = std::fs::read_to_string(template_file)?;
|
||||
println!("{template}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_template(id: String) -> Result<()> {
|
||||
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
|
||||
std::fs::remove_file(template_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_templates() -> Result<()> {
|
||||
let templates = std::fs::read_dir(defs::PROFILE_TEMPLATE_DIR);
|
||||
let Ok(templates) = templates else {
|
||||
return Ok(());
|
||||
};
|
||||
for template in templates {
|
||||
let template = template?;
|
||||
let template = template.file_name();
|
||||
if let Some(template) = template.to_str() {
|
||||
println!("{template}");
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_sepolies() -> Result<()> {
|
||||
let path = Path::new(defs::PROFILE_SELINUX_DIR);
|
||||
if !path.exists() {
|
||||
log::info!("profile sepolicy dir not exists.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sepolicies =
|
||||
std::fs::read_dir(path).with_context(|| "profile sepolicy dir open failed.".to_string())?;
|
||||
for sepolicy in sepolicies {
|
||||
let Ok(sepolicy) = sepolicy else {
|
||||
log::info!("profile sepolicy dir read failed.");
|
||||
continue;
|
||||
};
|
||||
let sepolicy = sepolicy.path();
|
||||
if sepolicy::apply_file(&sepolicy).is_ok() {
|
||||
log::info!("profile sepolicy applied: {sepolicy:?}");
|
||||
} else {
|
||||
log::info!("profile sepolicy apply failed: {sepolicy:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
81
userspace/ksud/src/restorecon.rs
Normal file
81
userspace/ksud/src/restorecon.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
#[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};
|
||||
use jwalk::{Parallelism::Serial, WalkDir};
|
||||
|
||||
use crate::defs;
|
||||
|
||||
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())
|
||||
&& let Result::Ok(con) = lgetfilecon(&path)
|
||||
&& (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(())
|
||||
}
|
||||
744
userspace/ksud/src/sepolicy.rs
Normal file
744
userspace/ksud/src/sepolicy.rs
Normal file
|
|
@ -0,0 +1,744 @@
|
|||
use std::{ffi, path::Path, vec};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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 expand to atomic statement, for example:
|
||||
/// allow domain1 domain2:file1 { read write }; would be expand 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn apply_one_rule<'a>(statement: &'a PolicyStatement<'a>, strict: bool) -> Result<()> {
|
||||
let policies: Vec<AtomicStatement> = statement.try_into()?;
|
||||
|
||||
for policy in policies {
|
||||
let ffi_policy = FfiPolicy::from(policy);
|
||||
let cmd = crate::ksucalls::SetSepolicyCmd {
|
||||
cmd: 0,
|
||||
arg: &ffi_policy as *const _ as u64,
|
||||
};
|
||||
if let Err(e) = crate::ksucalls::set_sepolicy(&cmd) {
|
||||
log::warn!("apply rule {:?} failed: {}", statement, e);
|
||||
if strict {
|
||||
return Err(anyhow::anyhow!("apply rule {:?} failed: {}", statement, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
fn apply_one_rule<'a>(_statement: &'a PolicyStatement<'a>, _strict: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn live_patch(policy: &str) -> Result<()> {
|
||||
let result = parse_sepolicy(policy.trim(), false)?;
|
||||
for statement in result {
|
||||
println!("{statement:?}");
|
||||
apply_one_rule(&statement, false)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_file<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
let input = std::fs::read_to_string(path)?;
|
||||
live_patch(&input)
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
320
userspace/ksud/src/su.rs
Normal file
320
userspace/ksud/src/su.rs
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
use crate::{
|
||||
defs,
|
||||
utils::{self, umask},
|
||||
};
|
||||
use anyhow::{Context, Ok, Result, bail};
|
||||
use getopts::Options;
|
||||
use libc::c_int;
|
||||
use log::{error, warn};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::{env, ffi::CStr, path::PathBuf, process::Command};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use crate::ksucalls::get_wrapped_fd;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{
|
||||
process::getuid,
|
||||
thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid},
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn grant_root(global_mnt: bool) -> Result<()> {
|
||||
crate::ksucalls::grant_root()?;
|
||||
|
||||
let mut command = Command::new("sh");
|
||||
let command = unsafe {
|
||||
command.pre_exec(move || {
|
||||
if global_mnt {
|
||||
let _ = utils::switch_mnt_ns(1);
|
||||
}
|
||||
Result::Ok(())
|
||||
})
|
||||
};
|
||||
// add /data/adb/ksu/bin to PATH
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
add_path_to_env(defs::BINARY_DIR)?;
|
||||
Err(command.exec().into())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn grant_root(_global_mnt: bool) -> Result<()> {
|
||||
unimplemented!("grant_root is only available on android");
|
||||
}
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("KernelSU\n\nUsage: {program} [options] [-] [user [argument...]]");
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn set_identity(uid: u32, gid: u32, groups: &[u32]) {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
rustix::thread::set_thread_groups(
|
||||
groups
|
||||
.iter()
|
||||
.map(|g| unsafe { Gid::from_raw(*g) })
|
||||
.collect::<Vec<_>>()
|
||||
.as_ref(),
|
||||
)
|
||||
.ok();
|
||||
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(target_os = "android")]
|
||||
fn wrap_tty(fd: c_int) {
|
||||
let inner_fn = move || -> Result<()> {
|
||||
if unsafe { libc::isatty(fd) != 1 } {
|
||||
warn!("not a tty: {fd}");
|
||||
return Ok(());
|
||||
}
|
||||
let new_fd = get_wrapped_fd(fd).context("get_wrapped_fd")?;
|
||||
if unsafe { libc::dup2(new_fd, fd) } == -1 {
|
||||
bail!("dup {new_fd} -> {fd} errno: {}", unsafe {
|
||||
*libc::__errno()
|
||||
});
|
||||
} else {
|
||||
unsafe { libc::close(new_fd) };
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = inner_fn() {
|
||||
error!("wrap tty {fd}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn root_shell() -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn root_shell() -> Result<()> {
|
||||
// we are root now, this was set in kernel!
|
||||
|
||||
use anyhow::anyhow;
|
||||
let env_args: Vec<String> = env::args().collect();
|
||||
let program = env_args[0].clone();
|
||||
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.optopt("g", "group", "Specify the primary group", "GROUP");
|
||||
opts.optmulti(
|
||||
"G",
|
||||
"supp-group",
|
||||
"Specify a supplementary group. The first specified supplementary group is also used as a primary group if the option -g is not specified.",
|
||||
"GROUP",
|
||||
);
|
||||
opts.optflag("W", "no-wrapper", "don't use ksu fd wrapper");
|
||||
|
||||
// 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(&program, opts);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
};
|
||||
|
||||
if matches.opt_present("h") {
|
||||
print_usage(&program, opts);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if matches.opt_present("v") {
|
||||
println!("{}:KernelSU", 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");
|
||||
let use_fd_wrapper = !matches.opt_present("W");
|
||||
|
||||
let groups = matches
|
||||
.opt_strs("G")
|
||||
.into_iter()
|
||||
.map(|g| g.parse::<u32>().map_err(|_| anyhow!("Invalid GID: {}", g)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// if -g provided, use it.
|
||||
let mut gid = matches
|
||||
.opt_str("g")
|
||||
.map(|g| g.parse::<u32>().map_err(|_| anyhow!("Invalid GID: {}", g)))
|
||||
.transpose()?;
|
||||
|
||||
// otherwise, use the first gid of groups.
|
||||
if gid.is_none() && !groups.is_empty() {
|
||||
gid = Some(groups[0]);
|
||||
}
|
||||
|
||||
// we've make 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 = getuid().as_raw();
|
||||
if free_idx < matches.free.len() {
|
||||
let name = &matches.free[free_idx];
|
||||
uid = unsafe {
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is no gid provided, use uid.
|
||||
let gid = gid.unwrap_or(uid);
|
||||
// https://github.com/topjohnwu/Magisk/blob/master/native/src/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/ksu/bin to PATH
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
add_path_to_env(defs::BINARY_DIR)?;
|
||||
|
||||
// when KSURC_PATH exists and ENV is not set, set ENV to KSURC_PATH
|
||||
if PathBuf::from(defs::KSURC_PATH).exists() && env::var("ENV").is_err() {
|
||||
command = command.env("ENV", defs::KSURC_PATH);
|
||||
}
|
||||
|
||||
// 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"))]
|
||||
if mount_master {
|
||||
let _ = utils::switch_mnt_ns(1);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if use_fd_wrapper {
|
||||
wrap_tty(0);
|
||||
wrap_tty(1);
|
||||
wrap_tty(2);
|
||||
}
|
||||
|
||||
set_identity(uid, gid, &groups);
|
||||
|
||||
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(())
|
||||
}
|
||||
88
userspace/ksud/src/uid_scanner.rs
Normal file
88
userspace/ksud/src/uid_scanner.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
os::unix::{
|
||||
fs::{PermissionsExt, symlink},
|
||||
process::CommandExt,
|
||||
},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use log::{info, warn};
|
||||
|
||||
const SCANNER_PATH: &str = "/data/adb/uid_scanner";
|
||||
const LINK_DIR: &str = "/data/adb/ksu/bin";
|
||||
const LINK_PATH: &str = "/data/adb/ksu/bin/uid_scanner";
|
||||
const SERVICE_DIR: &str = "/data/adb/service.d";
|
||||
const SERVICE_PATH: &str = "/data/adb/service.d/uid_scanner.sh";
|
||||
|
||||
pub fn start_uid_scanner_daemon() -> Result<()> {
|
||||
if !fs::exists(SCANNER_PATH)? {
|
||||
warn!("uid scanner binary not found at {}", SCANNER_PATH);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = fs::set_permissions(SCANNER_PATH, fs::Permissions::from_mode(0o755)) {
|
||||
warn!("failed to set permissions for {}: {}", SCANNER_PATH, e);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Err(e) = fs::create_dir_all(LINK_DIR) {
|
||||
warn!("failed to create {}: {}", LINK_DIR, e);
|
||||
} else if !fs::exists(LINK_PATH)? {
|
||||
match symlink(SCANNER_PATH, LINK_PATH) {
|
||||
Ok(_) => info!("created symlink {} -> {}", SCANNER_PATH, LINK_PATH),
|
||||
Err(e) => warn!("failed to create symlink: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::create_dir_all(SERVICE_DIR) {
|
||||
warn!("failed to create {}: {}", SERVICE_DIR, e);
|
||||
}
|
||||
|
||||
if !fs::exists(SERVICE_PATH)? {
|
||||
let content = include_str!("uid_scanner.sh");
|
||||
|
||||
match fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(SERVICE_PATH)
|
||||
.and_then(|mut f| {
|
||||
f.write_all(content.as_bytes())?;
|
||||
f.sync_all()?;
|
||||
fs::set_permissions(SERVICE_PATH, fs::Permissions::from_mode(0o755))
|
||||
}) {
|
||||
Ok(_) => info!("created service script {}", SERVICE_PATH),
|
||||
Err(e) => warn!("failed to write {}: {}", SERVICE_PATH, e),
|
||||
}
|
||||
}
|
||||
|
||||
info!("starting uid scanner daemon with highest priority");
|
||||
let mut cmd = Command::new(SCANNER_PATH);
|
||||
cmd.arg("start")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.current_dir("/");
|
||||
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
libc::nice(-20);
|
||||
libc::setsid();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
info!("uid scanner daemon started with pid: {}", child.id());
|
||||
std::mem::drop(child);
|
||||
}
|
||||
Err(e) => warn!("failed to start uid scanner daemon: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
5
userspace/ksud/src/uid_scanner.sh
Normal file
5
userspace/ksud/src/uid_scanner.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#!/system/bin/sh
|
||||
# KSU uid_scanner auto-restart script
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
sleep 10
|
||||
/data/adb/uid_scanner restart
|
||||
264
userspace/ksud/src/umount_manager.rs
Normal file
264
userspace/ksud/src/umount_manager.rs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
use anyhow::{Context, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::ksucalls::ksuctl;
|
||||
|
||||
const MAGIC_NUMBER_HEADER: &[u8; 4] = b"KUMT";
|
||||
const MAGIC_VERSION: u32 = 1;
|
||||
const CONFIG_FILE: &str = "/data/adb/ksu/.umount";
|
||||
const KSU_IOCTL_UMOUNT_MANAGER: u32 = 0xc0004b6b; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 0)
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct UmountEntry {
|
||||
pub path: String,
|
||||
pub flags: i32,
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct UmountConfig {
|
||||
pub entries: Vec<UmountEntry>,
|
||||
}
|
||||
|
||||
pub struct UmountManager {
|
||||
config: UmountConfig,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct UmountManagerCmd {
|
||||
pub operation: u32,
|
||||
pub path: [u8; 256],
|
||||
pub flags: i32,
|
||||
pub count: u32,
|
||||
pub entries_ptr: u64,
|
||||
}
|
||||
|
||||
impl Default for UmountManagerCmd {
|
||||
fn default() -> Self {
|
||||
UmountManagerCmd {
|
||||
operation: 0,
|
||||
path: [0; 256],
|
||||
flags: 0,
|
||||
count: 0,
|
||||
entries_ptr: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UmountManager {
|
||||
pub fn new(config_path: Option<PathBuf>) -> Result<Self> {
|
||||
let path = config_path.unwrap_or_else(|| PathBuf::from(CONFIG_FILE));
|
||||
|
||||
let config = if path.exists() {
|
||||
Self::load_config(&path)?
|
||||
} else {
|
||||
UmountConfig {
|
||||
entries: Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(UmountManager {
|
||||
config,
|
||||
config_path: path,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_config(path: &Path) -> Result<UmountConfig> {
|
||||
let data = fs::read(path).context("Failed to read config file")?;
|
||||
|
||||
if data.len() < 8 {
|
||||
return Err(anyhow!("Invalid config file: too small"));
|
||||
}
|
||||
|
||||
let header = &data[0..4];
|
||||
if header != MAGIC_NUMBER_HEADER {
|
||||
return Err(anyhow!("Invalid config file: wrong magic number"));
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
if version != MAGIC_VERSION {
|
||||
return Err(anyhow!("Unsupported config version: {}", version));
|
||||
}
|
||||
|
||||
let json_data = &data[8..];
|
||||
let config: UmountConfig =
|
||||
serde_json::from_slice(json_data).context("Failed to parse config JSON")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
let dir = self.config_path.parent().unwrap();
|
||||
fs::create_dir_all(dir).context("Failed to create config directory")?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(MAGIC_NUMBER_HEADER);
|
||||
data.extend_from_slice(&MAGIC_VERSION.to_le_bytes());
|
||||
|
||||
let json = serde_json::to_vec(&self.config).context("Failed to serialize config")?;
|
||||
data.extend_from_slice(&json);
|
||||
|
||||
fs::write(&self.config_path, &data).context("Failed to write config file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_entry(&mut self, path: &str, flags: i32) -> Result<()> {
|
||||
let exists = self.config.entries.iter().any(|e| e.path == path);
|
||||
if exists {
|
||||
return Err(anyhow!("Entry already exists: {}", path));
|
||||
}
|
||||
|
||||
let entry = UmountEntry {
|
||||
path: path.to_string(),
|
||||
flags,
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
self.config.entries.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_entry(&mut self, path: &str) -> Result<()> {
|
||||
let before = self.config.entries.len();
|
||||
self.config.entries.retain(|e| e.path != path);
|
||||
|
||||
if before == self.config.entries.len() {
|
||||
return Err(anyhow!("Entry not found: {}", path));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_entries(&self) -> Vec<UmountEntry> {
|
||||
self.config.entries.clone()
|
||||
}
|
||||
|
||||
pub fn clear_custom_entries(&mut self) -> Result<()> {
|
||||
self.config.entries.retain(|e| e.is_default);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_to_kernel(&self) -> Result<()> {
|
||||
for entry in &self.config.entries {
|
||||
Self::kernel_add_entry(entry)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kernel_add_entry(entry: &UmountEntry) -> Result<()> {
|
||||
let mut cmd = UmountManagerCmd {
|
||||
operation: 0,
|
||||
flags: entry.flags,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let path_bytes = entry.path.as_bytes();
|
||||
if path_bytes.len() >= cmd.path.len() {
|
||||
return Err(anyhow!("Path too long: {}", entry.path));
|
||||
}
|
||||
|
||||
cmd.path[..path_bytes.len()].copy_from_slice(path_bytes);
|
||||
|
||||
umount_manager_ioctl(&cmd).context(format!("Failed to add entry: {}", entry.path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_umount_manager() -> Result<UmountManager> {
|
||||
let manager = UmountManager::new(None)?;
|
||||
|
||||
if !Path::new(CONFIG_FILE).exists() {
|
||||
manager.save_config()?;
|
||||
}
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
pub fn add_umount_path(path: &str, flags: i32) -> Result<()> {
|
||||
let mut manager = init_umount_manager()?;
|
||||
manager.add_entry(path, flags)?;
|
||||
manager.save_config()?;
|
||||
println!("✓ Added umount path: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_umount_path(path: &str) -> Result<()> {
|
||||
let mut manager = init_umount_manager()?;
|
||||
manager.remove_entry(path)?;
|
||||
manager.save_config()?;
|
||||
println!("✓ Removed umount path: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_umount_paths() -> Result<()> {
|
||||
let manager = init_umount_manager()?;
|
||||
let entries = manager.list_entries();
|
||||
|
||||
if entries.is_empty() {
|
||||
println!("No umount paths configured");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{:<30} {:<8} {:<10}", "Path", "Flags", "Default");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
for entry in entries {
|
||||
println!(
|
||||
"{:<30} {:<8} {:<10}",
|
||||
entry.path,
|
||||
entry.flags,
|
||||
if entry.is_default { "Yes" } else { "No" }
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn umount_manager_ioctl(cmd: &UmountManagerCmd) -> std::io::Result<()> {
|
||||
let mut ioctl_cmd = *cmd;
|
||||
ksuctl(KSU_IOCTL_UMOUNT_MANAGER, &mut ioctl_cmd as *mut _)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn umount_manager_ioctl(_cmd: &UmountManagerCmd) -> std::io::Result<()> {
|
||||
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
|
||||
}
|
||||
|
||||
pub fn clear_custom_paths() -> Result<()> {
|
||||
let mut manager = init_umount_manager()?;
|
||||
manager.clear_custom_entries()?;
|
||||
manager.save_config()?;
|
||||
println!("✓ Cleared all custom paths");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_umount_config() -> Result<()> {
|
||||
let manager = init_umount_manager()?;
|
||||
manager.save_config()?;
|
||||
println!("✓ Configuration saved to: {}", CONFIG_FILE);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_and_apply_config() -> Result<()> {
|
||||
let manager = init_umount_manager()?;
|
||||
manager.apply_to_kernel()?;
|
||||
println!("✓ Configuration applied to kernel");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_config_to_kernel() -> Result<()> {
|
||||
let manager = init_umount_manager()?;
|
||||
manager.apply_to_kernel()?;
|
||||
println!(
|
||||
"✓ Applied {} entries to kernel",
|
||||
manager.list_entries().len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
229
userspace/ksud/src/utils.rs
Normal file
229
userspace/ksud/src/utils.rs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use std::{
|
||||
fs::{File, OpenOptions, create_dir_all, remove_file, write},
|
||||
io::{
|
||||
ErrorKind::{AlreadyExists, NotFound},
|
||||
Write,
|
||||
},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{
|
||||
process,
|
||||
thread::{LinkNameSpaceType, move_into_link_name_space},
|
||||
};
|
||||
|
||||
pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
let path = dir.as_ref();
|
||||
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(std::fs::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) {
|
||||
std::result::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);
|
||||
if dir.as_ref().is_dir() && result.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_binary<T: AsRef<Path>>(
|
||||
path: T,
|
||||
contents: &[u8],
|
||||
ignore_if_exist: bool,
|
||||
) -> Result<()> {
|
||||
if ignore_if_exist && path.as_ref().exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_dir_exists(path.as_ref().parent().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"{} does not have parent directory",
|
||||
path.as_ref().to_string_lossy()
|
||||
)
|
||||
})?)?;
|
||||
|
||||
if let Err(e) = remove_file(path.as_ref())
|
||||
&& e.kind() != NotFound
|
||||
{
|
||||
return Err(Error::from(e))
|
||||
.with_context(|| format!("failed to unlink {}", path.as_ref().display()));
|
||||
}
|
||||
|
||||
write(&path, contents)?;
|
||||
#[cfg(unix)]
|
||||
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 is_safe_mode() -> bool {
|
||||
let safemode = getprop("persist.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some()
|
||||
|| getprop("ro.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some();
|
||||
log::info!("safemode: {safemode}");
|
||||
if safemode {
|
||||
return true;
|
||||
}
|
||||
let safemode = ksucalls::check_kernel_safemode();
|
||||
log::info!("kernel_safemode: {safemode}");
|
||||
safemode
|
||||
}
|
||||
|
||||
pub fn get_zip_uncompressed_size(zip_path: &str) -> Result<u64> {
|
||||
let mut zip = zip::ZipArchive::new(std::fs::File::open(zip_path)?)?;
|
||||
let total: u64 = (0..zip.len())
|
||||
.map(|i| zip.by_index(i).unwrap().size())
|
||||
.sum();
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn switch_mnt_ns(pid: i32) -> Result<()> {
|
||||
use rustix::{
|
||||
fd::AsFd,
|
||||
fs::{Mode, OFlags, open},
|
||||
};
|
||||
let path = format!("/proc/{pid}/ns/mnt");
|
||||
let fd = open(path, OFlags::RDONLY, Mode::from_raw_mode(0))?;
|
||||
let current_dir = std::env::current_dir();
|
||||
move_into_link_name_space(fd.as_fd(), Some(LinkNameSpaceType::Mount))?;
|
||||
if let std::result::Result::Ok(current_dir) = current_dir {
|
||||
let _ = std::env::set_current_dir(current_dir);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 std::result::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) {
|
||||
process::umask(rustix::fs::Mode::from_raw_mode(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()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn link_ksud_to_bin() -> Result<()> {
|
||||
let ksu_bin = PathBuf::from(defs::DAEMON_PATH);
|
||||
let ksu_bin_link = PathBuf::from(defs::DAEMON_LINK_PATH);
|
||||
if ksu_bin.exists() && !ksu_bin_link.exists() {
|
||||
std::os::unix::fs::symlink(&ksu_bin, &ksu_bin_link)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install(magiskboot: Option<PathBuf>) -> Result<()> {
|
||||
ensure_dir_exists(defs::ADB_DIR)?;
|
||||
std::fs::copy(
|
||||
std::env::current_exe().with_context(|| "Failed to get self exe path")?,
|
||||
defs::DAEMON_PATH,
|
||||
)?;
|
||||
restorecon::lsetfilecon(defs::DAEMON_PATH, restorecon::ADB_CON)?;
|
||||
// install binary assets
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
link_ksud_to_bin()?;
|
||||
|
||||
if let Some(magiskboot) = magiskboot {
|
||||
ensure_dir_exists(defs::BINARY_DIR)?;
|
||||
let _ = std::fs::copy(magiskboot, defs::MAGISKBOOT_PATH);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall(magiskboot_path: Option<PathBuf>) -> Result<()> {
|
||||
if Path::new(defs::MODULE_DIR).exists() {
|
||||
println!("- Uninstall modules..");
|
||||
module::uninstall_all_modules()?;
|
||||
module::prune_modules()?;
|
||||
}
|
||||
println!("- Removing directories..");
|
||||
std::fs::remove_dir_all(defs::WORKING_DIR).ok();
|
||||
std::fs::remove_file(defs::DAEMON_PATH).ok();
|
||||
std::fs::remove_dir_all(defs::MODULE_DIR).ok();
|
||||
println!("- Restore boot image..");
|
||||
boot_patch::restore(None, magiskboot_path, true)?;
|
||||
println!("- Uninstall KernelSU manager..");
|
||||
Command::new("pm")
|
||||
.args(["uninstall", "com.sukisu.ultra"])
|
||||
.spawn()?;
|
||||
println!("- Rebooting in 5 seconds..");
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
Command::new("reboot").spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
4
userspace/meta-overlayfs/.gitignore
vendored
Normal file
4
userspace/meta-overlayfs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/out
|
||||
Cargo.lock
|
||||
*.log
|
||||
24
userspace/meta-overlayfs/Cargo.toml
Normal file
24
userspace/meta-overlayfs/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "meta-overlayfs"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
authors = ["KernelSU Developers"]
|
||||
description = "An implementation of a metamodule using OverlayFS for KernelSU"
|
||||
license = "GPL-3.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
hole-punch = { git = "https://github.com/tiann/hole-punch" }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = ["all-apis"] }
|
||||
procfs = "0.17"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z" # Minimize binary size
|
||||
lto = true # Link-time optimization
|
||||
codegen-units = 1 # Maximum optimization
|
||||
panic = "abort" # Reduce binary size
|
||||
58
userspace/meta-overlayfs/README.md
Normal file
58
userspace/meta-overlayfs/README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# meta-overlayfs
|
||||
|
||||
Official overlayfs mount handler for KernelSU metamodules.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
adb push meta-overlayfs-v1.0.0.zip /sdcard/
|
||||
adb shell su -c 'ksud module install /sdcard/meta-overlayfs-v1.0.0.zip'
|
||||
adb reboot
|
||||
```
|
||||
|
||||
Or install via KernelSU Manager → Modules.
|
||||
|
||||
**Note**: The metamodule is now installed as a regular module to `/data/adb/modules/meta-overlay/`, with a symlink created at `/data/adb/metamodule` pointing to it.
|
||||
|
||||
## How It Works
|
||||
|
||||
Uses dual-directory architecture for ext4 image support:
|
||||
|
||||
- **Metadata**: `/data/adb/modules/` - Contains `module.prop`, `disable`, `skip_mount` markers
|
||||
- **Content**: `/data/adb/metamodule/mnt/` - Contains `system/`, `vendor/` etc. directories from ext4 images
|
||||
|
||||
Scans metadata directory for enabled modules, then mounts their content directories as overlayfs layers.
|
||||
|
||||
### Supported Partitions
|
||||
|
||||
system, vendor, product, system_ext, odm, oem
|
||||
|
||||
### Read-Write Layer
|
||||
|
||||
Optional upperdir/workdir support via `/data/adb/modules/.rw/`:
|
||||
|
||||
```bash
|
||||
mkdir -p /data/adb/modules/.rw/system/{upperdir,workdir}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `MODULE_METADATA_DIR` - Metadata location (default: `/data/adb/modules/`)
|
||||
- `MODULE_CONTENT_DIR` - Content location (default: `/data/adb/metamodule/mnt/`)
|
||||
- `RUST_LOG` - Log level (debug, info, warn, error)
|
||||
|
||||
## Architecture
|
||||
|
||||
Automatically selects aarch64 or x86_64 binary during installation (~500KB).
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Output: `target/meta-overlayfs-v1.0.0.zip`
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0
|
||||
92
userspace/meta-overlayfs/build.sh
Normal file
92
userspace/meta-overlayfs/build.sh
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
OUTPUT_DIR="target"
|
||||
METAMODULE_DIR="metamodule"
|
||||
MODULE_OUTPUT_DIR="$OUTPUT_DIR/module"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Building meta-overlayfs v${VERSION}"
|
||||
echo "=========================================="
|
||||
|
||||
# Detect build tool
|
||||
if command -v cross >/dev/null 2>&1; then
|
||||
BUILD_TOOL="cross"
|
||||
echo "Using cross for compilation"
|
||||
else
|
||||
BUILD_TOOL="cargo-ndk"
|
||||
echo "Using cargo ndk for compilation"
|
||||
if ! command -v cargo-ndk >/dev/null 2>&1; then
|
||||
echo "Error: Neither cross nor cargo-ndk found!"
|
||||
echo "Please install one of them:"
|
||||
echo " - cross: cargo install cross"
|
||||
echo " - cargo-ndk: cargo install cargo-ndk"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean output directory
|
||||
echo "Cleaning output directory..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$MODULE_OUTPUT_DIR"
|
||||
|
||||
# Build for multiple architectures
|
||||
echo ""
|
||||
echo "Building for aarch64-linux-android..."
|
||||
if [ "$BUILD_TOOL" = "cross" ]; then
|
||||
cross build --release --target aarch64-linux-android
|
||||
else
|
||||
cargo ndk build -t arm64-v8a --release
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Building for x86_64-linux-android..."
|
||||
if [ "$BUILD_TOOL" = "cross" ]; then
|
||||
cross build --release --target x86_64-linux-android
|
||||
else
|
||||
cargo ndk build -t x86_64 --release
|
||||
fi
|
||||
|
||||
# Copy binaries
|
||||
echo ""
|
||||
echo "Copying binaries..."
|
||||
cp target/aarch64-linux-android/release/meta-overlayfs \
|
||||
"$MODULE_OUTPUT_DIR/meta-overlayfs-aarch64"
|
||||
cp target/x86_64-linux-android/release/meta-overlayfs \
|
||||
"$MODULE_OUTPUT_DIR/meta-overlayfs-x86_64"
|
||||
|
||||
# Copy metamodule files
|
||||
echo "Copying metamodule files..."
|
||||
cp "$METAMODULE_DIR"/module.prop "$MODULE_OUTPUT_DIR/"
|
||||
cp "$METAMODULE_DIR"/*.sh "$MODULE_OUTPUT_DIR/"
|
||||
|
||||
# Set permissions
|
||||
echo "Setting permissions..."
|
||||
chmod 755 "$MODULE_OUTPUT_DIR"/*.sh
|
||||
chmod 755 "$MODULE_OUTPUT_DIR"/meta-overlayfs-*
|
||||
|
||||
# Display binary sizes
|
||||
echo ""
|
||||
echo "Binary sizes:"
|
||||
echo " aarch64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-aarch64 | awk '{print $1}')"
|
||||
echo " x86_64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-x86_64 | awk '{print $1}')"
|
||||
|
||||
# Package
|
||||
echo ""
|
||||
echo "Packaging..."
|
||||
cd "$MODULE_OUTPUT_DIR"
|
||||
ZIP_NAME="meta-overlayfs-v${VERSION}.zip"
|
||||
zip -r "../$ZIP_NAME" .
|
||||
cd ../..
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Build completed successfully!"
|
||||
echo "Output: $OUTPUT_DIR/$ZIP_NAME"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "To install:"
|
||||
echo " adb push $OUTPUT_DIR/$ZIP_NAME /sdcard/"
|
||||
echo " adb shell su -c 'ksud module install /sdcard/$ZIP_NAME'"
|
||||
0
userspace/meta-overlayfs/metamodule/.gitkeep
Normal file
0
userspace/meta-overlayfs/metamodule/.gitkeep
Normal file
67
userspace/meta-overlayfs/metamodule/customize.sh
Normal file
67
userspace/meta-overlayfs/metamodule/customize.sh
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#!/system/bin/sh
|
||||
|
||||
ui_print "- Detecting device architecture..."
|
||||
|
||||
# Detect architecture using ro.product.cpu.abi
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
ui_print "- Detected ABI: $ABI"
|
||||
|
||||
# Select the correct binary based on architecture
|
||||
case "$ABI" in
|
||||
arm64-v8a)
|
||||
ARCH_BINARY="meta-overlayfs-aarch64"
|
||||
REMOVE_BINARY="meta-overlayfs-x86_64"
|
||||
ui_print "- Selected architecture: ARM64"
|
||||
;;
|
||||
x86_64)
|
||||
ARCH_BINARY="meta-overlayfs-x86_64"
|
||||
REMOVE_BINARY="meta-overlayfs-aarch64"
|
||||
ui_print "- Selected architecture: x86_64"
|
||||
;;
|
||||
*)
|
||||
abort "! Unsupported architecture: $ABI"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify the selected binary exists
|
||||
if [ ! -f "$MODPATH/$ARCH_BINARY" ]; then
|
||||
abort "! Binary not found: $ARCH_BINARY"
|
||||
fi
|
||||
|
||||
ui_print "- Installing $ARCH_BINARY as meta-overlayfs"
|
||||
|
||||
# Rename the selected binary to the generic name
|
||||
mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-overlayfs" || abort "! Failed to rename binary"
|
||||
|
||||
# Remove the unused binary
|
||||
rm -f "$MODPATH/$REMOVE_BINARY"
|
||||
|
||||
# Ensure the binary is executable
|
||||
chmod 755 "$MODPATH/meta-overlayfs" || abort "! Failed to set permissions"
|
||||
|
||||
ui_print "- Architecture-specific binary installed successfully"
|
||||
|
||||
# Create ext4 image for module content storage
|
||||
IMG_FILE="$MODPATH/modules.img"
|
||||
IMG_SIZE_MB=2048
|
||||
EXISTING_IMG="/data/adb/modules/$MODID/modules.img"
|
||||
|
||||
if [ -f "$EXISTING_IMG" ]; then
|
||||
ui_print "- Reusing modules image from previous install"
|
||||
"$MODPATH/meta-overlayfs" xcp "$EXISTING_IMG" "$IMG_FILE" || \
|
||||
abort "! Failed to copy existing modules image"
|
||||
else
|
||||
ui_print "- Creating 2GB ext4 image for module storage"
|
||||
|
||||
# Create sparse file (2GB logical size, 0 bytes actual)
|
||||
truncate -s ${IMG_SIZE_MB}M "$IMG_FILE" || \
|
||||
abort "! Failed to create image file"
|
||||
|
||||
# Format as ext4 with small journal (8MB) for safety with minimal overhead
|
||||
/system/bin/mke2fs -t ext4 -J size=8 -F "$IMG_FILE" >/dev/null 2>&1 || \
|
||||
abort "! Failed to format ext4 image"
|
||||
|
||||
ui_print "- Image created successfully (sparse file)"
|
||||
fi
|
||||
|
||||
ui_print "- Installation complete"
|
||||
99
userspace/meta-overlayfs/metamodule/metainstall.sh
Normal file
99
userspace/meta-overlayfs/metamodule/metainstall.sh
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# meta-overlayfs metainstall.sh
|
||||
# Module installation hook for ext4 image support
|
||||
############################################
|
||||
|
||||
# Constants
|
||||
IMG_FILE="/data/adb/metamodule/modules.img"
|
||||
MNT_DIR="/data/adb/metamodule/mnt"
|
||||
|
||||
# Ensure ext4 image is mounted
|
||||
ensure_image_mounted() {
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
ui_print "- Mounting modules image"
|
||||
mkdir -p "$MNT_DIR"
|
||||
mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || {
|
||||
abort "! Failed to mount modules image"
|
||||
}
|
||||
ui_print "- Image mounted successfully"
|
||||
else
|
||||
ui_print "- Image already mounted"
|
||||
fi
|
||||
}
|
||||
|
||||
# Determine whether this module should be moved into the ext4 image.
|
||||
# We only relocate payloads that expose system/ overlays and do not opt out via skip_mount.
|
||||
module_requires_overlay_move() {
|
||||
if [ -f "$MODPATH/skip_mount" ]; then
|
||||
ui_print "- skip_mount flag detected; keeping files under /data/adb/modules"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$MODPATH/system" ]; then
|
||||
ui_print "- No system/ directory detected; keeping files under /data/adb/modules"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Copy SELinux contexts from src tree to destination by mirroring each entry.
|
||||
copy_selinux_contexts() {
|
||||
command -v chcon >/dev/null 2>&1 || return 0
|
||||
|
||||
SRC="$1"
|
||||
DST="$2"
|
||||
|
||||
if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -e "$SRC" ] || [ ! -e "$DST" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
chcon --reference="$SRC" "$DST" 2>/dev/null || true
|
||||
|
||||
find "$SRC" -print | while IFS= read -r PATH_SRC; do
|
||||
if [ "$PATH_SRC" = "$SRC" ]; then
|
||||
continue
|
||||
fi
|
||||
REL_PATH="${PATH_SRC#"${SRC}/"}"
|
||||
PATH_DST="$DST/$REL_PATH"
|
||||
if [ -e "$PATH_DST" ] || [ -L "$PATH_DST" ]; then
|
||||
chcon --reference="$PATH_SRC" "$PATH_DST" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Post-installation: move partition directories to ext4 image
|
||||
post_install_to_image() {
|
||||
ui_print "- Copying module content to image"
|
||||
|
||||
set_perm_recursive "$MNT_DIR" 0 0 0755 0644
|
||||
|
||||
MOD_IMG_DIR="$MNT_DIR/$MODID"
|
||||
mkdir -p "$MOD_IMG_DIR"
|
||||
|
||||
# Move all partition directories
|
||||
for partition in system vendor product system_ext odm oem; do
|
||||
if [ -d "$MODPATH/$partition" ]; then
|
||||
ui_print "- Copying $partition/"
|
||||
cp -af "$MODPATH/$partition" "$MOD_IMG_DIR/" || {
|
||||
ui_print "! Warning: Failed to move $partition"
|
||||
continue
|
||||
}
|
||||
copy_selinux_contexts "$MODPATH/$partition" "$MOD_IMG_DIR/$partition"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
ui_print "- Using meta-overlayfs metainstall"
|
||||
|
||||
install_module
|
||||
|
||||
if module_requires_overlay_move; then
|
||||
ensure_image_mounted
|
||||
post_install_to_image
|
||||
else
|
||||
ui_print "- Skipping move to modules image"
|
||||
fi
|
||||
|
||||
ui_print "- Installation complete"
|
||||
65
userspace/meta-overlayfs/metamodule/metamount.sh
Normal file
65
userspace/meta-overlayfs/metamodule/metamount.sh
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/system/bin/sh
|
||||
# meta-overlayfs Module Mount Handler
|
||||
# This script is the entry point for dual-directory module mounting
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
IMG_FILE="$MODDIR/modules.img"
|
||||
MNT_DIR="$MODDIR/mnt"
|
||||
|
||||
# Log function
|
||||
log() {
|
||||
echo "[meta-overlayfs] $1"
|
||||
}
|
||||
|
||||
log "Starting module mount process"
|
||||
|
||||
# Ensure ext4 image is mounted
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
log "Image not mounted, mounting now..."
|
||||
|
||||
# Check if image file exists
|
||||
if [ ! -f "$IMG_FILE" ]; then
|
||||
log "ERROR: Image file not found at $IMG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create mount point
|
||||
mkdir -p "$MNT_DIR"
|
||||
|
||||
# Mount the ext4 image
|
||||
mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || {
|
||||
log "ERROR: Failed to mount image"
|
||||
exit 1
|
||||
}
|
||||
log "Image mounted successfully at $MNT_DIR"
|
||||
else
|
||||
log "Image already mounted at $MNT_DIR"
|
||||
fi
|
||||
|
||||
# Binary path (architecture-specific binary selected during installation)
|
||||
BINARY="$MODDIR/meta-overlayfs"
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
log "ERROR: Binary not found: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set dual-directory environment variables
|
||||
export MODULE_METADATA_DIR="/data/adb/modules"
|
||||
export MODULE_CONTENT_DIR="$MNT_DIR"
|
||||
|
||||
log "Metadata directory: $MODULE_METADATA_DIR"
|
||||
log "Content directory: $MODULE_CONTENT_DIR"
|
||||
log "Executing $BINARY"
|
||||
|
||||
# Execute the mount binary
|
||||
"$BINARY"
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
log "Mount failed with exit code $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
|
||||
log "Mount completed successfully"
|
||||
exit 0
|
||||
35
userspace/meta-overlayfs/metamodule/metauninstall.sh
Normal file
35
userspace/meta-overlayfs/metamodule/metauninstall.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-overlayfs metauninstall.sh
|
||||
# Module uninstallation hook for ext4 image cleanup
|
||||
############################################
|
||||
|
||||
# Constants
|
||||
MNT_DIR="/data/adb/metamodule/mnt"
|
||||
|
||||
if [ -z "$MODULE_ID" ]; then
|
||||
echo "! Error: MODULE_ID not provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "- Cleaning up module content from image: $MODULE_ID"
|
||||
|
||||
# Check if image is mounted
|
||||
if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
echo "! Warning: Image not mounted, skipping cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remove module content from image
|
||||
MOD_IMG_DIR="$MNT_DIR/$MODULE_ID"
|
||||
if [ -d "$MOD_IMG_DIR" ]; then
|
||||
echo " Removing $MOD_IMG_DIR"
|
||||
rm -rf "$MOD_IMG_DIR" || {
|
||||
echo "! Warning: Failed to remove module content from image"
|
||||
}
|
||||
echo "- Module content removed from image"
|
||||
else
|
||||
echo "- No module content found in image, skipping"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
8
userspace/meta-overlayfs/metamodule/module.prop
Normal file
8
userspace/meta-overlayfs/metamodule/module.prop
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id=meta-overlayfs
|
||||
metamodule=1
|
||||
name=OverlayFS MetaModule
|
||||
version=1.0.0
|
||||
versionCode=1
|
||||
author=KernelSU Developers
|
||||
description=An implementation of a metamodule using OverlayFS for KernelSU
|
||||
updateJson=https://raw.githubusercontent.com/tiann/KernelSU/main/userspace/meta-overlayfs/update.json
|
||||
3
userspace/meta-overlayfs/metamodule/post-mount.sh
Normal file
3
userspace/meta-overlayfs/metamodule/post-mount.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/system/bin/sh
|
||||
|
||||
ksud kernel nuke-ext4-sysfs /data/adb/modules/meta-overlayfs/mnt
|
||||
24
userspace/meta-overlayfs/metamodule/uninstall.sh
Normal file
24
userspace/meta-overlayfs/metamodule/uninstall.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-overlayfs uninstall.sh
|
||||
# Cleanup script for metamodule removal
|
||||
############################################
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
MNT_DIR="$MODDIR/mnt"
|
||||
|
||||
echo "- Uninstalling metamodule..."
|
||||
|
||||
# Unmount the ext4 image if mounted
|
||||
if mountpoint -q "$MNT_DIR" 2>/dev/null; then
|
||||
echo "- Unmounting image..."
|
||||
umount "$MNT_DIR" 2>/dev/null || {
|
||||
echo "- Warning: Failed to unmount cleanly"
|
||||
umount -l "$MNT_DIR" 2>/dev/null
|
||||
}
|
||||
echo "- Image unmounted"
|
||||
fi
|
||||
|
||||
echo "- Uninstall complete"
|
||||
|
||||
exit 0
|
||||
17
userspace/meta-overlayfs/src/defs.rs
Normal file
17
userspace/meta-overlayfs/src/defs.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Constants for KernelSU module mounting
|
||||
|
||||
// Dual-directory support for ext4 image
|
||||
pub const MODULE_METADATA_DIR: &str = "/data/adb/modules/";
|
||||
pub const MODULE_CONTENT_DIR: &str = "/data/adb/metamodule/mnt/";
|
||||
|
||||
// Legacy constant (for backwards compatibility)
|
||||
pub const _MODULE_DIR: &str = "/data/adb/modules/";
|
||||
|
||||
// Status marker files
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const _REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
// System directories
|
||||
pub const SYSTEM_RW_DIR: &str = "/data/adb/modules/.rw/";
|
||||
pub const KSU_OVERLAY_SOURCE: &str = "KSU";
|
||||
35
userspace/meta-overlayfs/src/main.rs
Normal file
35
userspace/meta-overlayfs/src/main.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
mod defs;
|
||||
mod mount;
|
||||
mod xcp;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if matches!(args.get(1), Some(cmd) if cmd == "xcp") {
|
||||
return xcp::run(&args[2..]);
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.init();
|
||||
|
||||
info!("meta-overlayfs v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Dual-directory support: metadata + content
|
||||
let metadata_dir = std::env::var("MODULE_METADATA_DIR")
|
||||
.unwrap_or_else(|_| defs::MODULE_METADATA_DIR.to_string());
|
||||
let content_dir = std::env::var("MODULE_CONTENT_DIR")
|
||||
.unwrap_or_else(|_| defs::MODULE_CONTENT_DIR.to_string());
|
||||
|
||||
info!("Metadata directory: {}", metadata_dir);
|
||||
info!("Content directory: {}", content_dir);
|
||||
|
||||
// Execute dual-directory mounting
|
||||
mount::mount_modules_systemlessly(&metadata_dir, &content_dir)?;
|
||||
|
||||
info!("Mount completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
376
userspace/meta-overlayfs/src/mount.rs
Normal file
376
userspace/meta-overlayfs/src/mount.rs
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
// Overlayfs mounting implementation
|
||||
// Migrated from ksud/src/mount.rs and ksud/src/init_event.rs
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use log::{info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use procfs::process::Process;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{fd::AsFd, fs::CWD, mount::*};
|
||||
|
||||
use crate::defs::{DISABLE_FILE_NAME, KSU_OVERLAY_SOURCE, SKIP_MOUNT_FILE_NAME, SYSTEM_RW_DIR};
|
||||
|
||||
#[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", KSU_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: {e:#}, fallback to mount");
|
||||
let mut data = format!("lowerdir={lowerdir_config}");
|
||||
if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) {
|
||||
data = format!("{data},upperdir={upperdir},workdir={workdir}");
|
||||
}
|
||||
mount(
|
||||
KSU_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"overlay",
|
||||
MountFlags::empty(),
|
||||
data,
|
||||
)?;
|
||||
}
|
||||
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()
|
||||
);
|
||||
let tree = open_tree(
|
||||
CWD,
|
||||
from.as_ref(),
|
||||
OpenTreeFlags::OPEN_TREE_CLOEXEC
|
||||
| OpenTreeFlags::OPEN_TREE_CLONE
|
||||
| OpenTreeFlags::AT_RECURSIVE,
|
||||
)?;
|
||||
move_mount(
|
||||
tree.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
to.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
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: {e:#}, fallback to bind mount");
|
||||
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 {mount_point}: {e:#}, revert");
|
||||
umount_dir(root).with_context(|| format!("failed to revert {root}"))?;
|
||||
bail!(e);
|
||||
}
|
||||
}
|
||||
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(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_overlay(
|
||||
_root: &String,
|
||||
_module_roots: &Vec<String>,
|
||||
_workdir: Option<PathBuf>,
|
||||
_upperdir: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
unimplemented!("mount_overlay is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
#[cfg(not(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<()> {
|
||||
unimplemented!("mount_overlayfs is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn bind_mount(_from: impl AsRef<Path>, _to: impl AsRef<Path>) -> Result<()> {
|
||||
unimplemented!("bind_mount is only supported on Linux/Android")
|
||||
}
|
||||
|
||||
// ========== Mount coordination logic (from init_event.rs) ==========
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
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(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_overlay(&partition, lowerdir, workdir, upperdir)
|
||||
}
|
||||
|
||||
/// Collect enabled module IDs from metadata directory
|
||||
///
|
||||
/// Reads module list and status from metadata directory, returns enabled module IDs
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn collect_enabled_modules(metadata_dir: &str) -> Result<Vec<String>> {
|
||||
let dir = std::fs::read_dir(metadata_dir)
|
||||
.with_context(|| format!("Failed to read metadata directory: {}", metadata_dir))?;
|
||||
|
||||
let mut enabled = Vec::new();
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let module_id = match entry.file_name().to_str() {
|
||||
Some(id) => id.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Check status markers
|
||||
if path.join(DISABLE_FILE_NAME).exists() {
|
||||
info!("Module {} is disabled, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.join(SKIP_MOUNT_FILE_NAME).exists() {
|
||||
info!("Module {} has skip_mount, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional: verify module.prop exists
|
||||
if !path.join("module.prop").exists() {
|
||||
warn!("Module {} has no module.prop, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Module {} enabled", module_id);
|
||||
enabled.push(module_id);
|
||||
}
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Dual-directory version of mount_modules_systemlessly
|
||||
///
|
||||
/// Parameters:
|
||||
/// - metadata_dir: Metadata directory, stores module.prop, disable, skip_mount, etc.
|
||||
/// - content_dir: Content directory, stores system/, vendor/ and other partition content (ext4 image mount point)
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_modules_systemlessly(metadata_dir: &str, content_dir: &str) -> Result<()> {
|
||||
info!("Scanning modules (dual-directory mode)");
|
||||
info!(" Metadata: {}", metadata_dir);
|
||||
info!(" Content: {}", content_dir);
|
||||
|
||||
// 1. Traverse metadata directory, collect enabled module IDs
|
||||
let enabled_modules = collect_enabled_modules(metadata_dir)?;
|
||||
|
||||
if enabled_modules.is_empty() {
|
||||
info!("No enabled modules found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Found {} enabled module(s)", enabled_modules.len());
|
||||
|
||||
// 2. Initialize partition lowerdir lists
|
||||
let partition = vec!["vendor", "product", "system_ext", "odm", "oem"];
|
||||
let mut system_lowerdir: Vec<String> = Vec::new();
|
||||
let mut partition_lowerdir: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for part in &partition {
|
||||
partition_lowerdir.insert((*part).to_string(), Vec::new());
|
||||
}
|
||||
|
||||
// 3. Read module content from content directory
|
||||
for module_id in &enabled_modules {
|
||||
let module_content_path = Path::new(content_dir).join(module_id);
|
||||
|
||||
if !module_content_path.exists() {
|
||||
warn!("Module {} has no content directory, skipping", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Processing module: {}", module_id);
|
||||
|
||||
// Collect system partition
|
||||
let system_path = module_content_path.join("system");
|
||||
if system_path.is_dir() {
|
||||
system_lowerdir.push(system_path.display().to_string());
|
||||
info!(" + system/");
|
||||
}
|
||||
|
||||
// Collect other partitions
|
||||
for part in &partition {
|
||||
let part_path = module_content_path.join(part);
|
||||
if part_path.is_dir()
|
||||
&& let Some(v) = partition_lowerdir.get_mut(*part)
|
||||
{
|
||||
v.push(part_path.display().to_string());
|
||||
info!(" + {}/", part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Mount partitions
|
||||
info!("Mounting partitions...");
|
||||
|
||||
if let Err(e) = mount_partition("system", &system_lowerdir) {
|
||||
warn!("mount system failed: {e:#}");
|
||||
}
|
||||
|
||||
for (k, v) in partition_lowerdir {
|
||||
if let Err(e) = mount_partition(&k, &v) {
|
||||
warn!("mount {k} failed: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
info!("All partitions processed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_modules_systemlessly(_metadata_dir: &str, _content_dir: &str) -> Result<()> {
|
||||
unimplemented!("mount_modules_systemlessly is only supported on Linux/Android")
|
||||
}
|
||||
90
userspace/meta-overlayfs/src/xcp.rs
Normal file
90
userspace/meta-overlayfs/src/xcp.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use hole_punch::*;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Handle the `xcp` command: copy sparse file with optional hole punching.
|
||||
pub fn run(args: &[String]) -> Result<()> {
|
||||
let mut positional: Vec<&str> = Vec::with_capacity(2);
|
||||
let mut punch_hole = false;
|
||||
|
||||
for arg in args {
|
||||
match arg.as_str() {
|
||||
"--punch-hole" => punch_hole = true,
|
||||
"-h" | "--help" => {
|
||||
print_usage();
|
||||
return Ok(());
|
||||
}
|
||||
_ => positional.push(arg),
|
||||
}
|
||||
}
|
||||
|
||||
if positional.len() < 2 {
|
||||
print_usage();
|
||||
bail!("xcp requires source and destination paths");
|
||||
}
|
||||
if positional.len() > 2 {
|
||||
bail!("unexpected argument: {}", positional[2]);
|
||||
}
|
||||
|
||||
copy_sparse_file(positional[0], positional[1], punch_hole)
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!("Usage: meta-overlayfs xcp <src> <dst> [--punch-hole]");
|
||||
}
|
||||
|
||||
// TODO: use libxcp to improve the speed if cross's MSRV is 1.70
|
||||
pub fn copy_sparse_file<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
src: P,
|
||||
dst: Q,
|
||||
punch_hole: bool,
|
||||
) -> Result<()> {
|
||||
let mut src_file = File::open(src.as_ref())
|
||||
.with_context(|| format!("failed to open {}", src.as_ref().display()))?;
|
||||
let mut dst_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(dst.as_ref())
|
||||
.with_context(|| format!("failed to open {}", dst.as_ref().display()))?;
|
||||
|
||||
dst_file.set_len(src_file.metadata()?.len())?;
|
||||
|
||||
let segments = src_file.scan_chunks()?;
|
||||
for segment in segments {
|
||||
if let SegmentType::Data = segment.segment_type {
|
||||
let start = segment.start;
|
||||
let end = segment.end + 1;
|
||||
|
||||
src_file.seek(SeekFrom::Start(start))?;
|
||||
dst_file.seek(SeekFrom::Start(start))?;
|
||||
|
||||
let mut buffer = [0; 4096];
|
||||
let mut total_bytes_copied = 0;
|
||||
|
||||
while total_bytes_copied < end - start {
|
||||
let bytes_to_read =
|
||||
std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied);
|
||||
let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?;
|
||||
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if punch_hole && buffer[..bytes_read].iter().all(|&x| x == 0) {
|
||||
dst_file.seek(SeekFrom::Current(bytes_read as i64))?;
|
||||
total_bytes_copied += bytes_read as u64;
|
||||
continue;
|
||||
}
|
||||
dst_file.write_all(&buffer[..bytes_read])?;
|
||||
total_bytes_copied += bytes_read as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
2
userspace/su/.gitignore
vendored
Normal file
2
userspace/su/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/obj
|
||||
/libs
|
||||
6
userspace/su/jni/Android.mk
Normal file
6
userspace/su/jni/Android.mk
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := su
|
||||
LOCAL_SRC_FILES := su.c
|
||||
include $(BUILD_EXECUTABLE)
|
||||
3
userspace/su/jni/Application.mk
Normal file
3
userspace/su/jni/Application.mk
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
APP_ABI := arm64-v8a x86_64 armeabi-v7a
|
||||
APP_PLATFORM := android-24
|
||||
APP_STL := none
|
||||
54
userspace/su/jni/su.c
Normal file
54
userspace/su/jni/su.c
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/xattr.h>
|
||||
#include <limits.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <termios.h>
|
||||
|
||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||
#define CMD_GRANT_ROOT 0
|
||||
#define CMD_ENABLE_SU 15
|
||||
|
||||
int main(int argc, char **argv, char **envp) {
|
||||
unsigned long result = 0;
|
||||
|
||||
if (argc >= 2 && strcmp(argv[1], "--disable-sucompat") == 0) {
|
||||
prctl(KERNEL_SU_OPTION, CMD_ENABLE_SU, 0L, 0L, (unsigned long)&result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
prctl(KERNEL_SU_OPTION, CMD_GRANT_ROOT, 0L, 0L, (unsigned long)&result);
|
||||
if (result != KERNEL_SU_OPTION) {
|
||||
const char *error = "Access Denied: sucompat not permitted\n";
|
||||
write(STDERR_FILENO, error, strlen(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct termios term;
|
||||
if (ioctl(STDIN_FILENO, TCGETS, &term) == 0) {
|
||||
char tty_path[PATH_MAX];
|
||||
ssize_t len = readlink("/proc/self/fd/0", tty_path, sizeof(tty_path) - 1);
|
||||
if (len > 0) {
|
||||
tty_path[len] = '\0';
|
||||
const char *selinux_ctx = "u:object_r:devpts:s0";
|
||||
setxattr(tty_path, "security.selinux", selinux_ctx, strlen(selinux_ctx) + 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const char *default_args[] = { "/system/bin/su", NULL };
|
||||
if (argc < 1 || !argv) {
|
||||
argv = (char **)default_args;
|
||||
} else {
|
||||
argv[0] = "/system/bin/su";
|
||||
}
|
||||
|
||||
execve("/data/adb/ksud", argv, envp);
|
||||
|
||||
const char *error = "Error: Failed to execve /data/adb/ksud\n";
|
||||
write(STDERR_FILENO, error, strlen(error));
|
||||
return 1;
|
||||
}
|
||||
2
userspace/user_scanner/.gitignore
vendored
Normal file
2
userspace/user_scanner/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/obj
|
||||
/libs
|
||||
8
userspace/user_scanner/jni/Android.mk
Normal file
8
userspace/user_scanner/jni/Android.mk
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := uid_scanner
|
||||
LOCAL_SRC_FILES := uid_scanner.c
|
||||
LOCAL_LDLIBS := -llog
|
||||
LOCAL_CFLAGS := -Wall -Wextra -std=c99
|
||||
include $(BUILD_EXECUTABLE)
|
||||
3
userspace/user_scanner/jni/Application.mk
Normal file
3
userspace/user_scanner/jni/Application.mk
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
APP_ABI := arm64-v8a x86_64 armeabi-v7a
|
||||
APP_PLATFORM := android-35
|
||||
APP_STL := none
|
||||
996
userspace/user_scanner/jni/uid_scanner.c
Normal file
996
userspace/user_scanner/jni/uid_scanner.c
Normal file
|
|
@ -0,0 +1,996 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <android/log.h>
|
||||
#include <time.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#define LOG_TAG "User_UID_Scanner"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
// Paths and constants
|
||||
#define USER_DATA_BASE_PATH "/data/user_de"
|
||||
#define KSU_UID_LIST_PATH "/data/misc/user_uid/uid_list"
|
||||
#define PROC_COMM_PATH "/proc/ksu_uid_scanner"
|
||||
#define PID_FILE_PATH "/data/misc/user_uid/uid_scanner.pid"
|
||||
#define LOG_FILE_PATH "/data/misc/user_uid/uid_scanner.log"
|
||||
#define CONFIG_FILE_PATH "/data/misc/user_uid/uid_scanner.conf"
|
||||
|
||||
#define MAX_PACKAGE_NAME 256
|
||||
#define MAX_PATH_LEN 512
|
||||
#define MAX_LOG_SIZE (1024 * 1024) // 1MB
|
||||
#define MAX_USERS 8
|
||||
#define MAX_RETRIES 3
|
||||
#define RETRY_DELAY 60
|
||||
|
||||
typedef enum {
|
||||
LANG_EN = 0,
|
||||
LANG_ZH = 1
|
||||
} language_t;
|
||||
|
||||
struct scanner_config {
|
||||
language_t language;
|
||||
int multi_user_scan;
|
||||
int scan_interval;
|
||||
int log_level;
|
||||
int auto_scan;
|
||||
};
|
||||
|
||||
struct uid_data {
|
||||
int uid;
|
||||
char package[MAX_PACKAGE_NAME];
|
||||
struct uid_data *next;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char *en;
|
||||
const char *zh;
|
||||
} message_t;
|
||||
|
||||
// Global variables
|
||||
static volatile int manual_scan_flag = 0;
|
||||
static volatile int should_exit = 0;
|
||||
static volatile int should_reload = 0;
|
||||
static struct uid_data *uid_list_head = NULL;
|
||||
static int log_fd = -1;
|
||||
|
||||
static struct scanner_config config = {
|
||||
.language = LANG_EN,
|
||||
.multi_user_scan = 0,
|
||||
.scan_interval = 5,
|
||||
.log_level = 1,
|
||||
.auto_scan = 0
|
||||
};
|
||||
|
||||
int save_config(void);
|
||||
|
||||
// message dictionary
|
||||
static const message_t messages[] = {
|
||||
{"Signal %d received", "收到信号 %d"},
|
||||
{"Reload signal", "重载信号"},
|
||||
{"User signal", "用户信号"},
|
||||
{"Log rotated", "日志轮转"},
|
||||
{"Fork failed: %s", "Fork失败: %s"},
|
||||
{"setsid failed: %s", "setsid失败: %s"},
|
||||
{"Second fork failed: %s", "第二次fork失败: %s"},
|
||||
{"chdir failed: %s", "目录切换失败: %s"},
|
||||
{"PID file create failed %s: %s", "PID文件创建失败 %s: %s"},
|
||||
{"PID file created: %d", "PID文件已创建: %d"},
|
||||
{"Daemon not running", "守护进程未运行"},
|
||||
{"Stopping daemon (PID: %d)", "停止守护进程 (PID: %d)"},
|
||||
{"Kill signal failed: %s", "终止信号失败: %s"},
|
||||
{"Daemon stopped", "守护进程已停止"},
|
||||
{"Force terminating", "强制终止中"},
|
||||
{"Daemon killed", "守护进程已杀死"},
|
||||
{"Cannot stop daemon", "无法停止守护进程"},
|
||||
{"Restarting daemon", "重启守护进程"},
|
||||
{"Cannot stop old daemon", "无法停止旧守护进程"},
|
||||
{"Starting new daemon", "启动新守护进程"},
|
||||
{"Status: Not running", "状态: 未运行"},
|
||||
{"Status: Running (PID: %d)", "状态: 运行中 (PID: %d)"},
|
||||
{"Recent logs:", "最近日志:"},
|
||||
{"Status: Stopped (stale PID)", "状态: 已停止 (陈旧PID)"},
|
||||
{"Sending reload signal (PID: %d)", "发送重载信号 (PID: %d)"},
|
||||
{"Reload signal sent", "重载信号已发送"},
|
||||
{"Reload signal failed: %s", "重载信号失败: %s"},
|
||||
{"Directory open failed %s: %s", "目录打开失败 %s: %s"},
|
||||
{"Scan started", "扫描开始"},
|
||||
{"Package name too long: %s", "包名过长: %s"},
|
||||
{"File stat failed %s: %s", "文件状态获取失败 %s: %s"},
|
||||
{"Memory allocation failed", "内存分配失败"},
|
||||
{"Scan complete, found %d packages", "扫描完成,发现 %d 个包"},
|
||||
{"Whitelist file open failed %s: %s", "白名单文件打开失败 %s: %s"},
|
||||
{"Whitelist written %d entries", "白名单写入 %d 个条目"},
|
||||
{"Kernel comm file open failed %s: %s", "内核通信文件打开失败 %s: %s"},
|
||||
{"Kernel comm write failed %s: %s", "内核通信写入失败 %s: %s"},
|
||||
{"Kernel notified", "内核已通知"},
|
||||
{"Performing scan and update", "执行扫描和更新"},
|
||||
{"Scan failed", "扫描失败"},
|
||||
{"Whitelist write failed", "白名单写入失败"},
|
||||
{"Scan completed successfully", "扫描成功完成"},
|
||||
{"Whitelist not found: %s", "白名单未找到: %s"},
|
||||
{"Current whitelist:", "当前白名单:"},
|
||||
{"One-time scan", "一次性扫描"},
|
||||
{"Invalid argument: %s", "无效参数: %s"},
|
||||
{"Daemon already running", "守护进程已运行"},
|
||||
{"Starting daemon", "启动守护进程"},
|
||||
{"Daemon startup failed", "守护进程启动失败"},
|
||||
{"Daemon started", "守护进程已启动"},
|
||||
{"Reload request received", "收到重载请求"},
|
||||
{"Kernel rescan request", "内核重扫描请求"},
|
||||
{"Daemon exiting", "守护进程退出中"},
|
||||
{"Daemon exited", "守护进程已退出"},
|
||||
{"Config loaded", "配置已加载"},
|
||||
{"Config saved", "配置已保存"},
|
||||
{"Config load failed: %s", "配置加载失败: %s"},
|
||||
{"Config save failed: %s", "配置保存失败: %s"},
|
||||
{"Language switched to English", "语言切换到英文"},
|
||||
{"Language switched to Chinese", "语言切换到中文"},
|
||||
{"Multi-user scan enabled", "多用户扫描启用"},
|
||||
{"Multi-user scan disabled", "多用户扫描禁用"},
|
||||
{"Scanning directory: %s", "扫描目录: %s"},
|
||||
{"Found %d users", "发现 %d 个用户"},
|
||||
{"Using fallback user detection", "使用备用用户检测"},
|
||||
{"Auto scan enabled", "自动扫描启用"},
|
||||
{"Auto scan disabled", "自动扫描禁用"},
|
||||
{"Auto scan disabled, daemon loaded", "自动扫描禁用,守护进程已加载"},
|
||||
{"Auto scan disabled, skipping", "自动扫描禁用,跳过"},
|
||||
{"Auto scan disabled, ignoring kernel request", "自动扫描禁用,忽略内核请求"},
|
||||
{"Retry attempt %d/%d", "重试 %d/%d"},
|
||||
{"Max retries reached, waiting %d seconds", "达到最大重试次数,等待 %d 秒"},
|
||||
{"Operation failed after retries", "重试后操作失败"},
|
||||
{"Auto scan disabled, operation not allowed", "自动扫描禁用,操作不被允许"},
|
||||
{"Manual scan requested, ignoring auto_scan setting", "手动扫描请求,忽略自动扫描设置"}
|
||||
};
|
||||
|
||||
#define MSG_COUNT (sizeof(messages) / sizeof(messages[0]))
|
||||
|
||||
const char* get_message(int msg_id) {
|
||||
if (msg_id < 0 || msg_id >= (int)MSG_COUNT) {
|
||||
return "Unknown message";
|
||||
}
|
||||
return (config.language == LANG_ZH) ? messages[msg_id].zh : messages[msg_id].en;
|
||||
}
|
||||
|
||||
void write_log(const char *level, int msg_id, ...) {
|
||||
char buffer[1024];
|
||||
char formatted_msg[1024];
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm_info = localtime(&now);
|
||||
va_list args;
|
||||
|
||||
va_start(args, msg_id);
|
||||
vsnprintf(formatted_msg, sizeof(formatted_msg), get_message(msg_id), args);
|
||||
va_end(args);
|
||||
|
||||
strftime(buffer, 64, "[%H:%M:%S]", tm_info);
|
||||
snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " %s: %s", level, formatted_msg);
|
||||
|
||||
if (log_fd != -1) {
|
||||
dprintf(log_fd, "%s\n", buffer);
|
||||
fsync(log_fd);
|
||||
}
|
||||
|
||||
if (strcmp(level, "ERROR") == 0) {
|
||||
LOGE("%s", formatted_msg);
|
||||
} else {
|
||||
LOGI("%s", formatted_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Retry wrapper for operations
|
||||
int retry_operation(int (*operation)(void), const char *op_name) {
|
||||
(void)op_name;
|
||||
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
int result = operation();
|
||||
if (result == 0) {
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
write_log("WARN", 69, attempt, MAX_RETRIES); // Retry attempt X/Y
|
||||
sleep(1);
|
||||
} else {
|
||||
write_log("ERROR", 70, RETRY_DELAY); // Max retries reached
|
||||
sleep(RETRY_DELAY);
|
||||
write_log("ERROR", 71); // Operation failed after retries
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ensure_directory_exists(void) {
|
||||
const char *path = "/data/misc/user_uid";
|
||||
struct stat st;
|
||||
|
||||
if (stat(path, &st) != 0) {
|
||||
if (mkdir(path, 0777) != 0) {
|
||||
LOGE("Failed to create directory %s: %s", path, strerror(errno));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (chmod(path, 0777) != 0) {
|
||||
LOGE("Failed to chmod directory %s: %s", path, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
void parse_config_line(const char *key, const char *value) {
|
||||
if (strcmp(key, "language") == 0) {
|
||||
config.language = (strcmp(value, "zh") == 0) ? LANG_ZH : LANG_EN;
|
||||
} else if (strcmp(key, "multi_user_scan") == 0) {
|
||||
config.multi_user_scan = atoi(value);
|
||||
} else if (strcmp(key, "scan_interval") == 0) {
|
||||
config.scan_interval = atoi(value);
|
||||
if (config.scan_interval < 1) config.scan_interval = 5;
|
||||
} else if (strcmp(key, "log_level") == 0) {
|
||||
config.log_level = atoi(value);
|
||||
} else if (strcmp(key, "auto_scan") == 0) {
|
||||
config.auto_scan = atoi(value);
|
||||
}
|
||||
}
|
||||
|
||||
int load_config(void) {
|
||||
FILE *fp = fopen(CONFIG_FILE_PATH, "r");
|
||||
if (!fp) {
|
||||
write_log("WARN", 56, "配置文件不存在,使用默认配置");
|
||||
return save_config();
|
||||
}
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
if (line[0] == '#' || line[0] == '\0') continue;
|
||||
|
||||
char key[64], value[64];
|
||||
if (sscanf(line, "%63[^=]=%63s", key, value) == 2) {
|
||||
parse_config_line(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
write_log("INFO", 54); // Config loaded
|
||||
write_log("INFO", config.auto_scan ? 64 : 65); // 记录当前自动扫描状态
|
||||
return 0;
|
||||
}
|
||||
|
||||
int save_config(void) {
|
||||
ensure_directory_exists();
|
||||
|
||||
FILE *fp = fopen(CONFIG_FILE_PATH, "w");
|
||||
if (!fp) {
|
||||
write_log("ERROR", 57, strerror(errno)); // Config save failed
|
||||
return -1;
|
||||
}
|
||||
|
||||
fprintf(fp, "# UID Scanner Configuration\n");
|
||||
fprintf(fp, "# Language: en (English) or zh (Chinese)\n");
|
||||
fprintf(fp, "language=%s\n", (config.language == LANG_ZH) ? "zh" : "en");
|
||||
fprintf(fp, "# Multi-user scanning: 0=disabled, 1=enabled\n");
|
||||
fprintf(fp, "multi_user_scan=%d\n", config.multi_user_scan);
|
||||
fprintf(fp, "# Scan interval in seconds\n");
|
||||
fprintf(fp, "scan_interval=%d\n", config.scan_interval);
|
||||
fprintf(fp, "# Log level: 0=minimal, 1=normal, 2=verbose\n");
|
||||
fprintf(fp, "log_level=%d\n", config.log_level);
|
||||
fprintf(fp, "# Auto scan: 0=disabled, 1=enabled\n");
|
||||
fprintf(fp, "auto_scan=%d\n", config.auto_scan);
|
||||
|
||||
fclose(fp);
|
||||
write_log("INFO", 55); // Config saved
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_language(language_t lang) {
|
||||
config.language = lang;
|
||||
save_config();
|
||||
write_log("INFO", (lang == LANG_ZH) ? 59 : 58);
|
||||
}
|
||||
|
||||
void set_multi_user_scan(int enabled) {
|
||||
config.multi_user_scan = enabled;
|
||||
save_config();
|
||||
write_log("INFO", enabled ? 60 : 61);
|
||||
}
|
||||
|
||||
void set_auto_scan(int enabled) {
|
||||
config.auto_scan = enabled;
|
||||
save_config();
|
||||
write_log("INFO", enabled ? 64 : 65);
|
||||
}
|
||||
|
||||
void signal_handler(int sig) {
|
||||
switch (sig) {
|
||||
case SIGTERM:
|
||||
case SIGINT:
|
||||
should_exit = 1;
|
||||
write_log("INFO", 0, sig);
|
||||
break;
|
||||
case SIGHUP:
|
||||
should_reload = 1;
|
||||
write_log("INFO", 1);
|
||||
break;
|
||||
case SIGUSR1:
|
||||
should_reload = 1;
|
||||
write_log("INFO", 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void manage_log_file(void) {
|
||||
struct stat st;
|
||||
if (log_fd == -1 || fstat(log_fd, &st) != 0) return;
|
||||
|
||||
if (st.st_size > MAX_LOG_SIZE) {
|
||||
close(log_fd);
|
||||
char backup_path[MAX_PATH_LEN];
|
||||
snprintf(backup_path, sizeof(backup_path), "%s.old", LOG_FILE_PATH);
|
||||
rename(LOG_FILE_PATH, backup_path);
|
||||
log_fd = open(LOG_FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
if (log_fd != -1) {
|
||||
write_log("INFO", 3); // Log rotated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup_daemon_stdio(void) {
|
||||
close(STDIN_FILENO);
|
||||
close(STDOUT_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
open("/dev/null", O_RDONLY);
|
||||
open("/dev/null", O_WRONLY);
|
||||
open("/dev/null", O_WRONLY);
|
||||
}
|
||||
|
||||
int daemonize(void) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
LOGE(get_message(4), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (pid > 0) exit(0);
|
||||
|
||||
if (setsid() < 0) {
|
||||
LOGE(get_message(5), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
signal(SIGHUP, SIG_IGN);
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
LOGE(get_message(6), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (pid > 0) exit(0);
|
||||
|
||||
umask(0);
|
||||
if (chdir("/") < 0) {
|
||||
LOGE(get_message(7), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
setup_daemon_stdio();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int write_pid_file(void) {
|
||||
ensure_directory_exists();
|
||||
FILE *fp = fopen(PID_FILE_PATH, "w");
|
||||
if (!fp) {
|
||||
write_log("ERROR", 8, PID_FILE_PATH, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
fprintf(fp, "%d\n", getpid());
|
||||
fclose(fp);
|
||||
write_log("INFO", 9, getpid());
|
||||
return 0;
|
||||
}
|
||||
|
||||
pid_t read_pid_file(void) {
|
||||
FILE *fp = fopen(PID_FILE_PATH, "r");
|
||||
if (!fp) return 0;
|
||||
|
||||
pid_t pid = 0;
|
||||
if (fscanf(fp, "%d", &pid) != 1) pid = 0;
|
||||
fclose(fp);
|
||||
return pid;
|
||||
}
|
||||
|
||||
int is_daemon_running(void) {
|
||||
pid_t pid = read_pid_file();
|
||||
if (pid <= 0) return 0;
|
||||
|
||||
if (kill(pid, 0) == 0) {
|
||||
return 1;
|
||||
} else {
|
||||
unlink(PID_FILE_PATH);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int stop_daemon(void) {
|
||||
pid_t pid = read_pid_file();
|
||||
if (pid <= 0) {
|
||||
printf("%s\n", get_message(10));
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf(get_message(11), pid);
|
||||
printf("\n");
|
||||
|
||||
if (kill(pid, SIGTERM) != 0) {
|
||||
printf(get_message(12), strerror(errno));
|
||||
printf("\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Wait up to 30 seconds
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (kill(pid, 0) != 0) {
|
||||
printf("%s\n", get_message(13));
|
||||
unlink(PID_FILE_PATH);
|
||||
return 0;
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
printf("%s\n", get_message(14));
|
||||
if (kill(pid, SIGKILL) == 0) {
|
||||
printf("%s\n", get_message(15));
|
||||
unlink(PID_FILE_PATH);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("%s\n", get_message(16));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int restart_daemon(void) {
|
||||
printf("%s\n", get_message(17));
|
||||
stop_daemon();
|
||||
sleep(2);
|
||||
|
||||
if (is_daemon_running()) {
|
||||
printf("%s\n", get_message(18));
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("%s\n", get_message(19));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void show_status(void) {
|
||||
pid_t pid = read_pid_file();
|
||||
if (pid <= 0) {
|
||||
printf("%s\n", get_message(20));
|
||||
return;
|
||||
}
|
||||
|
||||
if (kill(pid, 0) == 0) {
|
||||
printf(get_message(21), pid);
|
||||
printf("\n");
|
||||
|
||||
if (access(LOG_FILE_PATH, R_OK) == 0) {
|
||||
printf("\n%s\n", get_message(22));
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd), "tail -n 10 %s", LOG_FILE_PATH);
|
||||
system(cmd);
|
||||
}
|
||||
} else {
|
||||
printf("%s\n", get_message(23));
|
||||
unlink(PID_FILE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
void reload_daemon(void) {
|
||||
pid_t pid = read_pid_file();
|
||||
if (pid <= 0 || kill(pid, 0) != 0) {
|
||||
printf("%s\n", get_message(10));
|
||||
return;
|
||||
}
|
||||
|
||||
printf(get_message(24), pid);
|
||||
printf("\n");
|
||||
|
||||
if (kill(pid, SIGUSR1) == 0) {
|
||||
printf("%s\n", get_message(25));
|
||||
} else {
|
||||
printf(get_message(26), strerror(errno));
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
int get_users_from_pm(char user_dirs[][MAX_PATH_LEN], int max_users) {
|
||||
FILE *fp = popen("pm list users 2>/dev/null | grep 'UserInfo{' | sed 's/.*UserInfo{\\([0-9]*\\):.*/\\1/'", "r");
|
||||
if (!fp) return 0;
|
||||
|
||||
int user_count = 0;
|
||||
char line[64];
|
||||
while (fgets(line, sizeof(line), fp) && user_count < max_users) {
|
||||
int user_id = atoi(line);
|
||||
if (user_id >= 0) {
|
||||
snprintf(user_dirs[user_count], MAX_PATH_LEN, "%s/%d", USER_DATA_BASE_PATH, user_id);
|
||||
if (access(user_dirs[user_count], F_OK) == 0) {
|
||||
user_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pclose(fp);
|
||||
return user_count;
|
||||
}
|
||||
|
||||
int get_users_from_directory_scan(char user_dirs[][MAX_PATH_LEN], int max_users) {
|
||||
DIR *dir = opendir(USER_DATA_BASE_PATH);
|
||||
if (!dir) {
|
||||
write_log("ERROR", 27, USER_DATA_BASE_PATH, strerror(errno));
|
||||
snprintf(user_dirs[0], MAX_PATH_LEN, "%s/0", USER_DATA_BASE_PATH);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int user_count = 0;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL && user_count < max_users) {
|
||||
if (entry->d_type == DT_DIR) {
|
||||
char *endptr;
|
||||
long user_id = strtol(entry->d_name, &endptr, 10);
|
||||
if (*endptr == '\0' && strlen(entry->d_name) > 0 && user_id >= 0) {
|
||||
snprintf(user_dirs[user_count], MAX_PATH_LEN, "%s/%s", USER_DATA_BASE_PATH, entry->d_name);
|
||||
user_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
if (user_count == 0) {
|
||||
snprintf(user_dirs[0], MAX_PATH_LEN, "%s/0", USER_DATA_BASE_PATH);
|
||||
user_count = 1;
|
||||
}
|
||||
|
||||
return user_count;
|
||||
}
|
||||
|
||||
int get_user_directories(char user_dirs[][MAX_PATH_LEN], int max_users) {
|
||||
if (!config.multi_user_scan) {
|
||||
snprintf(user_dirs[0], MAX_PATH_LEN, "%s/0", USER_DATA_BASE_PATH);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int user_count = get_users_from_pm(user_dirs, max_users);
|
||||
if (user_count > 0) return user_count;
|
||||
|
||||
return get_users_from_directory_scan(user_dirs, max_users);
|
||||
}
|
||||
|
||||
void free_uid_list(void) {
|
||||
struct uid_data *current = uid_list_head;
|
||||
while (current) {
|
||||
struct uid_data *next = current->next;
|
||||
free(current);
|
||||
current = next;
|
||||
}
|
||||
uid_list_head = NULL;
|
||||
}
|
||||
|
||||
struct uid_data* create_uid_entry(int uid, const char *package_name) {
|
||||
struct uid_data *data = malloc(sizeof(struct uid_data));
|
||||
if (!data) {
|
||||
write_log("ERROR", 31);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data->uid = uid;
|
||||
strncpy(data->package, package_name, MAX_PACKAGE_NAME - 1);
|
||||
data->package[MAX_PACKAGE_NAME - 1] = '\0';
|
||||
data->next = uid_list_head;
|
||||
return data;
|
||||
}
|
||||
|
||||
int scan_single_directory(const char *dir_path) {
|
||||
DIR *dir = opendir(dir_path);
|
||||
if (!dir) {
|
||||
write_log("ERROR", 27, dir_path, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (should_exit) break;
|
||||
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
||||
if (entry->d_type != DT_DIR) continue;
|
||||
|
||||
if (strlen(entry->d_name) >= MAX_PACKAGE_NAME) {
|
||||
write_log("WARN", 29, entry->d_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
char path[MAX_PATH_LEN];
|
||||
snprintf(path, sizeof(path), "%s/%s", dir_path, entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) {
|
||||
write_log("ERROR", 30, path, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
struct uid_data *data = create_uid_entry(st.st_uid, entry->d_name);
|
||||
if (data) {
|
||||
uid_list_head = data;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
|
||||
int perform_uid_scan(void) {
|
||||
char user_dirs[MAX_USERS][MAX_PATH_LEN];
|
||||
int total_count = 0;
|
||||
|
||||
free_uid_list();
|
||||
|
||||
int user_count = get_user_directories(user_dirs, MAX_USERS);
|
||||
if (user_count <= 0) return -1;
|
||||
|
||||
write_log("INFO", 28);
|
||||
write_log("INFO", 63, user_count);
|
||||
|
||||
for (int i = 0; i < user_count && !should_exit; i++) {
|
||||
write_log("INFO", 62, user_dirs[i]);
|
||||
total_count += scan_single_directory(user_dirs[i]);
|
||||
}
|
||||
|
||||
write_log("INFO", 32, total_count);
|
||||
return total_count;
|
||||
}
|
||||
|
||||
int write_uid_whitelist(void) {
|
||||
ensure_directory_exists();
|
||||
|
||||
FILE *fp = fopen(KSU_UID_LIST_PATH, "w");
|
||||
if (!fp) {
|
||||
write_log("ERROR", 33, KSU_UID_LIST_PATH, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
struct uid_data *current = uid_list_head;
|
||||
while (current) {
|
||||
fprintf(fp, "%d %s\n", current->uid, current->package);
|
||||
current = current->next;
|
||||
count++;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
write_log("INFO", 34, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
void notify_kernel_update(void) {
|
||||
int fd = open(PROC_COMM_PATH, O_WRONLY);
|
||||
if (fd < 0) {
|
||||
write_log("ERROR", 35, PROC_COMM_PATH, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
if (write(fd, "UPDATED", 7) != 7) {
|
||||
write_log("ERROR", 36, PROC_COMM_PATH, strerror(errno));
|
||||
} else {
|
||||
write_log("INFO", 37);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int check_kernel_request(void) {
|
||||
FILE *fp = fopen(PROC_COMM_PATH, "r");
|
||||
if (!fp) return 0;
|
||||
|
||||
char status[16];
|
||||
int result = 0;
|
||||
if (fgets(status, sizeof(status), fp) != NULL) {
|
||||
result = (strncmp(status, "RESCAN", 6) == 0);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Retry wrapper functions
|
||||
int scan_operation(void) {
|
||||
return perform_uid_scan() < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
int write_operation(void) {
|
||||
return write_uid_whitelist() < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
void perform_scan_update(void) {
|
||||
if (!config.auto_scan && !manual_scan_flag) {
|
||||
write_log("WARN", 72); // Auto scan disabled, operation not allowed
|
||||
return;
|
||||
}
|
||||
|
||||
write_log("INFO", 38);
|
||||
|
||||
if (retry_operation(scan_operation, "scan") != 0) {
|
||||
write_log("ERROR", 39);
|
||||
return;
|
||||
}
|
||||
|
||||
if (retry_operation(write_operation, "write") != 0) {
|
||||
write_log("ERROR", 40);
|
||||
return;
|
||||
}
|
||||
|
||||
notify_kernel_update();
|
||||
write_log("INFO", 41);
|
||||
}
|
||||
|
||||
void perform_manual_scan_update(void) {
|
||||
manual_scan_flag = 1;
|
||||
write_log("INFO", 73); // Manual scan requested, ignoring auto_scan setting
|
||||
write_log("INFO", 38);
|
||||
|
||||
if (retry_operation(scan_operation, "scan") != 0) {
|
||||
write_log("ERROR", 39);
|
||||
return;
|
||||
}
|
||||
|
||||
if (retry_operation(write_operation, "write") != 0) {
|
||||
write_log("ERROR", 40);
|
||||
return;
|
||||
}
|
||||
|
||||
notify_kernel_update();
|
||||
write_log("INFO", 41);
|
||||
}
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
if (config.language == LANG_ZH) {
|
||||
printf("用法: %s [选项]\n", prog);
|
||||
printf("KSU UID 扫描器 - 管理UID白名单\n\n");
|
||||
printf("选项:\n");
|
||||
printf(" start 启动守护进程\n");
|
||||
printf(" stop 停止守护进程\n");
|
||||
printf(" restart 重启守护进程\n");
|
||||
printf(" status 显示守护进程状态\n");
|
||||
printf(" reload 重新加载守护进程配置\n");
|
||||
printf(" -s, --scan 执行一次扫描并退出 (忽略auto_scan设置)\n");
|
||||
printf(" -l, --list 列出当前UID白名单\n");
|
||||
printf(" --lang <en|zh> 设置语言 (英文|中文)\n");
|
||||
printf(" --multi-user <0|1> 设置多用户扫描 (0=禁用, 1=启用)\n");
|
||||
printf(" --auto-scan <0|1> 设置自动扫描 (0=禁用, 1=启用)\n");
|
||||
printf(" --config 显示当前配置\n");
|
||||
printf(" -h, --help 显示此帮助信息\n");
|
||||
} else {
|
||||
printf("Usage: %s [options]\n", prog);
|
||||
printf("KSU UID Scanner - Manage UID whitelist\n\n");
|
||||
printf("Options:\n");
|
||||
printf(" start Start daemon\n");
|
||||
printf(" stop Stop daemon\n");
|
||||
printf(" restart Restart daemon\n");
|
||||
printf(" status Show daemon status\n");
|
||||
printf(" reload Reload daemon config\n");
|
||||
printf(" -s, --scan Perform one scan and exit (ignore auto_scan setting)\n");
|
||||
printf(" -l, --list List current UID whitelist\n");
|
||||
printf(" --lang <en|zh> Set language\n");
|
||||
printf(" --multi-user <0|1> Set multi-user scanning\n");
|
||||
printf(" --auto-scan <0|1> Set auto scanning\n");
|
||||
printf(" --config Show current config\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
}
|
||||
}
|
||||
|
||||
void list_whitelist(void) {
|
||||
FILE *fp = fopen(KSU_UID_LIST_PATH, "r");
|
||||
if (!fp) {
|
||||
printf(get_message(42), strerror(errno));
|
||||
printf("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("%s\n", get_message(43));
|
||||
printf("%-8s %-40s\n", "UID", (config.language == LANG_ZH) ? "包名" : "Package");
|
||||
printf("%-8s %-40s\n", "--------", "----------------------------------------");
|
||||
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
int uid;
|
||||
char package[256];
|
||||
if (sscanf(line, "%d %255s", &uid, package) == 2) {
|
||||
printf("%-8d %-40s\n", uid, package);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
void show_config(void) {
|
||||
if (config.language == LANG_ZH) {
|
||||
printf("当前配置:\n");
|
||||
printf(" 语言: %s\n", (config.language == LANG_ZH) ? "中文" : "英文");
|
||||
printf(" 多用户扫描: %s\n", config.multi_user_scan ? "启用" : "禁用");
|
||||
printf(" 自动扫描: %s\n", config.auto_scan ? "启用" : "禁用");
|
||||
printf(" 扫描间隔: %d 秒\n", config.scan_interval);
|
||||
printf(" 日志级别: %d\n", config.log_level);
|
||||
} else {
|
||||
printf("Current Configuration:\n");
|
||||
printf(" Language: %s\n", (config.language == LANG_ZH) ? "Chinese" : "English");
|
||||
printf(" Multi-user scan: %s\n", config.multi_user_scan ? "Enabled" : "Disabled");
|
||||
printf(" Auto scan: %s\n", config.auto_scan ? "Enabled" : "Disabled");
|
||||
printf(" Scan interval: %d seconds\n", config.scan_interval);
|
||||
printf(" Log level: %d\n", config.log_level);
|
||||
}
|
||||
}
|
||||
|
||||
int handle_config_command(int argc, char *argv[]) {
|
||||
if (strcmp(argv[1], "--lang") == 0) {
|
||||
if (argc < 3) return 1;
|
||||
if (strcmp(argv[2], "zh") == 0) {
|
||||
set_language(LANG_ZH);
|
||||
} else if (strcmp(argv[2], "en") == 0) {
|
||||
set_language(LANG_EN);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "--multi-user") == 0) {
|
||||
if (argc < 3) return 1;
|
||||
int value = atoi(argv[2]);
|
||||
if (value != 0 && value != 1) return 1;
|
||||
set_multi_user_scan(value);
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "--auto-scan") == 0) {
|
||||
if (argc < 3) return 1;
|
||||
int value = atoi(argv[2]);
|
||||
if (value != 0 && value != 1) return 1;
|
||||
set_auto_scan(value);
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "--config") == 0) {
|
||||
show_config();
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int handle_single_command(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
if (strcmp(argv[1], "-s") == 0 || strcmp(argv[1], "--scan") == 0) {
|
||||
printf("%s\n", get_message(44));
|
||||
manual_scan_flag = 1;
|
||||
perform_manual_scan_update();
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "-l") == 0 || strcmp(argv[1], "--list") == 0) {
|
||||
list_whitelist();
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "status") == 0) {
|
||||
show_status();
|
||||
return 0;
|
||||
} else if (strcmp(argv[1], "stop") == 0) {
|
||||
return stop_daemon();
|
||||
} else if (strcmp(argv[1], "reload") == 0) {
|
||||
reload_daemon();
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void setup_signal_handlers(void) {
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGHUP, signal_handler);
|
||||
signal(SIGUSR1, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
void init_daemon_logging(void) {
|
||||
ensure_directory_exists();
|
||||
log_fd = open(LOG_FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
}
|
||||
|
||||
void cleanup_daemon_resources(void) {
|
||||
write_log("INFO", 52);
|
||||
free_uid_list();
|
||||
unlink(PID_FILE_PATH);
|
||||
if (log_fd != -1) close(log_fd);
|
||||
write_log("INFO", 53);
|
||||
}
|
||||
|
||||
void run_daemon_loop(void) {
|
||||
load_config();
|
||||
|
||||
write_log("INFO", 49);
|
||||
|
||||
if (!config.auto_scan) {
|
||||
write_log("INFO", 66);
|
||||
} else {
|
||||
perform_scan_update();
|
||||
}
|
||||
|
||||
while (!should_exit) {
|
||||
if (should_reload) {
|
||||
load_config();
|
||||
|
||||
if (!config.auto_scan) {
|
||||
write_log("INFO", 67);
|
||||
} else {
|
||||
write_log("INFO", 50);
|
||||
perform_scan_update();
|
||||
}
|
||||
should_reload = 0;
|
||||
}
|
||||
|
||||
if (check_kernel_request()) {
|
||||
if (!config.auto_scan) {
|
||||
write_log("INFO", 68);
|
||||
} else {
|
||||
write_log("INFO", 51);
|
||||
perform_scan_update();
|
||||
}
|
||||
}
|
||||
|
||||
manage_log_file();
|
||||
|
||||
int sleep_iterations = config.scan_interval * 10;
|
||||
for (int i = 0; i < sleep_iterations && !should_exit && !should_reload; i++) {
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
load_config();
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = handle_config_command(argc, argv);
|
||||
if (result >= 0) return result;
|
||||
|
||||
result = handle_single_command(argc, argv);
|
||||
if (result >= 0) return result;
|
||||
|
||||
if (strcmp(argv[1], "restart") == 0) {
|
||||
if (restart_daemon() != 0) return 1;
|
||||
} else if (strcmp(argv[1], "start") != 0) {
|
||||
printf(get_message(45), argv[1]);
|
||||
printf("\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_daemon_running()) {
|
||||
printf("%s\n", get_message(46));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("%s\n", get_message(47));
|
||||
if (daemonize() != 0) {
|
||||
printf("%s\n", get_message(48));
|
||||
return 1;
|
||||
}
|
||||
|
||||
init_daemon_logging();
|
||||
if (write_pid_file() != 0) exit(1);
|
||||
setup_signal_handlers();
|
||||
run_daemon_loop();
|
||||
cleanup_daemon_resources();
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue