diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt index 3444e1c41c..3f917975c6 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -299,6 +299,7 @@ object VectorDaemon { */ fun softReboot() { Log.w(TAG, "Soft reboot: restarting the primary zygote") + Dex2OatServer.onSoftReboot() forgetDeliberateBoot() SystemProperties.set("ctl.restart", "zygote") } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt index 7db27300fc..7c461a3890 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -1,9 +1,13 @@ package org.matrix.vector.daemon.env import android.net.LocalServerSocket +import android.net.LocalSocket +import android.net.LocalSocketAddress import android.os.Build import android.os.FileObserver import android.os.SELinux +import android.os.SystemClock +import android.os.SystemProperties import android.system.ErrnoException import android.system.Os import android.system.OsConstants @@ -25,6 +29,7 @@ val DEX2OAT_MOUNT_FAILED = IManagerService.DEX2OAT_MOUNT_FAILED val DEX2OAT_SEPOLICY_INCORRECT = IManagerService.DEX2OAT_SEPOLICY_INCORRECT val DEX2OAT_SELINUX_PERMISSIVE = IManagerService.DEX2OAT_SELINUX_PERMISSIVE val DEX2OAT_CRASHED = IManagerService.DEX2OAT_CRASHED +val DEX2OAT_INJECTED = IManagerService.DEX2OAT_INJECTED object Dex2OatServer { private const val WRAPPER32 = "bin/dex2oat32" @@ -85,6 +90,7 @@ object Dex2OatServer { private fun onSelinuxEvent() { synchronized(this) { + if (compatibility == DEX2OAT_INJECTED) return if (compatibility == DEX2OAT_CRASHED) { SelinuxObserver.stopWatching() return @@ -209,7 +215,88 @@ object Dex2OatServer { doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]) } + private fun injectionTargetName(): String { + val useArtService = SystemProperties.get("dalvik.vm.useartservice") + return if (Build.VERSION.SDK_INT >= 35 || + (Build.VERSION.SDK_INT == 34 && (useArtService == "true" || useArtService == "1"))) + "artd" + else "installd" + } + + private fun processExists(name: String): Boolean { + val entries = File("/proc").list() ?: return false + for (entry in entries) { + if (entry.isEmpty() || !entry[0].isDigit()) continue + val command = + runCatching { + File("/proc/$entry/cmdline").readBytes().toString(Charsets.UTF_8) + } + .getOrNull() ?: continue + val executable = command.substringBefore('\u0000') + if (executable.isNotEmpty() && File(executable).name == name) return true + } + return false + } + + private fun probeMonitor(name: String): Boolean { + val path = "/data/adb/lspd/dex2oat_monitor_$name" + val deadline = SystemClock.uptimeMillis() + 1000L + do { + var socket: LocalSocket? = null + try { + socket = LocalSocket() + socket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) + runCatching { socket.setSoTimeout(250) } + val status = socket.inputStream.read() + if (status == 1) return true + if (status >= 0) return false + } catch (_: Throwable) { + } finally { + runCatching { socket?.close() } + } + try { + Thread.sleep(25L) + } catch (_: InterruptedException) { + return false + } + } while (SystemClock.uptimeMillis() < deadline) + return false + } + + private fun probeInjection(): Boolean { + val preferred = injectionTargetName() + if (processExists(preferred)) return probeMonitor(preferred) + val fallback = if (preferred == "artd") "installd" else "artd" + if (processExists(fallback)) return probeMonitor(fallback) + return probeMonitor(preferred) || probeMonitor(fallback) + } + + fun onSoftReboot() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return + if (probeInjection()) { + if (!notMounted()) doMount(false) + if (notMounted()) { + compatibility = DEX2OAT_INJECTED + return + } + } + if (compatibility == DEX2OAT_INJECTED) return + doMount(false) + doMount(true) + compatibility = if (notMounted()) DEX2OAT_MOUNT_FAILED else DEX2OAT_OK + } + fun start() { + if (probeInjection()) { + if (!notMounted()) doMount(false) + if (notMounted()) { + compatibility = DEX2OAT_INJECTED + Log.i(TAG, "dex2oat injection is active; wrapper is not mounted") + VectorDaemon.scope.launch { runSocketLoop() } + return + } + } + if (notMounted()) { doMount(true) if (notMounted()) { diff --git a/dex2oat/src/main/cpp/CMakeLists.txt b/dex2oat/src/main/cpp/CMakeLists.txt index ae4c3ae9c6..38550aeba8 100644 --- a/dex2oat/src/main/cpp/CMakeLists.txt +++ b/dex2oat/src/main/cpp/CMakeLists.txt @@ -2,15 +2,12 @@ cmake_minimum_required(VERSION 3.10) project(dex2oat) add_executable(dex2oat dex2oat.cpp) -add_library(oat_hook SHARED oat_hook.cpp) - -OPTION(LSPLT_BUILD_SHARED OFF) -add_subdirectory(${VECTOR_ROOT}/external/lsplt/lsplt/src/main/jni external) +add_library(oat_hook SHARED compiler_options.cpp) target_include_directories(oat_hook PUBLIC include) target_include_directories(dex2oat PUBLIC include) target_link_libraries(dex2oat log) -target_link_libraries(oat_hook log lsplt_static) +target_link_libraries(oat_hook log) if (DEFINED DEBUG_SYMBOLS_PATH) message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") diff --git a/dex2oat/src/main/cpp/compiler_options.cpp b/dex2oat/src/main/cpp/compiler_options.cpp new file mode 100644 index 0000000000..190ae7daa3 --- /dev/null +++ b/dex2oat/src/main/cpp/compiler_options.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include +#include +#include + +extern "C" __attribute__((visibility("default"))) void _ZN3art15CompilerOptionsC1Ev(void *self) { + using Constructor = void (*)(void *); + static Constructor original = + reinterpret_cast(dlsym(RTLD_NEXT, "_ZN3art15CompilerOptionsC1Ev")); + if (original == nullptr) return; + + original(self); + + constexpr size_t kMaxSlots = 10; + for (size_t i = 0; i < kMaxSlots; i++) { + auto *slot = reinterpret_cast(static_cast(self) + i * sizeof(uintptr_t)); + if (*slot == static_cast(-1)) { + *slot = 0; + break; + } + } + + unsetenv("LD_PRELOAD"); +} diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index 32719bea72..d16f6f19e2 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -9,14 +10,15 @@ #include #include #include -#include -#include #include "logging.h" -// Access to the process environment variables extern "C" char **environ; +#ifndef AT_EMPTY_PATH +#define AT_EMPTY_PATH 0x1000 +#endif + #if defined(__LP64__) #define LP_SELECT(lp32, lp64) lp64 #else @@ -27,29 +29,16 @@ namespace { constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac"; -/** - * Calculates a vector ID based on architecture and debug status. - */ -inline int get_id_vec(bool is64, bool is_debug) { +int get_id_vec(bool is64, bool is_debug) { return (static_cast(is64) << 1) | static_cast(is_debug); } -/** - * Wraps recvmsg with error logging. - */ -ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) { - ssize_t rec = recvmsg(sockfd, msg, flags); - if (rec < 0) { - PLOGE("recvmsg"); - } +ssize_t xrecvmsg(int sockfd, struct msghdr *msg) { + ssize_t rec = recvmsg(sockfd, msg, MSG_WAITALL); + if (rec < 0) PLOGE("recvmsg"); return rec; } -/** - * Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS. - * - * @return Pointer to the FD data on success, nullptr on failure. - */ void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { struct iovec iov = { .iov_base = &cnt, @@ -63,160 +52,136 @@ void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { .msg_controllen = bufsz, .msg_flags = 0}; - if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr; + if (xrecvmsg(sockfd, &msg) < 0) return nullptr; struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); - if (msg.msg_controllen != bufsz || cmsg == nullptr || cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { return nullptr; } - return CMSG_DATA(cmsg); } -/** - * Helper to receive a single FD from the socket. - */ int recv_fd(int sockfd) { char cmsgbuf[CMSG_SPACE(sizeof(int))]; void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1); if (data == nullptr) return -1; - int result; - std::memcpy(&result, data, sizeof(int)); + std::memcpy(&result, data, sizeof(result)); return result; } -/** - * Reads an integer acknowledgment from the socket. - */ int read_int(int fd) { int val; if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1; return val; } -/** - * Writes an integer command/ID to the socket. - */ void write_int(int fd, int val) { if (fd < 0) return; (void)write(fd, &val, sizeof(val)); } +int connect_daemon(const struct sockaddr_un &sock, socklen_t len) { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + PLOGE("socket"); + return -1; + } + if (connect(fd, reinterpret_cast(&sock), len) != 0) { + PLOGE("connect"); + close(fd); + return -1; + } + return fd; +} + +int copy_to_memfd(int fd) { + int mem_fd = static_cast(syscall(__NR_memfd_create, "liboat_hook_memfd", 0)); + if (mem_fd < 0) { + PLOGE("memfd_create"); + return fd; + } + + struct stat st {}; + if (fstat(fd, &st) != 0) { + PLOGE("fstat"); + close(mem_fd); + return fd; + } + + off_t offset = 0; + if (sendfile(mem_fd, fd, &offset, st.st_size) != st.st_size) { + PLOGE("sendfile"); + close(mem_fd); + return fd; + } + close(fd); + return mem_fd; +} + +int fallback(const char *cmd, char **argv) { + if (cmd == nullptr || cmd[0] != '/') return 1; + argv[0] = const_cast(cmd); + unsetenv("DEX2OAT_CMD"); + execve(cmd, argv, environ); + PLOGE("execve %s", cmd); + return 1; +} + } // namespace int main(int argc, char **argv) { + (void)argc; LOGD("dex2oat wrapper ppid=%d", getppid()); - // Prepare Unix domain socket address (Abstract Namespace) struct sockaddr_un sock = {}; sock.sun_family = AF_UNIX; - // sock.sun_path[0] is already \0, so we copy name into sun_path + 1 std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2); + socklen_t len = sizeof(sock.sun_family) + std::strlen(kSockName) + 1; - // Abstract socket length: family + leading \0 + string length - socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1; - - // 1. Get original dex2oat binary FD - int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (connect(sock_fd, reinterpret_cast(&sock), len)) { - PLOGE("failed to connect to %s", sock.sun_path + 1); - return 1; + char cmd_buf[256] {}; + const char *cmd = getenv("DEX2OAT_CMD"); + if (cmd != nullptr) { + std::snprintf(cmd_buf, sizeof(cmd_buf), "%s", cmd); + cmd = cmd_buf; } + const char *name = cmd != nullptr ? cmd : argv[0]; + bool is_debug = name != nullptr && std::strstr(name, "dex2oatd") != nullptr; - bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr); - write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug)); + int sock_fd = connect_daemon(sock, len); + if (sock_fd < 0) return fallback(cmd, argv); + write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug)); int stock_fd = recv_fd(sock_fd); - read_int(sock_fd); // Sync + read_int(sock_fd); close(sock_fd); + if (stock_fd < 0) return fallback(cmd, argv); - // 2. Get liboat_hook.so FD - sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (connect(sock_fd, reinterpret_cast(&sock), len)) { - PLOGE("failed to connect to %s", sock.sun_path + 1); - return 1; - } + sock_fd = connect_daemon(sock, len); + if (sock_fd < 0) return fallback(cmd, argv); write_int(sock_fd, LP_SELECT(4, 5)); int hooker_fd = recv_fd(sock_fd); - read_int(sock_fd); // Sync + read_int(sock_fd); close(sock_fd); + if (hooker_fd < 0) return fallback(cmd, argv); - if (hooker_fd == -1) { - LOGE("failed to read liboat_hook.so"); - } else { - int mem_fd = syscall(__NR_memfd_create, "liboat_hook_memfd", 0); - if (mem_fd >= 0) { - // Get the exact size of the original library - LOGD("Copying %d as mem_fd %d", hooker_fd, mem_fd); - struct stat st; - if (fstat(hooker_fd, &st) == 0) { - // Tell the kernel to copy the entire file directly to the memfd - off_t offset = 0; - sendfile(mem_fd, hooker_fd, &offset, st.st_size); - - // Swap the old FD with the new memfd - close(hooker_fd); - hooker_fd = mem_fd; - } else { - PLOGE("fstat failed"); - close(mem_fd); - } - } else { - PLOGE("memfd_create failed, falling back to original fd"); - } - } - - LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd); - - // Prepare arguments for execve - // Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0] - std::vector exec_argv; - - const char *linker_path = - LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64"); + hooker_fd = copy_to_memfd(hooker_fd); - char stock_fd_path[64]; - std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); - - exec_argv.push_back(linker_path); - exec_argv.push_back(stock_fd_path); - - // Append original arguments starting from argv[1] - for (int i = 1; i < argc; ++i) { - exec_argv.push_back(argv[i]); - } - - // Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot - // hook inlined target methods. - exec_argv.push_back("--inline-max-code-units=0"); - exec_argv.push_back(nullptr); - - // Setup Environment variables - // Clear LD_LIBRARY_PATH to let the linker use internal config + char preload[64]; + std::snprintf(preload, sizeof(preload), "/proc/self/fd/%d", hooker_fd); + setenv("LD_PRELOAD", preload, 1); unsetenv("LD_LIBRARY_PATH"); - // Set LD_PRELOAD to point to the hooker library FD - std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); - LOGD("Inject oat hook via %s", preload_val.data()); - setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1); - - // Pass original argv[0] as DEX2OAT_CMD - if (argv[0]) { - setenv("DEX2OAT_CMD", argv[0], 1); - LOGD("DEX2OAT_CMD set to %s", argv[0]); + if (cmd != nullptr) { + argv[0] = const_cast(cmd); + unsetenv("DEX2OAT_CMD"); } - LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path); - - // Perform the execution - execve(linker_path, const_cast(exec_argv.data()), environ); - - // If we reach here, execve failed - PLOGE("execve failed"); - return 2; + LOGD("executing %s with %s", argv[0], preload); + syscall(__NR_execveat, stock_fd, "", argv, environ, AT_EMPTY_PATH); + PLOGE("execveat failed"); + return fallback(cmd, argv); } diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h deleted file mode 100644 index 46aead8292..0000000000 --- a/dex2oat/src/main/cpp/include/oat.h +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ART_RUNTIME_OAT_OAT_H_ -#define ART_RUNTIME_OAT_OAT_H_ - -#include -#include -#include -#include - -#include "base_macros.h" -#include "macros.h" - -namespace art { - -enum class InstructionSet; - -class EXPORT PACKED(4) OatHeader { -public: - static constexpr std::array kOatMagic{{'o', 'a', 't', '\n'}}; - // Last oat version changed reason: Ensure oat checksum determinism across hosts and devices. - static constexpr std::array kOatVersion{{'2', '5', '9', '\0'}}; - - static constexpr const char* kDex2OatCmdLineKey = "dex2oat-cmdline"; - static constexpr const char* kDebuggableKey = "debuggable"; - static constexpr const char* kNativeDebuggableKey = "native-debuggable"; - static constexpr const char* kCompilerFilter = "compiler-filter"; - static constexpr const char* kClassPathKey = "classpath"; - static constexpr const char* kBootClassPathKey = "bootclasspath"; - static constexpr const char* kBootClassPathChecksumsKey = "bootclasspath-checksums"; - static constexpr const char* kApexVersionsKey = "apex-versions"; - static constexpr const char* kConcurrentCopying = "concurrent-copying"; - static constexpr const char* kCompilationReasonKey = "compilation-reason"; - static constexpr const char* kRequiresImage = "requires-image"; - - // Fields listed here are key value store fields that are deterministic across hosts and - // devices, meaning they should have exactly the same value when the oat file is generated on - // different hosts and devices for the same app / boot image and for the same device model with - // the same compiler options. If you are adding a new field that doesn't hold this property, put - // it in `kNonDeterministicFieldsAndLengths` and assign a length limit. - // - // When writing the oat header, the non-deterministic fields are padded to their length limits - // and excluded from the oat checksum computation. This makes the oat checksum deterministic - // across hosts and devices, which is important for Cloud Compilation, where we generate an oat - // file on a host and use it on a device. - static constexpr std::array kDeterministicFields{ - kDebuggableKey, kNativeDebuggableKey, kCompilerFilter, - kClassPathKey, kBootClassPathKey, kBootClassPathChecksumsKey, - kConcurrentCopying, kCompilationReasonKey, kRequiresImage, - }; - - static constexpr std::array, 2> - kNonDeterministicFieldsAndLengths{ - std::make_pair(kDex2OatCmdLineKey, 2048), - std::make_pair(kApexVersionsKey, 1024), - }; - - static constexpr const char kTrueValue[] = "true"; - static constexpr const char kFalseValue[] = "false"; - - // Added helper to access the key_value_store_ field, which could be fragile across - // different Android versions and compiler optimizations. - const uint8_t* getKeyValueStore() const { return key_value_store_; } - - void ComputeChecksum(/*inout*/ uint32_t* checksum) const; - -private: - std::array magic_; - std::array version_; - uint32_t oat_checksum_; - - InstructionSet instruction_set_; - uint32_t instruction_set_features_bitmap_; - uint32_t dex_file_count_; - uint32_t oat_dex_files_offset_; - uint32_t bcp_bss_info_offset_; - // Offset of the oat header (i.e. start of the oat data) in the ELF file. - // It is used to additional validation of the oat header as it is not - // page-aligned in the memory. - uint32_t base_oat_offset_; - uint32_t executable_offset_; - uint32_t jni_dlsym_lookup_trampoline_offset_; - uint32_t jni_dlsym_lookup_critical_trampoline_offset_; - uint32_t quick_generic_jni_trampoline_offset_; - uint32_t quick_imt_conflict_trampoline_offset_; - uint32_t quick_resolution_trampoline_offset_; - uint32_t quick_to_interpreter_bridge_offset_; - uint32_t nterp_trampoline_offset_; - - uint32_t key_value_store_size_; - uint8_t key_value_store_[0]; // note variable width data at end - - DISALLOW_COPY_AND_ASSIGN(OatHeader); -}; -} // namespace art -#endif // ART_RUNTIME_OAT_OAT_H_ diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp deleted file mode 100644 index cc4cdb1b9d..0000000000 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logging.h" -#include "oat.h" - -/** - * This library is injected into dex2oat to intercept the generation of OAT headers. Our wrapper - * runs dex2oat via the linker with extra flags. Without this hook, the resulting OAT file would - * record the transferred fd path of wrapper and the extra flags in its "dex2oat-cmdline" key, which - * can be used to detect the wrapper. - */ - -namespace { -const std::string_view kParamToRemove = "--inline-max-code-units=0"; -std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path -} // namespace - -/** - * Sanitizes the command line string by: - * 1. Replacing the first token (the linker/binary path) with the original dex2oat path. - * 2. Removing the specific optimization flag we injected. - */ -std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) { - std::vector tokens; - std::string current; - - // Simple split by space - for (char c : sv) { - if (c == ' ') { - if (!current.empty()) { - tokens.push_back(std::move(current)); - current.clear(); - } - } else { - current.push_back(c); - } - } - if (!current.empty()) tokens.push_back(std::move(current)); - - // 1. Replace the command path (argv[0]) - if (!tokens.empty()) { - tokens[0] = std::string(new_cmd_path); - } - - // 2. Remove the injected parameter if it exists - auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove)); - tokens.erase(it, tokens.end()); - - // 3. Join tokens back into a single string - std::string result; - for (size_t i = 0; i < tokens.size(); ++i) { - result += tokens[i]; - if (i != tokens.size() - 1) result += ' '; - } - return result; -} - -/** - * Re-serializes the Key-Value map back into the OAT header memory space. - */ -uint8_t* WriteKeyValueStore(const std::map& key_values, uint8_t* store) { - LOGD("Writing KeyValueStore back to memory"); - char* data_ptr = reinterpret_cast(store); - - for (const auto& [key, value] : key_values) { - // Copy key + null terminator - std::memcpy(data_ptr, key.c_str(), key.length() + 1); - data_ptr += key.length() + 1; - // Copy value + null terminator - std::memcpy(data_ptr, value.c_str(), value.length() + 1); - data_ptr += value.length() + 1; - } - LOGD("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); - - return reinterpret_cast(data_ptr); -} - -// Helper function to test if a header field could have variable length -bool IsNonDeterministic(const std::string_view& key) { - auto variable_fields = art::OatHeader::kNonDeterministicFieldsAndLengths; - return std::any_of(variable_fields.begin(), variable_fields.end(), - [&key](const auto& pair) { return pair.first.compare(key) == 0; }); -} - -/** - * Parses the OAT KeyValueStore and spoofs the "dex2oat-cmdline" entry. - * - * @return true if the store was modified in-place or successfully rebuilt. - */ -bool SpoofKeyValueStore(uint8_t* store) { - if (!store) return false; - - uint32_t* const store_size_ptr = reinterpret_cast(store - sizeof(uint32_t)); - uint32_t const store_size = *store_size_ptr; - - const char* ptr = reinterpret_cast(store); - const char* const store_end = ptr + store_size; - std::map new_store_map; - LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size); - - bool store_modified = false; - - while (ptr < store_end && *ptr != '\0') { - // Find key - const char* key_end = reinterpret_cast(std::memchr(ptr, 0, store_end - ptr)); - if (!key_end) break; - std::string_view key(ptr, key_end - ptr); - - // Find value - const char* value_start = key_end + 1; - if (value_start >= store_end) break; - const char* value_end = - reinterpret_cast(std::memchr(value_start, 0, store_end - value_start)); - if (!value_end) break; - std::string_view value(value_start, value_end - value_start); - - const bool has_padding = - value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key); - - if (key == art::OatHeader::kDex2OatCmdLineKey && - value.find(kParamToRemove) != std::string_view::npos) { - std::string cleaned_cmd = process_cmd(value, g_binary_path); - LOGI("Spoofing cmdline: Original size %zu -> New size %zu", value.length(), - cleaned_cmd.length()); - - // We can overwrite in-place if the padding is enabled - if (has_padding) { - LOGI("In-place spoofing dex2oat-cmdline (padding detected)"); - - // Zero out the entire original value range to be safe - size_t original_capacity = value.length(); - std::memset(const_cast(value_start), 0, original_capacity); - - // Write the new command. - std::memcpy(const_cast(value_start), cleaned_cmd.c_str(), - std::min(cleaned_cmd.length(), original_capacity)); - return true; - } - - // Standard logic: store in map and rebuild later - new_store_map[std::string(key)] = std::move(cleaned_cmd); - store_modified = true; - } else { - new_store_map[std::string(key)] = std::string(value); - LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); - } - - ptr = value_end + 1; - if (has_padding) { - while (*ptr == '\0') { - ptr++; - } - } - } - - if (store_modified) { - uint8_t* const new_store_end = WriteKeyValueStore(new_store_map, store); - *store_size_ptr = new_store_end - store; - LOGI("Store size set to %u", *store_size_ptr); - return true; - } - return false; -} - -#define DCL_HOOK_FUNC(ret, func, ...) \ - ret (*old_##func)(__VA_ARGS__) = nullptr; \ - ret new_##func(__VA_ARGS__) - -// For Android version < 16 -DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { - uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - - SpoofKeyValueStore(key_value_store); - - return key_value_store; -} - -// For Android version 16+ : Intercept during checksum calculation -DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { - auto* oat_header = reinterpret_cast(header); - uint8_t* const store = const_cast(oat_header->getKeyValueStore()); - - SpoofKeyValueStore(store); - - // Call original to compute checksum on our modified data - old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); - LOGV("OAT Checksum recalculated: 0x%08X", *checksum); -} - -#undef DCL_HOOK_FUNC - -void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) { - if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) { - LOGE("Failed to register PLT hook: %s", symbol); - } -} - -#define PLT_HOOK_REGISTER_SYM(DEV, INODE, SYM, NAME) \ - register_hook(DEV, INODE, SYM, reinterpret_cast(new_##NAME), \ - reinterpret_cast(&old_##NAME)) - -#define PLT_HOOK_REGISTER(DEV, INODE, NAME) PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) - -__attribute__((constructor)) static void initialize() { - dev_t dev = 0; - ino_t inode = 0; - - // Locate the dex2oat binary in memory to get its device and inode for PLT hooking - for (const auto& info : lsplt::MapInfo::Scan()) { - if (info.path.find("bin/dex2oat") != std::string::npos) { - dev = info.dev; - inode = info.inode; - if (g_binary_path.empty()) g_binary_path = std::string(info.path); - LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, - (uintmax_t)inode); - break; - } - } - - if (dev == 0) { - LOGE("Could not locate dex2oat memory map"); - return; - } - - // Register hook for the standard KeyValueStore getter - PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); - - // If the standard store hook fails (e.g., on Android 16+), try the Checksum hook - if (!lsplt::CommitHook()) { - PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); - lsplt::CommitHook(); - } -} diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt index 725ff534dc..1d4bf958ce 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt @@ -427,7 +427,9 @@ class HomeViewModel( // deletes `dalvik.vm.dex2oat-flags` when it mounts the wrapper over dex2oat and sets // it when it unmounts, so either route suppresses the inlining. A wrapper that is not // OK therefore only costs anything when the property is not carrying the flag either. - if (dex2oat != IManagerService.DEX2OAT_OK && !inliningDisabled) { + if (dex2oat != IManagerService.DEX2OAT_OK && + dex2oat != IManagerService.DEX2OAT_INJECTED && + !inliningDisabled) { add(HealthIssue.Dex2oatWrapperBroken) } } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt index f63f0dd9c9..45900be852 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/SystemStatusScreen.kt @@ -814,7 +814,8 @@ private fun buildSections( str(R.string.info_dex2oat), dex2oatLabel(context, status.dex2oatWrapperState), health = - if (status.dex2oatWrapperState == IManagerService.DEX2OAT_OK) + if (status.dex2oatWrapperState == IManagerService.DEX2OAT_OK || + status.dex2oatWrapperState == IManagerService.DEX2OAT_INJECTED) Health.Good else Health.Bad, monospace = false, @@ -832,17 +833,29 @@ private fun buildSections( ) } -private fun dex2oatLabel(context: Context, state: Int): String = - context.getString( - when (state) { - IManagerService.DEX2OAT_OK -> R.string.info_supported - IManagerService.DEX2OAT_CRASHED -> R.string.info_dex2oat_crashed - IManagerService.DEX2OAT_MOUNT_FAILED -> R.string.info_dex2oat_mount_failed - IManagerService.DEX2OAT_SELINUX_PERMISSIVE -> R.string.info_dex2oat_selinux_permissive - IManagerService.DEX2OAT_SEPOLICY_INCORRECT -> R.string.info_dex2oat_sepolicy_incorrect - else -> R.string.info_unsupported - } - ) +private fun dex2oatLabel(context: Context, state: Int): String { + val label = + context.getString( + when (state) { + IManagerService.DEX2OAT_OK -> R.string.info_supported + IManagerService.DEX2OAT_INJECTED -> R.string.info_supported + IManagerService.DEX2OAT_CRASHED -> R.string.info_dex2oat_crashed + IManagerService.DEX2OAT_MOUNT_FAILED -> R.string.info_dex2oat_mount_failed + IManagerService.DEX2OAT_SELINUX_PERMISSIVE -> + R.string.info_dex2oat_selinux_permissive + IManagerService.DEX2OAT_SEPOLICY_INCORRECT -> + R.string.info_dex2oat_sepolicy_incorrect + else -> R.string.info_unsupported + } + ) + return when (state) { + IManagerService.DEX2OAT_OK -> + "$label (${context.getString(R.string.info_mounted)})" + IManagerService.DEX2OAT_INJECTED -> + "$label (${context.getString(R.string.info_injected)})" + else -> label + } +} @Composable private fun FrameworkToggle( diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 932f431a3d..709b2c5d1e 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -42,6 +42,7 @@ محمَّلة غير محمَّلة محقون + مُركّب غير محقون إشعار الحالة إشعار دائم يبيّن أن Vector يعمل. diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index 55a712de70..42d958dd90 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -42,6 +42,7 @@ Geladen Nicht geladen Injiziert + Eingehängt Nicht injiziert Statusbenachrichtigung Eine dauerhafte Benachrichtigung, die anzeigt, dass Vector läuft. diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index d3fc8ad5cd..4dc9b083ad 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -42,6 +42,7 @@ Cargada No cargada Inyectado + Montado No inyectado Notificación de estado Una notificación permanente que indica que Vector está en marcha. diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index d4c8a06702..ae23b833ca 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -42,6 +42,7 @@ بارگذاری‌شده بارگذاری‌نشده تزریق‌شده + متصل‌شده تزریق‌نشده اعلان وضعیت اعلانی ماندگار که نشان می‌دهد Vector در حال اجراست. diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index 3a401d36b6..f10dbbe176 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -42,6 +42,7 @@ Chargée Non chargée Injecté + Monté Non injecté Notification d\'état Une notification persistante indiquant que Vector fonctionne. diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index 3c6c6390e6..324e83fd30 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -46,6 +46,7 @@ Dimuat Tidak dimuat Terinjeksi + Terpasang Tidak terinjeksi Notifikasi status Notifikasi permanen yang menandakan Vector sedang berjalan. diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index e71e14e18c..46d2d07330 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -42,6 +42,7 @@ Caricati Non caricati Iniettato + Montato Non iniettato Notifica di stato Una notifica permanente che indica che Vector è in esecuzione. diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index 385c875830..783c84b048 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -46,6 +46,7 @@ נטענה לא נטענה הוזרק + מחובר לא הוזרק התראת מצב התראה קבועה שמראה ש-Vector פועל. diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index 3b75f0c3f6..3fc7e6c89d 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -42,6 +42,7 @@ 読み込まれています 読み込まれていません 注入されています + マウント済み 注入されていません ステータス通知 Vector が実行中であることを示す常時表示の通知です。 diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index 533f397354..f497da7eb3 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -42,6 +42,7 @@ 로드됨 로드되지 않음 주입됨 + 마운트됨 주입되지 않음 상태 알림 Vector가 실행 중임을 알리는 상시 알림입니다. diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index e8f40a59c6..2464781ece 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -83,6 +83,7 @@ Załadowane Niezaładowane Wstrzyknięty + Zamontowany Niewstrzyknięty Powiadomienie o stanie Stałe powiadomienie pokazujące, że Vector jest uruchomiony. diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index 3977bb60ae..aacb7e8c81 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -42,6 +42,7 @@ Carregada Não carregada Injetado + Montado Não injetado Notificação de status Uma notificação permanente mostrando que o Vector está em execução. diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index c0c7f53709..d58ec83e11 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -38,6 +38,7 @@ Загружена Не загружена Внедрён + Смонтировано Не внедрён Уведомление о состоянии Постоянное уведомление о том, что Vector работает. diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index 701a438e64..2c7f586531 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -42,6 +42,7 @@ Yüklendi Yüklenmedi Enjekte edildi + Bağlandı Enjekte edilmedi Durum bildirimi Vector\'ün çalıştığını gösteren kalıcı bir bildirim. diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index 29cb7052f0..8b92023e1e 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -42,6 +42,7 @@ Завантажено Не завантажено Впроваджено + Змонтовано Не впроваджено Сповіщення про стан Постійне сповіщення, яке показує, що Vector працює. diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index c592b94e78..19e0611e16 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -42,6 +42,7 @@ Đã nạp Chưa nạp Đã tiêm + Đã gắn kết Chưa tiêm Thông báo trạng thái Một thông báo thường trực cho biết Vector đang chạy. diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 5068e3cf5a..75fafdb191 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -42,6 +42,7 @@ 已加载 未加载 已注入 + 已挂载 未注入 状态通知 一条常驻通知,用于显示 Vector 正在运行。 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index 76a243e73c..3db47cd386 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -42,6 +42,7 @@ 已載入 未載入 已注入 + 已掛載 未注入 狀態通知 一則常駐通知,用於顯示 Vector 正在執行。 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index ec54516e14..ad875439ce 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -87,6 +87,7 @@ Loaded Not loaded Injected + Mounted Not injected Status notification A persistent notification showing that Vector is running. diff --git a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl index 19319ba12a..b5b9c888d5 100644 --- a/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl @@ -239,6 +239,8 @@ interface IManagerService { */ const int DEX2OAT_SEPOLICY_INCORRECT = 4; + const int DEX2OAT_INJECTED = 5; + // ---- module configuration --------------------------------------------------------------------- /** diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 049ca7b8e9..616f8b0233 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -86,6 +86,14 @@ for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framewor extract "$ZIPFILE" "$file" "$MODPATH" done +if [ "$IS64BIT" = true ]; then + extract "$ZIPFILE" "zn_modules.$ABI64.txt" "$MODPATH" + mv "$MODPATH/zn_modules.$ABI64.txt" "$MODPATH/zn_modules.txt" +else + extract "$ZIPFILE" "zn_modules.$ABI32.txt" "$MODPATH" + mv "$MODPATH/zn_modules.$ABI32.txt" "$MODPATH/zn_modules.txt" +fi + ui_print "- Extracting Zygisk libraries" mkdir -p "$MODPATH/zygisk" diff --git a/zygisk/module/sepolicy.rule b/zygisk/module/sepolicy.rule index 8c96645da4..863fa203d7 100644 --- a/zygisk/module/sepolicy.rule +++ b/zygisk/module/sepolicy.rule @@ -1,18 +1,24 @@ allow dex2oat dex2oat_exec file execute_no_trans -allow dex2oat system_linker_exec file execute_no_trans allow shell shell dir write type xposed_file file_type typeattribute xposed_file mlstrustedobject -allow {dex2oat installd isolated_app shell} xposed_file {file dir} * +allow {dex2oat installd isolated_app shell artd} xposed_file {file dir} * +allow {dex2oat installd artd} adb_data_file file {open read getattr map execute} +allow {dex2oat installd artd} adb_data_file dir search allow dex2oat {unlabeled tmpfs} file * allow zygote dex2oat unix_stream_socket * +allow artd dex2oat process2 nosuid_transition +allow installd dex2oat process2 nosuid_transition + +allow artd artd process execmem +allow installd installd process execmem + type xposed_data file_type typeattribute xposed_data mlstrustedobject allow * xposed_data {file dir} * -# `serial_vector` is in no service_contexts, so it is labelled default_android_service allow system_server default_android_service service_manager find diff --git a/zygisk/module/zn_modules.arm64-v8a.txt b/zygisk/module/zn_modules.arm64-v8a.txt new file mode 100644 index 0000000000..5824d0d531 --- /dev/null +++ b/zygisk/module/zn_modules.arm64-v8a.txt @@ -0,0 +1,2 @@ +name=artd companion zygisk/arm64-v8a.so +name=installd companion zygisk/arm64-v8a.so diff --git a/zygisk/module/zn_modules.armeabi-v7a.txt b/zygisk/module/zn_modules.armeabi-v7a.txt new file mode 100644 index 0000000000..7ac1cbe885 --- /dev/null +++ b/zygisk/module/zn_modules.armeabi-v7a.txt @@ -0,0 +1,2 @@ +name=artd companion zygisk/armeabi-v7a.so +name=installd companion zygisk/armeabi-v7a.so diff --git a/zygisk/module/zn_modules.x86.txt b/zygisk/module/zn_modules.x86.txt new file mode 100644 index 0000000000..f2d71b187e --- /dev/null +++ b/zygisk/module/zn_modules.x86.txt @@ -0,0 +1,2 @@ +name=artd companion zygisk/x86.so +name=installd companion zygisk/x86.so diff --git a/zygisk/module/zn_modules.x86_64.txt b/zygisk/module/zn_modules.x86_64.txt new file mode 100644 index 0000000000..c66cb54031 --- /dev/null +++ b/zygisk/module/zn_modules.x86_64.txt @@ -0,0 +1,2 @@ +name=artd companion zygisk/x86_64.so +name=installd companion zygisk/x86_64.so diff --git a/zygisk/src/main/cpp/CMakeLists.txt b/zygisk/src/main/cpp/CMakeLists.txt index 52a4794faa..59964852b3 100644 --- a/zygisk/src/main/cpp/CMakeLists.txt +++ b/zygisk/src/main/cpp/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) add_subdirectory(${VECTOR_ROOT}/native native) -add_library(${PROJECT_NAME} SHARED module.cpp ipc_bridge.cpp) +add_library(${PROJECT_NAME} SHARED module.cpp ipc_bridge.cpp dex2oat_inject.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) target_link_libraries(${PROJECT_NAME} native log) diff --git a/zygisk/src/main/cpp/dex2oat_inject.cpp b/zygisk/src/main/cpp/dex2oat_inject.cpp new file mode 100644 index 0000000000..6b1932c734 --- /dev/null +++ b/zygisk/src/main/cpp/dex2oat_inject.cpp @@ -0,0 +1,532 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "zygisk_next_api.h" + +extern "C" char **environ; + +namespace vector::native::dex2oat { + +namespace { + +constexpr const char *kWrapper32 = "/data/adb/modules/zygisk_vector/bin/dex2oat32"; +constexpr const char *kWrapper64 = "/data/adb/modules/zygisk_vector/bin/dex2oat64"; +constexpr const char *kMonitorDir = "/data/adb/lspd"; +constexpr const char *kMonitorPrefix = "dex2oat_monitor_"; +constexpr const char *kPreloadPrefix = "LD_PRELOAD="; +constexpr const char *kCmdPrefix = "DEX2OAT_CMD="; +constexpr char kRequestMagic = 'V'; + +constexpr size_t kMaxArgv = 256; +constexpr size_t kMaxEnvp = 256; +constexpr size_t kMaxMonitors = 4; +constexpr size_t kMaxMonitorName = 32; + +using ExecveFn = int (*)(const char *, char *const[], char *const[]); +using ExecvFn = int (*)(const char *, char *const[]); +using PosixSpawnFn = int (*)(pid_t *, const char *, const posix_spawn_file_actions_t *, + const posix_spawnattr_t *, char *const[], char *const[]); + +const ZygiskNextAPI *g_api = nullptr; + +ExecveFn g_orig_execve = nullptr; +ExecvFn g_orig_execv = nullptr; +PosixSpawnFn g_orig_posix_spawn = nullptr; +PosixSpawnFn g_orig_posix_spawnp = nullptr; + +std::atomic g_ready{-1}; +std::atomic g_arming{false}; +std::atomic g_spawn_depth{0}; + +struct MonitorSlot { + std::atomic claimed{false}; + std::atomic serving{false}; + std::atomic status{0}; + char name[kMaxMonitorName] {}; +}; + +MonitorSlot g_monitors[kMaxMonitors]; +pthread_mutex_t g_monitor_lock = PTHREAD_MUTEX_INITIALIZER; + +struct TransformBuffers { + char *argv[kMaxArgv + 2]; + char *envp[kMaxEnvp + 2]; + const char *path; + char cmd[4200]; +}; + +bool WriteAll(int fd, const void *data, size_t size) { + const char *p = static_cast(data); + size_t written = 0; + while (written < size) { + ssize_t n = write(fd, p + written, size - written); + if (n <= 0) return false; + written += static_cast(n); + } + return true; +} + +bool ReadAll(int fd, void *data, size_t size) { + char *p = static_cast(data); + size_t done = 0; + while (done < size) { + ssize_t n = read(fd, p + done, size - done); + if (n <= 0) return false; + done += static_cast(n); + } + return true; +} + +std::string ProcessName() { + char buffer[256] {}; + int fd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ssize_t n = read(fd, buffer, sizeof(buffer) - 1); + close(fd); + if (n > 0) { + buffer[n] = '\0'; + std::string name(buffer); + auto nul = name.find('\0'); + if (nul != std::string::npos) name.resize(nul); + auto slash = name.rfind('/'); + if (slash != std::string::npos) name.erase(0, slash + 1); + if (!name.empty()) return name; + } + } + + fd = open("/proc/self/comm", O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ssize_t n = read(fd, buffer, sizeof(buffer) - 1); + close(fd); + if (n > 0) { + buffer[n] = '\0'; + std::string name(buffer); + while (!name.empty() && (name.back() == '\n' || name.back() == '\r')) name.pop_back(); + if (!name.empty()) return name; + } + } + return "unknown"; +} + +std::string SafeName(const std::string &name) { + std::string out; + for (char c : name) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || + c == '-' || c == '.') { + out.push_back(c); + } + } + if (out.empty() || out == "." || out == "..") return "unknown"; + return out; +} + +bool IsDex2Oat(const char *path, char *const argv[]) { + if (path != nullptr && strstr(path, "dex2oat") != nullptr) return true; + return argv != nullptr && argv[0] != nullptr && strstr(argv[0], "dex2oat") != nullptr; +} + +bool IsArtExec(const char *path, char *const argv[]) { + if (path != nullptr && strstr(path, "art_exec") != nullptr) return true; + return argv != nullptr && argv[0] != nullptr && strstr(argv[0], "art_exec") != nullptr; +} + +bool Is64BitExecutable(const char *path, char *const argv[]) { + const char *candidate = path; + if (candidate == nullptr && argv != nullptr) candidate = argv[0]; + if (candidate != nullptr) { + int fd = open(candidate, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + unsigned char header[5] {}; + ssize_t n = read(fd, header, sizeof(header)); + close(fd); + if (n == 5 && header[0] == 0x7f && header[1] == 'E' && header[2] == 'L' && + header[3] == 'F') { + return header[4] == 2; + } + } + if (strstr(candidate, "64") != nullptr) return true; + if (strstr(candidate, "32") != nullptr) return false; + } + return sizeof(void *) == 8; +} + +bool WrapperMounted(const char *path, bool is64) { + if (path == nullptr) return false; + struct stat target {}; + struct stat wrapper {}; + if (stat(path, &target) != 0) return false; + if (stat(is64 ? kWrapper64 : kWrapper32, &wrapper) != 0) return false; + return target.st_dev == wrapper.st_dev && target.st_ino == wrapper.st_ino; +} + +bool WrapperReady(bool is64) { + struct stat wrapper {}; + return stat(is64 ? kWrapper64 : kWrapper32, &wrapper) == 0; +} + +bool BuildEnv(char *const env_in[], const char *cmd, TransformBuffers &buffers) { + if (cmd == nullptr) return false; + + snprintf(buffers.cmd, sizeof(buffers.cmd), "%s%s", kCmdPrefix, cmd); + + size_t envc = 0; + char *const *env = env_in != nullptr ? env_in : environ; + for (size_t i = 0; env != nullptr && env[i] != nullptr; i++) { + if (strncmp(env[i], kPreloadPrefix, strlen(kPreloadPrefix)) == 0) continue; + if (strncmp(env[i], kCmdPrefix, strlen(kCmdPrefix)) == 0) continue; + if (envc + 2 >= kMaxEnvp) return false; + buffers.envp[envc++] = env[i]; + } + buffers.envp[envc++] = buffers.cmd; + buffers.envp[envc] = nullptr; + return true; +} + +bool Build(const char *path, char *const argv_in[], char *const env_in[], bool is64, + TransformBuffers &buffers) { + if (argv_in == nullptr) return false; + + const char *cmd = nullptr; + if (path != nullptr && path[0] == '/') { + cmd = path; + } else if (argv_in[0] != nullptr && argv_in[0][0] == '/') { + cmd = argv_in[0]; + } else { + return false; + } + + size_t argc = 0; + while (argv_in[argc] != nullptr) { + if (argc >= kMaxArgv) return false; + buffers.argv[argc] = argv_in[argc]; + argc++; + } + buffers.argv[argc] = nullptr; + + buffers.path = is64 ? kWrapper64 : kWrapper32; + return BuildEnv(env_in, cmd, buffers); +} + +bool BuildArtExec(const char *path, char *const argv_in[], char *const env_in[], + TransformBuffers &buffers) { + size_t argc = 0; + size_t separator = SIZE_MAX; + while (argv_in[argc] != nullptr) { + if (argc >= kMaxArgv) return false; + if (separator == SIZE_MAX && strcmp(argv_in[argc], "--") == 0) separator = argc; + argc++; + } + if (separator == SIZE_MAX || separator + 1 >= argc) return false; + + const char *target = argv_in[separator + 1]; + if (target == nullptr || strstr(target, "dex2oat") == nullptr) return false; + bool is64 = Is64BitExecutable(target, nullptr); + if (WrapperMounted(target, is64)) return false; + if (!WrapperReady(is64)) return false; + + const char *wrapper = is64 ? kWrapper64 : kWrapper32; + size_t out = 0; + for (size_t i = 0; i < argc; i++) { + buffers.argv[out++] = i == separator + 1 ? const_cast(wrapper) : argv_in[i]; + } + buffers.argv[out] = nullptr; + + buffers.path = path; + return BuildEnv(env_in, target, buffers); +} + +bool Transform(const char *path, char *const argv_in[], char *const env_in[], + TransformBuffers &buffers) { + buffers.path = path; + if (IsDex2Oat(path, argv_in)) { + bool is64 = Is64BitExecutable(path, argv_in); + if (WrapperMounted(path, is64)) return false; + if (!WrapperReady(is64)) return false; + return Build(path, argv_in, env_in, is64, buffers); + } + if (IsArtExec(path, argv_in)) return BuildArtExec(path, argv_in, env_in, buffers); + return false; +} + +int HookExecve(const char *path, char *const argv[], char *const envp[]) { + if (g_orig_execve == nullptr) return -1; + if (g_spawn_depth.load(std::memory_order_relaxed) > 0) { + return g_orig_execve(path, argv, envp); + } + if (!IsDex2Oat(path, argv) && !IsArtExec(path, argv)) { + return g_orig_execve(path, argv, envp); + } + TransformBuffers buffers; + if (!Transform(path, argv, envp, buffers)) return g_orig_execve(path, argv, envp); + return g_orig_execve(buffers.path, buffers.argv, buffers.envp); +} + +int HookExecv(const char *path, char *const argv[]) { + if (g_orig_execv == nullptr) return -1; + if (g_spawn_depth.load(std::memory_order_relaxed) > 0) return g_orig_execv(path, argv); + if (!IsDex2Oat(path, argv) && !IsArtExec(path, argv)) return g_orig_execv(path, argv); + TransformBuffers buffers; + if (!Transform(path, argv, nullptr, buffers)) return g_orig_execv(path, argv); + if (g_orig_execve != nullptr) return g_orig_execve(buffers.path, buffers.argv, buffers.envp); + char **saved = environ; + environ = buffers.envp; + int result = g_orig_execv(buffers.path, buffers.argv); + environ = saved; + return result; +} + +int HookPosixSpawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *actions, + const posix_spawnattr_t *attr, char *const argv[], char *const envp[]) { + if (g_orig_posix_spawn == nullptr) return -1; + if (g_spawn_depth.load(std::memory_order_relaxed) > 0) { + return g_orig_posix_spawn(pid, path, actions, attr, argv, envp); + } + if (!IsDex2Oat(path, argv) && !IsArtExec(path, argv)) { + return g_orig_posix_spawn(pid, path, actions, attr, argv, envp); + } + TransformBuffers buffers; + if (!Transform(path, argv, envp, buffers)) { + return g_orig_posix_spawn(pid, path, actions, attr, argv, envp); + } + g_spawn_depth.fetch_add(1, std::memory_order_relaxed); + int result = + g_orig_posix_spawn(pid, buffers.path, actions, attr, buffers.argv, buffers.envp); + g_spawn_depth.fetch_sub(1, std::memory_order_relaxed); + return result; +} + +int HookPosixSpawnp(pid_t *pid, const char *path, const posix_spawn_file_actions_t *actions, + const posix_spawnattr_t *attr, char *const argv[], char *const envp[]) { + if (g_orig_posix_spawnp == nullptr) return -1; + if (g_spawn_depth.load(std::memory_order_relaxed) > 0) { + return g_orig_posix_spawnp(pid, path, actions, attr, argv, envp); + } + if (!IsDex2Oat(path, argv) && !IsArtExec(path, argv)) { + return g_orig_posix_spawnp(pid, path, actions, attr, argv, envp); + } + TransformBuffers buffers; + if (!Transform(path, argv, envp, buffers)) { + return g_orig_posix_spawnp(pid, path, actions, attr, argv, envp); + } + g_spawn_depth.fetch_add(1, std::memory_order_relaxed); + int result = + g_orig_posix_spawnp(pid, buffers.path, actions, attr, buffers.argv, buffers.envp); + g_spawn_depth.fetch_sub(1, std::memory_order_relaxed); + return result; +} + +template +bool InstallAt(void *target, T replacement, T *original) { + if (target == nullptr) return false; + + void *backup = nullptr; + int result = ZN_FAILED; + if (g_api != nullptr && g_api->inlineHook != nullptr) { + result = g_api->inlineHook(target, reinterpret_cast(replacement), &backup); + } + if (result != ZN_SUCCESS) { + if (HookInline(target, reinterpret_cast(replacement), &backup) != 0) return false; + } + *original = reinterpret_cast(backup); + return true; +} + +bool InstallHooks() { + void *execve_target = dlsym(RTLD_DEFAULT, "execve"); + void *execv_target = dlsym(RTLD_DEFAULT, "execv"); + void *spawn_target = dlsym(RTLD_DEFAULT, "posix_spawn"); + void *spawnp_target = dlsym(RTLD_DEFAULT, "posix_spawnp"); + + bool execve = InstallAt(execve_target, HookExecve, &g_orig_execve); + if (execv_target != execve_target) InstallAt(execv_target, HookExecv, &g_orig_execv); + bool spawn = InstallAt(spawn_target, HookPosixSpawn, &g_orig_posix_spawn); + bool spawnp = spawnp_target != spawn_target && + InstallAt(spawnp_target, HookPosixSpawnp, &g_orig_posix_spawnp); + return execve && (spawn || spawnp); +} + +bool ArmHooks() { + if (!InstallHooks()) { + LOGW("dex2oat injection: cannot hook exec"); + return false; + } + LOGI("dex2oat injection: armed in {}", ProcessName().c_str()); + return true; +} + +void ReportStatus(void *self_handle, bool armed) { + if (g_api == nullptr || g_api->connectCompanion == nullptr) return; + + int socket = g_api->connectCompanion(self_handle); + if (socket < 0) { + LOGW("dex2oat injection: the companion is unreachable"); + return; + } + + struct timeval timeout {}; + timeout.tv_sec = 2; + setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + std::string name = SafeName(ProcessName()); + if (name.size() > 255) name.resize(255); + unsigned char header[3] = {static_cast(kRequestMagic), + static_cast(sizeof(void *) == 8 ? 1 : 0), + static_cast(name.size())}; + char status = armed ? 1 : 0; + if (!WriteAll(socket, header, sizeof(header)) || !WriteAll(socket, name.data(), name.size()) || + !WriteAll(socket, &status, 1)) { + LOGW("dex2oat injection: cannot report the status: {}", strerror(errno)); + } + close(socket); +} + +MonitorSlot *ClaimMonitor(const std::string &name) { + pthread_mutex_lock(&g_monitor_lock); + MonitorSlot *slot = nullptr; + for (auto &entry : g_monitors) { + if (entry.claimed.load(std::memory_order_acquire)) { + if (strncmp(entry.name, name.c_str(), sizeof(entry.name) - 1) == 0) { + slot = &entry; + break; + } + continue; + } + slot = &entry; + strncpy(slot->name, name.c_str(), sizeof(slot->name) - 1); + slot->name[sizeof(slot->name) - 1] = '\0'; + slot->status.store(0); + slot->claimed.store(true, std::memory_order_release); + break; + } + pthread_mutex_unlock(&g_monitor_lock); + return slot; +} + +void *ServeMonitor(void *cookie) { + auto *slot = static_cast(cookie); + std::string path = std::string(kMonitorDir) + "/" + kMonitorPrefix + slot->name; + unlink(path.c_str()); + + int listener = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (listener < 0) { + LOGE("dex2oat injection: cannot create {}: {}", path.c_str(), strerror(errno)); + return nullptr; + } + + struct sockaddr_un address {}; + address.sun_family = AF_UNIX; + strncpy(address.sun_path, path.c_str(), sizeof(address.sun_path) - 1); + if (bind(listener, reinterpret_cast(&address), sizeof(address)) != 0 || + listen(listener, 8) != 0) { + LOGE("dex2oat injection: cannot bind {}: {}", path.c_str(), strerror(errno)); + close(listener); + return nullptr; + } + chmod(path.c_str(), 0600); + LOGI("dex2oat injection: reporting status on {}", path.c_str()); + + for (;;) { + int client = accept(listener, nullptr, nullptr); + if (client < 0) { + if (errno == EINTR) continue; + break; + } + char status = slot->status.load(std::memory_order_acquire); + if (write(client, &status, 1) != 1) { + LOGD("dex2oat injection: a monitor client left early"); + } + close(client); + } + close(listener); + return nullptr; +} + +} + +void OnModuleLoaded(void *self_handle, const ZygiskNextAPI *api) { + g_api = api; + if (api == nullptr) return; + if (android_get_device_api_level() < 29) return; + + int ready = g_ready.load(std::memory_order_acquire); + if (ready < 0 && !g_arming.exchange(true)) { + bool armed = ArmHooks(); + g_ready.store(armed ? 1 : 0, std::memory_order_release); + g_arming.store(false); + ready = armed ? 1 : 0; + } + ReportStatus(self_handle, ready == 1); +} + +void OnCompanionLoaded() { + LOGI("dex2oat injection: companion loaded in pid {}", static_cast(getpid())); +} + +void OnModuleConnected(int fd) { + struct timeval timeout {}; + timeout.tv_sec = 5; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + unsigned char header[3] {}; + if (!ReadAll(fd, header, sizeof(header)) || header[0] != kRequestMagic) { + close(fd); + return; + } + + uint8_t name_length = header[2]; + std::string name(name_length, '\0'); + if (name_length > 0 && !ReadAll(fd, name.data(), name_length)) { + close(fd); + return; + } + + char status = 0; + if (!ReadAll(fd, &status, 1)) { + close(fd); + return; + } + close(fd); + + MonitorSlot *slot = ClaimMonitor(SafeName(name)); + if (slot == nullptr) return; + slot->status.store(status == 1 ? 1 : 0, std::memory_order_release); + if (slot->serving.exchange(true)) return; + + mkdir(kMonitorDir, 0700); + pthread_t thread; + if (pthread_create(&thread, nullptr, ServeMonitor, slot) != 0) { + slot->serving.store(false); + LOGE("dex2oat injection: cannot start the monitor thread"); + return; + } + pthread_detach(thread); +} + +} + +extern "C" __attribute__((visibility("default"))) ZygiskNextModule zn_module = { + ZYGISK_NEXT_API_VERSION, vector::native::dex2oat::OnModuleLoaded}; + +extern "C" __attribute__((visibility("default"))) ZygiskNextCompanionModule zn_companion_module = { + ZYGISK_NEXT_API_VERSION, vector::native::dex2oat::OnCompanionLoaded, + vector::native::dex2oat::OnModuleConnected}; diff --git a/zygisk/src/main/cpp/include/zygisk_next_api.h b/zygisk/src/main/cpp/include/zygisk_next_api.h new file mode 100644 index 0000000000..7e04897bef --- /dev/null +++ b/zygisk/src/main/cpp/include/zygisk_next_api.h @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ZYGISK_NEXT_API_VERSION 4 +#define ZYGISK_NEXT_HYOS_API_VERSION 1 + +#define ZN_SUCCESS 0 +#define ZN_FAILED 1 + +struct ZnSymbolResolver; +struct ZygiskNextRuntime; + +struct ZygiskNextAPI { + // Hook API + + // Do plt hook at symbol specified by the param `symbol` of library + // specified by the param `base_addr` The plt address of `symbol` in the + // library will be replaced with hook_handler, and its original value will + // be put to the address specified by `original` (can be null). You can use + // this api to do caller-oriented hook If you want to unhook, please call + // this function with hook_handler = original If hook succeed, returns + // ZN_SUCCESS, otherwise ZN_FAILED + int (*pltHook)(void* base_addr, const char* symbol, void* hook_handler, + void** original); + + // Do inline hook at the address specified by `target`, replace it with a + // new function specified by `addr`, and the param `original` receives the + // address of original function. You can use this api to achieve a global + // hook in current process. In the current implementation , an address can + // only hook once, so the module can't hook an address which is already + // hooked by an another module, except that the module unhooked it. If + // hooking succeed, returns ZN_SUCCESS, otherwise ZN_FAILED + int (*inlineHook)(void* target, void* addr, void** original); + + // Unhook the address which is formerly hooked. + // If hook succeed, returns ZN_SUCCESS, otherwise ZN_FAILED + int (*inlineUnhook)(void* target); + + // Symbol Resolver API + + // Obtain a new ZnSymbolResolver object + // `path` is required, which specifies the path of library to resolve. It + // can be an absolute path or just the file name of library, e.g. + // /system/lib64/libc.so or libc.so . If `base_addr` is non-zero, it will be + // used as the base address of the library. Otherwise, Zygisk Next will try + // to find out the base address of the specified library in this process. If + // succeed, it returns a valid pointer to the symbol resolver, otherwise + // nullptr is returned. + struct ZnSymbolResolver* (*newSymbolResolver)(const char* path, + void* base_addr); + + // Release the ZnSymbolResolver object pointed by `resolver`. + void (*freeSymbolResolver)(struct ZnSymbolResolver* resolver); + + // Retrieve the base address of the library of the resolver image in the + // process. + void* (*getBaseAddress)(struct ZnSymbolResolver* resolver); + + // Lookup the address of symbol by name or prefix (if `prefix` is true) + // If the symbol exists, the function returns its address, otherwise returns + // nullptr. If `size` is not nullptr, the size of the symbol will be put to + // *size . In the current implementation, gnu_debugdata resolution is + // supported. + void* (*symbolLookup)(struct ZnSymbolResolver* resolver, const char* name, + bool prefix, size_t* size); + + // Walk through the symbol table of the library, the callback will receive + // the name, the address, and the size of each symbol. Returning false in + // the callback means stop the walking. + void (*forEachSymbols)(struct ZnSymbolResolver* resolver, + bool (*callback)(const char* name, void* addr, + size_t size, void* data), + void* data); + + // Companion API + + // Create a unix sock stream connection to your declared companion process. + // The value of `handle` is the `self_handle` which you've received from + // onModuleLoaded. On success, it returns the file descriptor refer to the + // socket, otherwise -1 is returned. Please close this file descriptor by + // yourself. + int (*connectCompanion)(void* handle); + + // Return the runtime-specific API for the current process, or null if the + // process does not expose a supported runtime. The returned object remains + // valid for the lifetime of the process. Inspect its type before passing a + // runtime-specific module structure to registerModule. + const struct ZygiskNextRuntime* (*getRuntime)(void); +}; + +// Callbacks of an injected library +struct ZygiskNextModule { + // Please fill this with the target version of your module, e.g. + // ZYGISK_NEXT_API_VERSION + int target_api_version; + + // This callback will be called after all needed library of the main + // executable are loaded, and before the entry (i.e. `main`) of the main + // executable is called. + void (*onModuleLoaded)(void* self_handle, const struct ZygiskNextAPI* api); +}; + +enum ZygiskNextRuntimeType { + ZN_RUNTIME_HYOS = 1, +}; + +// Read-only process specialization information for applications forked by +// /system_ext/bin/hyos_spawner. The callback runs after the uid, gid, +// supplementary groups, and SELinux app context have been applied. The string +// pointers are non-null and only valid for the duration of the callback. +struct ZnHyosAppSpecializeArgs { + const char* process_name; + const char* package_name; + const char* se_info; +}; + +// HyperOS Rust Runtime callbacks registered through +// ZygiskNextRuntime.registerModule. +struct ZygiskNextHyosModule { + int target_api_version; + void (*onAppSpecialized)(const struct ZnHyosAppSpecializeArgs* args); +}; + +// Runtime-specific API returned by getRuntime. The module structure accepted +// by registerModule is selected by type. The runtime copies the structure +// before registerModule returns. +struct ZygiskNextRuntime { + enum ZygiskNextRuntimeType type; + int api_version; + int (*registerModule)(const void* module); +}; + +// Callbacks of a companion library +struct ZygiskNextCompanionModule { + int target_api_version; + + void (*onCompanionLoaded)(); + + // This callback will be called when your Zygisk Next module is trying to + // establish a connection with your companion module, i.e. + // `connectCompanion` is called. The `fd` param will be a unix sock stream + // file descriptor. Please close this file descriptor after use by yourself. + void (*onModuleConnected)(int fd); +}; + +// Please define your `zn_module` in your source file. +extern __attribute__((visibility("default"), + unused)) struct ZygiskNextModule zn_module; +extern __attribute__(( + visibility("default"), + unused)) struct ZygiskNextCompanionModule zn_companion_module; + +#ifdef __cplusplus +} +#endif