From d4a0286e13cb34ab43ced80be0254ec327915f20 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Sun, 6 Apr 2025 02:04:59 -0700 Subject: [PATCH] Migrate magiskinit selinux.cpp to Rust --- native/src/Android.mk | 1 - native/src/base/cstr.rs | 15 +- native/src/base/files.rs | 6 +- native/src/base/mount.rs | 15 +- native/src/include/consts.rs | 1 + native/src/init/init.hpp | 20 +++ native/src/init/lib.rs | 27 +++- native/src/init/selinux.cpp | 246 ------------------------------ native/src/init/selinux.rs | 280 +++++++++++++++++++++++++++++++++++ native/src/sepolicy/lib.rs | 10 +- 10 files changed, 361 insertions(+), 260 deletions(-) delete mode 100644 native/src/init/selinux.cpp create mode 100644 native/src/init/selinux.rs diff --git a/native/src/Android.mk b/native/src/Android.mk index d4a418db7..beb189f1e 100644 --- a/native/src/Android.mk +++ b/native/src/Android.mk @@ -66,7 +66,6 @@ LOCAL_SRC_FILES := \ init/mount.cpp \ init/rootdir.cpp \ init/getinfo.cpp \ - init/selinux.cpp \ init/init-rs.cpp LOCAL_LDFLAGS := -static diff --git a/native/src/base/cstr.rs b/native/src/base/cstr.rs index 14b2de868..73c002765 100644 --- a/native/src/base/cstr.rs +++ b/native/src/base/cstr.rs @@ -1,4 +1,4 @@ -use cxx::{type_id, ExternType}; +use cxx::{ExternType, type_id}; use libc::c_char; use std::borrow::Borrow; use std::cmp::min; @@ -479,6 +479,11 @@ impl FsPath { unsafe { mem::transmute(value.as_ref()) } } + #[inline(always)] + pub const fn from_utfcstr(value: &Utf8CStr) -> &FsPath { + unsafe { mem::transmute(value) } + } + #[inline(always)] pub fn from_mut + ?Sized>(value: &mut T) -> &mut FsPath { unsafe { mem::transmute(value.as_mut()) } @@ -781,14 +786,10 @@ macro_rules! cstr { #[macro_export] macro_rules! raw_cstr { - ($str:expr) => {{ - $crate::cstr!($str).as_ptr() - }}; + ($str:expr) => {{ $crate::cstr!($str).as_ptr() }}; } #[macro_export] macro_rules! path { - ($str:expr) => {{ - $crate::FsPath::from($crate::cstr!($str)) - }}; + ($str:expr) => {{ $crate::FsPath::from_utfcstr($crate::cstr!($str)) }}; } diff --git a/native/src/base/files.rs b/native/src/base/files.rs index e1470eec0..d254e6bdd 100644 --- a/native/src/base/files.rs +++ b/native/src/base/files.rs @@ -199,7 +199,7 @@ impl FsPath { } pub fn create(&self, flags: i32, mode: mode_t) -> io::Result { - Ok(File::from(open_fd!(self, flags, mode)?)) + Ok(File::from(open_fd!(self, O_CREAT | flags, mode)?)) } pub fn exists(&self) -> bool { @@ -431,6 +431,10 @@ impl FsPath { pub fn create_symlink_to(&self, target: &FsPath) -> io::Result<()> { unsafe { libc::symlink(target.as_ptr(), self.as_ptr()).as_os_err() } } + + pub fn mkfifo(&self, mode: mode_t) -> io::Result<()> { + unsafe { libc::mkfifo(self.as_ptr(), mode).as_os_err() } + } } impl FsPathFollow { diff --git a/native/src/base/mount.rs b/native/src/base/mount.rs index a1076d15c..bb0c6278f 100644 --- a/native/src/base/mount.rs +++ b/native/src/base/mount.rs @@ -1,4 +1,4 @@ -use crate::{FsPath, LibcReturn}; +use crate::{FsPath, LibcReturn, Utf8CStr}; use libc::c_ulong; use std::ptr; @@ -29,6 +29,19 @@ impl FsPath { } } + pub fn remount_with_data(&self, data: &Utf8CStr) -> std::io::Result<()> { + unsafe { + libc::mount( + ptr::null(), + self.as_ptr(), + ptr::null(), + libc::MS_REMOUNT, + data.as_ptr().cast(), + ) + .as_os_err() + } + } + pub fn move_mount_to(&self, path: &FsPath) -> std::io::Result<()> { unsafe { libc::mount( diff --git a/native/src/include/consts.rs b/native/src/include/consts.rs index 029d1e0f0..05870c6af 100644 --- a/native/src/include/consts.rs +++ b/native/src/include/consts.rs @@ -27,3 +27,4 @@ pub const DEVICEDIR: &str = concatcp!(INTERNAL_DIR, "/device"); pub const PREINITDEV: &str = concatcp!(DEVICEDIR, "/preinit"); pub const ROOTOVL: &str = concatcp!(INTERNAL_DIR, "/rootdir"); pub const ROOTMNT: &str = concatcp!(ROOTOVL, "/.mount_list"); +pub const SELINUXMOCK: &str = concatcp!(INTERNAL_DIR, "/selinux"); diff --git a/native/src/init/init.hpp b/native/src/init/init.hpp index b92a5fc4b..d799722af 100644 --- a/native/src/init/init.hpp +++ b/native/src/init/init.hpp @@ -11,10 +11,30 @@ #include #include +#include #include "init-rs.hpp" int magisk_proxy_main(int, char *argv[]); rust::Utf8CStr backup_init(); +// Expose some constants to Rust + +static inline rust::Utf8CStr split_plat_cil() { + return SPLIT_PLAT_CIL; +}; + +static inline rust::Utf8CStr preload_lib() { + return PRELOAD_LIB; +} + +static inline rust::Utf8CStr preload_policy() { + return PRELOAD_POLICY; +} + +static inline rust::Utf8CStr preload_ack() { + return PRELOAD_ACK; +} + + #endif diff --git a/native/src/init/lib.rs b/native/src/init/lib.rs index 8d3e020f0..a2d18df24 100644 --- a/native/src/init/lib.rs +++ b/native/src/init/lib.rs @@ -3,11 +3,12 @@ #![feature(try_blocks)] #![allow(clippy::missing_safety_doc)] +use base::FsPath; use logging::setup_klog; // Has to be pub so all symbols in that crate is included pub use magiskpolicy; use mount::{is_device_mounted, switch_root}; -use rootdir::{inject_magisk_rc, OverlayAttr}; +use rootdir::{OverlayAttr, inject_magisk_rc}; #[path = "../include/consts.rs"] mod consts; @@ -16,6 +17,7 @@ mod init; mod logging; mod mount; mod rootdir; +mod selinux; mod twostage; #[cxx::bridge] @@ -56,6 +58,12 @@ pub mod ffi { unsafe fn magisk_proxy_main(argc: i32, argv: *mut *mut c_char) -> i32; fn backup_init() -> Utf8CStrRef<'static>; + + // Constants + fn split_plat_cil() -> Utf8CStrRef<'static>; + fn preload_lib() -> Utf8CStrRef<'static>; + fn preload_policy() -> Utf8CStrRef<'static>; + fn preload_ack() -> Utf8CStrRef<'static>; } #[namespace = "rust"] @@ -81,6 +89,7 @@ pub mod ffi { type OverlayAttr; fn parse_config_file(self: &mut MagiskInit); fn mount_overlay(self: &mut MagiskInit, dest: Utf8CStrRef); + fn handle_sepolicy(self: &mut MagiskInit); fn restore_overlay_contexts(self: &MagiskInit); } unsafe extern "C++" { @@ -94,7 +103,21 @@ pub mod ffi { fn collect_devices(self: &MagiskInit); fn mount_preinit_dir(self: &MagiskInit); unsafe fn find_block(self: &MagiskInit, partname: *const c_char) -> u64; - fn handle_sepolicy(self: &mut MagiskInit); unsafe fn patch_fissiond(self: &mut MagiskInit, tmp_path: *const c_char); } } + +#[inline(always)] +fn preload_lib() -> &'static FsPath { + FsPath::from(ffi::preload_lib()) +} + +#[inline(always)] +fn preload_policy() -> &'static FsPath { + FsPath::from(ffi::preload_policy()) +} + +#[inline(always)] +fn preload_ack() -> &'static FsPath { + FsPath::from(ffi::preload_ack()) +} diff --git a/native/src/init/selinux.cpp b/native/src/init/selinux.cpp deleted file mode 100644 index 77dd25a08..000000000 --- a/native/src/init/selinux.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include -#include - -#include -#include - -#include "init.hpp" - -using namespace std; - -#define POLICY_VERSION "/selinux_version" - -#define MOCK_VERSION SELINUXMOCK "/version" -#define MOCK_LOAD SELINUXMOCK "/load" -#define MOCK_ENFORCE SELINUXMOCK "/enforce" -#define MOCK_REQPROT SELINUXMOCK "/checkreqprot" - -static void mock_fifo(const char *target, const char *mock) { - LOGD("Hijack [%s]\n", target); - mkfifo(mock, 0666); - xmount(mock, target, nullptr, MS_BIND, nullptr); -} - -static void mock_file(const char *target, const char *mock) { - LOGD("Hijack [%s]\n", target); - close(xopen(mock, O_CREAT | O_RDONLY, 0666)); - xmount(mock, target, nullptr, MS_BIND, nullptr); -} - -enum SePatchStrategy { - // 2SI, Android 10+ - // On 2SI devices, the 2nd stage init is always a dynamic executable. - // This meant that instead of going through convoluted hacks, we can just - // LD_PRELOAD and replace security_load_policy with our own implementation. - LD_PRELOAD, - // Treble enabled, Android 8.0+ - // selinuxfs is mounted in init.cpp. Errors when mounting selinuxfs is ignored, - // which means that we can directly mount selinuxfs ourselves and hijack nodes in it. - SELINUXFS, - // Dynamic patching, Android 6.0 - 7.1 - // selinuxfs is mounted in libselinux's selinux_android_load_policy(). Errors when - // mounting selinuxfs is fatal, which means we need to block init's control flow after - // it mounted selinuxfs for us, then we can hijack nodes in it. - LEGACY, -}; - -void MagiskInit::handle_sepolicy() noexcept { - xmkdir(SELINUXMOCK, 0711); - - // Read all custom rules into memory - string rules; - auto rule = "/data/" PREINITMIRR "/sepolicy.rule"; - if (xaccess(rule, R_OK) == 0) { - LOGD("Loading custom sepolicy patch: [%s]\n", rule); - full_read(rule, rules); - } - - // Step 0: determine strategy - - SePatchStrategy strat; - - if (access("/system/bin/init", F_OK) == 0) { - strat = LD_PRELOAD; - } else { - auto init = mmap_data("/init"); - if (init.contains(SPLIT_PLAT_CIL)) { - // Supports split policy - strat = SELINUXFS; - } else if (init.contains(POLICY_VERSION)) { - // Does not support split policy, hijack /selinux_version - strat = LEGACY; - } else { - LOGE("Unknown sepolicy setup, abort...\n"); - return; - } - } - - // Step 1: setup for intercepting init boot control flow - - switch (strat) { - case LD_PRELOAD: { - LOGI("SePatchStrategy: LD_PRELOAD\n"); - - cp_afc("init-ld", PRELOAD_LIB); - setenv("LD_PRELOAD", PRELOAD_LIB, 1); - mkfifo(PRELOAD_ACK, 0666); - break; - } - case SELINUXFS: { - LOGI("SePatchStrategy: SELINUXFS\n"); - - if (access(SELINUX_ENFORCE, F_OK) != 0) { - // selinuxfs was not already mounted, mount it ourselves - - // Remount procfs with proper options - xmount(nullptr, "/proc", nullptr, MS_REMOUNT, "hidepid=2,gid=3009"); - - // Preserve sysfs and procfs - decltype(mount_list) new_mount_list; - std::remove_copy_if( - mount_list.begin(), mount_list.end(), - std::back_inserter(new_mount_list), - [](const auto &s) { return s == "/proc" || s == "/sys"; }); - new_mount_list.swap(mount_list); - - // Mount selinuxfs - xmount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, nullptr); - } - - mock_file(SELINUX_LOAD, MOCK_LOAD); - mock_fifo(SELINUX_ENFORCE, MOCK_ENFORCE); - break; - } - case LEGACY: { - LOGI("SePatchStrategy: LEGACY\n"); - - if (access(POLICY_VERSION, F_OK) != 0) { - // The file does not exist, create one - close(xopen(POLICY_VERSION, O_RDONLY | O_CREAT, 0644)); - } - - // The only purpose of this is to block init's control flow after it mounts selinuxfs - // and before it calls security_load_policy(). - // Target: selinux_android_load_policy() -> set_policy_index() -> open(POLICY_VERSION) - mock_fifo(POLICY_VERSION, MOCK_VERSION); - break; - } - } - - // Create a new process waiting for init operations - if (xfork()) { - return; - } - - // Step 2: wait for selinuxfs to be mounted (only for LEGACY) - - if (strat == LEGACY) { - // Busy wait until selinuxfs is mounted - while (access(SELINUX_ENFORCE, F_OK)) { - // Retry every 100ms - usleep(100000); - } - - // On Android 6.0, init does not call security_getenforce() first; instead it directly - // call security_setenforce() after security_load_policy(). What's even worse, it opens the - // enforce node with O_RDWR, which will not block when opening FIFO files. As a workaround, - // we do not mock the enforce node, and block init with mock checkreqprot instead. - // Android 7.0 - 7.1 doesn't have this issue, but for simplicity, let's just use the - // same blocking strategy for both since it also works just fine. - - mock_file(SELINUX_LOAD, MOCK_LOAD); - mock_fifo(SELINUX_REQPROT, MOCK_REQPROT); - - // This will unblock init at selinux_android_load_policy() -> set_policy_index(). - close(xopen(MOCK_VERSION, O_WRONLY)); - - xumount2(POLICY_VERSION, MNT_DETACH); - - // libselinux does not read /selinux_version after open; instead it mmap the file, - // which can never succeed on FIFO files. This is fine as set_policy_index() will just - // fallback to the default index 0. - } - - // Step 3: obtain sepolicy, patch, and load the patched sepolicy - - if (strat == LD_PRELOAD) { - // This open will block until preload.so finish writing the sepolicy - owned_fd ack_fd = xopen(PRELOAD_ACK, O_WRONLY); - - auto sepol = SePolicy::from_file(PRELOAD_POLICY); - - // Remove the files before loading the policy - unlink(PRELOAD_POLICY); - unlink(PRELOAD_ACK); - - sepol.magisk_rules(); - sepol.load_rules(rules); - sepol.to_file(SELINUX_LOAD); - - // restore mounted files' context after sepolicy loaded - restore_overlay_contexts(); - - // Write ack to restore preload.so's control flow - xwrite(ack_fd, &ack_fd, 1); - } else { - int mock_enforce = -1; - - if (strat == LEGACY) { - // Busy wait until sepolicy is fully written. - struct stat st{}; - decltype(st.st_size) sz; - do { - sz = st.st_size; - // Check every 100ms - usleep(100000); - xstat(MOCK_LOAD, &st); - } while (sz == 0 || sz != st.st_size); - } else { - // This open will block until init calls security_getenforce(). - mock_enforce = xopen(MOCK_ENFORCE, O_WRONLY); - } - - // Cleanup the hijacks - umount2("/init", MNT_DETACH); - xumount2(SELINUX_LOAD, MNT_DETACH); - umount2(SELINUX_ENFORCE, MNT_DETACH); - umount2(SELINUX_REQPROT, MNT_DETACH); - - auto sepol = SePolicy::from_file(MOCK_LOAD); - sepol.magisk_rules(); - sepol.load_rules(rules); - sepol.to_file(SELINUX_LOAD); - - // For some reason, restorecon on /init won't work in some cases - setxattr("/init", XATTR_NAME_SELINUX, "u:object_r:init_exec:s0", 24, 0); - - // restore mounted files' context after sepolicy loaded - restore_overlay_contexts(); - - // We need to make sure the actual init process is blocked until sepolicy is loaded, - // or else restorecon will fail and re-exec won't change context, causing boot failure. - // We (ab)use the fact that init either reads the enforce node, or writes the checkreqprot - // node, and because both has been replaced with FIFO files, init will block until we - // handle it, effectively hijacking its control flow until the patched sepolicy is loaded. - - if (strat == LEGACY) { - // init is blocked on checkreqprot, write to the real node first, then - // unblock init by opening the mock FIFO. - owned_fd real_req = xopen(SELINUX_REQPROT, O_WRONLY); - xwrite(real_req, "0", 1); - owned_fd mock_req = xopen(MOCK_REQPROT, O_RDONLY); - full_read(mock_req); - } else { - // security_getenforce was called - string data = full_read(SELINUX_ENFORCE); - xwrite(mock_enforce, data.data(), data.length()); - close(mock_enforce); - } - } - - // At this point, the init process will be unblocked - // and continue on with restorecon + re-exec. - - // Terminate process - exit(0); -} diff --git a/native/src/init/selinux.rs b/native/src/init/selinux.rs new file mode 100644 index 000000000..8abf61142 --- /dev/null +++ b/native/src/init/selinux.rs @@ -0,0 +1,280 @@ +use crate::consts::{PREINITMIRR, SELINUXMOCK}; +use crate::ffi::{MagiskInit, split_plat_cil}; +use crate::{preload_ack, preload_lib, preload_policy}; +use base::const_format::concatcp; +use base::{ + BytesExt, FsPath, LibcReturn, LoggedResult, MappedFile, ResultExt, cstr, debug, error, info, + libc, path, raw_cstr, +}; +use magiskpolicy::ffi::SePolicy; +use std::io::{Read, Write}; +use std::ptr; +use std::thread::sleep; +use std::time::Duration; + +const POLICY_VERSION: &FsPath = path!("/selinux_version"); + +const MOCK_VERSION: &FsPath = path!(concatcp!(SELINUXMOCK, "/version")); +const MOCK_LOAD: &FsPath = path!(concatcp!(SELINUXMOCK, "/load")); +const MOCK_ENFORCE: &FsPath = path!(concatcp!(SELINUXMOCK, "/enforce")); +const MOCK_REQPROT: &FsPath = path!(concatcp!(SELINUXMOCK, "/checkreqprot")); + +const SELINUX_MNT: &str = "/sys/fs/selinux"; +const SELINUX_ENFORCE: &FsPath = path!(concatcp!(SELINUX_MNT, "/enforce")); +const SELINUX_LOAD: &FsPath = path!(concatcp!(SELINUX_MNT, "/load")); +const SELINUX_REQPROT: &FsPath = path!(concatcp!(SELINUX_MNT, "/checkreqprot")); + +enum SePatchStrategy { + // 2SI, Android 10+ + // On 2SI devices, the 2nd stage init is always a dynamic executable. + // This meant that instead of going through convoluted hacks, we can just + // LD_PRELOAD and replace security_load_policy with our own implementation. + LdPreload, + // Treble enabled, Android 8.0+ + // selinuxfs is mounted in init.cpp. Errors when mounting selinuxfs is ignored, + // which means that we can directly mount selinuxfs ourselves and hijack nodes in it. + SelinuxFs, + // Dynamic patching, Android 6.0 - 7.1 + // selinuxfs is mounted in libselinux's selinux_android_load_policy(). Errors when + // mounting selinuxfs is fatal, which means we need to block init's control flow after + // it mounted selinuxfs for us, then we can hijack nodes in it. + Legacy, +} + +// Note for non-LD_PRELOAD strategy: +// +// We need to make sure the actual init process is blocked until sepolicy is loaded, +// or else restorecon will fail and re-exec won't change context, causing boot failure. +// We (ab)use the fact that init either reads the enforce node, or writes the checkreqprot +// node, and because both has been replaced with FIFO files, init will block until we +// handle it, effectively hijacking its control flow until the patched sepolicy is loaded. + +fn mock_fifo(target: &FsPath, mock: &FsPath) -> LoggedResult<()> { + debug!("Hijack [{}]", target); + mock.mkfifo(0o666)?; + mock.bind_mount_to(target).log() +} + +fn mock_file(target: &FsPath, mock: &FsPath) -> LoggedResult<()> { + debug!("Hijack [{}]", target); + drop(mock.create(libc::O_RDONLY, 0o666)?); + mock.bind_mount_to(target).log() +} + +impl MagiskInit { + pub(crate) fn handle_sepolicy(&mut self) { + self.handle_sepolicy_impl().ok(); + } + + fn cleanup_and_load(&self, rules: &str) { + // Cleanup the hijacks + path!("/init").unmount().ok(); + SELINUX_LOAD.unmount().log_ok(); + SELINUX_ENFORCE.unmount().ok(); + SELINUX_REQPROT.unmount().ok(); + + let mut sepol = SePolicy::from_file(MOCK_LOAD); + sepol.magisk_rules(); + sepol.load_rules(rules); + sepol.to_file(SELINUX_LOAD); + + // For some reason, restorecon on /init won't work in some cases + path!("/init") + .follow_link() + .set_secontext(cstr!("u:object_r:init_exec:s0")) + .ok(); + + // restore mounted files' context after sepolicy loaded + self.restore_overlay_contexts(); + } + + fn handle_sepolicy_impl(&mut self) -> LoggedResult<()> { + path!(SELINUXMOCK).mkdir(0o711)?; + + let mut rules = String::new(); + let rule_file = path!(concatcp!("/data/", PREINITMIRR, "/sepolicy.rule")); + if rule_file.exists() { + debug!("Loading custom sepolicy patch: [{}]", rule_file); + rule_file.open(libc::O_RDONLY)?.read_to_string(&mut rules)?; + } + + // Step 0: determine strategy + + let strat: SePatchStrategy; + + if path!("/system/bin/init").exists() { + strat = SePatchStrategy::LdPreload; + } else { + let init = MappedFile::open(cstr!("/init"))?; + if init.contains(split_plat_cil().as_str().as_bytes()) { + // Supports split policy + strat = SePatchStrategy::SelinuxFs; + } else if init.contains(POLICY_VERSION.as_bytes()) { + // Does not support split policy, hijack /selinux_version + strat = SePatchStrategy::Legacy; + } else { + error!("Unknown sepolicy setup, abort..."); + return Ok(()); + } + } + + // Step 1: setup for intercepting init boot control flow + + match strat { + SePatchStrategy::LdPreload => { + info!("SePatchStrategy: LD_PRELOAD"); + + path!("init-ld").copy_to(preload_lib())?; + unsafe { + libc::setenv(raw_cstr!("LD_PRELOAD"), preload_lib().as_ptr(), 1); + } + preload_ack().mkfifo(0o666)?; + } + SePatchStrategy::SelinuxFs => { + info!("SePatchStrategy: SELINUXFS"); + + if !SELINUX_ENFORCE.exists() { + // selinuxfs was not already mounted, mount it ourselves + + // Remount procfs with proper options + path!("/proc").remount_with_data(cstr!("hidepid=2,gid=3009"))?; + + // Preserve sysfs and procfs + self.mount_list.retain(|s| s != "/proc" && s != "/sys"); + + // Mount selinuxfs + unsafe { + libc::mount( + raw_cstr!("selinuxfs"), + raw_cstr!(SELINUX_MNT), + raw_cstr!("selinuxfs"), + 0, + ptr::null(), + ) + .as_os_err()?; + } + } + + mock_file(SELINUX_LOAD, MOCK_LOAD)?; + mock_fifo(SELINUX_ENFORCE, MOCK_ENFORCE)?; + } + SePatchStrategy::Legacy => { + info!("SePatchStrategy: LEGACY"); + + if !POLICY_VERSION.exists() { + // The file does not exist, create one + drop(POLICY_VERSION.create(libc::O_RDONLY, 0o666)?); + } + + // The only purpose of this is to block init's control flow after it mounts + // selinuxfs and before it calls security_load_policy(). + // selinux_android_load_policy() -> set_policy_index() -> open(POLICY_VERSION) + mock_fifo(POLICY_VERSION, MOCK_VERSION)?; + } + } + + // Create a new process waiting for init operations + let pid = unsafe { libc::fork().check_os_err()? }; + if pid != 0 { + return Ok(()); + } + + // Step 2: wait for selinuxfs to be mounted (only for LEGACY) + + let wait = Duration::from_millis(100); + + if matches!(strat, SePatchStrategy::Legacy) { + // Busy wait until selinuxfs is mounted + while !SELINUX_ENFORCE.exists() { + // Retry every 100ms + sleep(wait); + } + + // On Android 6.0, init does not call security_getenforce() first; instead it directly + // call security_setenforce() after security_load_policy(). What's even worse, it opens + // the enforce node with O_RDWR, which will not block when opening FIFO files. + // As a workaround, we do not mock the enforce node, and block init with mocking + // checkreqprot instead. + // Android 7.0 - 7.1 doesn't have this issue, but for simplicity, let's just use the + // same blocking strategy for both since it also works just fine. + + mock_file(SELINUX_LOAD, MOCK_LOAD)?; + mock_fifo(SELINUX_REQPROT, MOCK_REQPROT)?; + + // This will unblock init at selinux_android_load_policy() -> set_policy_index(). + drop(MOCK_VERSION.open(libc::O_WRONLY)?); + + POLICY_VERSION.unmount()?; + + // libselinux does not read /selinux_version after open; instead it mmap the file, + // which can never succeed on FIFO files. This is fine as set_policy_index() will just + // fallback to the default index 0. + } + + // Step 3: obtain sepolicy, patch, and load the patched sepolicy + + match strat { + SePatchStrategy::LdPreload => { + // This open will block until preload.so finish writing the sepolicy + let mut ack_fd = preload_ack().open(libc::O_WRONLY)?; + + let mut sepol = SePolicy::from_file(preload_policy()); + + // Remove the files before loading the policy + preload_policy().remove()?; + preload_ack().remove()?; + + sepol.magisk_rules(); + sepol.load_rules(&rules); + sepol.to_file(SELINUX_LOAD); + + self.restore_overlay_contexts(); + + // Write ack to restore preload.so's control flow + ack_fd.write_all("0".as_bytes())?; + } + SePatchStrategy::SelinuxFs => { + // This open will block until init calls security_getenforce(). + let mut mock_enforce = MOCK_ENFORCE.open(libc::O_WRONLY)?; + + self.cleanup_and_load(&rules); + + // security_getenforce was called, read from real and redirect to mock + let mut data = vec![]; + SELINUX_ENFORCE + .open(libc::O_RDONLY)? + .read_to_end(&mut data)?; + mock_enforce.write_all(&data)?; + } + SePatchStrategy::Legacy => { + let mut sz = 0_usize; + // Busy wait until sepolicy is fully written. + loop { + let attr = MOCK_LOAD.get_attr()?; + if sz != 0 && sz == attr.st.st_size as usize { + break; + } + sz = attr.st.st_size as usize; + // Check every 100ms + sleep(wait); + } + + self.cleanup_and_load(&rules); + + // init is blocked on checkreqprot, write to the real node first, then + // unblock init by opening the mock FIFO. + SELINUX_REQPROT + .open(libc::O_WRONLY)? + .write_all("0".as_bytes())?; + let mut v = vec![]; + MOCK_REQPROT.open(libc::O_RDONLY)?.read_to_end(&mut v)?; + } + } + + // At this point, the init process will be unblocked + // and continue on with restorecon + re-exec. + + // Terminate process + std::process::exit(0); + } +} diff --git a/native/src/sepolicy/lib.rs b/native/src/sepolicy/lib.rs index ec22d0b29..4db32aa1e 100644 --- a/native/src/sepolicy/lib.rs +++ b/native/src/sepolicy/lib.rs @@ -87,14 +87,20 @@ pub mod ffi { fn parse_statement(self: &mut SePolicy, statement: Utf8CStrRef); fn magisk_rules(self: &mut SePolicy); fn load_rule_file(self: &mut SePolicy, filename: Utf8CStrRef); - fn load_rules(self: &mut SePolicy, rules: &CxxString); + #[cxx_name = "load_rules"] + fn load_rules_for_cxx(self: &mut SePolicy, rules: &CxxString); #[Self = SePolicy] fn xperm_to_string(perm: &Xperm) -> String; } } impl SePolicy { - fn load_rules(self: &mut SePolicy, rules: &CxxString) { + fn load_rules_for_cxx(self: &mut SePolicy, rules: &CxxString) { + let mut cursor = Cursor::new(rules.as_bytes()); + self.load_rules_from_reader(&mut cursor); + } + + pub fn load_rules(self: &mut SePolicy, rules: &str) { let mut cursor = Cursor::new(rules.as_bytes()); self.load_rules_from_reader(&mut cursor); }