Magisk/native/src/init/selinux.cpp
topjohnwu c9eac0c438 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.
2025-02-28 09:39:10 -08:00

247 lines
8.7 KiB
C++

#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);
}