diff --git a/native/jni/include/magisk.hpp b/native/jni/include/magisk.hpp index c145d38c4..02c30e417 100644 --- a/native/jni/include/magisk.hpp +++ b/native/jni/include/magisk.hpp @@ -27,6 +27,7 @@ extern std::string MAGISKTMP; #define SHELLPTS INTLROOT "/pts" #define ROOTMNT ROOTOVL "/.mount_list" #define ZYGISKBIN INTLROOT "/zygisk" +#define SELINUXMOCK INTLROOT "/selinux" constexpr const char *applet_names[] = { "su", "resetprop", nullptr }; constexpr const char *init_applet[] = { "magiskpolicy", "supolicy", nullptr }; diff --git a/native/jni/include/magiskpolicy.hpp b/native/jni/include/magiskpolicy.hpp index ea6ddc449..9153ca956 100644 --- a/native/jni/include/magiskpolicy.hpp +++ b/native/jni/include/magiskpolicy.hpp @@ -2,6 +2,7 @@ #include #include +#include #define ALL nullptr @@ -9,10 +10,11 @@ struct policydb; class sepolicy { public: - typedef const char * c_str; + using c_str = const char *; ~sepolicy(); // Public static factory functions + static sepolicy *from_data(char *data, size_t len); static sepolicy *from_file(c_str file); static sepolicy *from_split(); static sepolicy *compile_split(); @@ -20,6 +22,7 @@ public: // External APIs bool to_file(c_str file); void parse_statement(c_str stmt); + void load_rules(const std::string &rules); void load_rule_file(c_str file); // Operation on types diff --git a/native/jni/init/getinfo.cpp b/native/jni/init/getinfo.cpp index 2a07d39af..67afb81e6 100644 --- a/native/jni/init/getinfo.cpp +++ b/native/jni/init/getinfo.cpp @@ -72,6 +72,7 @@ static kv_pairs parse_bootconfig(string_view str) { #define test_bit(bit, array) (array[bit / 8] & (1 << (bit % 8))) static bool check_key_combo() { + LOGD("Running in recovery mode, waiting for key...\n"); uint8_t bitmask[(KEY_MAX + 1) / 8]; vector events; constexpr const char *name = "/event"; @@ -226,7 +227,6 @@ void load_kernel_info(BootConfig *config) { parse_prop_file("/.backup/.magisk", [=](auto key, auto value) -> bool { if (key == "RECOVERYMODE" && value == "true") { - LOGD("Running in recovery mode, waiting for key...\n"); config->skip_initramfs = config->emulator || !check_key_combo(); return false; } diff --git a/native/jni/init/init.cpp b/native/jni/init/init.cpp index 97d8f3ac2..fe9cd1f51 100644 --- a/native/jni/init/init.cpp +++ b/native/jni/init/init.cpp @@ -109,7 +109,6 @@ static int test_main(int argc, char *argv[]) { #endif // ENABLE_TEST static int magisk_proxy_main(int argc, char *argv[]) { - setup_klog(); auto init = make_unique(argv); init->start(); return 1; diff --git a/native/jni/init/init.hpp b/native/jni/init/init.hpp index 64ea90646..32dc3112f 100644 --- a/native/jni/init/init.hpp +++ b/native/jni/init/init.hpp @@ -5,7 +5,7 @@ using kv_pairs = std::vector>; // For API 28 AVD, it uses legacy SAR setup that requires // special hacks in magiskinit to work properly. We do not // necessarily want this enabled in production builds. -#define ENABLE_AVD_HACK 0 +#define ENABLE_AVD_HACK 1 struct BootConfig { bool skip_initramfs; @@ -36,7 +36,6 @@ struct fstab_entry { fstab_entry &operator=(fstab_entry&&) = default; }; -#define INIT_SOCKET "MAGISKINIT" #define DEFAULT_DT_DIR "/proc/device-tree/firmware/android" extern std::vector mount_list; @@ -57,7 +56,6 @@ protected: char **argv = nullptr; [[noreturn]] void exec_init(); - void read_dt_fstab(std::vector &fstab); public: BaseInit(char *argv[], BootConfig *config = nullptr) : config(config), argv(argv) {} virtual ~BaseInit() = default; @@ -75,12 +73,12 @@ protected: // running magiskinit on legacy SAR AVD emulator bool avd_hack = false; #else - // Make it const so compiler can optimize hacks out of the code - static const bool avd_hack = false; + // Make it constexpr so compiler can optimize hacks out of the code + static constexpr bool avd_hack = false; #endif - void mount_with_dt(); bool patch_sepolicy(const char *file); + void hijack_sepolicy(); void setup_tmp(const char *path); void mount_rules_dir(const char *dev_base, const char *mnt_base); void patch_rw_root(); @@ -94,7 +92,6 @@ protected: void backup_files(); void patch_ro_root(); - void mount_system_root(); public: using MagiskInit::MagiskInit; }; @@ -140,14 +137,14 @@ public: class LegacySARInit : public SARBase { private: - bool early_mount(); + bool mount_system_root(); void first_stage_prep(); public: LegacySARInit(char *argv[], BootConfig *config) : SARBase(argv, config) { LOGD("%s\n", __FUNCTION__); }; void start() override { - if (early_mount()) + if (mount_system_root()) first_stage_prep(); else patch_ro_root(); @@ -176,6 +173,7 @@ public: class MagiskProxy : public MagiskInit { public: explicit MagiskProxy(char *argv[]) : MagiskInit(argv) { + setup_klog(); LOGD("%s\n", __FUNCTION__); } void start() override; diff --git a/native/jni/init/mount.cpp b/native/jni/init/mount.cpp index 0f4af83c3..156b791c9 100644 --- a/native/jni/init/mount.cpp +++ b/native/jni/init/mount.cpp @@ -104,7 +104,7 @@ if (access(#val, F_OK) == 0) {\ entry.val = rtrim(full_read(#val)); \ } -void BaseInit::read_dt_fstab(vector &fstab) { +static void read_dt_fstab(BootConfig *config, vector &fstab) { if (access(config->dt_dir, F_OK) != 0) return; @@ -152,13 +152,28 @@ void BaseInit::read_dt_fstab(vector &fstab) { } } -void MagiskInit::mount_with_dt() { +static void mount_with_dt(BootConfig *config) { vector fstab; - read_dt_fstab(fstab); + read_dt_fstab(config, fstab); for (const auto &entry : fstab) { if (is_lnk(entry.mnt_point.data())) continue; - if (avd_hack && entry.mnt_point == "/system") { + // Derive partname from dev + sprintf(blk_info.partname, "%s%s", basename(entry.dev.data()), config->slot); + setup_block(true); + xmkdir(entry.mnt_point.data(), 0755); + xmount(blk_info.block_dev, entry.mnt_point.data(), entry.type.data(), MS_RDONLY, nullptr); + mount_list.push_back(entry.mnt_point); + } +} + +static void avd_hack_mount(BootConfig *config) { + vector fstab; + read_dt_fstab(config, fstab); + for (const auto &entry : fstab) { + if (is_lnk(entry.mnt_point.data())) + continue; + if (entry.mnt_point == "/system") { // When we force AVD to disable SystemAsRoot, it will always add system // to dt fstab. We actually already mounted it as root, so skip this one. continue; @@ -168,11 +183,8 @@ void MagiskInit::mount_with_dt() { setup_block(true); xmkdir(entry.mnt_point.data(), 0755); xmount(blk_info.block_dev, entry.mnt_point.data(), entry.type.data(), MS_RDONLY, nullptr); - if (!avd_hack) { - // When avd_hack is true, do not add any early mount partitions to mount_list - // as we will actually forcefully disable original init's early mount - mount_list.push_back(entry.mnt_point); - } + // Do not add any early mount partitions to mount_list as we will + // actually forcefully disable original init's early mount. } } @@ -300,7 +312,7 @@ void RootFSInit::early_mount() { LOGD("Restoring /init\n"); rename(backup_init(), "/init"); - mount_with_dt(); + mount_with_dt(config); } void SARBase::backup_files() { @@ -316,8 +328,10 @@ void SARBase::backup_files() { magisk_cfg = mmap_data("/data/.backup/.magisk"); } -void SARBase::mount_system_root() { - LOGD("Early mount system_root\n"); +bool LegacySARInit::mount_system_root() { + backup_files(); + + LOGD("Mounting system_root\n"); strcpy(blk_info.block_dev, "/dev/root"); do { @@ -344,31 +358,36 @@ void SARBase::mount_system_root() { // We don't really know what to do at this point... LOGE("Cannot find root partition, abort\n"); exit(1); + mount_root: xmkdir("/system_root", 0755); - if (xmount("/dev/root", "/system_root", "ext4", MS_RDONLY, nullptr)) - xmount("/dev/root", "/system_root", "erofs", MS_RDONLY, nullptr); -} -bool LegacySARInit::early_mount() { - backup_files(); - mount_system_root(); + if (xmount("/dev/root", "/system_root", "ext4", MS_RDONLY, nullptr)) { + if (xmount("/dev/root", "/system_root", "erofs", MS_RDONLY, nullptr)) { + // We don't really know what to do at this point... + LOGE("Cannot mount root partition, abort\n"); + exit(1); + } + } + switch_root("/system_root"); // Use the apex folder to determine whether 2SI (Android 10+) bool is_two_stage = access("/apex", F_OK) == 0; LOGD("is_two_stage: [%d]\n", is_two_stage); - if (!is_two_stage) { - // Make dev writable - xmkdir("/dev", 0755); - xmount("tmpfs", "/dev", "tmpfs", 0, "mode=755"); - mount_list.emplace_back("/dev"); #if ENABLE_AVD_HACK - avd_hack = config->emulator; -#endif - mount_with_dt(); + if (!is_two_stage) { + if (config->emulator) { + avd_hack = true; + // Make dev writable + xmkdir("/dev", 0755); + xmount("tmpfs", "/dev", "tmpfs", 0, "mode=755"); + mount_list.emplace_back("/dev"); + avd_hack_mount(config); + } } +#endif return is_two_stage; } diff --git a/native/jni/init/rootdir.cpp b/native/jni/init/rootdir.cpp index 740068736..fca282fc8 100644 --- a/native/jni/init/rootdir.cpp +++ b/native/jni/init/rootdir.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include "init.hpp" #include "magiskrc.inc" @@ -139,6 +139,94 @@ bool MagiskInit::patch_sepolicy(const char *file) { return patch_init; } +#define MOCK_LOAD SELINUXMOCK "/load" +#define MOCK_ENFORCE SELINUXMOCK "/enforce" +#define REAL_SELINUXFS SELINUXMOCK "/fs" + +void MagiskInit::hijack_sepolicy() { + // Read all custom rules into memory + string rules; + if (!custom_rules_dir.empty()) { + if (auto dir = xopen_dir(custom_rules_dir.data())) { + for (dirent *entry; (entry = xreaddir(dir.get()));) { + auto rule_file = custom_rules_dir + "/" + entry->d_name + "/sepolicy.rule"; + if (xaccess(rule_file.data(), R_OK) == 0) { + LOGD("Load custom sepolicy patch: [%s]\n", rule_file.data()); + full_read(rule_file.data(), rules); + rules += '\n'; + } + } + } + } + + // Hijack the "load" and "enforce" node in selinuxfs to manipulate + // the actual sepolicy being loaded into the kernel + + // We need to preserve sysfs and selinuxfs after re-exec + mount_list.erase(std::remove_if( + mount_list.begin(), mount_list.end(), + [](const string &s) { return s == "/sys"; }), mount_list.end()); + + if (access(SELINUX_ENFORCE, F_OK) != 0) { + // selinuxfs needs to be mounted + xmount("selinuxfs", SELINUX_MNT, "selinuxfs", 0, nullptr); + } + + LOGD("Hijack [" SELINUX_LOAD "] and [" SELINUX_ENFORCE "]\n"); + + xmkdir(SELINUXMOCK, 0); + mkfifo(MOCK_LOAD, 0600); + mkfifo(MOCK_ENFORCE, 0644); + xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr); + xmount(MOCK_ENFORCE, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr); + + // Create a new process waiting for original init to load sepolicy into our fifo + if (xfork()) { + // In parent, return and continue boot process + return; + } + + // Read full sepolicy + int fd = xopen(MOCK_LOAD, O_RDONLY); + string policy = fd_full_read(fd); + close(fd); + auto sepol = unique_ptr(sepolicy::from_data(policy.data(), policy.length())); + + sepol->magisk_rules(); + sepol->load_rules(rules); + + // Mount selinuxfs to another path + xmkdir(REAL_SELINUXFS, 0755); + xmount("selinuxfs", REAL_SELINUXFS, "selinuxfs", 0, nullptr); + + // This open will block until the actual init calls security_getenforce + fd = xopen(MOCK_ENFORCE, O_WRONLY); + + // Cleanup the hijacks + umount2("/init", MNT_DETACH); + xumount2(SELINUX_LOAD, MNT_DETACH); + xumount2(SELINUX_ENFORCE, MNT_DETACH); + + // Load patched policy + sepol->to_file(REAL_SELINUXFS "/load"); + + // Write to mock "enforce" 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 security_getenforce reads the "enforce" file, 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. + + xwrite(fd, "0", 1); + close(fd); + + // At this point, the actual init process will be unblocked + // and continue on with restorecon + re-exec. + + // Terminate process + exit(0); +} + static void recreate_sbin(const char *mirror, bool use_bind_mount) { auto dp = xopen_dir(mirror); int src = dirfd(dp.get()); @@ -199,22 +287,18 @@ static void patch_socket_name(const char *path) { } #define ROOTMIR MIRRDIR "/system_root" -#define MONOPOLICY "/sepolicy" #define NEW_INITRC "/system/etc/init/hw/init.rc" void SARBase::patch_ro_root() { string tmp_dir; - const char *sepol; if (access("/sbin", F_OK) == 0) { tmp_dir = "/sbin"; - sepol = "/sbin/.se"; } else { char buf[8]; gen_rand_str(buf, sizeof(buf)); tmp_dir = "/dev/"s + buf; xmkdir(tmp_dir.data(), 0); - sepol = "/dev/.se"; } setup_tmp(tmp_dir.data()); @@ -231,20 +315,14 @@ void SARBase::patch_ro_root() { if (tmp_dir == "/sbin") recreate_sbin(ROOTMIR "/sbin", true); - // Patch init - int patch_count; - { + xmkdir(ROOTOVL, 0); + + // Handle avd hack + if (avd_hack) { int src = xopen("/init", O_RDONLY | O_CLOEXEC); auto init = mmap_data("/init"); - patch_count = init.patch({ - make_pair(SPLIT_PLAT_CIL, "xxx"), /* Force loading monolithic sepolicy */ - make_pair(MONOPOLICY, sepol) /* Redirect /sepolicy to custom path */ - }); - if (avd_hack) { - // Force disable early mount on original init - init.patch({ make_pair("android,fstab", "xxx") }); - } - xmkdir(ROOTOVL, 0); + // Force disable early mount on original init + init.patch({ make_pair("android,fstab", "xxx") }); int dest = xopen(ROOTOVL "/init", O_CREAT | O_WRONLY | O_CLOEXEC, 0); xwrite(dest, init.buf, init.sz); fclone_attr(src, dest); @@ -252,30 +330,6 @@ void SARBase::patch_ro_root() { close(dest); } - if (patch_count != 2) { - // init is dynamically linked, need to patch libselinux - const char *path = "/system/lib64/libselinux.so"; - if (access(path, F_OK) != 0) { - path = "/system/lib/libselinux.so"; - if (access(path, F_OK) != 0) - path = nullptr; - } - if (path) { - char ovl[128]; - sprintf(ovl, ROOTOVL "%s", path); - auto lib = mmap_data(path); - lib.patch({ make_pair(MONOPOLICY, sepol) }); - xmkdirs(dirname(ovl), 0755); - int dest = xopen(ovl, O_CREAT | O_WRONLY | O_CLOEXEC, 0); - xwrite(dest, lib.buf, lib.sz); - close(dest); - clone_attr(path, ovl); - } - } - - // sepolicy - patch_sepolicy(sepol); - // Handle overlay.d restore_folder(ROOTOVL, overlays); overlays.clear(); @@ -321,6 +375,8 @@ void SARBase::patch_ro_root() { write(dest, magic_mount_list.data(), magic_mount_list.length()); close(dest); + hijack_sepolicy(); + chdir("/"); } diff --git a/native/jni/magiskpolicy/policydb.cpp b/native/jni/magiskpolicy/policydb.cpp index 2438d7e96..d035c256c 100644 --- a/native/jni/magiskpolicy/policydb.cpp +++ b/native/jni/magiskpolicy/policydb.cpp @@ -79,6 +79,27 @@ static void load_cil(struct cil_db *db, const char *file) { LOGD("cil_add [%s]\n", file); } +sepolicy *sepolicy::from_data(char *data, size_t len) { + LOGD("Load policy from data\n"); + + policy_file_t pf; + policy_file_init(&pf); + pf.data = data; + pf.len = len; + pf.type = PF_USE_MEMORY; + + auto db = static_cast(xmalloc(sizeof(policydb_t))); + if (policydb_init(db) || policydb_read(db, &pf, 0)) { + LOGE("Fail to load policy from data\n"); + free(db); + return nullptr; + } + + auto sepol = new sepolicy(); + sepol->db = db; + return sepol; +} + sepolicy *sepolicy::from_file(const char *file) { LOGD("Load policy from: %s\n", file); diff --git a/native/jni/magiskpolicy/rules.cpp b/native/jni/magiskpolicy/rules.cpp index 1a6d406d8..c20ab9f38 100644 --- a/native/jni/magiskpolicy/rules.cpp +++ b/native/jni/magiskpolicy/rules.cpp @@ -124,6 +124,9 @@ void sepolicy::magisk_rules() { // Let everyone access tmpfs files (for SAR sbin overlay) allow(ALL, "tmpfs", "file", ALL); + // Allow magiskinit daemon to handle mock selinuxfs + allow("kernel", "tmpfs", "fifo_file", "write"); + // For relabelling files allow("rootfs", "labeledfs", "filesystem", "associate"); allow(SEPOL_FILE_TYPE, "pipefs", "filesystem", "associate"); diff --git a/native/jni/magiskpolicy/statement.cpp b/native/jni/magiskpolicy/statement.cpp index 19c4c55a6..201eb0c69 100644 --- a/native/jni/magiskpolicy/statement.cpp +++ b/native/jni/magiskpolicy/statement.cpp @@ -313,7 +313,38 @@ void sepolicy::parse_statement(const char *stmt) { } void sepolicy::load_rule_file(const char *file) { - file_readline(true, file, [=](string_view line) -> bool { + file_readline(true, file, [&](string_view line) -> bool { + if (line.empty() || line[0] == '#') + return true; + parse_statement(line.data()); + return true; + }); +} + +void sepolicy::load_rules(const string &rules) { + struct cookie { + const string &s; + size_t pos; + }; + cookie c{rules, 0}; + FILE *fp = funopen(&c, /* read */ [](void *v, char *buf, int sz) -> int { + auto c = reinterpret_cast(v); + if (c->pos == c->s.length()) + return 0; + size_t end = std::min(c->pos + sz, c->s.length()); + int len = end - c->pos; + memcpy(buf, c->s.data() + c->pos, len); + c->pos = end; + return len; + }, /* write */ [](auto, auto, auto) -> int { + return 0; + }, /* seek */ [](auto, auto, auto) -> fpos_t { + return 0; + }, /* close */ [](auto) -> int { + return 0; + }); + + file_readline(true, fp, [&](string_view line) -> bool { if (line.empty() || line[0] == '#') return true; parse_statement(line.data()); diff --git a/native/jni/utils/compat/compat.cpp b/native/jni/utils/compat/compat.cpp index e208e53d8..da6867959 100644 --- a/native/jni/utils/compat/compat.cpp +++ b/native/jni/utils/compat/compat.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include extern "C" { @@ -103,6 +105,10 @@ int faccessat(int dirfd, const char *pathname, int mode, int flags) { return syscall(__NR_faccessat, dirfd, pathname, mode, flags); } +int mkfifo(const char *path, mode_t mode) { + return mknod(path, (mode & ~S_IFMT) | S_IFIFO, 0); +} + #define SPLIT_64(v) (unsigned)((v) & 0xFFFFFFFF), (unsigned)((v) >> 32) #if defined(__arm__) diff --git a/native/jni/utils/files.cpp b/native/jni/utils/files.cpp index 83ca0e5ee..60f615a16 100644 --- a/native/jni/utils/files.cpp +++ b/native/jni/utils/files.cpp @@ -291,18 +291,29 @@ void full_read(const char *filename, void **buf, size_t *size) { close(fd); } -string fd_full_read(int fd) { +void fd_full_read(int fd, string &str) { char buf[4096]; - string str; for (ssize_t len; (len = xread(fd, buf, sizeof(buf))) > 0;) str.insert(str.end(), buf, buf + len); +} + +void full_read(const char *filename, string &str) { + if (int fd = xopen(filename, O_RDONLY | O_CLOEXEC); fd >= 0) { + fd_full_read(fd, str); + close(fd); + } +} + +string fd_full_read(int fd) { + string str; + fd_full_read(fd, str); return str; } string full_read(const char *filename) { - int fd = xopen(filename, O_RDONLY | O_CLOEXEC); - run_finally f([=]{ close(fd); }); - return fd < 0 ? "" : fd_full_read(fd); + string str; + full_read(filename, str); + return str; } void write_zero(int fd, size_t size) { @@ -315,10 +326,7 @@ void write_zero(int fd, size_t size) { } } -void file_readline(bool trim, const char *file, const function &fn) { - FILE *fp = xfopen(file, "re"); - if (fp == nullptr) - return; +void file_readline(bool trim, FILE *fp, const function &fn) { size_t len = 1024; char *buf = (char *) malloc(len); char *start; diff --git a/native/jni/utils/files.hpp b/native/jni/utils/files.hpp index 555ab1461..b277424a6 100644 --- a/native/jni/utils/files.hpp +++ b/native/jni/utils/files.hpp @@ -77,10 +77,19 @@ void fclone_attr(int src, int dest); void clone_attr(const char *src, const char *dest); void fd_full_read(int fd, void **buf, size_t *size); void full_read(const char *filename, void **buf, size_t *size); +void fd_full_read(int fd, std::string &str); +void full_read(const char *filename, std::string &str); std::string fd_full_read(int fd); std::string full_read(const char *filename); void write_zero(int fd, size_t size); -void file_readline(bool trim, const char *file, const std::function &fn); +void file_readline(bool trim, FILE *fp, const std::function &fn); +static inline void file_readline( + bool trim, const char *file, const std::function &fn) { + FILE *fp = xfopen(file, "re"); + if (fp == nullptr) + return; + file_readline(trim, fp, fn); +} static inline void file_readline(const char *file, const std::function &fn) { file_readline(false, file, fn);