add main branch files
This commit is contained in:
2
userspace/ksud/.gitignore
vendored
Normal file
2
userspace/ksud/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.cargo/
|
||||
1615
userspace/ksud/Cargo.lock
generated
Normal file
1615
userspace/ksud/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
userspace/ksud/Cargo.toml
Normal file
61
userspace/ksud/Cargo.toml
Normal file
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "zakomk"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
const_format = "0.2"
|
||||
zip = { version = "2", default-features = false }
|
||||
zip-extensions = { version = "0.8", features = [
|
||||
"deflate",
|
||||
"deflate64",
|
||||
"time",
|
||||
"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 = "7"
|
||||
getopts = "0.2"
|
||||
sha256 = "1"
|
||||
sha1 = "0.10"
|
||||
tempfile = "3"
|
||||
chrono = "0.4"
|
||||
regex-lite = "0.1"
|
||||
fs4 = "0.13"
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies]
|
||||
rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", 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"] }
|
||||
procfs = "0.17"
|
||||
loopdev = { git = "https://github.com/Kernel-SU/loopdev" }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = { version = "0.14", default-features = false }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
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
Normal file
BIN
userspace/ksud/bin/aarch64/busybox
Normal file
Binary file not shown.
BIN
userspace/ksud/bin/aarch64/ksuinit
Normal file
BIN
userspace/ksud/bin/aarch64/ksuinit
Normal 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/x86_64/busybox
Normal file
BIN
userspace/ksud/bin/x86_64/busybox
Normal file
Binary file not shown.
BIN
userspace/ksud/bin/x86_64/ksuinit
Normal file
BIN
userspace/ksud/bin/x86_64/ksuinit
Normal 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.
56
userspace/ksud/build.rs
Normal file
56
userspace/ksud/build.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::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::new(std::io::ErrorKind::Other, "Failed to parse git count"))?;
|
||||
let version_code = 12000 + 500 + 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::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to read git describe stdout",
|
||||
)
|
||||
})?;
|
||||
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)))
|
||||
}
|
||||
50
userspace/ksud/src/assets.rs
Normal file
50
userspace/ksud/src/assets.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
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(not(all(target_arch = "x86_64", target_os = "android")))]
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "bin/aarch64"]
|
||||
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)
|
||||
}
|
||||
5
userspace/ksud/src/banner
Normal file
5
userspace/ksud/src/banner
Normal file
@@ -0,0 +1,5 @@
|
||||
____ _ _ _ ____ _____ _____
|
||||
/ ___|| |__(_) |__ | ___|| ____|_ _|
|
||||
\___ \| '_ \ | '_ \|___ \| _| | |
|
||||
___) | | | | | | | ___) | |___ | |
|
||||
|____/|_| |_|_| |_| |____/|_____| |_|
|
||||
714
userspace/ksud/src/boot_patch.rs
Normal file
714
userspace/ksud/src/boot_patch.rs
Normal file
@@ -0,0 +1,714 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use anyhow::ensure;
|
||||
use regex_lite::Regex;
|
||||
use which::which;
|
||||
|
||||
use crate::defs;
|
||||
use crate::defs::BACKUP_FILENAME;
|
||||
use crate::defs::{KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX};
|
||||
use crate::{assets, 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")]
|
||||
pub 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) {
|
||||
if 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, cmd: &str) -> Result<()> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("cpio")
|
||||
.arg("ramdisk.cpio")
|
||||
.arg(cmd)
|
||||
.status()?;
|
||||
|
||||
ensure!(status.success(), "magiskboot cpio {} failed", cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_magisk_patched(magiskboot: &Path, workdir: &Path) -> Result<bool> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.args(["cpio", "ramdisk.cpio", "test"])
|
||||
.status()?;
|
||||
|
||||
// 0: stock, 1: magisk
|
||||
Ok(status.code() == Some(1))
|
||||
}
|
||||
|
||||
fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path) -> Result<bool> {
|
||||
let status = Command::new(magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.args(["cpio", "ramdisk.cpio", "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 skip_init = kmi.starts_with("android12-");
|
||||
|
||||
let (bootimage, bootdevice) = find_boot_image(&image, skip_init, false, false, workdir)?;
|
||||
|
||||
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 is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?;
|
||||
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, &format!("exists {BACKUP_FILENAME}")).is_ok() {
|
||||
do_cpio_cmd(
|
||||
&magiskboot,
|
||||
workdir,
|
||||
&format!("extract {0} {0}", 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, "rm kernelsu.ko")?;
|
||||
|
||||
// if init.real exists, restore it
|
||||
let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok();
|
||||
if status {
|
||||
do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?;
|
||||
} else {
|
||||
let ramdisk = workdir.join("ramdisk.cpio");
|
||||
std::fs::remove_file(ramdisk)?;
|
||||
}
|
||||
|
||||
println!("- Repacking boot image");
|
||||
let status = Command::new(&magiskboot)
|
||||
.current_dir(workdir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.arg("repack")
|
||||
.arg(bootimage.display().to_string())
|
||||
.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>,
|
||||
) -> Result<()> {
|
||||
let result = do_patch(image, kernel, kmod, init, ota, flash, out, magiskboot, kmi);
|
||||
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>,
|
||||
) -> 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 skip_init = kmi.starts_with("android12-");
|
||||
|
||||
let (bootimage, bootdevice) =
|
||||
find_boot_image(&image, skip_init, ota, is_replace_kernel, workdir)?;
|
||||
|
||||
let bootimage = bootimage.display().to_string();
|
||||
|
||||
// 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")?;
|
||||
}
|
||||
|
||||
// magiskboot unpack boot.img
|
||||
// magiskboot cpio ramdisk.cpio 'cp init init.real'
|
||||
// magiskboot cpio ramdisk.cpio 'add 0755 ksuinit init'
|
||||
// magiskboot cpio ramdisk.cpio 'add 0755 <kmod> kernelsu.ko'
|
||||
|
||||
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 no_ramdisk = !workdir.join("ramdisk.cpio").exists();
|
||||
let is_magisk_patched = is_magisk_patched(&magiskboot, workdir)?;
|
||||
ensure!(
|
||||
no_ramdisk || !is_magisk_patched,
|
||||
"Cannot work with Magisk patched image"
|
||||
);
|
||||
|
||||
println!("- Adding KernelSU LKM");
|
||||
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?;
|
||||
|
||||
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, "exists init");
|
||||
if status.is_ok() {
|
||||
do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?;
|
||||
}
|
||||
|
||||
need_backup = flash;
|
||||
}
|
||||
|
||||
do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?;
|
||||
do_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if need_backup {
|
||||
if let Err(e) = do_backup(&magiskboot, workdir, &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!("{:x}", result))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> 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,
|
||||
&format!("add 0755 {0} {0}", 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>,
|
||||
skip_init: bool,
|
||||
ota: bool,
|
||||
is_replace_kernel: bool,
|
||||
workdir: &Path,
|
||||
) -> 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 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()
|
||||
}
|
||||
};
|
||||
|
||||
let init_boot_exist =
|
||||
Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists();
|
||||
let boot_partition = if !is_replace_kernel && init_boot_exist && !skip_init {
|
||||
format!("/dev/block/by-name/init_boot{slot_suffix}")
|
||||
} else {
|
||||
format!("/dev/block/by-name/boot{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))
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
390
userspace/ksud/src/cli.rs
Normal file
390
userspace/ksud/src/cli.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use clap::Parser;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::defs::KSUD_VERBOSE_LOG_FILE;
|
||||
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,
|
||||
|
||||
#[arg(short, long, default_value_t = cfg!(debug_assertions))]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
|
||||
/// 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>,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
/// For developers
|
||||
Debug {
|
||||
#[command(subcommand)]
|
||||
command: Debug,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum BootInfo {
|
||||
/// show current kmi version
|
||||
CurrentKmi,
|
||||
|
||||
/// show supported kmi versions
|
||||
SupportedKmi,
|
||||
}
|
||||
|
||||
#[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("me.weishu.kernelsu"))]
|
||||
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,
|
||||
|
||||
Mount,
|
||||
|
||||
/// For testing
|
||||
Test,
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Restore module <id>
|
||||
Restore {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// enable module <id>
|
||||
Enable {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// disable module <id>
|
||||
Disable {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// run action for module <id>
|
||||
Action {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// list all modules
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum 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,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() {
|
||||
log::set_max_level(LevelFilter::Info);
|
||||
}
|
||||
|
||||
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::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Restore { id } => module::restore_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(),
|
||||
}
|
||||
}
|
||||
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::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::Mount => init_event::mount_modules_systemlessly(),
|
||||
Debug::Test => assets::ensure_binaries(false),
|
||||
},
|
||||
|
||||
Commands::BootPatch {
|
||||
boot,
|
||||
init,
|
||||
kernel,
|
||||
module,
|
||||
ota,
|
||||
flash,
|
||||
out,
|
||||
magiskboot,
|
||||
kmi,
|
||||
} => crate::boot_patch::patch(boot, kernel, module, init, ota, flash, out, magiskboot, kmi),
|
||||
|
||||
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::SupportedKmi => {
|
||||
let kmi = crate::assets::list_supported_kmi()?;
|
||||
kmi.iter().for_each(|kmi| println!("{}", kmi));
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Commands::BootRestore {
|
||||
boot,
|
||||
magiskboot,
|
||||
flash,
|
||||
} => crate::boot_patch::restore(boot, magiskboot, flash),
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
log::error!("Error: {:?}", e);
|
||||
}
|
||||
result
|
||||
}
|
||||
52
userspace/ksud/src/debug.rs
Normal file
52
userspace/ksud/src/debug.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use anyhow::{Context, Ok, Result, ensure};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
42
userspace/ksud/src/defs.rs
Normal file
42
userspace/ksud/src/defs.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
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 KSU_MOUNT_SOURCE: &str = "KSU";
|
||||
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/");
|
||||
|
||||
// warning: this directory should not change, or you need to change the code in module_installer.sh!!!
|
||||
pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
|
||||
pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose");
|
||||
|
||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const UPDATE_FILE_NAME: &str = "update";
|
||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
pub const 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";
|
||||
|
||||
pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs");
|
||||
pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount");
|
||||
187
userspace/ksud/src/init_event.rs
Normal file
187
userspace/ksud/src/init_event.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use crate::defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH};
|
||||
use crate::module::{handle_updated_modules, prune_modules};
|
||||
use crate::{assets, defs, ksucalls, restorecon, utils};
|
||||
use anyhow::{Context, Result};
|
||||
use log::{info, warn};
|
||||
use rustix::fs::{MountFlags, mount};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn on_post_data_fs() -> Result<()> {
|
||||
ksucalls::report_post_fs_data();
|
||||
|
||||
utils::umask(0);
|
||||
|
||||
#[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 = utils::is_safe_mode();
|
||||
|
||||
if 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);
|
||||
}
|
||||
}
|
||||
|
||||
assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?;
|
||||
|
||||
// tell kernel that we've mount the module, so that it can do some optimization
|
||||
ksucalls::report_module_mounted();
|
||||
|
||||
// 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) = prune_modules() {
|
||||
warn!("prune modules failed: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = handle_updated_modules() {
|
||||
warn!("handle updated 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);
|
||||
}
|
||||
|
||||
// mount temp dir
|
||||
if !Path::new(NO_TMPFS_PATH).exists() {
|
||||
if let Err(e) = mount(KSU_MOUNT_SOURCE, utils::get_tmp_path(), "tmpfs", MountFlags::empty(), "") {
|
||||
warn!("do temp dir mount failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("no tmpfs requested");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// mount module systemlessly by magic mount
|
||||
if !Path::new(NO_MOUNT_PATH).exists() {
|
||||
if let Err(e) = mount_modules_systemlessly() {
|
||||
warn!("do systemless mount failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("no mount requested");
|
||||
}
|
||||
|
||||
run_stage("post-mount", true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
crate::magic_mount::magic_mount()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
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}");
|
||||
}
|
||||
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(())
|
||||
}
|
||||
445
userspace/ksud/src/installer.sh
Normal file
445
userspace/ksud/src/installer.sh
Normal file
@@ -0,0 +1,445 @@
|
||||
#!/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 /proc/$$/fd/*; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_actions() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_cleanup() {
|
||||
:
|
||||
}
|
||||
|
||||
#######################
|
||||
# Installation Related
|
||||
#######################
|
||||
|
||||
# find_block [partname...]
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=`grep_prop DEVNAME $UEVENT`
|
||||
PARTNAME=`grep_prop PARTNAME $UEVENT`
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# setup_mntpoint <mountpoint>
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
# mount_name <partname(s)> <mountpoint> <flag>
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
# mount_ro_ensure <partname(s)> <mountpoint>
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=`grep_cmdline androidboot.slot_suffix`
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=`grep_cmdline androidboot.slot`
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2&>/dev/null
|
||||
umount /system_root 2&>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
SYSTEM_ROOT=false
|
||||
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
|
||||
fi
|
||||
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
|
||||
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
|
||||
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
# Mount sepolicy rules dir locations in recovery (best effort)
|
||||
if ! $BOOTMODE; then
|
||||
mount_name "cache cac" /cache
|
||||
mount_name metadata /metadata
|
||||
mount_name persist /persist
|
||||
fi
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
#################
|
||||
# Module Related
|
||||
#################
|
||||
|
||||
set_perm() {
|
||||
chown $2:$3 $1 || return 1
|
||||
chmod $4 $1 || return 1
|
||||
local CON=$5
|
||||
[ -z $CON ] && CON=u:object_r:system_file:s0
|
||||
chcon $CON $1 || return 1
|
||||
}
|
||||
|
||||
set_perm_recursive() {
|
||||
find $1 -type d 2>/dev/null | while read dir; do
|
||||
set_perm $dir $2 $3 $4 $6
|
||||
done
|
||||
find $1 -type f -o -type l 2>/dev/null | while read file; do
|
||||
set_perm $file $2 $3 $5 $6
|
||||
done
|
||||
}
|
||||
|
||||
mktouch() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
[ -z $2 ] && touch $1 || echo $2 > $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_remove() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
mknod $1 c 0 0
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
|
||||
request_zip_size_check() {
|
||||
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
|
||||
}
|
||||
|
||||
boot_actions() { return; }
|
||||
|
||||
# Require ZIPFILE to be set
|
||||
is_legacy_script() {
|
||||
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
|
||||
return $?
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
PARTITION="$1"
|
||||
REQUIRE_SYMLINK="$2"
|
||||
if [ ! -e "$MODPATH/system/$PARTITION" ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then
|
||||
ui_print "- Handle partition /$PARTITION"
|
||||
ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION"
|
||||
fi
|
||||
}
|
||||
|
||||
# Require OUTFD, ZIPFILE to be set
|
||||
install_module() {
|
||||
rm -rf $TMPDIR
|
||||
mkdir -p $TMPDIR
|
||||
chcon u:object_r:system_file:s0 $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
mount_partitions
|
||||
api_level_arch_detect
|
||||
|
||||
# Setup busybox and binaries
|
||||
if $BOOTMODE; then
|
||||
boot_actions
|
||||
else
|
||||
recovery_actions
|
||||
fi
|
||||
|
||||
# Extract prop file
|
||||
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
|
||||
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
|
||||
|
||||
local MODDIRNAME=modules
|
||||
$BOOTMODE && MODDIRNAME=modules_update
|
||||
local MODULEROOT=$NVBASE/$MODDIRNAME
|
||||
MODID=`grep_prop id $TMPDIR/module.prop`
|
||||
MODNAME=`grep_prop name $TMPDIR/module.prop`
|
||||
MODAUTH=`grep_prop author $TMPDIR/module.prop`
|
||||
MODPATH=$MODULEROOT/$MODID
|
||||
|
||||
# Create mod paths
|
||||
rm -rf $MODPATH
|
||||
mkdir -p $MODPATH
|
||||
|
||||
if is_legacy_script; then
|
||||
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
|
||||
|
||||
# Load install script
|
||||
. $TMPDIR/install.sh
|
||||
|
||||
# Callbacks
|
||||
print_modname
|
||||
on_install
|
||||
|
||||
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
|
||||
$SKIPMOUNT && touch $MODPATH/skip_mount
|
||||
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
|
||||
cp -af $TMPDIR/module.prop $MODPATH/module.prop
|
||||
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
|
||||
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
|
||||
|
||||
ui_print "- Setting permissions"
|
||||
set_permissions
|
||||
else
|
||||
print_title "$MODNAME" "by $MODAUTH"
|
||||
print_title "Powered by 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_partition vendor true
|
||||
handle_partition system_ext true
|
||||
handle_partition product true
|
||||
handle_partition odm false
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
|
||||
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
|
||||
fi
|
||||
|
||||
# Remove stuff that doesn't belong to modules and clean up any empty directories
|
||||
rm -rf \
|
||||
$MODPATH/system/placeholder $MODPATH/customize.sh \
|
||||
$MODPATH/README.md $MODPATH/.git*
|
||||
rmdir -p $MODPATH 2>/dev/null
|
||||
|
||||
cd /
|
||||
$BOOTMODE || recovery_cleanup
|
||||
rm -rf $TMPDIR
|
||||
|
||||
ui_print "- Done"
|
||||
}
|
||||
|
||||
##########
|
||||
# Presets
|
||||
##########
|
||||
|
||||
# Detect whether in boot mode
|
||||
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||
|
||||
NVBASE=/data/adb
|
||||
TMPDIR=/dev/tmp
|
||||
POSTFSDATAD=$NVBASE/post-fs-data.d
|
||||
SERVICED=$NVBASE/service.d
|
||||
|
||||
# Some modules dependents on this
|
||||
export MAGISK_VER=25.2
|
||||
export MAGISK_VER_CODE=25200
|
||||
43
userspace/ksud/src/ksucalls.rs
Normal file
43
userspace/ksud/src/ksucalls.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
const EVENT_POST_FS_DATA: u64 = 1;
|
||||
const EVENT_BOOT_COMPLETED: u64 = 2;
|
||||
const EVENT_MODULE_MOUNTED: u64 = 3;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn get_version() -> i32 {
|
||||
rustix::process::ksu_get_version()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn get_version() -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn report_event(event: u64) {
|
||||
rustix::process::ksu_report_event(event)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
fn report_event(_event: u64) {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn check_kernel_safemode() -> bool {
|
||||
rustix::process::ksu_check_kernel_safemode()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn check_kernel_safemode() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
434
userspace/ksud/src/magic_mount.rs
Normal file
434
userspace/ksud/src/magic_mount.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
use crate::defs::{
|
||||
DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME,
|
||||
};
|
||||
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
|
||||
use crate::restorecon::{lgetfilecon, lsetfilecon};
|
||||
use crate::utils::{ensure_dir_exists, get_work_dir};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use extattr::lgetxattr;
|
||||
use rustix::fs::{
|
||||
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
|
||||
chmod, chown, mount, move_mount, unmount,
|
||||
};
|
||||
use rustix::mount::mount_change;
|
||||
use rustix::path::Arg;
|
||||
use std::cmp::PartialEq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fs;
|
||||
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
|
||||
use std::os::unix::fs::{FileTypeExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
enum NodeFileType {
|
||||
RegularFile,
|
||||
Directory,
|
||||
Symlink,
|
||||
Whiteout,
|
||||
}
|
||||
|
||||
impl NodeFileType {
|
||||
fn from_file_type(file_type: FileType) -> Option<Self> {
|
||||
if file_type.is_file() {
|
||||
Some(RegularFile)
|
||||
} else if file_type.is_dir() {
|
||||
Some(Directory)
|
||||
} else if file_type.is_symlink() {
|
||||
Some(Symlink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
file_type: NodeFileType,
|
||||
children: HashMap<String, Node>,
|
||||
// the module that owned this node
|
||||
module_path: Option<PathBuf>,
|
||||
replace: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn collect_module_files<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
|
||||
let dir = module_dir.as_ref();
|
||||
let mut has_file = false;
|
||||
for entry in dir.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let node = match self.children.entry(name.clone()) {
|
||||
Entry::Occupied(o) => Some(o.into_mut()),
|
||||
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
|
||||
};
|
||||
|
||||
if let Some(node) = node {
|
||||
has_file |= if node.file_type == Directory {
|
||||
node.collect_module_files(dir.join(&node.name))? || node.replace
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(has_file)
|
||||
}
|
||||
|
||||
fn new_root<T: ToString>(name: T) -> Self {
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let path = entry.path();
|
||||
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
|
||||
Some(Whiteout)
|
||||
} else {
|
||||
NodeFileType::from_file_type(metadata.file_type())
|
||||
};
|
||||
if let Some(file_type) = file_type {
|
||||
let mut replace = false;
|
||||
if file_type == Directory {
|
||||
if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) {
|
||||
if String::from_utf8_lossy(&v) == "y" {
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(Node {
|
||||
name: name.to_string(),
|
||||
file_type,
|
||||
children: Default::default(),
|
||||
module_path: Some(path),
|
||||
replace,
|
||||
skip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_module_files() -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = Path::new(MODULE_DIR);
|
||||
let mut has_file = false;
|
||||
for entry in module_root.read_dir()?.flatten() {
|
||||
if !entry.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.path().join(DISABLE_FILE_NAME).exists()
|
||||
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_system = entry.path().join("system");
|
||||
if !mod_system.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("collecting {}", entry.path().display());
|
||||
|
||||
has_file |= system.collect_module_files(&mod_system)?;
|
||||
}
|
||||
|
||||
if has_file {
|
||||
for (partition, require_symlink) in [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
] {
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
|
||||
let name = partition.to_string();
|
||||
if let Some(node) = system.children.remove(&name) {
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
entry: &DirEntry,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
current: Node,
|
||||
has_tmpfs: bool,
|
||||
) -> Result<()> {
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root file {}!", path.display());
|
||||
}
|
||||
}
|
||||
Symlink => {
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"create module symlink {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(module_path, &work_dir_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root symlink {}!", path.display());
|
||||
}
|
||||
}
|
||||
Directory => {
|
||||
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
|
||||
if !has_tmpfs && !create_tmpfs {
|
||||
for it in &mut current.children {
|
||||
let (name, node) = it;
|
||||
let real_path = path.join(name);
|
||||
let need = match node.file_type {
|
||||
Symlink => true,
|
||||
Whiteout => real_path.exists(),
|
||||
_ => {
|
||||
if let Ok(metadata) = real_path.symlink_metadata() {
|
||||
let file_type = NodeFileType::from_file_type(metadata.file_type())
|
||||
.unwrap_or(Whiteout);
|
||||
file_type != node.file_type || file_type == Symlink
|
||||
} else {
|
||||
// real path not exists
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if need {
|
||||
if current.module_path.is_none() {
|
||||
log::error!(
|
||||
"cannot create tmpfs on {}, ignore: {name}",
|
||||
path.display()
|
||||
);
|
||||
node.skip = true;
|
||||
continue;
|
||||
}
|
||||
create_tmpfs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_tmpfs = has_tmpfs || create_tmpfs;
|
||||
|
||||
if has_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs skeleton for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir_all(&work_dir_path)?;
|
||||
let (metadata, path) = if path.exists() {
|
||||
(path.metadata()?, &path)
|
||||
} else if let Some(module_path) = ¤t.module_path {
|
||||
(module_path.metadata()?, module_path)
|
||||
} else {
|
||||
bail!("cannot mount root dir {}!", path.display());
|
||||
};
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(&work_dir_path, &work_dir_path).context("bind self")?;
|
||||
}
|
||||
|
||||
if path.exists() && !current.replace {
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let result = if let Some(node) = current.children.remove(&name) {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
} else if has_tmpfs {
|
||||
mount_mirror(&path, &work_dir_path, &entry)
|
||||
.with_context(|| format!("mount mirror {}/{name}", path.display()))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.replace {
|
||||
if current.module_path.is_none() {
|
||||
bail!(
|
||||
"dir {} is declared as replaced but it is root!",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
log::debug!("dir {} is replaced", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, node) in current.children.into_iter() {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
{
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"moving tmpfs {} -> {}",
|
||||
work_dir_path.display(),
|
||||
path.display()
|
||||
);
|
||||
move_mount(&work_dir_path, &path).context("move self")?;
|
||||
mount_change(&path, MountPropagationFlags::PRIVATE).context("make self private")?;
|
||||
}
|
||||
}
|
||||
Whiteout => {
|
||||
log::debug!("file {} is removed", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magic_mount() -> Result<()> {
|
||||
if let Some(root) = collect_module_files()? {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
let tmp_dir = PathBuf::from(get_work_dir());
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let result = do_magic_mount("/", &tmp_dir, root, false);
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
fs::remove_dir(tmp_dir).ok();
|
||||
result
|
||||
} else {
|
||||
log::info!("no modules to mount, skipping!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
20
userspace/ksud/src/main.rs
Normal file
20
userspace/ksud/src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod apk_sign;
|
||||
mod assets;
|
||||
mod boot_patch;
|
||||
mod cli;
|
||||
mod debug;
|
||||
mod defs;
|
||||
mod init_event;
|
||||
mod ksucalls;
|
||||
#[cfg(target_os = "android")]
|
||||
mod magic_mount;
|
||||
mod module;
|
||||
mod profile;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod su;
|
||||
mod utils;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
cli::run()
|
||||
}
|
||||
498
userspace/ksud/src/module.rs
Normal file
498
userspace/ksud/src/module.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{
|
||||
assets, defs, ksucalls,
|
||||
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::{info, warn};
|
||||
|
||||
use std::fs::{copy, rename};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, remove_dir_all, remove_file, 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};
|
||||
#[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"
|
||||
);
|
||||
|
||||
fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", INSTALL_MODULE_SCRIPT])
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
)
|
||||
.env("KSU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
ensure!(result.success(), "Failed to install module script");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// becuase we use something like A-B update
|
||||
// we need to update the module state after the boot_completed
|
||||
// if someone(such as the module) install a module before the boot_completed
|
||||
// then it may cause some problems, just forbid it
|
||||
fn ensure_boot_completed() -> Result<()> {
|
||||
// ensure getprop sys.boot_completed == 1
|
||||
if getprop("sys.boot_completed").as_deref() != Some("1") {
|
||||
bail!("Android is Booting!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> {
|
||||
let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file);
|
||||
if create {
|
||||
ensure_file_exists(module_state_file)
|
||||
} else {
|
||||
if module_state_file.exists() {
|
||||
remove_file(module_state_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum ModuleType {
|
||||
All,
|
||||
Active,
|
||||
Updated,
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if !modules_dir.is_dir() {
|
||||
warn!("{} is not a directory, skip", modules_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
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 == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if module_type == ModuleType::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(ModuleType::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(())
|
||||
}
|
||||
|
||||
fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(|| {
|
||||
// ignore the error?
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
}
|
||||
command = command
|
||||
.current_dir(path.as_ref().parent().unwrap())
|
||||
.arg("sh")
|
||||
.arg(path.as_ref())
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env("KSU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
);
|
||||
|
||||
let result = if wait {
|
||||
command.status().map(|_| ())
|
||||
} else {
|
||||
command.spawn().map(|_| ())
|
||||
};
|
||||
result.map_err(|err| anyhow!("Failed to exec {}: {}", path.as_ref().display(), err))
|
||||
}
|
||||
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
foreach_active_module(|module| {
|
||||
let script_path = module.join(format!("{stage}.sh"));
|
||||
if !script_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
exec_script(&script_path, block)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> {
|
||||
let script_dir = Path::new(defs::ADB_DIR).join(dir);
|
||||
if !script_dir.exists() {
|
||||
info!("{} not exists, skip", script_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dir = 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(ModuleType::All, |module| {
|
||||
if module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists() {
|
||||
if let Err(e) = exec_script(uninstaller, true) {
|
||||
warn!("Failed to exec uninstaller: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
} else {
|
||||
remove_file(module.join(defs::UPDATE_FILE_NAME)).ok();
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_updated_modules() -> Result<()> {
|
||||
let modules_root = Path::new(MODULE_DIR);
|
||||
foreach_module(ModuleType::Updated, |module| {
|
||||
if !module.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(name) = module.file_name() {
|
||||
let old_dir = modules_root.join(name);
|
||||
if old_dir.exists() {
|
||||
if let Err(e) = remove_dir_all(&old_dir) {
|
||||
log::error!("Failed to remove old {}: {}", old_dir.display(), e);
|
||||
}
|
||||
}
|
||||
if let Err(e) = rename(module, &old_dir) {
|
||||
log::error!("Failed to move new module {}: {}", module.display(), e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
fn inner(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();
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
println!("- Preparing Zip");
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// ensure modules_update exists
|
||||
ensure_dir_exists(MODULE_UPDATE_DIR)?;
|
||||
setsyscon(MODULE_UPDATE_DIR)?;
|
||||
|
||||
let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id);
|
||||
ensure_clean_dir(&update_module_dir)?;
|
||||
info!("module dir: {}", update_module_dir.display());
|
||||
|
||||
let do_install = || -> Result<()> {
|
||||
// unzip the image and move it to modules_update/<id> dir
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&update_module_dir)?;
|
||||
|
||||
// set permission and selinux context for $MOD/system
|
||||
let module_system_dir = update_module_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
exec_install_script(zip)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
update_module_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
info!("Module install successfully!");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
let result = do_install();
|
||||
if result.is_err() {
|
||||
remove_dir_all(&update_module_dir).ok();
|
||||
}
|
||||
result
|
||||
}
|
||||
let result = inner(zip);
|
||||
if let Err(ref e) = result {
|
||||
println!("- Error: {e}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, true)
|
||||
}
|
||||
|
||||
pub fn restore_uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, false)
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
let action_script_path = format!("/data/adb/modules/{}/action.sh", id);
|
||||
exec_script(&action_script_path, true)
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, false)
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, true)
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
mark_all_modules(defs::DISABLE_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn uninstall_all_modules() -> Result<()> {
|
||||
mark_all_modules(defs::REMOVE_FILE_NAME)
|
||||
}
|
||||
|
||||
fn mark_all_modules(flag_file: &str) -> Result<()> {
|
||||
let dir = std::fs::read_dir(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(())
|
||||
}
|
||||
|
||||
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
// 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());
|
||||
let module_prop = path.join("module.prop");
|
||||
if !module_prop.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = std::fs::read(&module_prop);
|
||||
let Ok(content) = content else {
|
||||
warn!("Failed to read file: {}", module_prop.display());
|
||||
continue;
|
||||
};
|
||||
let mut module_prop_map: HashMap<String, String> = HashMap::new();
|
||||
let encoding = encoding_rs::UTF_8;
|
||||
let result =
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding).read_into(|k, v| {
|
||||
module_prop_map.insert(k, v);
|
||||
});
|
||||
|
||||
let dir_id = entry.file_name().to_string_lossy().to_string();
|
||||
module_prop_map.insert("dir_id".to_owned(), dir_id.clone());
|
||||
|
||||
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
|
||||
info!("Use dir name as module id: {dir_id}");
|
||||
module_prop_map.insert("id".to_owned(), dir_id.clone());
|
||||
}
|
||||
|
||||
// Add enabled, update, remove flags
|
||||
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let web = path.join(defs::MODULE_WEB_DIR).exists();
|
||||
let action = path.join(defs::MODULE_ACTION_SH).exists();
|
||||
|
||||
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
|
||||
module_prop_map.insert("update".to_owned(), update.to_string());
|
||||
module_prop_map.insert("remove".to_owned(), remove.to_string());
|
||||
module_prop_map.insert("web".to_owned(), web.to_string());
|
||||
module_prop_map.insert("action".to_owned(), action.to_string());
|
||||
|
||||
if result.is_err() {
|
||||
warn!("Failed to parse module.prop: {}", module_prop.display());
|
||||
continue;
|
||||
}
|
||||
modules.push(module_prop_map);
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
pub fn list_modules() -> Result<()> {
|
||||
let modules = _list_modules(defs::MODULE_DIR);
|
||||
println!("{}", serde_json::to_string_pretty(&modules)?);
|
||||
Ok(())
|
||||
}
|
||||
79
userspace/ksud/src/profile.rs
Normal file
79
userspace/ksud/src/profile.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::utils::ensure_dir_exists;
|
||||
use crate::{defs, sepolicy};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
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 crate::defs;
|
||||
use anyhow::Result;
|
||||
use jwalk::{Parallelism::Serial, WalkDir};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use anyhow::{Context, Ok};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use extattr::{Flags as XattrFlags, lsetxattr};
|
||||
|
||||
pub const SYSTEM_CON: &str = "u:object_r:system_file:s0";
|
||||
pub const ADB_CON: &str = "u:object_r:adb_data_file:s0";
|
||||
pub const UNLABEL_CON: &str = "u:object_r:unlabeled:s0";
|
||||
|
||||
const SELINUX_XATTR: &str = "security.selinux";
|
||||
|
||||
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
|
||||
format!(
|
||||
"Failed to change SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
|
||||
format!(
|
||||
"Failed to get SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let con = String::from_utf8_lossy(&con);
|
||||
Ok(con.to_string())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
lsetfilecon(path, SYSTEM_CON)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
setsyscon(&path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
if let Result::Ok(con) = lgetfilecon(&path) {
|
||||
if con == ADB_CON || con == UNLABEL_CON || con.is_empty() {
|
||||
lsetfilecon(&path, SYSTEM_CON)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restorecon() -> Result<()> {
|
||||
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
|
||||
restore_modules_con(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
738
userspace/ksud/src/sepolicy.rs
Normal file
738
userspace/ksud/src/sepolicy.rs
Normal file
@@ -0,0 +1,738 @@
|
||||
use anyhow::{Result, bail};
|
||||
use derive_new::new;
|
||||
use nom::{
|
||||
AsChar, IResult, Parser,
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_while, take_while_m_n, take_while1},
|
||||
character::complete::{space0, space1},
|
||||
combinator::map,
|
||||
};
|
||||
use std::{ffi, path::Path, vec};
|
||||
|
||||
type SeObject<'a> = Vec<&'a str>;
|
||||
|
||||
fn is_sepolicy_char(c: char) -> bool {
|
||||
c.is_alphanum() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn parse_single_word(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(is_sepolicy_char).parse(input)
|
||||
}
|
||||
|
||||
fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, (_, words, _)) = (
|
||||
tag("{"),
|
||||
take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()),
|
||||
tag("}"),
|
||||
)
|
||||
.parse(input)?;
|
||||
Ok((input, words.split_whitespace().collect()))
|
||||
}
|
||||
|
||||
fn parse_single_obj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, word) = take_while1(is_sepolicy_char).parse(input)?;
|
||||
Ok((input, vec![word]))
|
||||
}
|
||||
|
||||
fn parse_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, _) = tag("*").parse(input)?;
|
||||
Ok((input, vec!["*"]))
|
||||
}
|
||||
|
||||
// 1. a single sepolicy word
|
||||
// 2. { obj1 obj2 obj3 ...}
|
||||
// 3. *
|
||||
fn parse_seobj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
trait SeObjectParser<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct NormalPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
perm: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct XPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
operation: &'a str,
|
||||
perm_set: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeState<'a> {
|
||||
op: &'a str,
|
||||
stype: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeAttr<'a> {
|
||||
stype: SeObject<'a>,
|
||||
sattr: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Type<'a> {
|
||||
name: &'a str,
|
||||
attrs: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Attr<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeTransition<'a> {
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
object_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeChange<'a> {
|
||||
op: &'a str,
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct GenFsCon<'a> {
|
||||
fs_name: &'a str,
|
||||
partial_path: &'a str,
|
||||
fs_context: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PolicyStatement<'a> {
|
||||
// "allow *source_type *target_type *class *perm_set"
|
||||
// "deny *source_type *target_type *class *perm_set"
|
||||
// "auditallow *source_type *target_type *class *perm_set"
|
||||
// "dontaudit *source_type *target_type *class *perm_set"
|
||||
NormalPerm(NormalPerm<'a>),
|
||||
|
||||
// "allowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "auditallowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "dontauditxperm *source_type *target_type *class operation xperm_set"
|
||||
XPerm(XPerm<'a>),
|
||||
|
||||
// "permissive ^type"
|
||||
// "enforce ^type"
|
||||
TypeState(TypeState<'a>),
|
||||
|
||||
// "type type_name ^(attribute)"
|
||||
Type(Type<'a>),
|
||||
|
||||
// "typeattribute ^type ^attribute"
|
||||
TypeAttr(TypeAttr<'a>),
|
||||
|
||||
// "attribute ^attribute"
|
||||
Attr(Attr<'a>),
|
||||
|
||||
// "type_transition source_type target_type class default_type (object_name)"
|
||||
TypeTransition(TypeTransition<'a>),
|
||||
|
||||
// "type_change source_type target_type class default_type"
|
||||
// "type_member source_type target_type class default_type"
|
||||
TypeChange(TypeChange<'a>),
|
||||
|
||||
// "genfscon fs_name partial_path fs_context"
|
||||
GenFsCon(GenFsCon<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for NormalPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allow"),
|
||||
tag("deny"),
|
||||
tag("auditallow"),
|
||||
tag("dontaudit"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm) = parse_seobj(input)?;
|
||||
Ok((input, NormalPerm::new(op, source, target, class, perm)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for XPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allowxperm"),
|
||||
tag("auditallowxperm"),
|
||||
tag("dontauditxperm"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, operation) = parse_single_word(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm_set) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
XPerm::new(op, source, target, class, operation, perm_set),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeState<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("permissive"), tag("enforce"))).parse(input)?;
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeState::new(op, stype)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Type<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("type")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, name) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((input, Type::new(name, vec!["domain"]))); // default to domain
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attrs) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, Type::new(name, attrs)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeAttr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("typeattribute"), tag("attradd"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeAttr::new(stype, attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Attr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("attribute")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, Attr::new(attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeTransition<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("type_transition"), tag("name_transition"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, None),
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, object) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, Some(object)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeChange<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("type_change"), tag("type_member"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, TypeChange::new(op, source, target, class, default)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for GenFsCon<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (input, _) = tag("genfscon")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, fs) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, path) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, context) = parse_single_word(input)?;
|
||||
Ok((input, GenFsCon::new(fs, path, context)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PolicyStatement<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, statement) = alt((
|
||||
map(NormalPerm::parse, PolicyStatement::NormalPerm),
|
||||
map(XPerm::parse, PolicyStatement::XPerm),
|
||||
map(TypeState::parse, PolicyStatement::TypeState),
|
||||
map(Type::parse, PolicyStatement::Type),
|
||||
map(TypeAttr::parse, PolicyStatement::TypeAttr),
|
||||
map(Attr::parse, PolicyStatement::Attr),
|
||||
map(TypeTransition::parse, PolicyStatement::TypeTransition),
|
||||
map(TypeChange::parse, PolicyStatement::TypeChange),
|
||||
map(GenFsCon::parse, PolicyStatement::GenFsCon),
|
||||
))
|
||||
.parse(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = take_while(|c| c == ';')(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
Ok((input, statement))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result<Vec<PolicyStatement<'a>>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut statements = vec![];
|
||||
|
||||
for line in input.split(['\n', ';']) {
|
||||
let trimmed_line = line.trim();
|
||||
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) {
|
||||
statements.push(statement);
|
||||
} else if strict {
|
||||
bail!("Failed to parse policy statement: {}", line)
|
||||
}
|
||||
}
|
||||
Ok(statements)
|
||||
}
|
||||
|
||||
const SEPOLICY_MAX_LEN: usize = 128;
|
||||
|
||||
const CMD_NORMAL_PERM: u32 = 1;
|
||||
const CMD_XPERM: u32 = 2;
|
||||
const CMD_TYPE_STATE: u32 = 3;
|
||||
const CMD_TYPE: u32 = 4;
|
||||
const CMD_TYPE_ATTR: u32 = 5;
|
||||
const CMD_ATTR: u32 = 6;
|
||||
const CMD_TYPE_TRANSITION: u32 = 7;
|
||||
const CMD_TYPE_CHANGE: u32 = 8;
|
||||
const CMD_GENFSCON: u32 = 9;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum PolicyObject {
|
||||
All, // for "*", stand for all objects, and is NULL in ffi
|
||||
One([u8; SEPOLICY_MAX_LEN]),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PolicyObject {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(s: &str) -> Result<Self> {
|
||||
anyhow::ensure!(s.len() <= SEPOLICY_MAX_LEN, "policy object too long");
|
||||
if s == "*" {
|
||||
return Ok(PolicyObject::All);
|
||||
}
|
||||
let mut buf = [0u8; SEPOLICY_MAX_LEN];
|
||||
buf[..s.len()].copy_from_slice(s.as_bytes());
|
||||
Ok(PolicyObject::One(buf))
|
||||
}
|
||||
}
|
||||
|
||||
/// atomic statement, such as: allow domain1 domain2:file1 read;
|
||||
/// normal statement would be 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 {
|
||||
if !rustix::process::ksu_set_policy(&FfiPolicy::from(policy)) {
|
||||
log::warn!("apply rule: {:?} failed.", statement);
|
||||
if strict {
|
||||
return Err(anyhow::anyhow!("apply rule {:?} failed.", statement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
285
userspace/ksud/src/su.rs
Normal file
285
userspace/ksud/src/su.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use getopts::Options;
|
||||
use std::env;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::CStr, process::Command};
|
||||
|
||||
use crate::{
|
||||
defs,
|
||||
utils::{self, umask},
|
||||
};
|
||||
|
||||
#[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<()> {
|
||||
rustix::process::ksu_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(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.optflag(
|
||||
"s",
|
||||
"shell",
|
||||
"use SHELL instead of the default /system/bin/sh",
|
||||
);
|
||||
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",
|
||||
);
|
||||
|
||||
// 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 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(target_arch = "aarch64")]
|
||||
let pw = libc::getpwnam(name.as_ptr()).as_ref();
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let pw = libc::getpwnam(name.as_ptr() as *const i8).as_ref();
|
||||
|
||||
match pw {
|
||||
Some(pw) => pw.pw_uid,
|
||||
None => name.parse::<u32>().unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
260
userspace/ksud/src/utils.rs
Normal file
260
userspace/ksud/src/utils.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use std::{
|
||||
fs::{self, 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) {
|
||||
Result::Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == AlreadyExists && file.as_ref().is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from(err))
|
||||
.with_context(|| format!("{} is not a regular file", file.as_ref().display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
|
||||
let result = create_dir_all(&dir).map_err(Error::from);
|
||||
if dir.as_ref().is_dir() {
|
||||
result
|
||||
} else if result.is_ok() {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
if 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 _ = writeln!(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()
|
||||
}
|
||||
|
||||
fn is_ok_empty(dir: &str) -> bool {
|
||||
use std::result::Result::Ok;
|
||||
|
||||
match fs::read_dir(dir) {
|
||||
Ok(mut entries) => entries.next().is_none(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tmp_path() -> String {
|
||||
let dirs = [
|
||||
"/debug_ramdisk",
|
||||
"/patch_hw",
|
||||
"/oem",
|
||||
"/root",
|
||||
"/sbin",
|
||||
];
|
||||
|
||||
// find empty directory
|
||||
for dir in dirs {
|
||||
if is_ok_empty(dir) {
|
||||
return dir.to_string();
|
||||
}
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
pub fn get_work_dir() -> String {
|
||||
let tmp_path = get_tmp_path();
|
||||
format!("{}/workdir/", tmp_path)
|
||||
}
|
||||
|
||||
#[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("/proc/self/exe", 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", "me.weishu.kernelsu"])
|
||||
.spawn()?;
|
||||
println!("- Rebooting in 5 seconds..");
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
Command::new("reboot").spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user