Files
Magisk/native/src/boot/cpio.rs
2025-12-08 23:31:04 -08:00

854 lines
25 KiB
Rust

#![allow(clippy::useless_conversion)]
use argh::FromArgs;
use base::argh;
use bytemuck::{Pod, Zeroable, from_bytes};
use num_traits::cast::AsPrimitive;
use size::{Base, Size, Style};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{Cursor, Read, Write};
use std::mem::size_of;
use std::process::exit;
use std::str;
use crate::check_env;
use crate::compress::{get_decoder, get_encoder};
use crate::ffi::FileFormat;
use crate::patch::{patch_encryption, patch_verity};
use base::libc::{
S_IFBLK, S_IFCHR, S_IFDIR, S_IFLNK, S_IFMT, S_IFREG, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP,
S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, dev_t, gid_t, major, makedev, minor, mknod,
mode_t, uid_t,
};
use base::nix::fcntl::OFlag;
use base::{
BytesExt, EarlyExitExt, LoggedResult, MappedFile, OptionExt, ResultExt, Utf8CStr, Utf8CStrBuf,
WriteExt, cstr, log_err,
};
#[derive(FromArgs)]
struct CpioCommand {
#[argh(subcommand)]
action: CpioAction,
}
#[derive(FromArgs)]
#[argh(subcommand)]
enum CpioAction {
Test(Test),
Restore(Restore),
Patch(Patch),
Exists(Exists),
Backup(Backup),
Remove(Remove),
Move(Move),
Extract(Extract),
MakeDir(MakeDir),
Link(Link),
Add(Add),
List(List),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "test")]
struct Test {}
#[derive(FromArgs)]
#[argh(subcommand, name = "restore")]
struct Restore {}
#[derive(FromArgs)]
#[argh(subcommand, name = "patch")]
struct Patch {}
#[derive(FromArgs)]
#[argh(subcommand, name = "exists")]
struct Exists {
#[argh(positional, arg_name = "entry")]
path: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "backup")]
struct Backup {
#[argh(positional, arg_name = "orig")]
origin: String,
#[argh(switch, short = 'n')]
skip_compress: bool,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "rm")]
struct Remove {
#[argh(positional, arg_name = "entry")]
path: String,
#[argh(switch, short = 'r')]
recursive: bool,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "mv")]
struct Move {
#[argh(positional, arg_name = "source")]
from: String,
#[argh(positional, arg_name = "dest")]
to: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "extract")]
struct Extract {
#[argh(positional, greedy)]
paths: Vec<String>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "mkdir")]
struct MakeDir {
#[argh(positional, from_str_fn(parse_mode))]
mode: mode_t,
#[argh(positional, arg_name = "entry")]
dir: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "ln")]
struct Link {
#[argh(positional, arg_name = "entry")]
src: String,
#[argh(positional, arg_name = "target")]
dst: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "add")]
struct Add {
#[argh(positional, from_str_fn(parse_mode))]
mode: mode_t,
#[argh(positional, arg_name = "entry")]
path: String,
#[argh(positional, arg_name = "infile")]
file: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "ls")]
struct List {
#[argh(positional, default = r#"String::from("/")"#)]
path: String,
#[argh(switch, short = 'r')]
recursive: bool,
}
pub(crate) fn print_cpio_usage() {
eprintln!(
r#"Usage: magiskboot cpio <incpio> [commands...]
Do cpio commands to <incpio> (modifications are done in-place).
Each command is a single argument; add quotes for each command.
Supported commands:
exists ENTRY
Return 0 if ENTRY exists, else return 1
ls [-r] [PATH]
List PATH ("/" by default); specify [-r] to list recursively
rm [-r] ENTRY
Remove ENTRY, specify [-r] to remove recursively
mkdir MODE ENTRY
Create directory ENTRY with permissions MODE
ln TARGET ENTRY
Create a symlink to TARGET with the name ENTRY
mv SOURCE DEST
Move SOURCE to DEST
add MODE ENTRY INFILE
Add INFILE as ENTRY with permissions MODE; replaces ENTRY if exists
extract [ENTRY OUT]
Extract ENTRY to OUT, or extract all entries to current directory
test
Test the cpio's status. Return values:
0:stock 1:Magisk 2:unsupported
patch
Apply ramdisk patches
Configure with env variables: KEEPVERITY KEEPFORCEENCRYPT
backup ORIG [-n]
Create ramdisk backups from ORIG, specify [-n] to skip compression
restore
Restore ramdisk from ramdisk backup stored within incpio
"#
)
}
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C, packed)]
struct CpioHeader {
magic: [u8; 6],
ino: [u8; 8],
mode: [u8; 8],
uid: [u8; 8],
gid: [u8; 8],
nlink: [u8; 8],
mtime: [u8; 8],
filesize: [u8; 8],
devmajor: [u8; 8],
devminor: [u8; 8],
rdevmajor: [u8; 8],
rdevminor: [u8; 8],
namesize: [u8; 8],
check: [u8; 8],
}
struct Cpio {
entries: BTreeMap<String, Box<CpioEntry>>,
}
struct CpioEntry {
mode: mode_t,
uid: uid_t,
gid: gid_t,
rdevmajor: dev_t,
rdevminor: dev_t,
data: Vec<u8>,
}
impl Cpio {
fn new() -> Self {
Self {
entries: BTreeMap::new(),
}
}
fn load_from_data(data: &[u8]) -> LoggedResult<Self> {
let mut cpio = Cpio::new();
let mut pos = 0_usize;
while pos < data.len() {
let hdr_sz = size_of::<CpioHeader>();
let hdr = from_bytes::<CpioHeader>(&data[pos..(pos + hdr_sz)]);
if &hdr.magic != b"070701" {
return log_err!("invalid cpio magic");
}
pos += hdr_sz;
let name_sz = x8u(&hdr.namesize)? as usize;
let name = Utf8CStr::from_bytes(&data[pos..(pos + name_sz)])?.to_string();
pos += name_sz;
pos = align_4(pos);
if name == "." || name == ".." {
continue;
}
if name == "TRAILER!!!" {
match data[pos..].find(b"070701") {
Some(x) => pos += x,
None => break,
}
continue;
}
let file_sz = x8u(&hdr.filesize)? as usize;
let entry = Box::new(CpioEntry {
mode: x8u(&hdr.mode)?.as_(),
uid: x8u(&hdr.uid)?.as_(),
gid: x8u(&hdr.gid)?.as_(),
rdevmajor: x8u(&hdr.rdevmajor)?.as_(),
rdevminor: x8u(&hdr.rdevminor)?.as_(),
data: data[pos..(pos + file_sz)].to_vec(),
});
pos += file_sz;
cpio.entries.insert(name, entry);
pos = align_4(pos);
}
Ok(cpio)
}
fn load_from_file(path: &Utf8CStr) -> LoggedResult<Self> {
eprintln!("Loading cpio: [{path}]");
let file = MappedFile::open(path)?;
Self::load_from_data(file.as_ref())
}
fn dump(&self, path: &str) -> LoggedResult<()> {
eprintln!("Dumping cpio: [{path}]");
let mut file = File::create(path)?;
let mut pos = 0usize;
let mut inode = 300000i64;
for (name, entry) in &self.entries {
pos += file.write(
format!(
"070701{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}",
inode,
entry.mode,
entry.uid,
entry.gid,
1,
0,
entry.data.len(),
0,
0,
entry.rdevmajor,
entry.rdevminor,
name.len() + 1,
0
).as_bytes(),
)?;
pos += file.write(name.as_bytes())?;
pos += file.write(&[0])?;
file.write_zeros(align_4(pos) - pos)?;
pos = align_4(pos);
pos += file.write(&entry.data)?;
file.write_zeros(align_4(pos) - pos)?;
pos = align_4(pos);
inode += 1;
}
pos += file.write(
format!("070701{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}",
inode, 0o755, 0, 0, 1, 0, 0, 0, 0, 0, 0, 11, 0
).as_bytes()
)?;
pos += file.write("TRAILER!!!\0".as_bytes())?;
file.write_zeros(align_4(pos) - pos)?;
Ok(())
}
fn rm(&mut self, path: &str, recursive: bool) {
let path = norm_path(path);
if self.entries.remove(&path).is_some() {
eprintln!("Removed entry [{path}]");
}
if recursive {
let path = path + "/";
self.entries.retain(|k, _| {
if k.starts_with(&path) {
eprintln!("Removed entry [{k}]");
false
} else {
true
}
})
}
}
fn extract_entry(&self, path: &str, out: &mut String) -> LoggedResult<()> {
let entry = self
.entries
.get(path)
.ok_or_log_msg(|w| w.write_str("No such file"))?;
eprintln!("Extracting entry [{path}] to [{out}]");
let out = Utf8CStr::from_string(out);
let mut buf = cstr::buf::default();
// Make sure its parent directories exist
if let Some(dir) = out.parent_dir() {
buf.push_str(dir);
buf.mkdirs(0o755)?;
}
let mode: mode_t = (entry.mode & 0o777).into();
match entry.mode & S_IFMT {
S_IFDIR => out.mkdir(mode)?,
S_IFREG => {
let mut file = out.create(
OFlag::O_CREAT | OFlag::O_TRUNC | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
mode,
)?;
file.write_all(&entry.data)?;
}
S_IFLNK => {
buf.clear();
buf.push_str(str::from_utf8(entry.data.as_slice())?);
out.create_symlink_to(&buf)?;
}
S_IFBLK | S_IFCHR => {
let dev = makedev(entry.rdevmajor.try_into()?, entry.rdevminor.try_into()?);
unsafe { mknod(out.as_ptr().cast(), entry.mode, dev) };
}
_ => {
return log_err!("unknown entry type");
}
}
Ok(())
}
fn extract(&self, path: Option<&mut String>, out: Option<&mut String>) -> LoggedResult<()> {
let path = path.map(|s| norm_path(s.as_str()));
if let (Some(path), Some(out)) = (&path, out) {
return self.extract_entry(path, out);
} else {
for path in self.entries.keys() {
if path == "." || path == ".." {
continue;
}
self.extract_entry(path, &mut path.clone())?;
}
}
Ok(())
}
fn exists(&self, path: &str) -> bool {
self.entries.contains_key(&norm_path(path))
}
fn add(&mut self, mode: mode_t, path: &str, file: &mut String) -> LoggedResult<()> {
if path.ends_with('/') {
return log_err!("path cannot end with / for add");
}
let file = Utf8CStr::from_string(file);
let attr = file.get_attr()?;
let mut content = Vec::<u8>::new();
let rdevmajor: dev_t;
let rdevminor: dev_t;
// Treat symlinks as regular files as symlinks are created by the 'ln TARGET ENTRY' command
let mode = if attr.is_file() || attr.is_symlink() {
rdevmajor = 0;
rdevminor = 0;
file.open(OFlag::O_RDONLY | OFlag::O_CLOEXEC)?
.read_to_end(&mut content)?;
mode | S_IFREG
} else {
rdevmajor = major(attr.st.st_rdev.as_()).as_();
rdevminor = minor(attr.st.st_rdev.as_()).as_();
if attr.is_block_device() {
mode | S_IFBLK
} else if attr.is_char_device() {
mode | S_IFCHR
} else {
return log_err!("unsupported file type");
}
};
self.entries.insert(
norm_path(path),
Box::new(CpioEntry {
mode,
uid: 0,
gid: 0,
rdevmajor,
rdevminor,
data: content,
}),
);
eprintln!("Add file [{path}] ({mode:04o})");
Ok(())
}
fn mkdir(&mut self, mode: mode_t, dir: &str) {
self.entries.insert(
norm_path(dir),
Box::new(CpioEntry {
mode: mode | S_IFDIR,
uid: 0,
gid: 0,
rdevmajor: 0,
rdevminor: 0,
data: vec![],
}),
);
eprintln!("Create directory [{dir}] ({mode:04o})");
}
fn ln(&mut self, src: &str, dst: &str) {
self.entries.insert(
norm_path(dst),
Box::new(CpioEntry {
mode: S_IFLNK,
uid: 0,
gid: 0,
rdevmajor: 0,
rdevminor: 0,
data: norm_path(src).as_bytes().to_vec(),
}),
);
eprintln!("Create symlink [{dst}] -> [{src}]");
}
fn mv(&mut self, from: &str, to: &str) -> LoggedResult<()> {
let entry = self
.entries
.remove(&norm_path(from))
.ok_or_log_msg(|w| w.write_fmt(format_args!("No such entry {from}")))?;
self.entries.insert(norm_path(to), entry);
eprintln!("Move [{from}] -> [{to}]");
Ok(())
}
fn ls(&self, path: &str, recursive: bool) {
let path = norm_path(path);
let path = if path.is_empty() {
path
} else {
"/".to_string() + path.as_str()
};
for (name, entry) in &self.entries {
let p = "/".to_string() + name.as_str();
let Some(p) = p.strip_prefix(&path) else {
continue;
};
if !p.is_empty() && !p.starts_with('/') {
continue;
}
if !recursive && !p.is_empty() && p.matches('/').count() > 1 {
continue;
}
println!("{entry}\t{name}");
}
}
}
const MAGISK_PATCHED: i32 = 1 << 0;
const UNSUPPORTED_CPIO: i32 = 1 << 1;
impl Cpio {
fn patch(&mut self) {
let keep_verity = check_env("KEEPVERITY");
let keep_force_encrypt = check_env("KEEPFORCEENCRYPT");
eprintln!(
"Patch with flag KEEPVERITY=[{keep_verity}] KEEPFORCEENCRYPT=[{keep_force_encrypt}]"
);
self.entries.retain(|name, entry| {
let fstab = (!keep_verity || !keep_force_encrypt)
&& entry.mode & S_IFMT == S_IFREG
&& !name.starts_with(".backup")
&& !name.starts_with("twrp")
&& !name.starts_with("recovery")
&& name.starts_with("fstab");
if !keep_verity {
if fstab {
eprintln!("Found fstab file [{name}]");
let len = patch_verity(entry.data.as_mut_slice());
if len != entry.data.len() {
entry.data.resize(len, 0);
}
} else if name == "verity_key" {
return false;
}
}
if !keep_force_encrypt && fstab {
let len = patch_encryption(entry.data.as_mut_slice());
if len != entry.data.len() {
entry.data.resize(len, 0);
}
}
true
});
}
fn test(&self) -> i32 {
for file in [
"sbin/launch_daemonsu.sh",
"sbin/su",
"init.xposed.rc",
"boot/sbin/launch_daemonsu.sh",
] {
if self.exists(file) {
return UNSUPPORTED_CPIO;
}
}
for file in [
".backup/.magisk",
"init.magisk.rc",
"overlay/init.magisk.rc",
] {
if self.exists(file) {
return MAGISK_PATCHED;
}
}
0
}
fn restore(&mut self) -> LoggedResult<()> {
let mut backups = HashMap::<String, Box<CpioEntry>>::new();
let mut rm_list = String::new();
self.entries
.extract_if(.., |name, _| name.starts_with(".backup/"))
.for_each(|(name, mut entry)| {
if name == ".backup/.rmlist" {
if let Ok(data) = str::from_utf8(&entry.data) {
rm_list.push_str(data);
}
} else if name != ".backup/.magisk" {
let new_name = if name.ends_with(".xz") && entry.decompress() {
&name[8..name.len() - 3]
} else {
&name[8..]
};
eprintln!("Restore [{name}] -> [{new_name}]");
backups.insert(new_name.to_string(), entry);
}
});
self.rm(".backup", false);
if rm_list.is_empty() && backups.is_empty() {
self.entries.clear();
return Ok(());
}
for rm in rm_list.split('\0') {
if !rm.is_empty() {
self.rm(rm, false);
}
}
self.entries.extend(backups);
Ok(())
}
fn backup(&mut self, origin: &mut String, skip_compress: bool) -> LoggedResult<()> {
let mut backups = HashMap::<String, Box<CpioEntry>>::new();
let mut rm_list = String::new();
backups.insert(
".backup".to_string(),
Box::new(CpioEntry {
mode: S_IFDIR,
uid: 0,
gid: 0,
rdevmajor: 0,
rdevminor: 0,
data: vec![],
}),
);
let origin = Utf8CStr::from_string(origin);
let mut o = Cpio::load_from_file(origin)?;
o.rm(".backup", true);
self.rm(".backup", true);
let mut left_iter = o.entries.into_iter();
let mut right_iter = self.entries.iter();
let mut lhs = left_iter.next();
let mut rhs = right_iter.next();
loop {
enum Action<'a> {
Backup(String, Box<CpioEntry>),
Record(&'a String),
Noop,
}
// Move the iterator forward if needed
if lhs.is_none() {
lhs = left_iter.next();
}
if rhs.is_none() {
rhs = right_iter.next();
}
let action = match (lhs.take(), rhs.take()) {
(Some((ln, le)), Some((rn, re))) => match ln.as_str().cmp(rn.as_str()) {
Ordering::Less => {
// Put rhs back
rhs = Some((rn, re));
Action::Backup(ln, le)
}
Ordering::Greater => {
// Put lhs back
lhs = Some((ln, le));
Action::Record(rn)
}
Ordering::Equal => {
if re.data != le.data {
Action::Backup(ln, le)
} else {
Action::Noop
}
}
},
(Some((ln, le)), None) => Action::Backup(ln, le),
(None, Some((rn, _))) => Action::Record(rn),
(None, None) => break,
};
match action {
Action::Backup(name, mut entry) => {
let backup = if !skip_compress && entry.compress() {
format!(".backup/{name}.xz")
} else {
format!(".backup/{name}")
};
eprintln!("Backup [{name}] -> [{backup}]");
backups.insert(backup, entry);
}
Action::Record(name) => {
eprintln!("Record new entry: [{name}] -> [.backup/.rmlist]");
rm_list.push_str(&format!("{name}\0"));
}
Action::Noop => {}
}
}
if !rm_list.is_empty() {
backups.insert(
".backup/.rmlist".to_string(),
Box::new(CpioEntry {
mode: S_IFREG,
uid: 0,
gid: 0,
rdevmajor: 0,
rdevminor: 0,
data: rm_list.as_bytes().to_vec(),
}),
);
}
self.entries.extend(backups);
Ok(())
}
}
impl CpioEntry {
pub(crate) fn compress(&mut self) -> bool {
if self.mode & S_IFMT != S_IFREG {
return false;
}
let Ok(data): std::io::Result<Vec<u8>> = (try {
let mut encoder = get_encoder(FileFormat::XZ, Vec::new())?;
encoder.write_all(&self.data)?;
encoder.finish()?
}) else {
eprintln!("xz compression failed");
return false;
};
self.data = data;
true
}
pub(crate) fn decompress(&mut self) -> bool {
if self.mode & S_IFMT != S_IFREG {
return false;
}
let Ok(data): std::io::Result<Vec<u8>> = (try {
let mut decoder = get_decoder(FileFormat::XZ, Cursor::new(&self.data))?;
let mut data = Vec::new();
std::io::copy(decoder.as_mut(), &mut data)?;
data
}) else {
eprintln!("xz compression failed");
return false;
};
self.data = data;
true
}
}
impl Display for CpioEntry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}{}{}{}{}{}\t{}\t{}\t{}\t{}:{}",
match self.mode & S_IFMT {
S_IFDIR => "d",
S_IFREG => "-",
S_IFLNK => "l",
S_IFBLK => "b",
S_IFCHR => "c",
_ => "?",
},
if self.mode & S_IRUSR != 0 { "r" } else { "-" },
if self.mode & S_IWUSR != 0 { "w" } else { "-" },
if self.mode & S_IXUSR != 0 { "x" } else { "-" },
if self.mode & S_IRGRP != 0 { "r" } else { "-" },
if self.mode & S_IWGRP != 0 { "w" } else { "-" },
if self.mode & S_IXGRP != 0 { "x" } else { "-" },
if self.mode & S_IROTH != 0 { "r" } else { "-" },
if self.mode & S_IWOTH != 0 { "w" } else { "-" },
if self.mode & S_IXOTH != 0 { "x" } else { "-" },
self.uid,
self.gid,
Size::from_bytes(self.data.len())
.format()
.with_style(Style::Abbreviated)
.with_base(Base::Base10),
self.rdevmajor,
self.rdevminor,
)
}
}
pub(crate) fn cpio_commands(file: &Utf8CStr, cmds: &Vec<String>) -> LoggedResult<()> {
let mut cpio = if file.exists() {
Cpio::load_from_file(file)?
} else {
Cpio::new()
};
for cmd in cmds {
if cmd.starts_with('#') {
continue;
}
let mut cmd = CpioCommand::from_args(
&["magiskboot", "cpio", file],
cmd.split(' ')
.filter(|x| !x.is_empty())
.collect::<Vec<_>>()
.as_slice(),
)
.on_early_exit(print_cpio_usage);
match &mut cmd.action {
CpioAction::Test(_) => exit(cpio.test()),
CpioAction::Restore(_) => cpio.restore()?,
CpioAction::Patch(_) => cpio.patch(),
CpioAction::Exists(Exists { path }) => {
return if cpio.exists(path) {
Ok(())
} else {
log_err!()
};
}
CpioAction::Backup(Backup {
origin,
skip_compress,
}) => cpio.backup(origin, *skip_compress)?,
CpioAction::Remove(Remove { path, recursive }) => cpio.rm(path, *recursive),
CpioAction::Move(Move { from, to }) => cpio.mv(from, to)?,
CpioAction::MakeDir(MakeDir { mode, dir }) => cpio.mkdir(*mode, dir),
CpioAction::Link(Link { src, dst }) => cpio.ln(src, dst),
CpioAction::Add(Add { mode, path, file }) => cpio.add(*mode, path, file)?,
CpioAction::Extract(Extract { paths }) => {
if !paths.is_empty() && paths.len() != 2 {
log_err!("invalid arguments")?;
}
let mut it = paths.iter_mut();
cpio.extract(it.next(), it.next())?;
}
CpioAction::List(List { path, recursive }) => {
cpio.ls(path.as_str(), *recursive);
return Ok(());
}
};
}
cpio.dump(file)?;
Ok(())
}
fn x8u(x: &[u8; 8]) -> LoggedResult<u32> {
// parse hex
let mut ret = 0u32;
let s = str::from_utf8(x).log_with_msg(|w| w.write_str("bad cpio header"))?;
for c in s.chars() {
ret = ret * 16
+ c.to_digit(16)
.ok_or_log_msg(|w| w.write_str("bad cpio header"))?;
}
Ok(ret)
}
#[inline(always)]
fn align_4(x: usize) -> usize {
(x + 3) & !3
}
#[inline(always)]
fn norm_path(path: &str) -> String {
path.split('/')
.filter(|x| !x.is_empty())
.intersperse("/")
.collect()
}
fn parse_mode(s: &str) -> Result<mode_t, String> {
mode_t::from_str_radix(s, 8).map_err(|e| e.to_string())
}