Introduce new sepolicy strategy for legacy devices

The existing sepolicy patching strategy looks like this:

1. 2SI: use LD_PRELOAD to hijack `security_load_policy`
2. Split policy: devices using split policy implies it also needs to
   do early mount, which means fstab is stored in device tree.
   So we do the following:
   - Hijack the fstab node in the device tree in sysfs
   - Wait for init to mount selinuxfs for us
   - Hijack selinuxfs to intercept sepolicy loading
3. Monolithic policy: directly patch `/sepolicy`

Method #1 and #2 both has the magiskinit pre-init daemon handling
the sepolicy patching and loading process, while method #3 gives us
zero control over sepolicy loading process. Downsides:

a. Pre-init daemon bypasses the need to guess which sepolicy init
   will load, because the original init will literally send the stock
   sepolicy file directly to us with this approach.
b. If we want to add more features/functionalities during the sepolicy
   patching process, we will leave out devices using method #3

In order to solve these issues, we completely redesign the sepolicy
patching strategy for non-2SI devices. Instead of limiting usage of
pre-init daemon to early mount devices, we always intercept the
sepolicy loading process regardless of the Android version and device
setup. This will give us a unified implementation for sepolicy patching,
and will make it easier to develop further new features down the line.
This commit is contained in:
topjohnwu 2025-02-27 01:54:32 -08:00 committed by John Wu
parent b6b34f7612
commit c9eac0c438
8 changed files with 236 additions and 153 deletions

View File

@ -1,10 +1,20 @@
#pragma once
#define DEFAULT_DT_DIR "/proc/device-tree/firmware/android"
#define REDIR_PATH "/data/magiskinit"
#define PRELOAD_LIB "/dev/preload.so"
#define PRELOAD_POLICY "/dev/sepolicy"
#define PRELOAD_ACK "/dev/ack"
#ifdef __cplusplus
#include <base.hpp> #include <base.hpp>
#include <stream.hpp> #include <stream.hpp>
#include "init-rs.hpp" #include "init-rs.hpp"
#define DEFAULT_DT_DIR "/proc/device-tree/firmware/android"
#define REDIR_PATH "/data/magiskinit"
int magisk_proxy_main(int, char *argv[]); int magisk_proxy_main(int, char *argv[]);
rust::Utf8CStr backup_init(); rust::Utf8CStr backup_init();
#endif

View File

@ -16,7 +16,6 @@ mod init;
mod logging; mod logging;
mod mount; mod mount;
mod rootdir; mod rootdir;
mod selinux;
mod twostage; mod twostage;
#[cxx::bridge] #[cxx::bridge]
@ -80,7 +79,6 @@ pub mod ffi {
// MagiskInit // MagiskInit
extern "Rust" { extern "Rust" {
type OverlayAttr; type OverlayAttr;
fn patch_sepolicy(self: &MagiskInit, src: Utf8CStrRef, out: Utf8CStrRef);
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 restore_overlay_contexts(self: &MagiskInit); fn restore_overlay_contexts(self: &MagiskInit);
@ -96,7 +94,7 @@ 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 hijack_sepolicy(self: &mut MagiskInit) -> bool; 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);
} }
} }

View File

@ -1,26 +1,29 @@
#include <stdlib.h> #include <stdlib.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <dlfcn.h>
#include "init.hpp"
__attribute__((constructor)) __attribute__((constructor))
static void preload_init() { static void preload_init() {
// Make sure our next exec won't get bugged // Make sure our next exec won't get bugged
unsetenv("LD_PRELOAD"); unsetenv("LD_PRELOAD");
unlink("/dev/preload.so"); unlink(PRELOAD_LIB);
} }
int security_load_policy(void *data, size_t len) { int security_load_policy(void *data, size_t len) {
int (*load_policy)(void *, size_t) = dlsym(RTLD_NEXT, "security_load_policy"); int policy = open(PRELOAD_POLICY, O_WRONLY | O_CREAT, 0644);
// Skip checking errors, because if we cannot find the symbol, there if (policy < 0) return -1;
// isn't much we can do other than crashing anyways.
int result = load_policy(data, len); // Write the policy
write(policy, data, len);
close(policy);
// Wait for ack // Wait for ack
int fd = open("/sys/fs/selinux/enforce", O_RDONLY); int ack = open(PRELOAD_ACK, O_RDONLY);
char c; char c;
read(fd, &c, 1); read(ack, &c, 1);
close(fd); close(ack);
return result; return 0;
} }

