Magisk/native/src/init/selinux.cpp

247 lines
8.7 KiB
C++
Raw Normal View History

2022-03-16 21:41:20 -07:00
#include <sys/mount.h>
#include <sys/xattr.h>
2022-03-16 21:41:20 -07:00
2023-11-08 01:46:02 -08:00
#include <consts.hpp>
2022-03-29 22:26:38 -07:00
#include <sepolicy.hpp>
2022-03-16 21:41:20 -07:00
#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"
2022-03-16 21:41:20 -07:00
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);
}
2022-03-16 21:41:20 -07:00
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;
2022-03-16 21:41:20 -07:00
} 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;
}
2022-03-16 21:41:20 -07:00
}
// 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;
}
}
2022-03-16 21:41:20 -07:00
// Create a new process waiting for init operations
if (xfork()) {
return;
2022-03-16 21:41:20 -07:00
}
// Step 2: wait for selinuxfs to be mounted (only for LEGACY)
2022-03-16 21:41:20 -07:00
if (strat == LEGACY) {
// Busy wait until selinuxfs is mounted
while (access(SELINUX_ENFORCE, F_OK)) {
// Retry every 100ms
usleep(100000);
}
2022-03-16 21:41:20 -07:00
// 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.
2022-03-16 21:41:20 -07:00
}
// 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);
2022-03-16 21:41:20 -07:00
auto sepol = SePolicy::from_file(PRELOAD_POLICY);
2022-03-16 21:41:20 -07:00
// 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);
2022-03-16 21:41:20 -07:00
// 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);
}
2022-03-16 21:41:20 -07:00
// 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);
}
}
2022-03-16 21:41:20 -07:00
// At this point, the init process will be unblocked
// and continue on with restorecon + re-exec.
// Terminate process
exit(0);
}