From c9eac0c438494e1c795f8571e751180ea52a7eef Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Thu, 27 Feb 2025 01:54:32 -0800 Subject: [PATCH] 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. --- native/src/init/init.hpp | 16 +- native/src/init/lib.rs | 4 +- native/src/init/preload.c | 23 +- native/src/init/rootdir.cpp | 21 +- native/src/init/selinux.cpp | 290 ++++++++++++++++------- native/src/init/selinux.rs | 29 --- native/src/sepolicy/include/sepolicy.hpp | 1 + native/src/sepolicy/rules.rs | 5 +- 8 files changed, 236 insertions(+), 153 deletions(-) delete mode 100644 native/src/init/selinux.rs diff --git a/native/src/init/init.hpp b/native/src/init/init.hpp index 656b05d46..b92a5fc4b 100644 --- a/native/src/init/init.hpp +++ b/native/src/init/init.hpp @@ -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 #include #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[]); rust::Utf8CStr backup_init(); + +#endif diff --git a/native/src/init/lib.rs b/native/src/init/lib.rs index 4a598a966..8d3e020f0 100644 --- a/native/src/init/lib.rs +++ b/native/src/init/lib.rs @@ -16,7 +16,6 @@ mod init; mod logging; mod mount; mod rootdir; -mod selinux; mod twostage; #[cxx::bridge] @@ -80,7 +79,6 @@ pub mod ffi { // MagiskInit extern "Rust" { type OverlayAttr; - fn patch_sepolicy(self: &MagiskInit, src: Utf8CStrRef, out: Utf8CStrRef); fn parse_config_file(self: &mut MagiskInit); fn mount_overlay(self: &mut MagiskInit, dest: Utf8CStrRef); fn restore_overlay_contexts(self: &MagiskInit); @@ -96,7 +94,7 @@ 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 hijack_sepolicy(self: &mut MagiskInit) -> bool; + fn handle_sepolicy(self: &mut MagiskInit); unsafe fn patch_fissiond(self: &mut MagiskInit, tmp_path: *const c_char); } } diff --git a/native/src/init/preload.c b/native/src/init/preload.c index dde74d5d4..1c9542424 100644 --- a/native/src/init/preload.c +++ b/native/src/init/preload.c @@ -1,26 +1,29 @@ #include #include #include -#include + +#include "init.hpp" __attribute__((constructor)) static void preload_init() { // Make sure our next exec won't get bugged unsetenv("LD_PRELOAD"); - unlink("/dev/preload.so"); + unlink(PRELOAD_LIB); } int security_load_policy(void *data, size_t len) { - int (*load_policy)(void *, size_t) = dlsym(RTLD_NEXT, "security_load_policy"); - // Skip checking errors, because if we cannot find the symbol, there - // isn't much we can do other than crashing anyways. - int result = load_policy(data, len); + int policy = open(PRELOAD_POLICY, O_WRONLY | O_CREAT, 0644); + if (policy < 0) return -1; + + // Write the policy + write(policy, data, len); + close(policy); // Wait for ack - int fd = open("/sys/fs/selinux/enforce", O_RDONLY); + int ack = open(PRELOAD_ACK, O_RDONLY); char c; - read(fd, &c, 1); - close(fd); + read(ack, &c, 1); + close(ack); - return result; + return 0; } diff --git a/native/src/init/rootdir.cpp b/native/src/init/rootdir.cpp index 7add415b6..b6f1699b3 100644 --- a/native/src/init/rootdir.cpp +++ b/native/src/init/rootdir.cpp @@ -329,15 +329,7 @@ void MagiskInit::patch_ro_root() noexcept { // Extract overlay archives extract_files(false); - // Oculus Go will use a special sepolicy if unlocked - 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"); - } - } + handle_sepolicy(); unlink("init-ld"); // Mount rootdir @@ -368,12 +360,6 @@ void MagiskInit::patch_rw_root() noexcept { if (patch_rc_scripts("/", "/sbin", true)) patch_fissiond("/sbin"); - bool treble; - { - auto init = mmap_data("/init"); - treble = init.contains(SPLIT_PLAT_CIL); - } - xmkdir(PRE_TMPSRC, 0); xmount("tmpfs", PRE_TMPSRC, "tmpfs", 0, "mode=755"); xmkdir(PRE_TMPDIR, 0); @@ -383,10 +369,7 @@ void MagiskInit::patch_rw_root() noexcept { // Extract overlay archives extract_files(true); - bool patch = !treble && access("/sepolicy", F_OK) == 0; - if (patch || !hijack_sepolicy()) { - patch_sepolicy("/sepolicy", "/sepolicy"); - } + handle_sepolicy(); unlink("init-ld"); chdir("/"); diff --git a/native/src/init/selinux.cpp b/native/src/init/selinux.cpp index 91373bd4b..77dd25a08 100644 --- a/native/src/init/selinux.cpp +++ b/native/src/init/selinux.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -7,122 +8,235 @@ 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_ENFORCE SELINUXMOCK "/enforce" +#define MOCK_REQPROT SELINUXMOCK "/checkreqprot" -bool MagiskInit::hijack_sepolicy() noexcept { - xmkdir(SELINUXMOCK, 0); +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); +} - if (access("/system/bin/init", F_OK) == 0) { - // On 2SI devices, the 2nd stage init file is always a dynamic executable. - // This meant that instead of going through convoluted methods trying to alter - // and block init's control flow, we can just LD_PRELOAD and replace the - // security_load_policy function with our own implementation. - cp_afc("init-ld", "/dev/preload.so"); - setenv("LD_PRELOAD", "/dev/preload.so", 1); - } +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); +} - // Hijack the "load" and "enforce" node in selinuxfs to manipulate - // the actual sepolicy being loaded into the kernel - auto hijack = [&] { - LOGD("Hijack [" SELINUX_LOAD "]\n"); - close(xopen(MOCK_LOAD, O_CREAT | O_RDONLY, 0600)); - xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr); - LOGD("Hijack [" SELINUX_ENFORCE "]\n"); - mkfifo(MOCK_ENFORCE, 0644); - xmount(MOCK_ENFORCE, SELINUX_ENFORCE, 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, +}; - string dt_compat; - if (access(SELINUX_ENFORCE, F_OK) != 0) { - // 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(); - } +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); - 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 if (xfork()) { - // In parent, return and continue boot process - return true; + return; } - if (!dt_compat.empty()) { - // 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); + // Step 2: wait for selinuxfs to be mounted (only for LEGACY) - char buf[4096]; - ssprintf(buf, sizeof(buf), "%s/fstab/compatible", config.dt_dir.data()); - xumount2(buf, MNT_DETACH); + if (strat == LEGACY) { + // Busy wait until selinuxfs is mounted + 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()); - close(fd); + 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. } - // This open will block until init calls security_getenforce - int fd = xopen(MOCK_ENFORCE, O_WRONLY); + // Step 3: obtain sepolicy, patch, and load the patched sepolicy - // Cleanup the hijacks - umount2("/init", MNT_DETACH); - xumount2(SELINUX_LOAD, MNT_DETACH); - xumount2(SELINUX_ENFORCE, MNT_DETACH); + if (strat == LD_PRELOAD) { + // This open will block until preload.so finish writing the sepolicy + owned_fd ack_fd = xopen(PRELOAD_ACK, O_WRONLY); - // Load and patch policy - auto sepol = SePolicy::from_file(MOCK_LOAD); - sepol.magisk_rules(); - sepol.load_rules(rules); + auto sepol = SePolicy::from_file(PRELOAD_POLICY); - // Load patched policy into kernel - sepol.to_file(SELINUX_LOAD); + // Remove the files before loading the policy + unlink(PRELOAD_POLICY); + unlink(PRELOAD_ACK); - // restore mounted files' context after sepolicy loaded - restore_overlay_contexts(); + sepol.magisk_rules(); + sepol.load_rules(rules); + sepol.to_file(SELINUX_LOAD); - // Write to the enforce node ONLY after sepolicy is loaded. 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 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. + // restore mounted files' context after sepolicy loaded + restore_overlay_contexts(); - string enforce = full_read(SELINUX_ENFORCE); - xwrite(fd, enforce.data(), enforce.length()); - close(fd); + // 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. diff --git a/native/src/init/selinux.rs b/native/src/init/selinux.rs deleted file mode 100644 index 0400a1e18..000000000 --- a/native/src/init/selinux.rs +++ /dev/null @@ -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(); - } - } -} diff --git a/native/src/sepolicy/include/sepolicy.hpp b/native/src/sepolicy/include/sepolicy.hpp index ef348f286..3fc7f7e65 100644 --- a/native/src/sepolicy/include/sepolicy.hpp +++ b/native/src/sepolicy/include/sepolicy.hpp @@ -21,3 +21,4 @@ #define SELINUX_POLICY SELINUX_MNT "/policy" #define SELINUX_LOAD SELINUX_MNT "/load" #define SELINUX_VERSION SELINUX_MNT "/policyvers" +#define SELINUX_REQPROT SELINUX_MNT "/checkreqprot" diff --git a/native/src/sepolicy/rules.rs b/native/src/sepolicy/rules.rs index ae9abea4a..7d1dd0a8b 100644 --- a/native/src/sepolicy/rules.rs +++ b/native/src/sepolicy/rules.rs @@ -104,8 +104,11 @@ impl SePolicy { // For tmpfs overlay on 2SI, Zygisk on lower Android versions and AVD scripts 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(["kernel"], ["tmpfs"], ["fifo_file"], ["write"]); + allow(["kernel"], ["tmpfs"], ["fifo_file"], ["open", "read", "write"]); // For relabelling files allow(["rootfs"], ["labeledfs", "tmpfs"], ["filesystem"], ["associate"]);