mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-04-29 22:14:27 +02:00
Migrate magiskinit selinux.cpp to Rust
This commit is contained in:
parent
83e66767ff
commit
d4a0286e13
@ -66,7 +66,6 @@ LOCAL_SRC_FILES := \
|
|||||||
init/mount.cpp \
|
init/mount.cpp \
|
||||||
init/rootdir.cpp \
|
init/rootdir.cpp \
|
||||||
init/getinfo.cpp \
|
init/getinfo.cpp \
|
||||||
init/selinux.cpp \
|
|
||||||
init/init-rs.cpp
|
init/init-rs.cpp
|
||||||
|
|
||||||
LOCAL_LDFLAGS := -static
|
LOCAL_LDFLAGS := -static
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use cxx::{type_id, ExternType};
|
use cxx::{ExternType, type_id};
|
||||||
use libc::c_char;
|
use libc::c_char;
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
@ -479,6 +479,11 @@ impl FsPath {
|
|||||||
unsafe { mem::transmute(value.as_ref()) }
|
unsafe { mem::transmute(value.as_ref()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn from_utfcstr(value: &Utf8CStr) -> &FsPath {
|
||||||
|
unsafe { mem::transmute(value) }
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_mut<T: AsMut<Utf8CStr> + ?Sized>(value: &mut T) -> &mut FsPath {
|
pub fn from_mut<T: AsMut<Utf8CStr> + ?Sized>(value: &mut T) -> &mut FsPath {
|
||||||
unsafe { mem::transmute(value.as_mut()) }
|
unsafe { mem::transmute(value.as_mut()) }
|
||||||
@ -781,14 +786,10 @@ macro_rules! cstr {
|
|||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! raw_cstr {
|
macro_rules! raw_cstr {
|
||||||
($str:expr) => {{
|
($str:expr) => {{ $crate::cstr!($str).as_ptr() }};
|
||||||
$crate::cstr!($str).as_ptr()
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! path {
|
macro_rules! path {
|
||||||
($str:expr) => {{
|
($str:expr) => {{ $crate::FsPath::from_utfcstr($crate::cstr!($str)) }};
|
||||||
$crate::FsPath::from($crate::cstr!($str))
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,7 @@ impl FsPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(&self, flags: i32, mode: mode_t) -> io::Result<File> {
|
pub fn create(&self, flags: i32, mode: mode_t) -> io::Result<File> {
|
||||||
Ok(File::from(open_fd!(self, flags, mode)?))
|
Ok(File::from(open_fd!(self, O_CREAT | flags, mode)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exists(&self) -> bool {
|
pub fn exists(&self) -> bool {
|
||||||
@ -431,6 +431,10 @@ impl FsPath {
|
|||||||
pub fn create_symlink_to(&self, target: &FsPath) -> io::Result<()> {
|
pub fn create_symlink_to(&self, target: &FsPath) -> io::Result<()> {
|
||||||
unsafe { libc::symlink(target.as_ptr(), self.as_ptr()).as_os_err() }
|
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 {
|
impl FsPathFollow {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{FsPath, LibcReturn};
|
use crate::{FsPath, LibcReturn, Utf8CStr};
|
||||||
use libc::c_ulong;
|
use libc::c_ulong;
|
||||||
use std::ptr;
|
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<()> {
|
pub fn move_mount_to(&self, path: &FsPath) -> std::io::Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::mount(
|
libc::mount(
|
||||||
|
@ -27,3 +27,4 @@ pub const DEVICEDIR: &str = concatcp!(INTERNAL_DIR, "/device");
|
|||||||
pub const PREINITDEV: &str = concatcp!(DEVICEDIR, "/preinit");
|
pub const PREINITDEV: &str = concatcp!(DEVICEDIR, "/preinit");
|
||||||
pub const ROOTOVL: &str = concatcp!(INTERNAL_DIR, "/rootdir");
|
pub const ROOTOVL: &str = concatcp!(INTERNAL_DIR, "/rootdir");
|
||||||
pub const ROOTMNT: &str = concatcp!(ROOTOVL, "/.mount_list");
|
pub const ROOTMNT: &str = concatcp!(ROOTOVL, "/.mount_list");
|
||||||
|
pub const SELINUXMOCK: &str = concatcp!(INTERNAL_DIR, "/selinux");
|
||||||
|
@ -11,10 +11,30 @@
|
|||||||
|
|
||||||
#include <base.hpp>
|
#include <base.hpp>
|
||||||
#include <stream.hpp>
|
#include <stream.hpp>
|
||||||
|
#include <sepolicy.hpp>
|
||||||
|
|
||||||
#include "init-rs.hpp"
|
#include "init-rs.hpp"
|
||||||
|
|
||||||
int magisk_proxy_main(int, char *argv[]);
|
int magisk_proxy_main(int, char *argv[]);
|
||||||
rust::Utf8CStr backup_init();
|
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
|
#endif
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
#![feature(try_blocks)]
|
#![feature(try_blocks)]
|
||||||
#![allow(clippy::missing_safety_doc)]
|
#![allow(clippy::missing_safety_doc)]
|
||||||
|
|
||||||
|
use base::FsPath;
|
||||||
use logging::setup_klog;
|
use logging::setup_klog;
|
||||||
// Has to be pub so all symbols in that crate is included
|
// Has to be pub so all symbols in that crate is included
|
||||||
pub use magiskpolicy;
|
pub use magiskpolicy;
|
||||||
use mount::{is_device_mounted, switch_root};
|
use mount::{is_device_mounted, switch_root};
|
||||||
use rootdir::{inject_magisk_rc, OverlayAttr};
|
use rootdir::{OverlayAttr, inject_magisk_rc};
|
||||||
|
|
||||||
#[path = "../include/consts.rs"]
|
#[path = "../include/consts.rs"]
|
||||||
mod consts;
|
mod consts;
|
||||||
@ -16,6 +17,7 @@ mod init;
|
|||||||
mod logging;
|
mod logging;
|
||||||
mod mount;
|
mod mount;
|
||||||
mod rootdir;
|
mod rootdir;
|
||||||
|
mod selinux;
|
||||||
mod twostage;
|
mod twostage;
|
||||||
|
|
||||||
#[cxx::bridge]
|
#[cxx::bridge]
|
||||||
@ -56,6 +58,12 @@ pub mod ffi {
|
|||||||
|
|
||||||
unsafe fn magisk_proxy_main(argc: i32, argv: *mut *mut c_char) -> i32;
|
unsafe fn magisk_proxy_main(argc: i32, argv: *mut *mut c_char) -> i32;
|
||||||
fn backup_init() -> Utf8CStrRef<'static>;
|
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"]
|
#[namespace = "rust"]
|
||||||
@ -81,6 +89,7 @@ pub mod ffi {
|
|||||||
type OverlayAttr;
|
type OverlayAttr;
|
||||||
fn parse_config_file(self: &mut MagiskInit);
|
fn parse_config_file(self: &mut MagiskInit);
|
||||||
fn mount_overlay(self: &mut MagiskInit, dest: Utf8CStrRef);
|
fn mount_overlay(self: &mut MagiskInit, dest: Utf8CStrRef);
|
||||||
|
fn handle_sepolicy(self: &mut MagiskInit);
|
||||||
fn restore_overlay_contexts(self: &MagiskInit);
|
fn restore_overlay_contexts(self: &MagiskInit);
|
||||||
}
|
}
|
||||||
unsafe extern "C++" {
|
unsafe extern "C++" {
|
||||||
@ -94,7 +103,21 @@ pub mod ffi {
|
|||||||
fn collect_devices(self: &MagiskInit);
|
fn collect_devices(self: &MagiskInit);
|
||||||
fn mount_preinit_dir(self: &MagiskInit);
|
fn mount_preinit_dir(self: &MagiskInit);
|
||||||
unsafe fn find_block(self: &MagiskInit, partname: *const c_char) -> u64;
|
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);
|
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())
|
||||||
|
}
|
||||||
|
@ -1,246 +0,0 @@
|
|||||||
#include <sys/mount.h>
|
|
||||||
#include <sys/xattr.h>
|
|
||||||
|
|
||||||
#include <consts.hpp>
|
|
||||||
#include <sepolicy.hpp>
|
|
||||||
|
|
||||||
#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);
|
|
||||||
}
|
|
280
native/src/init/selinux.rs
Normal file
280
native/src/init/selinux.rs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -87,14 +87,20 @@ pub mod ffi {
|
|||||||
fn parse_statement(self: &mut SePolicy, statement: Utf8CStrRef);
|
fn parse_statement(self: &mut SePolicy, statement: Utf8CStrRef);
|
||||||
fn magisk_rules(self: &mut SePolicy);
|
fn magisk_rules(self: &mut SePolicy);
|
||||||
fn load_rule_file(self: &mut SePolicy, filename: Utf8CStrRef);
|
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]
|
#[Self = SePolicy]
|
||||||
fn xperm_to_string(perm: &Xperm) -> String;
|
fn xperm_to_string(perm: &Xperm) -> String;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SePolicy {
|
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());
|
let mut cursor = Cursor::new(rules.as_bytes());
|
||||||
self.load_rules_from_reader(&mut cursor);
|
self.load_rules_from_reader(&mut cursor);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user