diff --git a/zygisk/build.rs b/zygisk/build.rs index 04cb05f..922bc2e 100644 --- a/zygisk/build.rs +++ b/zygisk/build.rs @@ -11,8 +11,8 @@ //! //! See the fork's commit message for the full rationale. -use std::env; use std::path::PathBuf; +use std::{env, fs}; fn main() { println!("cargo:rerun-if-changed=build.rs"); @@ -24,9 +24,10 @@ fn main() { // the Android cdylib; skip the whole native build step. return; } - if !target.starts_with("aarch64") { - panic!("vpnhide-zygisk currently only supports aarch64-linux-android (target={target})"); - } + assert!( + target.starts_with("aarch64"), + "vpnhide-zygisk currently only supports aarch64-linux-android (target={target})" + ); // cargo-ndk sets ANDROID_NDK_HOME before invoking us. let ndk = env::var("ANDROID_NDK_HOME") @@ -96,9 +97,9 @@ fn main() { /// Typical layout: `/toolchains/llvm/prebuilt//lib/clang//lib/linux/…`. fn find_ndk_builtins(ndk: &str) -> Option { let base = PathBuf::from(ndk).join("toolchains/llvm/prebuilt"); - for host in std::fs::read_dir(&base).ok()?.flatten() { + for host in fs::read_dir(&base).ok()?.flatten() { let clang_dir = host.path().join("lib/clang"); - let Ok(versions) = std::fs::read_dir(&clang_dir) else { + let Ok(versions) = fs::read_dir(&clang_dir) else { continue; }; for v in versions.flatten() { diff --git a/zygisk/src/filter.rs b/zygisk/src/filter.rs index b73ed8a..96878cd 100644 --- a/zygisk/src/filter.rs +++ b/zygisk/src/filter.rs @@ -46,8 +46,7 @@ fn compact_lines(data: &mut [u8], mut hide: impl FnMut(&[u8]) -> bool) -> usize let line_end = data[read_pos..] .iter() .position(|&b| b == b'\n') - .map(|p| read_pos + p + 1) - .unwrap_or(len); + .map_or(len, |position| read_pos + position + 1); if !hide(&data[read_pos..line_end]) { let line_len = line_end - read_pos; @@ -96,12 +95,11 @@ pub fn filter_ipv6_route_buf(data: &mut [u8]) -> usize { /// drop lines whose interface name (trimmed, before the first ':') is a VPN /// interface. The two header lines have no `name:` token and are kept. pub fn filter_dev_buf(data: &mut [u8]) -> usize { - compact_lines(data, |line| match line.iter().position(|&b| b == b':') { - Some(colon) => { + compact_lines(data, |line| { + line.iter().position(|&b| b == b':').is_some_and(|colon| { let name = trim_ascii_ws(&line[..colon]); !name.is_empty() && is_vpn_iface_bytes(name) - } - None => false, + }) }) } @@ -215,7 +213,7 @@ fn parse_hex_u32(hex: &[u8]) -> Option { b'a'..=b'f' => b - b'a' + 10, _ => return None, }; - val = val.checked_shl(4)? | digit as u32; + val = val.checked_shl(4)? | u32::from(digit); } Some(val) } @@ -341,10 +339,8 @@ pub fn filter_netlink_dump(data: &mut [u8], vpn_indices: &[u32]) -> usize { vpn_indices.contains(&if_index) } else if nlmsg_type == RTM_NEWROUTE && nlmsg_len >= NLMSG_HDRLEN + RTMSG_HDRLEN { // Output interface is an RTA_OIF rtattr after struct rtmsg. - match route_oif(&data[read_pos..read_pos + nlmsg_len]) { - Some(oif) => vpn_indices.contains(&oif), - None => false, - } + route_oif(&data[read_pos..read_pos + nlmsg_len]) + .is_some_and(|oif| vpn_indices.contains(&oif)) } else { false }; @@ -374,6 +370,7 @@ pub fn filter_netlink_dump(data: &mut [u8], vpn_indices: &[u32]) -> usize { #[cfg(test)] mod tests { use super::*; + use core::str; #[test] fn detects_tun0() { @@ -390,7 +387,7 @@ face |bytes packets errs drop\n lo: 100 1 0 0\n wlan0: 200 tun0: 300 3 0 0\n" .to_vec(); let n = filter_dev_buf(&mut buf); - let out = core::str::from_utf8(&buf[..n]).unwrap(); + let out = str::from_utf8(&buf[..n]).unwrap(); assert!(out.contains("Inter-|"), "header kept"); assert!(out.contains("face |"), "second header kept"); assert!(out.contains("lo:"), "lo kept"); @@ -457,7 +454,7 @@ tun0: 300 3 0 0\n" rmnet0\tFEFFFFFF\t00000000\n"; let mut buf = input.to_vec(); let new_len = filter_route_buf(&mut buf); - let result = core::str::from_utf8(&buf[..new_len]).unwrap(); + let result = str::from_utf8(&buf[..new_len]).unwrap(); assert!(result.contains("Iface\t")); assert!(result.contains("wlan0\t")); assert!(result.contains("rmnet0\t")); @@ -479,7 +476,7 @@ tun0: 300 3 0 0\n" let input = b"Iface\tDest\nwg0\t00000000\nwlan0\t00000000\n"; let mut buf = input.to_vec(); let new_len = filter_route_buf(&mut buf); - let result = core::str::from_utf8(&buf[..new_len]).unwrap(); + let result = str::from_utf8(&buf[..new_len]).unwrap(); assert!(!result.contains("wg0")); assert!(result.contains("wlan0")); } diff --git a/zygisk/src/hooks.rs b/zygisk/src/hooks.rs index 6b63007..2af0962 100644 --- a/zygisk/src/hooks.rs +++ b/zygisk/src/hooks.rs @@ -1,16 +1,16 @@ //! The actual libc hook functions. //! -//! These are called INSTEAD OF the real libc symbols from any PLT we've -//! patched. Each hook calls the saved original function via a function -//! pointer stored in `statics` after `pltHookRegister` ran. We can't rely -//! on linkage-time names like `libc::ioctl` because those would resolve -//! back through our own PLT entry — which would either recurse infinitely -//! or, in a different library's process, not be hooked at all. +//! These replace the real libc symbols through shadowhook's inline trampolines. +//! Each hook calls the original function through the trampoline captured when +//! the inline hook was installed. We can't call linkage-time names such as +//! `libc::ioctl`, because they would resolve back through the hooked entry point +//! and recurse. //! //! ## Important safety notes //! -//! * The `*const ()` function pointers used by Zygisk's PLT hook API are -//! variadic in C (`ioctl` is `int ioctl(int fd, unsigned long req, ...)`) +//! * The `*const ()` function pointers returned by shadowhook represent +//! functions that may be variadic in C (`ioctl` is +//! `int ioctl(int fd, unsigned long req, ...)`) //! but Rust doesn't have first-class variadic functions. We work around //! this by declaring the relevant `ioctl` signatures we care about //! (`SIOCGIFNAME`, `SIOCGIFFLAGS`) as 3-argument forms and trusting the @@ -23,12 +23,17 @@ //! original. Panicking would abort the entire process. use core::cell::{Cell, RefCell}; -use core::ffi::{c_int, c_void}; +use core::ffi::{CStr, c_int, c_void}; use core::sync::atomic::{AtomicPtr, AtomicU8, Ordering}; +use core::{mem, ptr, slice}; use libc::{SIOCGIFCONF, SIOCGIFNAME, ifreq}; -use crate::filter::is_vpn_iface_bytes; +use crate::filter::{ + MAX_VPN_ADDRS, RTM_NEWADDR, RTM_NEWLINK, RTM_NEWROUTE, filter_dev_buf, filter_if_inet6_buf, + filter_ipv6_route_buf, filter_netlink_dump, filter_route_buf, filter_tcp4_buf, filter_tcp6_buf, + is_vpn_iface_bytes, is_vpn_iface_cstr, +}; /// `struct ifconf` from ``. Not exported by the `libc` crate. #[repr(C)] @@ -46,7 +51,6 @@ struct ifconf { // causing errors like `ioctl(SIOCGIFFLAGS) for "tun0" failed in ifaddrs` // and corrupting NFC/HCE payment flows). thread_local! { - #[allow(clippy::missing_const_for_thread_local)] static IN_GETIFADDRS: Cell = const { Cell::new(false) }; /// Reusable contiguous view for scatter/gather netlink recvmsg payloads. @@ -57,6 +61,17 @@ thread_local! { static NETLINK_IOV_BUF: RefCell> = RefCell::new(Vec::with_capacity(65536)); } +/// Run one libc operation without letting the ioctl hook filter nested calls. +/// Preserve the previous value so nested guarded operations remain correct. +fn with_ioctl_passthrough(operation: impl FnOnce() -> T) -> T { + IN_GETIFADDRS.with(|guard| { + let previous = guard.replace(true); + let result = operation(); + guard.set(previous); + result + }) +} + /// Returns true if `fd` is an `AF_NETLINK` socket. /// /// Used by recv/recvmsg hooks to skip TCP/UDP/Unix sockets entirely — @@ -69,7 +84,7 @@ thread_local! { /// chance accumulates to near-certain breakage. fn is_netlink_fd(fd: c_int) -> bool { let mut domain: c_int = 0; - let mut optlen = core::mem::size_of::() as libc::socklen_t; + let mut optlen = mem::size_of::() as libc::socklen_t; let rc = unsafe { libc::getsockopt( fd, @@ -113,7 +128,7 @@ fn set_errno(val: c_int) { // Saved originals // ============================================================================ -/// Declare a saved-original slot for one PLT-hooked libc function. +/// Declare a saved-original slot for one inline-hooked libc function. /// /// Each hook needs the same four items to call through to the function it /// replaced, so we generate them from one declaration instead of hand-copying @@ -121,8 +136,7 @@ fn set_errno(val: c_int) { /// - `static $slot: AtomicPtr` — the captured original pointer, /// - `type $ty` — its function-pointer shape (doc comments carry through), /// - `fn $getter() -> Option<$ty>` — None before install or if somehow null, -/// - `pub fn $setter(p: *const ())` — what `pltHookRegister` feeds the -/// original into. +/// - `pub fn $setter(p: *const ())` — stores the original trampoline. /// /// The slot is read/written with relaxed atomics: install happens-before any /// hook fires, so no stronger ordering is needed and no locks are taken. @@ -134,7 +148,7 @@ macro_rules! saved_original { fn $getter:ident; pub fn $setter:ident; ) => { - static $slot: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + static $slot: AtomicPtr = AtomicPtr::new(ptr::null_mut()); $(#[$tymeta])* type $ty = $sig; @@ -147,11 +161,11 @@ macro_rules! saved_original { } else { // SAFETY: the slot only ever holds a valid pointer of this // exact shape, stored once at install via the setter below. - Some(unsafe { core::mem::transmute::<*mut c_void, $ty>(raw) }) + Some(unsafe { mem::transmute::<*mut c_void, $ty>(raw) }) } } - /// Stash the original pointer returned by `pltHookRegister`. + /// Stash the original trampoline returned by shadowhook. pub fn $setter(p: *const ()) { $slot.store(p as *mut c_void, Ordering::Relaxed); } @@ -230,14 +244,14 @@ fn kernel_needs_socket_bind_hiding() -> bool { _ => {} } - let mut uts = core::mem::MaybeUninit::::zeroed(); + let mut uts = mem::MaybeUninit::::zeroed(); let hook = unsafe { if libc::uname(uts.as_mut_ptr()) != 0 { false } else { let uts = uts.assume_init(); let release = - core::slice::from_raw_parts(uts.release.as_ptr().cast::(), uts.release.len()); + slice::from_raw_parts(uts.release.as_ptr().cast::(), uts.release.len()); let end = release .iter() .position(|&byte| byte == 0) @@ -278,16 +292,16 @@ fn copy_from_self(src: *const c_void, dst: &mut [u8]) -> bool { let remote = libc::iovec { // process_vm_readv's ABI uses mutable iovec fields even though the // remote side is read-only. - iov_base: src as *mut c_void, + iov_base: src.cast_mut(), iov_len: dst.len(), }; let copied = unsafe { libc::syscall( libc::SYS_process_vm_readv, libc::getpid(), - &local as *const libc::iovec, + ptr::from_ref(&local), 1usize, - &remote as *const libc::iovec, + ptr::from_ref(&remote), 1usize, 0usize, ) @@ -306,11 +320,8 @@ fn ifindex_is_vpn(ifindex: c_int) -> Option { } let mut name = [0u8; libc::IFNAMSIZ]; - let result = IN_GETIFADDRS.with(|guard| { - let previous = guard.replace(true); - let result = unsafe { libc::if_indextoname(ifindex as u32, name.as_mut_ptr().cast()) }; - guard.set(previous); - result + let result = with_ioctl_passthrough(|| unsafe { + libc::if_indextoname(ifindex as u32, name.as_mut_ptr().cast()) }); (!result.is_null()).then(|| is_vpn_iface_bytes(&name)) } @@ -328,7 +339,7 @@ fn deny_ifindex_bind(ifindex: c_int, resolved_is_vpn: Option) -> bool { /// its native `EBADF`/`ENOTSOCK` ordering before we inspect the interface name. fn is_socket_fd(fd: c_int) -> bool { let mut socket_type: c_int = 0; - let mut len = core::mem::size_of::() as libc::socklen_t; + let mut len = mem::size_of::() as libc::socklen_t; unsafe { libc::getsockopt( fd, @@ -415,13 +426,13 @@ pub unsafe extern "C" fn hooked_setsockopt( } if optname == SO_BINDTOIFINDEX { - if optlen < core::mem::size_of::() as libc::socklen_t + if optlen < mem::size_of::() as libc::socklen_t || optlen > c_int::MAX as libc::socklen_t { return unsafe { real(fd, level, optname, optval, optlen) }; } - let mut raw = [0u8; core::mem::size_of::()]; + let mut raw = [0u8; mem::size_of::()]; if !copy_from_self(optval, &mut raw) { return unsafe { real(fd, level, optname, optval, optlen) }; } @@ -439,7 +450,7 @@ pub unsafe extern "C" fn hooked_setsockopt( level, optname, (&ifindex as *const c_int).cast(), - core::mem::size_of::() as libc::socklen_t, + mem::size_of::() as libc::socklen_t, ) }; } @@ -501,7 +512,7 @@ pub unsafe extern "C" fn hooked_ioctl( if ret == 0 && !arg.is_null() { let req = unsafe { &*(arg as *const ifreq) }; let name_bytes = unsafe { - core::slice::from_raw_parts(req.ifr_name.as_ptr().cast::(), req.ifr_name.len()) + slice::from_raw_parts(req.ifr_name.as_ptr().cast::(), req.ifr_name.len()) }; if is_vpn_iface_bytes(name_bytes) { set_errno(libc::ENODEV); @@ -516,7 +527,7 @@ pub unsafe extern "C" fn hooked_ioctl( if !arg.is_null() && is_siocgif(request) { let req = unsafe { &*(arg as *const ifreq) }; let name_bytes = unsafe { - core::slice::from_raw_parts(req.ifr_name.as_ptr().cast::(), req.ifr_name.len()) + slice::from_raw_parts(req.ifr_name.as_ptr().cast::(), req.ifr_name.len()) }; if is_vpn_iface_bytes(name_bytes) { set_errno(libc::ENODEV); @@ -553,21 +564,21 @@ unsafe fn filter_ifconf(ifc: *mut ifconf) { return; } - let entry_size = core::mem::size_of::() as c_int; + let entry_size = mem::size_of::() as c_int; let n = ifc.ifc_len / entry_size; let mut dst = 0i32; for i in 0..n { let entry = unsafe { &*ifc.ifc_req.offset(i as isize) }; let name_bytes = unsafe { - core::slice::from_raw_parts(entry.ifr_name.as_ptr().cast::(), entry.ifr_name.len()) + slice::from_raw_parts(entry.ifr_name.as_ptr().cast::(), entry.ifr_name.len()) }; if is_vpn_iface_bytes(name_bytes) { continue; } if dst != i { unsafe { - core::ptr::copy_nonoverlapping( + ptr::copy_nonoverlapping( ifc.ifc_req.offset(i as isize), ifc.ifc_req.offset(dst as isize), 1, @@ -581,7 +592,7 @@ unsafe fn filter_ifconf(ifc: *mut ifconf) { // A caller can inspect its buffer past the shortened ifc_len. Erase // only slots returned by the kernel and removed by compaction. unsafe { - core::ptr::write_bytes(ifc.ifc_req.offset(dst as isize), 0, (n - dst) as usize); + ptr::write_bytes(ifc.ifc_req.offset(dst as isize), 0, (n - dst) as usize); } } @@ -688,9 +699,7 @@ pub unsafe extern "C" fn hooked_getifaddrs(ifap: *mut *mut libc::ifaddrs) -> c_i // Set the thread-local guard so hooked_ioctl passes through while // libc's real getifaddrs runs (it internally calls ioctl for each // interface to get flags — we must not filter those). - IN_GETIFADDRS.with(|f| f.set(true)); - let rc = unsafe { real(ifap) }; - IN_GETIFADDRS.with(|f| f.set(false)); + let rc = with_ioctl_passthrough(|| unsafe { real(ifap) }); if rc != 0 || ifap.is_null() { return rc; @@ -708,8 +717,8 @@ pub unsafe extern "C" fn hooked_getifaddrs(ifap: *mut *mut libc::ifaddrs) -> c_i let is_vpn = if name_ptr.is_null() { false } else { - let name = core::ffi::CStr::from_ptr(name_ptr); - crate::filter::is_vpn_iface_cstr(name) + let name = CStr::from_ptr(name_ptr); + is_vpn_iface_cstr(name) }; if is_vpn { *slot = (*entry).ifa_next; @@ -914,7 +923,7 @@ pub unsafe extern "C" fn hooked_openat( }; if !pathname.is_null() { - let path = unsafe { core::ffi::CStr::from_ptr(pathname) }; + let path = unsafe { CStr::from_ptr(pathname) }; let path_bytes = path.to_bytes(); let matched = if path_bytes.first() == Some(&b'/') { @@ -1033,8 +1042,6 @@ unsafe fn open_filtered_proc_net( /// Dispatch to the right filter function based on the file type. fn apply_filter(data: &mut [u8], kind: ProcNetFile) -> usize { - use crate::filter::*; - match kind { ProcNetFile::Route => filter_route_buf(data), ProcNetFile::Ipv6Route => filter_ipv6_route_buf(data), @@ -1066,16 +1073,13 @@ fn apply_filter(data: &mut [u8], kind: ProcNetFile) -> usize { /// getifaddrs calls ioctl(SIOCGIFFLAGS) internally. Returns `false` if /// the real getifaddrs() symbol couldn't be resolved or the call failed; /// the closure is then never invoked. -unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs)) -> bool { +unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&CStr, &libc::ifaddrs)) -> bool { let Some(real) = real_getifaddrs() else { return false; }; - let mut ifap: *mut libc::ifaddrs = core::ptr::null_mut(); - - IN_GETIFADDRS.with(|flag| flag.set(true)); - let rc = unsafe { real(&mut ifap) }; - IN_GETIFADDRS.with(|flag| flag.set(false)); + let mut ifap: *mut libc::ifaddrs = ptr::null_mut(); + let rc = with_ioctl_passthrough(|| unsafe { real(&mut ifap) }); if rc != 0 || ifap.is_null() { return false; @@ -1089,8 +1093,8 @@ unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs if entry.ifa_name.is_null() { continue; } - let name = unsafe { core::ffi::CStr::from_ptr(entry.ifa_name) }; - if !crate::filter::is_vpn_iface_cstr(name) { + let name = unsafe { CStr::from_ptr(entry.ifa_name) }; + if !is_vpn_iface_cstr(name) { continue; } @@ -1105,13 +1109,11 @@ unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs /// real (unhooked) `getifaddrs`. Sets `IN_GETIFADDRS` guard so our /// ioctl hook doesn't interfere with libc's internal SIOCGIFFLAGS calls. fn collect_vpn_addrs() -> ( - [u32; crate::filter::MAX_VPN_ADDRS], + [u32; MAX_VPN_ADDRS], usize, - [[u32; 4]; crate::filter::MAX_VPN_ADDRS], + [[u32; 4]; MAX_VPN_ADDRS], usize, ) { - use crate::filter::MAX_VPN_ADDRS; - let mut addrs4 = [0u32; MAX_VPN_ADDRS]; let mut addrs6 = [[0u32; 4]; MAX_VPN_ADDRS]; let mut n4 = 0usize; @@ -1221,7 +1223,7 @@ pub unsafe extern "C" fn hooked_recvmsg(fd: c_int, msg: *mut libc::msghdr, flags hdr.msg_iovlen, in_iovecs, &mut scratch, - |data| crate::filter::filter_netlink_dump(data, &indices[..n]), + |data| filter_netlink_dump(data, &indices[..n]), ) }) else { return ret; @@ -1286,7 +1288,7 @@ unsafe fn rewrite_iovec_payload( if entry.iov_base.is_null() { return None; } - let bytes = unsafe { core::slice::from_raw_parts(entry.iov_base.cast::(), take) }; + let bytes = unsafe { slice::from_raw_parts(entry.iov_base.cast::(), take) }; scratch.extend_from_slice(bytes); remaining -= take; } @@ -1306,7 +1308,7 @@ unsafe fn rewrite_iovec_payload( continue; } unsafe { - core::ptr::copy_nonoverlapping( + ptr::copy_nonoverlapping( scratch.as_ptr().add(copied), entry.iov_base.cast::(), take, @@ -1349,16 +1351,13 @@ unsafe fn maybe_filter_netlink_buf(fd: c_int, buf: *mut u8, ret: isize) -> isize return ret; } - let data = unsafe { core::slice::from_raw_parts_mut(buf, ret as usize) }; + let data = unsafe { slice::from_raw_parts_mut(buf, ret as usize) }; // Quick check: first message type must be one we filter. Route dumps // (RTM_GETROUTE) come back as RTM_NEWROUTE and must not be skipped — // that gap was the issue #86 `if` leak. let nlmsg_type = u16::from_ne_bytes([data[4], data[5]]); - if nlmsg_type != crate::filter::RTM_NEWADDR - && nlmsg_type != crate::filter::RTM_NEWLINK - && nlmsg_type != crate::filter::RTM_NEWROUTE - { + if nlmsg_type != RTM_NEWADDR && nlmsg_type != RTM_NEWLINK && nlmsg_type != RTM_NEWROUTE { return ret; } @@ -1367,7 +1366,7 @@ unsafe fn maybe_filter_netlink_buf(fd: c_int, buf: *mut u8, ret: isize) -> isize return ret; } - crate::filter::filter_netlink_dump(data, &indices[..n]) as isize + filter_netlink_dump(data, &indices[..n]) as isize } // ============================================================================ @@ -1505,9 +1504,7 @@ pub unsafe extern "C" fn hooked_recvfrom_chk( /// Collect interface indices of VPN interfaces. Uses real_getifaddrs /// (with IN_GETIFADDRS guard) and `if_nametoindex` (which calls /// ioctl(SIOCGIFINDEX) — passed through by our ioctl hook). -fn collect_vpn_iface_indices() -> ([u32; crate::filter::MAX_VPN_ADDRS], usize) { - use crate::filter::MAX_VPN_ADDRS; - +fn collect_vpn_iface_indices() -> ([u32; MAX_VPN_ADDRS], usize) { let mut indices = [0u32; MAX_VPN_ADDRS]; let mut n = 0usize; @@ -1520,13 +1517,7 @@ fn collect_vpn_iface_indices() -> ([u32; crate::filter::MAX_VPN_ADDRS], usize) { // now blocks for VPN names. Run it under the IN_GETIFADDRS guard so // it passes through to the real index — otherwise this filter loses // the VPN indices and the netlink route dump (#86) stops filtering. - let idx = IN_GETIFADDRS.with(|f| { - let prev = f.get(); - f.set(true); - let i = libc::if_nametoindex(entry.ifa_name); - f.set(prev); - i - }); + let idx = with_ioctl_passthrough(|| libc::if_nametoindex(entry.ifa_name)); if idx == 0 || indices[..n].contains(&idx) { return; } diff --git a/zygisk/src/lib.rs b/zygisk/src/lib.rs index 196eb43..2195157 100644 --- a/zygisk/src/lib.rs +++ b/zygisk/src/lib.rs @@ -78,17 +78,25 @@ mod generated; mod hooks; mod shadowhook; -use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use core::ffi::{CStr, c_void}; +use core::ptr; +use std::fs; +use std::io::{self, Read}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::path::Path; +use std::process; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Once, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; -use log::{debug, error, info}; +use android_logger::Config; +use log::{LevelFilter, debug, error, info}; use vpnhide_protocol as protocol; use vpnhide_protocol::hook_ids::{Hook, ZYGISK_HOOK_MASK}; -use zygisk_api::ZygiskModule; use zygisk_api::api::ZygiskApi; use zygisk_api::api::v2::{AppSpecializeArgs, V2, ZygiskOption}; -use zygisk_api::jni::JNIEnv; +use zygisk_api::jni::{JNIEnv, objects::JString}; +use zygisk_api::{ZygiskModule, register_companion, register_module}; use crate::hooks::{ hooked_getifaddrs, hooked_ioctl, hooked_openat, hooked_recv, hooked_recvfrom, @@ -118,9 +126,9 @@ fn init_logger() { static INIT: Once = Once::new(); INIT.call_once(|| { android_logger::init_once( - android_logger::Config::default() + Config::default() .with_tag(LOG_TAG) - .with_max_level(log::LevelFilter::Trace), + .with_max_level(LevelFilter::Trace), ); }); } @@ -134,9 +142,9 @@ fn init_logger() { /// Same principle as HookLog.e on the Kotlin side. fn set_log_level(enabled: bool) { let level = if enabled { - log::LevelFilter::Trace + LevelFilter::Trace } else { - log::LevelFilter::Error + LevelFilter::Error }; log::set_max_level(level); } @@ -180,23 +188,26 @@ static CACHED_CONFIG: OnceLock = OnceLock::new(); /// privileges, bypassing the SELinux restrictions that block direct file /// access on Magisk. A missing/unreadable/invalid file fails closed: no /// targets, logging off. -fn load_config_from_dir_fd(dir_fd: std::os::fd::RawFd) -> ZygiskConfig { - use std::io::Read; - use std::os::fd::FromRawFd; +fn load_config_from_dir_fd(dir_fd: RawFd) -> ZygiskConfig { let empty = ZygiskConfig { targets: Vec::new(), debug: false, }; - let filename = std::ffi::CString::new(TARGETS_FILENAME).unwrap(); - let fd = unsafe { libc::openat(dir_fd, filename.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) }; + let fd = unsafe { + libc::openat( + dir_fd, + c"targets.txt".as_ptr(), + libc::O_RDONLY | libc::O_CLOEXEC, + ) + }; if fd < 0 { log::warn!( "can't open {TARGETS_FILENAME} via module dir fd: {}", - std::io::Error::last_os_error() + io::Error::last_os_error() ); return empty; } - let mut file = unsafe { std::fs::File::from_raw_fd(fd) }; + let mut file = unsafe { fs::File::from_raw_fd(fd) }; let mut content = Vec::new(); if let Err(e) = file.read_to_end(&mut content) { log::warn!("can't read {TARGETS_FILENAME}: {e}"); @@ -282,7 +293,12 @@ impl ZygiskModule for VpnHide { // UID is the targeting key (protocol §4.3). args.uid already holds the // UID this child will become, and one UID covers every process of a // multi-process app — so no package/sub-process name matching needed. - let uid = *args.uid as u32; + let Ok(uid) = u32::try_from(*args.uid) else { + self.target_hookmask.store(0, Ordering::Relaxed); + self.report_status.store(false, Ordering::Relaxed); + mark_cleanup(&mut api); + return; + }; // nice_name is still read, but only to recognise the VPN Hide app // itself for the status heartbeat — an identity check, not targeting. let package = read_jstring(&env, args.nice_name); @@ -329,14 +345,14 @@ impl ZygiskModule for VpnHide { } fn write_status_file() { - let boot_id = match std::fs::read_to_string("/proc/sys/kernel/random/boot_id") { + let boot_id = match fs::read_to_string("/proc/sys/kernel/random/boot_id") { Ok(v) => v.trim().to_string(), Err(err) => { error!("failed to read boot_id: {err}"); return; } }; - let timestamp = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(v) => v.as_secs(), Err(err) => { error!("failed to get timestamp: {err}"); @@ -347,16 +363,16 @@ fn write_status_file() { "version={}\nboot_id={}\npid={}\ntimestamp={}\n", env!("CARGO_PKG_VERSION"), boot_id, - std::process::id(), + process::id(), timestamp, ); - if let Some(parent) = std::path::Path::new(APP_STATUS_FILE).parent() { - if let Err(err) = std::fs::create_dir_all(parent) { + if let Some(parent) = Path::new(APP_STATUS_FILE).parent() { + if let Err(err) = fs::create_dir_all(parent) { error!("failed to create status dir {}: {err}", parent.display()); return; } } - match std::fs::write(APP_STATUS_FILE, content) { + match fs::write(APP_STATUS_FILE, content) { Ok(()) => info!("wrote zygisk status heartbeat to {APP_STATUS_FILE}"), Err(err) => error!("failed to write status heartbeat: {err}"), } @@ -368,6 +384,13 @@ fn mark_cleanup(api: &mut ZygiskApi<'_, V2>) { api.set_option(ZygiskOption::DlCloseModuleLibrary); } +struct HookDesc { + sym: &'static CStr, + hook_id: Hook, + replacement: *mut c_void, + store_orig: fn(*const ()), +} + /// Install selected inline hooks on `libc.so` via ByteDance shadowhook. /// /// * `ioctl` — catches `SIOCGIFNAME` / `SIOCGIFFLAGS` interface @@ -404,12 +427,6 @@ fn install_hooks(hookmask: u32) -> Result<(), String> { // itself is 12 bytes (3 instructions), safe for island-mode hooking. // recvfrom + __recvfrom_chk catch FORTIFY'd / direct callers that never // touch the `recv` symbol (issue #86 native route dump). - struct HookDesc { - sym: &'static core::ffi::CStr, - hook_id: Hook, - replacement: *mut core::ffi::c_void, - store_orig: fn(*const ()), - } let table = [ HookDesc { sym: c"ioctl", @@ -461,7 +478,7 @@ fn install_hooks(hookmask: u32) -> Result<(), String> { }, ]; - let mut installed: Vec<*mut core::ffi::c_void> = Vec::with_capacity(table.len()); + let mut installed: Vec<*mut c_void> = Vec::with_capacity(table.len()); for desc in table { if !zygisk_hook_enabled(hookmask, desc.hook_id) { continue; @@ -495,11 +512,11 @@ fn install_hooks(hookmask: u32) -> Result<(), String> { /// pointer on success — caller keeps it for the partial-install /// rollback in `install_hooks`. fn hook_libc_sym( - sym: &core::ffi::CStr, - new_fn: *mut core::ffi::c_void, + sym: &CStr, + new_fn: *mut c_void, store_orig: fn(*const ()), -) -> Result<*mut core::ffi::c_void, String> { - let mut orig: *mut core::ffi::c_void = core::ptr::null_mut(); +) -> Result<*mut c_void, String> { + let mut orig: *mut c_void = ptr::null_mut(); // SAFETY: `new_fn` has an ABI-compatible signature with the target // libc symbol; `&mut orig` is a valid writable pointer. let stub = unsafe { shadowhook::hook_sym(c"libc.so", sym, new_fn, &mut orig) }; @@ -549,8 +566,6 @@ fn scrub_shadowhook_maps() { // not in the filter list today, but a future addition would silently // break this function. let maps = { - use std::io::Read; - use std::os::fd::FromRawFd; let fd = unsafe { libc::open( c"/proc/self/maps".as_ptr(), @@ -561,7 +576,7 @@ fn scrub_shadowhook_maps() { log::warn!("scrub_shadowhook_maps: can't open /proc/self/maps"); return; } - let mut file = unsafe { std::fs::File::from_raw_fd(fd) }; + let mut file = unsafe { fs::File::from_raw_fd(fd) }; let mut buf = String::new(); if file.read_to_string(&mut buf).is_err() { log::warn!("scrub_shadowhook_maps: can't read /proc/self/maps"); @@ -616,7 +631,7 @@ fn scrub_shadowhook_maps() { } else { log::warn!( "prctl(PR_SET_VMA_ANON_NAME) failed for {start_hex}-{end_hex}: errno={}", - std::io::Error::last_os_error() + io::Error::last_os_error() ); } } @@ -631,15 +646,10 @@ fn scrub_shadowhook_maps() { /// sub-process name handling (the old package-name path did). #[inline(never)] fn target_hookmask(uid: u32) -> u32 { - match CACHED_CONFIG.get() { - Some(cfg) => cfg - .targets - .iter() - .find(|target| target.uid == uid) - .map(|target| target.hookmask) - .unwrap_or(0), - None => 0, - } + CACHED_CONFIG + .get() + .and_then(|cfg| cfg.targets.iter().find(|target| target.uid == uid)) + .map_or(0, |target| target.hookmask) } fn zygisk_hook_enabled(hookmask: u32, hook: Hook) -> bool { @@ -648,10 +658,7 @@ fn zygisk_hook_enabled(hookmask: u32, hook: Hook) -> bool { /// Decode a `JString` (as stored in `AppSpecializeArgs::nice_name`) into /// an owned Rust `String`. Returns None on any failure. -fn read_jstring<'a>( - env: &JNIEnv<'a>, - jstr: &zygisk_api::jni::objects::JString<'a>, -) -> Option { +fn read_jstring<'a>(env: &JNIEnv<'a>, jstr: &JString<'a>) -> Option { if jstr.is_null() { return None; } @@ -659,11 +666,11 @@ fn read_jstring<'a>( env_clone .get_string(jstr) .ok() - .and_then(|s| s.to_str().ok().map(|s| s.to_string())) + .and_then(|s| s.to_str().ok().map(str::to_owned)) } -zygisk_api::register_module!(VpnHide); +register_module!(VpnHide); // Empty companion — declared so we don't have to recompile the .so if we // ever want to add a root-privileged helper later. -zygisk_api::register_companion!(|_| ()); +register_companion!(|_| ()); diff --git a/zygisk/src/shadowhook.rs b/zygisk/src/shadowhook.rs index c9e0289..92b21b3 100644 --- a/zygisk/src/shadowhook.rs +++ b/zygisk/src/shadowhook.rs @@ -6,7 +6,7 @@ //! is linked as a static archive (`libshadowhook.a`) built for //! `aarch64-linux-android`; see `build.rs`. -use core::ffi::{c_char, c_int, c_void}; +use core::ffi::{CStr, c_char, c_int, c_void}; use std::sync::OnceLock; // `SHADOWHOOK_MODE_UNIQUE` from shadowhook.h. We never use shared mode, so a @@ -54,8 +54,8 @@ pub fn init_once() -> Result<(), c_int> { /// `new_fn` must be a valid function pointer with a signature ABI-compatible /// with the real target symbol. `out_orig` must be a valid writable pointer. pub unsafe fn hook_sym( - lib: &core::ffi::CStr, - sym: &core::ffi::CStr, + lib: &CStr, + sym: &CStr, new_fn: *mut c_void, out_orig: *mut *mut c_void, ) -> *mut c_void {