View File

@ -329,15 +329,7 @@ void MagiskInit::patch_ro_root() noexcept {
// Extract overlay archives // Extract overlay archives
extract_files(false); extract_files(false);
// Oculus Go will use a special sepolicy if unlocked handle_sepolicy();
if (access("/sepolicy.unlocked", F_OK) == 0) {
patch_sepolicy("/sepolicy.unlocked", ROOTOVL "/sepolicy.unlocked");
} else {
bool patch = access(SPLIT_PLAT_CIL, F_OK) != 0 && access("/sepolicy", F_OK) == 0;
if (patch || !hijack_sepolicy()) {
patch_sepolicy("/sepolicy", ROOTOVL "/sepolicy");
}
}
unlink("init-ld"); unlink("init-ld");
// Mount rootdir // Mount rootdir
@ -368,12 +360,6 @@ void MagiskInit::patch_rw_root() noexcept {
if (patch_rc_scripts("/", "/sbin", true)) if (patch_rc_scripts("/", "/sbin", true))
patch_fissiond("/sbin"); patch_fissiond("/sbin");
bool treble;
{
auto init = mmap_data("/init");
treble = init.contains(SPLIT_PLAT_CIL);
}
xmkdir(PRE_TMPSRC, 0); xmkdir(PRE_TMPSRC, 0);
xmount("tmpfs", PRE_TMPSRC, "tmpfs", 0, "mode=755"); xmount("tmpfs", PRE_TMPSRC, "tmpfs", 0, "mode=755");
xmkdir(PRE_TMPDIR, 0); xmkdir(PRE_TMPDIR, 0);
@ -383,10 +369,7 @@ void MagiskInit::patch_rw_root() noexcept {
// Extract overlay archives // Extract overlay archives
extract_files(true); extract_files(true);
bool patch = !treble && access("/sepolicy", F_OK) == 0; handle_sepolicy();
if (patch || !hijack_sepolicy()) {
patch_sepolicy("/sepolicy", "/sepolicy");
}
unlink("init-ld"); unlink("init-ld");
chdir("/"); chdir("/");

View File

@ -1,4 +1,5 @@
#include <sys/mount.h> #include <sys/mount.h>
#include <sys/xattr.h>
#include <consts.hpp> #include <consts.hpp>
#include <sepolicy.hpp> #include <sepolicy.hpp>
@ -7,122 +8,235 @@
using namespace std; using namespace std;
#define MOCK_COMPAT SELINUXMOCK "/compatible" #define POLICY_VERSION "/selinux_version"
#define MOCK_VERSION SELINUXMOCK "/version"
#define MOCK_LOAD SELINUXMOCK "/load" #define MOCK_LOAD SELINUXMOCK "/load"
#define MOCK_ENFORCE SELINUXMOCK "/enforce" #define MOCK_ENFORCE SELINUXMOCK "/enforce"
#define MOCK_REQPROT SELINUXMOCK "/checkreqprot"
bool MagiskInit::hijack_sepolicy() noexcept { static void mock_fifo(const char *target, const char *mock) {
xmkdir(SELINUXMOCK, 0); LOGD("Hijack [%s]\n", target);
mkfifo(mock, 0666);
xmount(mock, target, nullptr, MS_BIND, nullptr);
}
if (access("/system/bin/init", F_OK) == 0) { static void mock_file(const char *target, const char *mock) {
// On 2SI devices, the 2nd stage init file is always a dynamic executable. LOGD("Hijack [%s]\n", target);
// This meant that instead of going through convoluted methods trying to alter close(xopen(mock, O_CREAT | O_RDONLY, 0666));
// and block init's control flow, we can just LD_PRELOAD and replace the xmount(mock, target, nullptr, MS_BIND, nullptr);
// security_load_policy function with our own implementation. }
cp_afc("init-ld", "/dev/preload.so");
setenv("LD_PRELOAD", "/dev/preload.so", 1);
}
// Hijack the "load" and "enforce" node in selinuxfs to manipulate enum SePatchStrategy {
// the actual sepolicy being loaded into the kernel // 2SI, Android 10+
auto hijack = [&] { // On 2SI devices, the 2nd stage init is always a dynamic executable.
LOGD("Hijack [" SELINUX_LOAD "]\n"); // This meant that instead of going through convoluted hacks, we can just
close(xopen(MOCK_LOAD, O_CREAT | O_RDONLY, 0600)); // LD_PRELOAD and replace security_load_policy with our own implementation.
xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr); LD_PRELOAD,
LOGD("Hijack [" SELINUX_ENFORCE "]\n"); // Treble enabled, Android 8.0+
mkfifo(MOCK_ENFORCE, 0644); // selinuxfs is mounted in init.cpp. Errors when mounting selinuxfs is ignored,
xmount(MOCK_ENFORCE, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr); // 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,
};
string dt_compat; void MagiskInit::handle_sepolicy() noexcept {
if (access(SELINUX_ENFORCE, F_OK) != 0) { xmkdir(SELINUXMOCK, 0711);
// selinuxfs not mounted yet. Hijack the dt fstab nodes first
// and let the original init mount selinuxfs for us.
// This only happens on Android 8.0 - 9.0
char buf[4096];
ssprintf(buf, sizeof(buf), "%s/fstab/compatible", config.dt_dir.data());
dt_compat = full_read(buf);
if (dt_compat.empty()) {
// Device does not do early mount and uses monolithic policy
return false;
}
// Remount procfs with proper options
xmount(nullptr, "/proc", nullptr, MS_REMOUNT, "hidepid=2,gid=3009");
LOGD("Hijack [%s]\n", buf);
decltype(mount_list) new_mount_list;
// Preserve sysfs and procfs for hijacking
for (const auto &s: mount_list)
if (s != "/proc" && s != "/sys")
new_mount_list.emplace_back(s);
new_mount_list.swap(mount_list);
mkfifo(MOCK_COMPAT, 0444);
xmount(MOCK_COMPAT, buf, nullptr, MS_BIND, nullptr);
} else {
hijack();
}
// Read all custom rules into memory // Read all custom rules into memory
string rules; string rules;
auto rule = "/data/" PREINITMIRR "/sepolicy.rule"; auto rule = "/data/" PREINITMIRR "/sepolicy.rule";
if (xaccess(rule, R_OK) == 0) { if (xaccess(rule, R_OK) == 0) {
LOGD("Loading custom sepolicy patch: [%s]\n", rule); LOGD("Loading custom sepolicy patch: [%s]\n", rule);
rules = full_read(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 // Create a new process waiting for init operations
if (xfork()) { if (xfork()) {
// In parent, return and continue boot process return;
return true;
} }
if (!dt_compat.empty()) { // Step 2: wait for selinuxfs to be mounted (only for LEGACY)
// This open will block until init calls DoFirstStageMount
// The only purpose here is actually to wait for init to mount selinuxfs for us
int fd = xopen(MOCK_COMPAT, O_WRONLY);
char buf[4096]; if (strat == LEGACY) {
ssprintf(buf, sizeof(buf), "%s/fstab/compatible", config.dt_dir.data()); // Busy wait until selinuxfs is mounted
xumount2(buf, MNT_DETACH); while (access(SELINUX_ENFORCE, F_OK)) {
// Retry every 100ms
usleep(100000);
}
hijack(); // 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.
xwrite(fd, dt_compat.data(), dt_compat.size()); mock_file(SELINUX_LOAD, MOCK_LOAD);
close(fd); 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.
} }
// This open will block until init calls security_getenforce // Step 3: obtain sepolicy, patch, and load the patched sepolicy
int fd = xopen(MOCK_ENFORCE, O_WRONLY);
// Cleanup the hijacks if (strat == LD_PRELOAD) {
umount2("/init", MNT_DETACH); // This open will block until preload.so finish writing the sepolicy
xumount2(SELINUX_LOAD, MNT_DETACH); owned_fd ack_fd = xopen(PRELOAD_ACK, O_WRONLY);
xumount2(SELINUX_ENFORCE, MNT_DETACH);
// Load and patch policy auto sepol = SePolicy::from_file(PRELOAD_POLICY);
auto sepol = SePolicy::from_file(MOCK_LOAD);
sepol.magisk_rules();
sepol.load_rules(rules);
// Load patched policy into kernel // Remove the files before loading the policy
sepol.to_file(SELINUX_LOAD); unlink(PRELOAD_POLICY);
unlink(PRELOAD_ACK);
// restore mounted files' context after sepolicy loaded sepol.magisk_rules();
restore_overlay_contexts(); sepol.load_rules(rules);
sepol.to_file(SELINUX_LOAD);
// Write to the enforce node ONLY after sepolicy is loaded. We need to make sure // restore mounted files' context after sepolicy loaded
// the actual init process is blocked until sepolicy is loaded, or else restore_overlay_contexts();
// restorecon will fail and re-exec won't change context, causing boot failure.
// We (ab)use the fact that init reads the enforce node, and because
// it has been replaced with our FIFO file, init will block until we
// write something into the pipe, effectively hijacking its control flow.
string enforce = full_read(SELINUX_ENFORCE); // Write ack to restore preload.so's control flow
xwrite(fd, enforce.data(), enforce.length()); xwrite(ack_fd, &ack_fd, 1);
close(fd); } 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 // At this point, the init process will be unblocked
// and continue on with restorecon + re-exec. // and continue on with restorecon + re-exec.

View File

@ -1,29 +0,0 @@
use crate::ffi::MagiskInit;
use base::{debug, path, Utf8CStr};
use magiskpolicy::ffi::SePolicy;
impl MagiskInit {
pub(crate) fn patch_sepolicy(self: &MagiskInit, src: &Utf8CStr, out: &Utf8CStr) {
debug!("Patching monolithic policy");
let mut sepol = SePolicy::from_file(src);
sepol.magisk_rules();
// Custom rules
let rule = path!("/data/.magisk/preinit/sepolicy.rule");
if rule.exists() {
debug!("Loading custom sepolicy patch: [{}]", rule);
sepol.load_rule_file(rule);
}
debug!("Dumping sepolicy to: [{}]", out);
sepol.to_file(out);
// Remove OnePlus stupid debug sepolicy and use our own
let sepol_debug = path!("/sepolicy_debug");
if sepol_debug.exists() {
sepol_debug.remove().ok();
path!("/sepolicy").link_to(sepol_debug).ok();
}
}
}

View File

@ -21,3 +21,4 @@
#define SELINUX_POLICY SELINUX_MNT "/policy" #define SELINUX_POLICY SELINUX_MNT "/policy"
#define SELINUX_LOAD SELINUX_MNT "/load" #define SELINUX_LOAD SELINUX_MNT "/load"
#define SELINUX_VERSION SELINUX_MNT "/policyvers" #define SELINUX_VERSION SELINUX_MNT "/policyvers"
#define SELINUX_REQPROT SELINUX_MNT "/checkreqprot"

View File

@ -104,8 +104,11 @@ impl SePolicy {
// For tmpfs overlay on 2SI, Zygisk on lower Android versions and AVD scripts // For tmpfs overlay on 2SI, Zygisk on lower Android versions and AVD scripts
allow(["init", "zygote", "shell"], ["tmpfs"], ["file"], all); allow(["init", "zygote", "shell"], ["tmpfs"], ["file"], all);
// Allow magiskinit daemon to log to kmsg
allow(["kernel"], ["rootfs", "tmpfs"], ["chr_file"], ["write"]);
// Allow magiskinit daemon to handle mock selinuxfs // Allow magiskinit daemon to handle mock selinuxfs
allow(["kernel"], ["tmpfs"], ["fifo_file"], ["write"]); allow(["kernel"], ["tmpfs"], ["fifo_file"], ["open", "read", "write"]);
// For relabelling files // For relabelling files
allow(["rootfs"], ["labeledfs", "tmpfs"], ["filesystem"], ["associate"]); allow(["rootfs"], ["labeledfs", "tmpfs"], ["filesystem"], ["associate"]);