#[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::anyhow; use anyhow::bail; use anyhow::ensure; use anyhow::Context; use anyhow::Result; use which::which; 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)> { use regex::Regex; 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::().ok()) .ok_or_else(|| anyhow!("Major version parse error"))?; let minor = captures .get(2) .and_then(|m| m.as_str().parse::().ok()) .ok_or_else(|| anyhow!("Minor version parse error"))?; let patch = captures .get(3) .and_then(|m| m.as_str().parse::().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 { use regex::Regex; 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 { 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 { 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().map_or(false, |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 { parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules()) } #[cfg(not(target_os = "android"))] pub fn get_current_kmi() -> Result { bail!("Unsupported platform") } fn do_cpio_cmd(magiskboot: &Path, workding_dir: &Path, cmd: &str) -> Result<()> { let status = Command::new(magiskboot) .current_dir(workding_dir) .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, workding_dir: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workding_dir) .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, workding_dir: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workding_dir) .stdout(Stdio::null()) .stderr(Stdio::null()) .args(["cpio", "ramdisk.cpio", "exists kernelsu.ko"]) .status()?; Ok(status.success()) } fn dd, Q: AsRef>(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, magiskboot_path: Option, flash: bool, ) -> Result<()> { let workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?; let magiskboot = find_magiskboot(magiskboot_path, workding_dir.path())?; let (bootimage, bootdevice) = find_boot_image(&image, false, false, workding_dir.path())?; println!("- Unpacking boot image"); let status = Command::new(&magiskboot) .current_dir(workding_dir.path()) .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, workding_dir.path())?; 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, workding_dir.path(), "exists orig.ksu").is_ok() { do_cpio_cmd( &magiskboot, workding_dir.path(), "extract orig.ksu orig.ksu", )?; let sha = std::fs::read(workding_dir.path().join("orig.ksu"))?; let sha = String::from_utf8(sha)?; let sha = sha.trim(); let backup_path = format!("{KSU_BACKUP_DIR}/{sha}"); if Path::new(&backup_path).is_file() { new_boot = Some(PathBuf::from(backup_path)); from_backup = true; } else { println!("- Warning: no backup {KSU_BACKUP_DIR}/{KSU_BACKUP_FILE_PREFIX}{sha} found!"); } } else { println!("- Cannot found backup image!"); } if new_boot.is_none() { // remove kernelsu.ko do_cpio_cmd(&magiskboot, workding_dir.path(), "rm kernelsu.ko")?; // if init.real exists, restore it let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init.real").is_ok(); if status { do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init.real init")?; } else { let ramdisk = workding_dir.path().join("ramdisk.cpio"); std::fs::remove_file(ramdisk)?; } println!("- Repacking boot image"); let status = Command::new(&magiskboot) .current_dir(workding_dir.path()) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") .arg(bootimage.display().to_string()) .status()?; ensure!(status.success(), "magiskboot repack failed"); new_boot = Some(workding_dir.path().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, kernel: Option, kmod: Option, init: Option, ota: bool, flash: bool, out: Option, magiskboot: Option, kmi: Option, ) -> 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, kernel: Option, kmod: Option, init: Option, ota: bool, flash: bool, out: Option, magiskboot_path: Option, kmi: Option, ) -> 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 workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?; let (bootimage, bootdevice) = find_boot_image(&image, ota, is_replace_kernel, workding_dir.path())?; let bootimage = bootimage.display().to_string(); // try extract magiskboot/bootctl let _ = assets::ensure_binaries(false); // extract magiskboot let magiskboot = find_magiskboot(magiskboot_path, workding_dir.path())?; if let Some(kernel) = kernel { std::fs::copy(kernel, workding_dir.path().join("kernel")) .context("copy kernel from failed")?; } println!("- Preparing assets"); let kmod_file = workding_dir.path().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 let kmi = if let Some(kmi) = kmi { kmi } else { get_current_kmi().context("Unknown KMI, please choose LKM manually")? }; 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 = workding_dir.path().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 kernelsu.ko' println!("- Unpacking boot image"); let status = Command::new(&magiskboot) .current_dir(workding_dir.path()) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("unpack") .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot unpack failed"); let no_ramdisk = !workding_dir.path().join("ramdisk.cpio").exists(); let is_magisk_patched = is_magisk_patched(&magiskboot, workding_dir.path())?; ensure!( no_ramdisk || !is_magisk_patched, "Cannot work with Magisk patched image" ); println!("- Adding KernelSU LKM"); let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workding_dir.path())?; let mut need_backup = false; if !is_kernelsu_patched { // kernelsu.ko is not exist, backup init if necessary let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init"); if status.is_ok() { do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init init.real")?; } need_backup = flash; } do_cpio_cmd(&magiskboot, workding_dir.path(), "add 0755 init init")?; do_cpio_cmd( &magiskboot, workding_dir.path(), "add 0755 kernelsu.ko kernelsu.ko", )?; println!("- Repacking boot image"); // magiskboot repack boot.img let status = Command::new(&magiskboot) .current_dir(workding_dir.path()) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); let new_boot = workding_dir.path().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()?; } } #[cfg(target_os = "android")] if need_backup { let do_backup = move || -> Result<()> { println!("- Backup stock boot image"); // magiskboot cpio ramdisk.cpio 'add 0755 orig.ksu' let output = Command::new(&magiskboot) .current_dir(workding_dir.path()) .arg("sha1") .arg(&bootimage) .output()?; ensure!( output.status.success(), "Cannot calculate sha1 of original boot!" ); let output = String::from_utf8(output.stdout)?; let output = output.trim(); let backup_name = format!("{KSU_BACKUP_FILE_PREFIX}{output}"); let target = format!("{KSU_BACKUP_DIR}/{backup_name}"); std::fs::copy(&bootimage, &target).with_context(|| format!("backup to {target}"))?; std::fs::write(workding_dir.path().join("orig.ksu"), backup_name.as_bytes()) .context("write sha1")?; do_cpio_cmd( &magiskboot, workding_dir.path(), "add 0755 orig.ksu orig.ksu", )?; println!("- Stock image has been backup to"); println!("- {target}"); println!("- Clean up backup"); if let Ok(dir) = std::fs::read_dir("/data") { for entry in dir.flatten() { let path = entry.path(); if path.is_file() { 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(()) }; if let Err(e) = do_backup() { println!("- Warning: backup failed"); println!("- {:?}", e); } } println!("- Done!"); Ok(()) } fn flash_boot(bootdevice: &Option, 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, workding_dir: &Path) -> Result { 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 = workding_dir.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, ota: bool, is_replace_kernel: bool, workding_dir: &Path, ) -> Result<(PathBuf, Option)> { 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 { 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 { 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 = workding_dir.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(()) }