From 4af1d1b503518efbdb3a8830a4de6e426aa2a70f Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 22 Jan 2026 15:13:35 +0100 Subject: [PATCH 01/12] Use runtime apex linker to resolve library mismatches Manual library path injection via LD_LIBRARY_PATH has become unreliable due to symbol mismatches in core libraries (e.g., libc++) between the system and APEX partitions. Recent updates to liblog and libbase (in Android 16) have resulted in missing symbols like `__hash_memory` or `fmt` when the ART APEX binaries are forced to load system-partition shims. This commit switches the wrapper to execute the runtime APEX linker directly (/apex/com.android.runtime/bin/linker64). By passing the dex2oat binary to the linker via /proc/self/fd/, the linker can properly initialize internal namespaces and resolve dependencies from the correct APEX and bootstrap locations. Changes: - Replace fexecve(stock_fd) with execve(apex_linker). - Remove manual LD_LIBRARY_PATH construction. - Update sepolicy to allow dex2oat to execute the system linker. --- dex2oat/src/main/cpp/dex2oat.cpp | 49 ++++++++++++++--------- magisk-loader/magisk_module/sepolicy.rule | 1 + 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index a3e4863d3..104f9262d 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -122,27 +122,40 @@ int main(int argc, char **argv) { } LOGD("sock: %s %d", sock.sun_path + 1, stock_fd); - const char *new_argv[argc + 2]; - for (int i = 0; i < argc; i++) new_argv[i] = argv[i]; - new_argv[argc] = "--inline-max-code-units=0"; - new_argv[argc + 1] = NULL; - - if (getenv("LD_LIBRARY_PATH") == NULL) { - char const *libenv = LP_SELECT( - "LD_LIBRARY_PATH=/apex/com.android.art/lib:/apex/com.android.os.statsd/lib", - "LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64"); - putenv((char *)libenv); + int new_argc = argc + 2; // +1 for linker, +1 for --inline... + const char **exec_argv = (const char **)malloc(sizeof(char *) * (new_argc + 1)); + + const char *linker_path = + LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64"); + char stock_fd_path[64]; + snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); + + exec_argv[0] = linker_path; // The "executable" is actually the linker + exec_argv[1] = stock_fd_path; // The first argument to the linker is the binary to run + + // Copy original arguments + for (int i = 1; i < argc; i++) { + exec_argv[i + 1] = argv[i]; } - // Set LD_PRELOAD to load liboat_hook.so - const int STRING_BUFFER = 50; - char env_str[STRING_BUFFER]; - snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd); - putenv(env_str); - LOGD("Set env %s", env_str); + // Add the extra flag + exec_argv[new_argc - 1] = "--inline-max-code-units=0"; + exec_argv[new_argc] = NULL; + + // Clean up LD_LIBRARY_PATH: let the linker use its internal config + unsetenv("LD_LIBRARY_PATH"); + + // Set LD_PRELOAD for the hooker liboat_hook.so + char preload_str[128]; + snprintf(preload_str, sizeof(preload_str), "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd); + putenv(strdup(preload_str)); + + LOGD("Executing via linker: %s %s", linker_path, stock_fd_path); - fexecve(stock_fd, (char **)new_argv, environ); + execve(linker_path, (char **)exec_argv, environ); - PLOGE("fexecve failed"); + // If we reach here, execve failed + PLOGE("execve failed"); + free(exec_argv); return 2; } diff --git a/magisk-loader/magisk_module/sepolicy.rule b/magisk-loader/magisk_module/sepolicy.rule index 1c7f04d07..d0b254e05 100644 --- a/magisk-loader/magisk_module/sepolicy.rule +++ b/magisk-loader/magisk_module/sepolicy.rule @@ -1,4 +1,5 @@ allow dex2oat dex2oat_exec file execute_no_trans +allow dex2oat system_linker_exec file execute_no_trans allow shell shell dir write From e3f82fc1e827f0a516f63b41d15a0e37ea6a41ea Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 23 Jan 2026 00:48:09 +0100 Subject: [PATCH 02/12] Avoid using optimized memmove of libc In Release builds, libc uses SIMD (NEON) instructions. To achieve high throughput, the __memcpy_a53 routine reads memory in 16-byte or 32-byte chunks (using LDRD or VLD1). We also enble debug logs to track potential issues. --- dex2oat/src/main/cpp/include/logging.h | 8 ++------ dex2oat/src/main/cpp/oat_hook.cpp | 10 +++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dex2oat/src/main/cpp/include/logging.h b/dex2oat/src/main/cpp/include/logging.h index 07574fa73..bb2024545 100644 --- a/dex2oat/src/main/cpp/include/logging.h +++ b/dex2oat/src/main/cpp/include/logging.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #ifndef LOG_TAG #define LOG_TAG "LSPosedDex2Oat" @@ -14,7 +14,6 @@ #define LOGW(...) 0 #define LOGE(...) 0 #else -#ifndef NDEBUG #define LOGD(fmt, ...) \ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ "%s:%d#%s" \ @@ -25,10 +24,7 @@ "%s:%d#%s" \ ": " fmt, \ __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) -#else -#define LOGD(...) 0 -#define LOGV(...) 0 -#endif + #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 9913abe09..aab21fb10 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -17,6 +17,14 @@ const std::string_view param_to_remove = " --inline-max-code-units=0"; bool store_resized = false; +static void safe_memmove(uint8_t* dst, const uint8_t* src, size_t n) { + if (dst < src) { + for (size_t i = 0; i < n; i++) dst[i] = src[i]; + } else if (dst > src) { + for (size_t i = n; i > 0; i--) dst[i - 1] = src[i - 1]; + } +} + bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { if (store == nullptr || store_size == 0) { return false; @@ -67,7 +75,7 @@ bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { // memmove is required because the source and destination buffers overlap if (bytes_to_move > 0) { - memmove(destination, source, bytes_to_move); + safe_memmove(destination, source, bytes_to_move); } // 4. Update the total size of the store From 78153db29c23bb083c3485ab7d00fd35c4cecd21 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 23 Jan 2026 04:08:18 +0100 Subject: [PATCH 03/12] Add heuristic deduction of key_value_store size It seems that the PLT return value could be wrong on certain devices for the debug build. --- dex2oat/src/main/cpp/oat_hook.cpp | 68 +++++++++++++++++++------------ 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index aab21fb10..c1f568d2c 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -10,12 +10,13 @@ #include "oat.h" const std::string_view param_to_remove = " --inline-max-code-units=0"; +const std::string_view heuristic_store_boundary = "\0\0\0"; #define DCL_HOOK_FUNC(ret, func, ...) \ ret (*old_##func)(__VA_ARGS__); \ ret new_##func(__VA_ARGS__) -bool store_resized = false; +uint32_t new_store_size = 0; static void safe_memmove(uint8_t* dst, const uint8_t* src, size_t n) { if (dst < src) { @@ -25,14 +26,18 @@ static void safe_memmove(uint8_t* dst, const uint8_t* src, size_t n) { } } -bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { - if (store == nullptr || store_size == 0) { +uint32_t ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { + if (store == nullptr) { return false; } + // Our initial guess of key_value_store size + uint32_t current_store_size = 10 * 1024; + if (store_size != 0) current_store_size = store_size; + // Define the search space uint8_t* const store_begin = store; - uint8_t* const store_end = store + store_size; + uint8_t* store_end = store + current_store_size; // 1. Search for the parameter in the memory buffer auto it = std::search(store_begin, store_end, param_to_remove.begin(), param_to_remove.end()); @@ -40,7 +45,7 @@ bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { // Check if the parameter was found if (it == store_end) { LOGD("Parameter '%.*s' not found.", (int)param_to_remove.size(), param_to_remove.data()); - return false; + return 0; } uint8_t* location_of_param = it; @@ -49,12 +54,8 @@ bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { // 2. Check if there is padding immediately after the string uint8_t* const byte_after_param = location_of_param + param_to_remove.size(); bool has_padding = false; - - // Boundary check: ensure the byte after the parameter is within the buffer - if (byte_after_param + 1 < store_end) { - if (*(byte_after_param + 1) == '\0') { - has_padding = true; - } + if (byte_after_param + 1 < store_end && *(byte_after_param + 1) == '\0') { + has_padding = true; } // 3. Perform the conditional action @@ -62,12 +63,25 @@ bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { // CASE A: Padding exists. Overwrite the parameter with zeros. LOGD("Padding found. Overwriting parameter with zeros."); memset(location_of_param, 0, param_to_remove.size()); - return false; // Size did not change + return 0; // Return 0 to avoid actions based on the return value of this function. } else { // CASE B: No padding exists (or parameter is at the very end). // Remove the parameter by shifting the rest of the memory forward. LOGD("No padding found. Removing parameter and shifting memory."); + // 4. Deduce the key_value_store boundary via heuristic rules + if (store_size == 0) { + it = std::search(byte_after_param, store_end, heuristic_store_boundary.begin(), + heuristic_store_boundary.end()); + if (it == store_end) { + LOGD("Unable to deduce the key_value_store boundary"); + return 0; + } else { + current_store_size = it - store_begin; + store_end = it; + } + } + // Calculate what to move uint8_t* source = byte_after_param; uint8_t* destination = location_of_param; @@ -78,29 +92,33 @@ bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { safe_memmove(destination, source, bytes_to_move); } - // 4. Update the total size of the store - store_size -= param_to_remove.size(); - LOGD("Store size changed. New size: %u", store_size); - - return true; // Size changed + // 5. Update the total size of the store + current_store_size -= param_to_remove.size(); + LOGD("Store size changed. New size: %u", current_store_size); + return current_store_size; } } DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - if (store_resized) { - LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p\n", header); - size = size - param_to_remove.size(); + LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p, returns %u.\n", header, size); + if (new_store_size != 0) { + size = new_store_size; + LOGD("Overwrite the return value with %u.", size); } return size; } DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { - LOGD("OatHeader::GetKeyValueStore() called on object at %p\n", header); + LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); uint8_t* key_value_store_ = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); uint32_t key_value_store_size_ = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - store_resized = ModifyStoreInPlace(key_value_store_, key_value_store_size_); + if (key_value_store_size_ > 16 * 1024 || key_value_store_size_ < 512) { + LOGW("Invalid KeyValueStore size, to be deduced via boundary checking."); + key_value_store_size_ = 0; + } + new_store_size = ModifyStoreInPlace(key_value_store_, key_value_store_size_); return key_value_store_; } @@ -110,10 +128,10 @@ DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32 const uint8_t* key_value_store_ = oat_header->GetKeyValueStore(); uint32_t key_value_store_size_ = oat_header->GetKeyValueStoreSize(); LOGD("KeyValueStore via offset: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - store_resized = + new_store_size = ModifyStoreInPlace(const_cast(key_value_store_), key_value_store_size_); - if (store_resized) { - oat_header->SetKeyValueStoreSize(key_value_store_size_ - param_to_remove.size()); + if (new_store_size != 0) { + oat_header->SetKeyValueStoreSize(new_store_size); } old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); LOGD("ComputeChecksum called: %" PRIu32, *checksum); From ba7664b7ea877bc1cba475a0ab93d814d7ab6fa3 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 23 Jan 2026 17:37:51 +0100 Subject: [PATCH 04/12] Refactor the logic of patching We should also patch the cmdline path. Hence, it is better to work through all the store. --- dex2oat/src/main/cpp/dex2oat.cpp | 177 ++++++++++-------- dex2oat/src/main/cpp/oat_hook.cpp | 287 +++++++++++++++++++----------- 2 files changed, 290 insertions(+), 174 deletions(-) diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index 104f9262d..e775078ea 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -1,161 +1,196 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -// -// Created by Nullptr on 2022/4/1. -// - -#include -#include -#include #include #include #include +#include +#include +#include +#include +#include + #include "logging.h" +// Access to the process environment variables +extern "C" char **environ; + #if defined(__LP64__) #define LP_SELECT(lp32, lp64) lp64 #else #define LP_SELECT(lp32, lp64) lp32 #endif -#define ID_VEC(is64, is_debug) (((is64) << 1) | (is_debug)) +namespace { + +constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac"; -const char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac\0"; +/** + * Calculates a vector ID based on architecture and debug status. + */ +inline int get_id_vec(bool is64, bool is_debug) { + return (static_cast(is64) << 1) | static_cast(is_debug); +} -static ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) { - int rec = recvmsg(sockfd, msg, flags); +/** + * 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"); } return rec; } -static void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { +/** + * 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, .iov_len = sizeof(cnt), }; - struct msghdr msg = { - .msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz}; + struct msghdr msg = {.msg_name = nullptr, + .msg_namelen = 0, + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cmsgbuf, + .msg_controllen = bufsz, + .msg_flags = 0}; + + if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr; - xrecvmsg(sockfd, &msg, MSG_WAITALL); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); - if (msg.msg_controllen != bufsz || cmsg == NULL || + 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 NULL; + return nullptr; } return CMSG_DATA(cmsg); } -static int recv_fd(int sockfd) { +/** + * 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 == NULL) return -1; + if (data == nullptr) return -1; int result; - memcpy(&result, data, sizeof(int)); + std::memcpy(&result, data, sizeof(int)); return result; } -static int read_int(int fd) { +/** + * 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; } -static void write_int(int fd, int val) { +/** + * Writes an integer command/ID to the socket. + */ +void write_int(int fd, int val) { if (fd < 0) return; - write(fd, &val, sizeof(val)); + (void)write(fd, &val, sizeof(val)); } +} // namespace + int main(int argc, char **argv) { LOGD("dex2oat wrapper ppid=%d", getppid()); + + // Prepare Unix domain socket address (Abstract Namespace) struct sockaddr_un sock = {}; sock.sun_family = AF_UNIX; - strlcpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 1); + // 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); + // 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); - size_t len = sizeof(sa_family_t) + strlen(sock.sun_path + 1) + 1; - if (connect(sock_fd, (struct sockaddr *)&sock, len)) { + if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } - write_int(sock_fd, ID_VEC(LP_SELECT(0, 1), strstr(argv[0], "dex2oatd") != NULL)); + + 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 stock_fd = recv_fd(sock_fd); - read_int(sock_fd); + read_int(sock_fd); // Sync close(sock_fd); + // 2. Get liboat_hook.so FD sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (connect(sock_fd, (struct sockaddr *)&sock, len)) { + if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } + write_int(sock_fd, LP_SELECT(4, 5)); int hooker_fd = recv_fd(sock_fd); - read_int(sock_fd); + read_int(sock_fd); // Sync close(sock_fd); if (hooker_fd == -1) { - PLOGE("failed to read liboat_hook.so"); + LOGE("failed to read liboat_hook.so"); } - LOGD("sock: %s %d", sock.sun_path + 1, stock_fd); + LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd); - int new_argc = argc + 2; // +1 for linker, +1 for --inline... - const char **exec_argv = (const char **)malloc(sizeof(char *) * (new_argc + 1)); + // 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"); + char stock_fd_path[64]; - snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); + std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); - exec_argv[0] = linker_path; // The "executable" is actually the linker - exec_argv[1] = stock_fd_path; // The first argument to the linker is the binary to run + exec_argv.push_back(linker_path); + exec_argv.push_back(stock_fd_path); - // Copy original arguments - for (int i = 1; i < argc; i++) { - exec_argv[i + 1] = argv[i]; + // Append original arguments starting from argv[1] + for (int i = 1; i < argc; ++i) { + exec_argv.push_back(argv[i]); } - // Add the extra flag - exec_argv[new_argc - 1] = "--inline-max-code-units=0"; - exec_argv[new_argc] = NULL; + // Append performance/hooking flags + exec_argv.push_back("--inline-max-code-units=0"); + exec_argv.push_back(nullptr); - // Clean up LD_LIBRARY_PATH: let the linker use its internal config + // Setup Environment variables + // Clear LD_LIBRARY_PATH to let the linker use internal config unsetenv("LD_LIBRARY_PATH"); - // Set LD_PRELOAD for the hooker liboat_hook.so - char preload_str[128]; - snprintf(preload_str, sizeof(preload_str), "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd); - putenv(strdup(preload_str)); + // Set LD_PRELOAD to point to the hooker library FD + std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); + // Note: putenv requires a pointer to a string that lives as long as the environment + // We use setenv which manages its own memory. + 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]); + } - LOGD("Executing via linker: %s %s", linker_path, stock_fd_path); + LOGD("Executing via linker: %s executing %s", linker_path, stock_fd_path); - execve(linker_path, (char **)exec_argv, environ); + // Perform the execution + execve(linker_path, const_cast(exec_argv.data()), environ); // If we reach here, execve failed PLOGE("execve failed"); - free(exec_argv); return 2; } diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index c1f568d2c..813953616 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -1,148 +1,210 @@ #include -#include +#include #include +#include #include +#include #include #include +#include #include "logging.h" #include "oat.h" -const std::string_view param_to_remove = " --inline-max-code-units=0"; -const std::string_view heuristic_store_boundary = "\0\0\0"; - -#define DCL_HOOK_FUNC(ret, func, ...) \ - ret (*old_##func)(__VA_ARGS__); \ - ret new_##func(__VA_ARGS__) - -uint32_t new_store_size = 0; - -static void safe_memmove(uint8_t* dst, const uint8_t* src, size_t n) { - if (dst < src) { - for (size_t i = 0; i < n; i++) dst[i] = src[i]; - } else if (dst > src) { - for (size_t i = n; i > 0; i--) dst[i - 1] = src[i - 1]; +/** + * 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 linker path 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; // 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)); -uint32_t ModifyStoreInPlace(uint8_t* store, uint32_t store_size) { - if (store == nullptr) { - return false; + // 1. Replace the command path (argv[0]) + if (!tokens.empty()) { + tokens[0] = std::string(new_cmd_path); } - // Our initial guess of key_value_store size - uint32_t current_store_size = 10 * 1024; - if (store_size != 0) current_store_size = store_size; - - // Define the search space - uint8_t* const store_begin = store; - uint8_t* store_end = store + current_store_size; - - // 1. Search for the parameter in the memory buffer - auto it = std::search(store_begin, store_end, param_to_remove.begin(), param_to_remove.end()); + // 2. Remove the injected parameter if it exists + auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove)); + tokens.erase(it, tokens.end()); - // Check if the parameter was found - if (it == store_end) { - LOGD("Parameter '%.*s' not found.", (int)param_to_remove.size(), param_to_remove.data()); - return 0; + // 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; +} - uint8_t* location_of_param = it; - LOGD("Parameter found at offset %td.", location_of_param - store_begin); - - // 2. Check if there is padding immediately after the string - uint8_t* const byte_after_param = location_of_param + param_to_remove.size(); - bool has_padding = false; - if (byte_after_param + 1 < store_end && *(byte_after_param + 1) == '\0') { - has_padding = true; +/** + * Re-serializes the Key-Value map back into the OAT header memory space. + */ +void 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; } +} + +// 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; }); +} - // 3. Perform the conditional action - if (has_padding) { - // CASE A: Padding exists. Overwrite the parameter with zeros. - LOGD("Padding found. Overwriting parameter with zeros."); - memset(location_of_param, 0, param_to_remove.size()); - return 0; // Return 0 to avoid actions based on the return value of this function. - } else { - // CASE B: No padding exists (or parameter is at the very end). - // Remove the parameter by shifting the rest of the memory forward. - LOGD("No padding found. Removing parameter and shifting memory."); - - // 4. Deduce the key_value_store boundary via heuristic rules - if (store_size == 0) { - it = std::search(byte_after_param, store_end, heuristic_store_boundary.begin(), - heuristic_store_boundary.end()); - if (it == store_end) { - LOGD("Unable to deduce the key_value_store boundary"); - return 0; - } else { - current_store_size = it - store_begin; - store_end = it; +/** + * 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* key_value_store_, uint32_t size_limit) { + if (!key_value_store_) return false; + + const char* ptr = reinterpret_cast(key_value_store_); + const char* const end = ptr + size_limit; + std::map new_store; + bool modified = false; + LOGD("Parsing KeyValueStore [%p - %p]", ptr, end); + + while (ptr < end && *ptr != '\0') { + // Find key + const char* key_end = reinterpret_cast(std::memchr(ptr, 0, 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 >= end) break; + const char* value_end = + reinterpret_cast(std::memchr(value_start, 0, end - value_start)); + if (!value_end) break; + std::string_view value(value_start, value_end - value_start); + + const bool has_padding = + value_end + 1 < end && *(value_end + 1) == '\0' && IsNonDeterministic(key); + + if (key == "dex2oat-cmdline" && 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) { + LOGD("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; } - } - // Calculate what to move - uint8_t* source = byte_after_param; - uint8_t* destination = location_of_param; - size_t bytes_to_move = store_end - source; + // Standard logic: store in map and rebuild later + new_store[std::string(key)] = std::move(cleaned_cmd); + modified = true; + } else { + new_store[std::string(key)] = std::string(value); + LOGD("Parsed item:\t[%s:%s]", key.data(), value.data()); + } - // memmove is required because the source and destination buffers overlap - if (bytes_to_move > 0) { - safe_memmove(destination, source, bytes_to_move); + ptr = value_end + 1; + if (has_padding) { + while (*ptr == '\0') { + ptr++; + } } + } - // 5. Update the total size of the store - current_store_size -= param_to_remove.size(); - LOGD("Store size changed. New size: %u", current_store_size); - return current_store_size; + if (modified) { + WriteKeyValueStore(new_store, key_value_store_); + return true; } + return false; } +#define DCL_HOOK_FUNC(ret, func, ...) \ + ret (*old_##func)(__VA_ARGS__) = nullptr; \ + ret new_##func(__VA_ARGS__) + DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { - uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); + auto size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p, returns %u.\n", header, size); - if (new_store_size != 0) { - size = new_store_size; - LOGD("Overwrite the return value with %u.", size); - } return size; } +// For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { - LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); - uint8_t* key_value_store_ = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - uint32_t key_value_store_size_ = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - if (key_value_store_size_ > 16 * 1024 || key_value_store_size_ < 512) { - LOGW("Invalid KeyValueStore size, to be deduced via boundary checking."); - key_value_store_size_ = 0; - } - new_store_size = ModifyStoreInPlace(key_value_store_, key_value_store_size_); + uint8_t* key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); + uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); + LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, size); - return key_value_store_; + // Bounds check to avoid memory corruption on invalid headers + if (size > 0 && size < 64 * 1024) { + SpoofKeyValueStore(key_value_store, size); + } + return key_value_store; } +// For Android 16+ / Modern ART: Intercept during checksum calculation DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { - art::OatHeader* oat_header = reinterpret_cast(header); - const uint8_t* key_value_store_ = oat_header->GetKeyValueStore(); - uint32_t key_value_store_size_ = oat_header->GetKeyValueStoreSize(); - LOGD("KeyValueStore via offset: [addr: %p, size: %u]", key_value_store_, key_value_store_size_); - new_store_size = - ModifyStoreInPlace(const_cast(key_value_store_), key_value_store_size_); - if (new_store_size != 0) { - oat_header->SetKeyValueStoreSize(new_store_size); - } + auto* oat_header = reinterpret_cast(header); + LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); + + uint8_t* store = const_cast(oat_header->GetKeyValueStore()); + uint32_t size = oat_header->GetKeyValueStoreSize(); + LOGD("KeyValueStore via offset: [addr: %p, size: %u]", store, size); + + SpoofKeyValueStore(store, size); + + // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); - LOGD("ComputeChecksum called: %" PRIu32, *checksum); + LOGD("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) { - LOGD("RegisterHook: %s, %p, %p", symbol, new_func, old_func); if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) { - LOGE("Failed to register plt_hook \"%s\"\n", symbol); + LOGE("Failed to register PLT hook: %s", symbol); } } @@ -153,18 +215,37 @@ void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, v #define PLT_HOOK_REGISTER(DEV, INODE, NAME) PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) __attribute__((constructor)) static void initialize() { + // 1. Determine the target binary name + const char* env_cmd = getenv("DEX2OAT_CMD"); + if (env_cmd) { + g_binary_path = env_cmd; + } + dev_t dev = 0; ino_t inode = 0; - for (auto& info : lsplt::MapInfo::Scan()) { - if (info.path.starts_with("/apex/com.android.art/bin/dex2oat")) { + + // 2. 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; + } + + // 3. Register hooks for various ART versions PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv); PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); + + // If the standard store hook fails or we are on newer Android, try the Checksum hook if (!lsplt::CommitHook()) { PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); lsplt::CommitHook(); From 17007cfbf5cb6b50a7b83642565623e3c5e05eb4 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 23 Jan 2026 18:12:19 +0100 Subject: [PATCH 05/12] Adjust logging --- dex2oat/build.gradle.kts | 36 ++------------------------ dex2oat/src/main/cpp/dex2oat.cpp | 5 ++-- dex2oat/src/main/cpp/include/logging.h | 6 ++++- dex2oat/src/main/cpp/oat_hook.cpp | 15 ++++++----- 4 files changed, 17 insertions(+), 45 deletions(-) diff --git a/dex2oat/build.gradle.kts b/dex2oat/build.gradle.kts index 2390988a9..fd155f31c 100644 --- a/dex2oat/build.gradle.kts +++ b/dex2oat/build.gradle.kts @@ -1,47 +1,15 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - plugins { alias(libs.plugins.agp.lib) } android { - namespace = "org.lsposed.dex2oat" - - buildFeatures { - androidResources = false - buildConfig = false - prefab = true - prefabPublishing = true - } + namespace = "org.matrix.vector.dex2oat" - defaultConfig { - minSdk = 29 - } + androidResources.enable = false externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") } } - - prefab { - register("dex2oat") - } } diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index e775078ea..673dd39de 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -165,7 +165,7 @@ int main(int argc, char **argv) { exec_argv.push_back(argv[i]); } - // Append performance/hooking flags + // Append hooking flags to disable inline exec_argv.push_back("--inline-max-code-units=0"); exec_argv.push_back(nullptr); @@ -175,7 +175,6 @@ int main(int argc, char **argv) { // Set LD_PRELOAD to point to the hooker library FD std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); - // Note: putenv requires a pointer to a string that lives as long as the environment // We use setenv which manages its own memory. setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1); @@ -185,7 +184,7 @@ int main(int argc, char **argv) { LOGD("DEX2OAT_CMD set to %s", argv[0]); } - LOGD("Executing via linker: %s executing %s", linker_path, stock_fd_path); + LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path); // Perform the execution execve(linker_path, const_cast(exec_argv.data()), environ); diff --git a/dex2oat/src/main/cpp/include/logging.h b/dex2oat/src/main/cpp/include/logging.h index bb2024545..cc32431b0 100644 --- a/dex2oat/src/main/cpp/include/logging.h +++ b/dex2oat/src/main/cpp/include/logging.h @@ -14,6 +14,7 @@ #define LOGW(...) 0 #define LOGE(...) 0 #else +#ifndef NDEBUG #define LOGD(fmt, ...) \ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ "%s:%d#%s" \ @@ -24,7 +25,10 @@ "%s:%d#%s" \ ": " fmt, \ __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) - +#else +#define LOGD(...) 0 +#define LOGV(...) 0 +#endif #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 813953616..1af00e0c7 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -119,14 +119,15 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { const bool has_padding = value_end + 1 < end && *(value_end + 1) == '\0' && IsNonDeterministic(key); - if (key == "dex2oat-cmdline" && value.find(kParamToRemove) != std::string_view::npos) { + 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) { - LOGD("In-place spoofing dex2oat-cmdline (padding detected)"); + LOGI("In-place spoofing dex2oat-cmdline (padding detected)"); // Zero out the entire original value range to be safe size_t original_capacity = value.length(); @@ -143,7 +144,7 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { modified = true; } else { new_store[std::string(key)] = std::string(value); - LOGD("Parsed item:\t[%s:%s]", key.data(), value.data()); + LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); } ptr = value_end + 1; @@ -175,7 +176,7 @@ DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { uint8_t* key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, size); + LOGI("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, size); // Bounds check to avoid memory corruption on invalid headers if (size > 0 && size < 64 * 1024) { @@ -191,13 +192,13 @@ DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32 uint8_t* store = const_cast(oat_header->GetKeyValueStore()); uint32_t size = oat_header->GetKeyValueStoreSize(); - LOGD("KeyValueStore via offset: [addr: %p, size: %u]", store, size); + LOGI("KeyValueStore via offset: [addr: %p, size: %u]", store, size); SpoofKeyValueStore(store, size); // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); - LOGD("OAT Checksum recalculated: 0x%08X", *checksum); + LOGV("OAT Checksum recalculated: 0x%08X", *checksum); } #undef DCL_HOOK_FUNC @@ -230,7 +231,7 @@ __attribute__((constructor)) static void initialize() { 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, + LOGV("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, (uintmax_t)inode); break; } From c5a2b5763fafe8c7d047dd4276ef19a3a79982cc Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Jan 2026 13:34:36 +0100 Subject: [PATCH 06/12] Avoid overwriting target functions The previous helper functions indeed overwrite the target functions in the device binary, thus confuses the PLT hook. --- dex2oat/src/main/cpp/include/oat.h | 6 +++--- dex2oat/src/main/cpp/oat.cpp | 7 ++++--- dex2oat/src/main/cpp/oat_hook.cpp | 19 ++++++++----------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h index 375e90299..b816a1886 100644 --- a/dex2oat/src/main/cpp/include/oat.h +++ b/dex2oat/src/main/cpp/include/oat.h @@ -79,10 +79,10 @@ class EXPORT PACKED(4) OatHeader { return offsetof(OatHeader, key_value_store_); } - uint32_t GetKeyValueStoreSize() const; - const uint8_t* GetKeyValueStore() const; + uint32_t getKeyValueStoreSize() const; + const uint8_t* getKeyValueStore() const; - void SetKeyValueStoreSize(uint32_t new_size); + void setKeyValueStoreSize(uint32_t new_size); void ComputeChecksum(/*inout*/ uint32_t* checksum) const; diff --git a/dex2oat/src/main/cpp/oat.cpp b/dex2oat/src/main/cpp/oat.cpp index c50bb65fd..fa966c966 100644 --- a/dex2oat/src/main/cpp/oat.cpp +++ b/dex2oat/src/main/cpp/oat.cpp @@ -2,15 +2,16 @@ namespace art { -uint32_t OatHeader::GetKeyValueStoreSize() const { +// Helpers based on the header file, renamed intentionally to avoid confusing PLT hook targets. +uint32_t OatHeader::getKeyValueStoreSize() const { return *(uint32_t*)((uintptr_t)this + OatHeader::Get_key_value_store_size_Offset()); } -const uint8_t* OatHeader::GetKeyValueStore() const { +const uint8_t* OatHeader::getKeyValueStore() const { return (const uint8_t*)((uintptr_t)this + OatHeader::Get_key_value_store_Offset()); } -void OatHeader::SetKeyValueStoreSize(uint32_t new_size) { +void OatHeader::setKeyValueStoreSize(uint32_t new_size) { *reinterpret_cast((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size; } diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 1af00e0c7..abeac5e78 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -22,6 +22,7 @@ namespace { const std::string_view kParamToRemove = "--inline-max-code-units=0"; std::string g_binary_path; // The original binary path +int g_size_change = 0; } // namespace /** @@ -99,7 +100,6 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { const char* ptr = reinterpret_cast(key_value_store_); const char* const end = ptr + size_limit; std::map new_store; - bool modified = false; LOGD("Parsing KeyValueStore [%p - %p]", ptr, end); while (ptr < end && *ptr != '\0') { @@ -141,7 +141,7 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { // Standard logic: store in map and rebuild later new_store[std::string(key)] = std::move(cleaned_cmd); - modified = true; + g_size_change = cleaned_cmd.length() - value.length(); } else { new_store[std::string(key)] = std::string(value); LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); @@ -155,7 +155,7 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { } } - if (modified) { + if (g_size_change != 0) { WriteKeyValueStore(new_store, key_value_store_); return true; } @@ -169,7 +169,7 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { auto size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p, returns %u.\n", header, size); - return size; + return size + g_size_change; } // For Android version < 16 @@ -177,11 +177,7 @@ DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { uint8_t* key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); LOGI("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, size); - - // Bounds check to avoid memory corruption on invalid headers - if (size > 0 && size < 64 * 1024) { - SpoofKeyValueStore(key_value_store, size); - } + SpoofKeyValueStore(key_value_store, size); return key_value_store; } @@ -190,11 +186,12 @@ DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32 auto* oat_header = reinterpret_cast(header); LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); - uint8_t* store = const_cast(oat_header->GetKeyValueStore()); - uint32_t size = oat_header->GetKeyValueStoreSize(); + uint8_t* store = const_cast(oat_header->getKeyValueStore()); + uint32_t size = oat_header->getKeyValueStoreSize(); LOGI("KeyValueStore via offset: [addr: %p, size: %u]", store, size); SpoofKeyValueStore(store, size); + if (g_size_change != 0) oat_header->setKeyValueStoreSize(size + g_size_change); // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); From d5e3f47a60b56198b02682dcb58752d3bce521ba Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Jan 2026 15:10:45 +0100 Subject: [PATCH 07/12] Zero out bytes after shrinking KeyValueStore --- dex2oat/src/main/cpp/CMakeLists.txt | 2 +- dex2oat/src/main/cpp/include/oat.h | 16 +++++----------- dex2oat/src/main/cpp/oat.cpp | 18 ------------------ dex2oat/src/main/cpp/oat_hook.cpp | 28 ++++++++++++++++++---------- 4 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 dex2oat/src/main/cpp/oat.cpp diff --git a/dex2oat/src/main/cpp/CMakeLists.txt b/dex2oat/src/main/cpp/CMakeLists.txt index b181e3ffb..c4f5ba333 100644 --- a/dex2oat/src/main/cpp/CMakeLists.txt +++ b/dex2oat/src/main/cpp/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(dex2oat) add_executable(dex2oat dex2oat.cpp) -add_library(oat_hook SHARED oat_hook.cpp oat.cpp) +add_library(oat_hook SHARED oat_hook.cpp) OPTION(LSPLT_BUILD_SHARED OFF) add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external) diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h index b816a1886..4f05610e6 100644 --- a/dex2oat/src/main/cpp/include/oat.h +++ b/dex2oat/src/main/cpp/include/oat.h @@ -72,17 +72,11 @@ class EXPORT PACKED(4) OatHeader { static constexpr const char kTrueValue[] = "true"; static constexpr const char kFalseValue[] = "false"; - static constexpr size_t Get_key_value_store_size_Offset() { - return offsetof(OatHeader, key_value_store_size_); - } - static constexpr size_t Get_key_value_store_Offset() { - return offsetof(OatHeader, key_value_store_); - } - - uint32_t getKeyValueStoreSize() const; - const uint8_t* getKeyValueStore() const; - - void setKeyValueStoreSize(uint32_t new_size); + // Added helpers to access fields. + // NOTE: They are fragile due to different Android versions and compiler optimizations. + uint32_t getKeyValueStoreSize() const { return key_value_store_size_; } + const uint8_t* getKeyValueStore() const { return key_value_store_; } + void setKeyValueStoreSize(uint32_t new_size) { key_value_store_size_ = new_size; } void ComputeChecksum(/*inout*/ uint32_t* checksum) const; diff --git a/dex2oat/src/main/cpp/oat.cpp b/dex2oat/src/main/cpp/oat.cpp deleted file mode 100644 index fa966c966..000000000 --- a/dex2oat/src/main/cpp/oat.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "oat.h" - -namespace art { - -// Helpers based on the header file, renamed intentionally to avoid confusing PLT hook targets. -uint32_t OatHeader::getKeyValueStoreSize() const { - return *(uint32_t*)((uintptr_t)this + OatHeader::Get_key_value_store_size_Offset()); -} - -const uint8_t* OatHeader::getKeyValueStore() const { - return (const uint8_t*)((uintptr_t)this + OatHeader::Get_key_value_store_Offset()); -} - -void OatHeader::setKeyValueStoreSize(uint32_t new_size) { - *reinterpret_cast((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size; -} - -} // namespace art diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index abeac5e78..3bf8e4fe9 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -80,6 +80,12 @@ void WriteKeyValueStore(const std::map& key_values, ui std::memcpy(data_ptr, value.c_str(), value.length() + 1); data_ptr += value.length() + 1; } + LOGI("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); + + // If the store shrunk, zero out the space between the NEW end and the OLD end + if (g_size_change < 0) { + std::memset(data_ptr, 0, -g_size_change); + } } // Helper function to test if a header field could have variable length @@ -94,11 +100,11 @@ bool IsNonDeterministic(const std::string_view& key) { * * @return true if the store was modified in-place or successfully rebuilt. */ -bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { - if (!key_value_store_) return false; +bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { + if (!store) return false; - const char* ptr = reinterpret_cast(key_value_store_); - const char* const end = ptr + size_limit; + const char* ptr = reinterpret_cast(store); + const char* const end = ptr + store_size; std::map new_store; LOGD("Parsing KeyValueStore [%p - %p]", ptr, end); @@ -156,7 +162,7 @@ bool SpoofKeyValueStore(uint8_t* key_value_store_, uint32_t size_limit) { } if (g_size_change != 0) { - WriteKeyValueStore(new_store, key_value_store_); + WriteKeyValueStore(new_store, store); return true; } return false; @@ -175,20 +181,22 @@ DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header // For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { uint8_t* key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGI("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, size); - SpoofKeyValueStore(key_value_store, size); + uint32_t key_value_store_size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); + LOGI("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, key_value_store_size); + + SpoofKeyValueStore(key_value_store, key_value_store_size); + return key_value_store; } -// For Android 16+ / Modern ART: Intercept during checksum calculation +// For Android version 16+ : Intercept during checksum calculation DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { auto* oat_header = reinterpret_cast(header); LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); uint8_t* store = const_cast(oat_header->getKeyValueStore()); uint32_t size = oat_header->getKeyValueStoreSize(); - LOGI("KeyValueStore via offset: [addr: %p, size: %u]", store, size); + LOGI("KeyValueStore via header file: [addr: %p, size: %u]", store, size); SpoofKeyValueStore(store, size); if (g_size_change != 0) oat_header->setKeyValueStoreSize(size + g_size_change); From 97e2f556b85d261106f1c786aa57d2fa18fc3380 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Jan 2026 16:17:02 +0100 Subject: [PATCH 08/12] Remove log for debug build Otherwise, it crahses, that is strange --- dex2oat/src/main/cpp/oat_hook.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 3bf8e4fe9..4d5d7d713 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -174,7 +174,6 @@ bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { auto size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p, returns %u.\n", header, size); return size + g_size_change; } From 99c29f0030ddb5071a44a1ce7373932bdc745cc6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Jan 2026 17:17:24 +0100 Subject: [PATCH 09/12] Write size to memory directly The reinterpret_cast is important. --- dex2oat/src/main/cpp/include/oat.h | 6 +-- dex2oat/src/main/cpp/oat_hook.cpp | 64 +++++++++++++----------------- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h index 4f05610e6..869dab30b 100644 --- a/dex2oat/src/main/cpp/include/oat.h +++ b/dex2oat/src/main/cpp/include/oat.h @@ -72,11 +72,9 @@ class EXPORT PACKED(4) OatHeader { static constexpr const char kTrueValue[] = "true"; static constexpr const char kFalseValue[] = "false"; - // Added helpers to access fields. - // NOTE: They are fragile due to different Android versions and compiler optimizations. - uint32_t getKeyValueStoreSize() const { return key_value_store_size_; } + // Added helper to access fields. + // NOTE: It is fragile due to different Android versions and compiler optimizations. const uint8_t* getKeyValueStore() const { return key_value_store_; } - void setKeyValueStoreSize(uint32_t new_size) { key_value_store_size_ = new_size; } void ComputeChecksum(/*inout*/ uint32_t* checksum) const; diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 4d5d7d713..0c2b44054 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -22,7 +22,6 @@ namespace { const std::string_view kParamToRemove = "--inline-max-code-units=0"; std::string g_binary_path; // The original binary path -int g_size_change = 0; } // namespace /** @@ -68,7 +67,7 @@ std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) { /** * Re-serializes the Key-Value map back into the OAT header memory space. */ -void WriteKeyValueStore(const std::map& key_values, uint8_t* store) { +uint8_t* WriteKeyValueStore(const std::map& key_values, uint8_t* store) { LOGD("Writing KeyValueStore back to memory"); char* data_ptr = reinterpret_cast(store); @@ -82,10 +81,7 @@ void WriteKeyValueStore(const std::map& key_values, ui } LOGI("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); - // If the store shrunk, zero out the space between the NEW end and the OLD end - if (g_size_change < 0) { - std::memset(data_ptr, 0, -g_size_change); - } + return reinterpret_cast(data_ptr); } // Helper function to test if a header field could have variable length @@ -100,30 +96,35 @@ bool IsNonDeterministic(const std::string_view& key) { * * @return true if the store was modified in-place or successfully rebuilt. */ -bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { +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 end = ptr + store_size; - std::map new_store; - LOGD("Parsing KeyValueStore [%p - %p]", ptr, end); + 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); + + int size_change = 0; - while (ptr < end && *ptr != '\0') { + while (ptr < store_end && *ptr != '\0') { // Find key - const char* key_end = reinterpret_cast(std::memchr(ptr, 0, end - ptr)); + 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 >= end) break; + if (value_start >= store_end) break; const char* value_end = - reinterpret_cast(std::memchr(value_start, 0, end - value_start)); + 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 < end && *(value_end + 1) == '\0' && IsNonDeterministic(key); + value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key); if (key == art::OatHeader::kDex2OatCmdLineKey && value.find(kParamToRemove) != std::string_view::npos) { @@ -146,10 +147,10 @@ bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { } // Standard logic: store in map and rebuild later - new_store[std::string(key)] = std::move(cleaned_cmd); - g_size_change = cleaned_cmd.length() - value.length(); + new_store_map[std::string(key)] = std::move(cleaned_cmd); + size_change = cleaned_cmd.length() - value.length(); } else { - new_store[std::string(key)] = std::string(value); + new_store_map[std::string(key)] = std::string(value); LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); } @@ -161,8 +162,10 @@ bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { } } - if (g_size_change != 0) { - WriteKeyValueStore(new_store, store); + if (size_change != 0) { + 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; @@ -172,18 +175,11 @@ bool SpoofKeyValueStore(uint8_t* store, uint32_t store_size) { ret (*old_##func)(__VA_ARGS__) = nullptr; \ ret new_##func(__VA_ARGS__) -DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) { - auto size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - return size + g_size_change; -} - // For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { - uint8_t* key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - uint32_t key_value_store_size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header); - LOGI("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store, key_value_store_size); + uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); - SpoofKeyValueStore(key_value_store, key_value_store_size); + SpoofKeyValueStore(key_value_store); return key_value_store; } @@ -191,14 +187,9 @@ DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { // For Android version 16+ : Intercept during checksum calculation DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { auto* oat_header = reinterpret_cast(header); - LOGD("OatHeader::GetKeyValueStore() called on object at %p.", header); - - uint8_t* store = const_cast(oat_header->getKeyValueStore()); - uint32_t size = oat_header->getKeyValueStoreSize(); - LOGI("KeyValueStore via header file: [addr: %p, size: %u]", store, size); + uint8_t* const store = const_cast(oat_header->getKeyValueStore()); - SpoofKeyValueStore(store, size); - if (g_size_change != 0) oat_header->setKeyValueStoreSize(size + g_size_change); + SpoofKeyValueStore(store); // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); @@ -247,7 +238,6 @@ __attribute__((constructor)) static void initialize() { } // 3. Register hooks for various ART versions - PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv); PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); // If the standard store hook fails or we are on newer Android, try the Checksum hook From 17a14df42a7bc554d6d49b7788a062cad7b530e3 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 24 Jan 2026 19:30:59 +0100 Subject: [PATCH 10/12] No need to store size_change --- dex2oat/src/main/cpp/oat_hook.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index 0c2b44054..aec35537e 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -107,7 +107,7 @@ bool SpoofKeyValueStore(uint8_t* store) { std::map new_store_map; LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size); - int size_change = 0; + bool store_modified = false; while (ptr < store_end && *ptr != '\0') { // Find key @@ -148,7 +148,7 @@ bool SpoofKeyValueStore(uint8_t* store) { // Standard logic: store in map and rebuild later new_store_map[std::string(key)] = std::move(cleaned_cmd); - size_change = cleaned_cmd.length() - value.length(); + store_modified = true; } else { new_store_map[std::string(key)] = std::string(value); LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); @@ -162,7 +162,7 @@ bool SpoofKeyValueStore(uint8_t* store) { } } - if (size_change != 0) { + 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); @@ -240,7 +240,7 @@ __attribute__((constructor)) static void initialize() { // 3. Register hooks for various ART versions PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); - // If the standard store hook fails or we are on newer Android, try the Checksum hook + // If the standard store hook fails (on newer Android), try the Checksum hook if (!lsplt::CommitHook()) { PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); lsplt::CommitHook(); From f4f8c77c923319a2b4e6db18540d59307ccc79bf Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 09:28:31 +0100 Subject: [PATCH 11/12] Adjust logging and comments --- daemon/src/main/jni/logcat.cpp | 3 ++- dex2oat/src/main/cpp/dex2oat.cpp | 4 ++-- dex2oat/src/main/cpp/include/logging.h | 8 ++------ dex2oat/src/main/cpp/include/oat.h | 4 ++-- dex2oat/src/main/cpp/oat_hook.cpp | 26 ++++++++++---------------- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/daemon/src/main/jni/logcat.cpp b/daemon/src/main/jni/logcat.cpp index 33d796c8b..2e5897a39 100644 --- a/daemon/src/main/jni/logcat.cpp +++ b/daemon/src/main/jni/logcat.cpp @@ -229,7 +229,8 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv || - tag == "TEESimulator"sv || tag.starts_with("zygisk"sv))) [[unlikely]] { + tag == "TEESimulator"sv || tag.starts_with("Vector"sv) || + tag.starts_with("zygisk"sv))) [[unlikely]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { diff --git a/dex2oat/src/main/cpp/dex2oat.cpp b/dex2oat/src/main/cpp/dex2oat.cpp index 673dd39de..bf441c320 100644 --- a/dex2oat/src/main/cpp/dex2oat.cpp +++ b/dex2oat/src/main/cpp/dex2oat.cpp @@ -165,7 +165,8 @@ int main(int argc, char **argv) { exec_argv.push_back(argv[i]); } - // Append hooking flags to disable inline + // 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); @@ -175,7 +176,6 @@ int main(int argc, char **argv) { // Set LD_PRELOAD to point to the hooker library FD std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); - // We use setenv which manages its own memory. setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1); // Pass original argv[0] as DEX2OAT_CMD diff --git a/dex2oat/src/main/cpp/include/logging.h b/dex2oat/src/main/cpp/include/logging.h index cc32431b0..ab66ef34c 100644 --- a/dex2oat/src/main/cpp/include/logging.h +++ b/dex2oat/src/main/cpp/include/logging.h @@ -4,7 +4,7 @@ #include #ifndef LOG_TAG -#define LOG_TAG "LSPosedDex2Oat" +#define LOG_TAG "VectorDex2Oat" #endif #ifdef LOG_DISABLED @@ -15,11 +15,7 @@ #define LOGE(...) 0 #else #ifndef NDEBUG -#define LOGD(fmt, ...) \ - __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ - "%s:%d#%s" \ - ": " fmt, \ - __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGV(fmt, ...) \ __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \ "%s:%d#%s" \ diff --git a/dex2oat/src/main/cpp/include/oat.h b/dex2oat/src/main/cpp/include/oat.h index 869dab30b..46aead829 100644 --- a/dex2oat/src/main/cpp/include/oat.h +++ b/dex2oat/src/main/cpp/include/oat.h @@ -72,8 +72,8 @@ class EXPORT PACKED(4) OatHeader { static constexpr const char kTrueValue[] = "true"; static constexpr const char kFalseValue[] = "false"; - // Added helper to access fields. - // NOTE: It is fragile due to different Android versions and compiler optimizations. + // 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; diff --git a/dex2oat/src/main/cpp/oat_hook.cpp b/dex2oat/src/main/cpp/oat_hook.cpp index aec35537e..cc4cdb1b9 100644 --- a/dex2oat/src/main/cpp/oat_hook.cpp +++ b/dex2oat/src/main/cpp/oat_hook.cpp @@ -13,15 +13,15 @@ #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 linker path and the extra flags in its - * "dex2oat-cmdline" key, which can be used to detect the wrapper. + * 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; // The original binary path +std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path } // namespace /** @@ -79,7 +79,7 @@ uint8_t* WriteKeyValueStore(const std::map& key_values std::memcpy(data_ptr, value.c_str(), value.length() + 1); data_ptr += value.length() + 1; } - LOGI("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); + LOGD("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); return reinterpret_cast(data_ptr); } @@ -211,22 +211,16 @@ void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, v #define PLT_HOOK_REGISTER(DEV, INODE, NAME) PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) __attribute__((constructor)) static void initialize() { - // 1. Determine the target binary name - const char* env_cmd = getenv("DEX2OAT_CMD"); - if (env_cmd) { - g_binary_path = env_cmd; - } - dev_t dev = 0; ino_t inode = 0; - // 2. Locate the dex2oat binary in memory to get its device and inode for PLT hooking + // 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); - LOGV("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, + LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, (uintmax_t)inode); break; } @@ -237,10 +231,10 @@ __attribute__((constructor)) static void initialize() { return; } - // 3. Register hooks for various ART versions + // Register hook for the standard KeyValueStore getter PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); - // If the standard store hook fails (on newer Android), try the Checksum hook + // 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(); From ed09080dfd29025b96491850562cbd54f06d9ace Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 25 Jan 2026 10:04:32 +0100 Subject: [PATCH 12/12] [skip ci] Add README Manual library path injection via LD_LIBRARY_PATH has become unreliable due to symbol mismatches in core libraries (e.g., libc++) between the system and APEX partitions. Recent updates to `liblog` and `libbase` (in Android 16) have resulted in missing symbols like `__hash_memory` or `fmt` when the ART APEX binaries are forced to load system-partition shims. This commit switches the wrapper to execute the runtime APEX linker directly (/apex/com.android.runtime/bin/linker64). By passing the dex2oat binary to the linker via `/proc/self/fd/`, the linker can properly initialize internal namespaces and resolve dependencies from the correct APEX and bootstrap locations. Moreover, for the OatHeader hook, a bug introduced in 6703b45350bd61c56b8a901274b867a4ca1c8bda is fixed, where the target functions of PLT hooks are overwritten by the our helper functions. Details of the refactored project in explain in README. --- dex2oat/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 dex2oat/README.md diff --git a/dex2oat/README.md b/dex2oat/README.md new file mode 100644 index 000000000..4d4165c9f --- /dev/null +++ b/dex2oat/README.md @@ -0,0 +1,37 @@ +# VectorDex2Oat + +VectorDex2Oat is a specialized wrapper and instrumentation suite for the Android `dex2oat` (Ahead-of-Time compiler) binary. It is designed to intercept the compilation process, force specific compiler behaviors (specifically disabling method inlining), and transparently spoof the resulting OAT metadata to hide the presence of the wrapper. + +## Overview + +In the Android Runtime (ART), `dex2oat` compiles DEX files into OAT files. Modern ART optimizations often inline methods, making it difficult for instrumentation tools to hook specific function calls. + +This project consists of two primary components: +1. **dex2oat (Wrapper):** A replacement binary that intercepts the execution, communicates via Unix Domain Sockets to obtain the original compiler binary, and executes it with forced flags. +2. **liboat_hook.so (Hooker):** A shared library injected into the `dex2oat` process via `LD_PRELOAD` that utilizes PLT hooking to sanitize the OAT header's command-line metadata. + +## Key Features + +* **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable. +* **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk. +* **Metadata Spoofing:** Intercepts `art::OatHeader::ComputeChecksum` or `art::OatHeader::GetKeyValueStore` to remove traces of the wrapper and its injected flags from the final `.oat` file. +* **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper. + +## Architecture + +### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp) +The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it +1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`); +2. identifies the target architecture (32-bit vs 64-bit) and debug status; +3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library; +4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags; +5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD; +6. invokes the dynamic linker (`linker64`) to execute the compiler. + +### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp) +The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to: +1. Scan the memory map to find the `dex2oat` binary. +2. Locate and hook internal ART functions: + * [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) + * [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) +3. When the compiler attempts to write the "dex2oat-cmdline" key into the OAT header, the hooker intercepts the call, parses the key-value store, and removes the wrapper-specific flags and paths.