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"]);