Skip to content

Commit 1436f69

Browse files
authored
Refactor dex2oat wrapper to solve long-standing issues (#515)
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 (e.g., `/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 6703b45 is now fixed, where the target functions of PLT hooks are overwritten by the our helper functions. Details of the refactored project are explained in README.
1 parent 211bd5f commit 1436f69

10 files changed

Lines changed: 346 additions & 225 deletions

File tree

daemon/src/main/jni/logcat.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ void Logcat::ProcessBuffer(struct log_msg *buf) {
229229
tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) ||
230230
tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv ||
231231
tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv ||
232-
tag == "TEESimulator"sv || tag.starts_with("zygisk"sv))) [[unlikely]] {
232+
tag == "TEESimulator"sv || tag.starts_with("Vector"sv) ||
233+
tag.starts_with("zygisk"sv))) [[unlikely]] {
233234
verbose_print_count_ += PrintLogLine(entry, verbose_file_.get());
234235
}
235236
if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] {

dex2oat/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# VectorDex2Oat
2+
3+
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.
4+
5+
## Overview
6+
7+
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.
8+
9+
This project consists of two primary components:
10+
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.
11+
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.
12+
13+
## Key Features
14+
15+
* **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable.
16+
* **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk.
17+
* **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.
18+
* **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper.
19+
20+
## Architecture
21+
22+
### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp)
23+
The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it
24+
1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`);
25+
2. identifies the target architecture (32-bit vs 64-bit) and debug status;
26+
3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library;
27+
4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags;
28+
5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD;
29+
6. invokes the dynamic linker (`linker64`) to execute the compiler.
30+
31+
### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp)
32+
The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to:
33+
1. Scan the memory map to find the `dex2oat` binary.
34+
2. Locate and hook internal ART functions:
35+
* [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
36+
* [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
37+
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.

dex2oat/build.gradle.kts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,15 @@
1-
/*
2-
* This file is part of LSPosed.
3-
*
4-
* LSPosed is free software: you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License as published by
6-
* the Free Software Foundation, either version 3 of the License, or
7-
* (at your option) any later version.
8-
*
9-
* LSPosed is distributed in the hope that it will be useful,
10-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
* GNU General Public License for more details.
13-
*
14-
* You should have received a copy of the GNU General Public License
15-
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
16-
*
17-
* Copyright (C) 2022 LSPosed Contributors
18-
*/
19-
201
plugins {
212
alias(libs.plugins.agp.lib)
223
}
234

245
android {
25-
namespace = "org.lsposed.dex2oat"
26-
27-
buildFeatures {
28-
androidResources = false
29-
buildConfig = false
30-
prefab = true
31-
prefabPublishing = true
32-
}
6+
namespace = "org.matrix.vector.dex2oat"
337

34-
defaultConfig {
35-
minSdk = 29
36-
}
8+
androidResources.enable = false
379

3810
externalNativeBuild {
3911
cmake {
4012
path("src/main/cpp/CMakeLists.txt")
4113
}
4214
}
43-
44-
prefab {
45-
register("dex2oat")
46-
}
4715
}

dex2oat/src/main/cpp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
22
project(dex2oat)
33

44
add_executable(dex2oat dex2oat.cpp)
5-
add_library(oat_hook SHARED oat_hook.cpp oat.cpp)
5+
add_library(oat_hook SHARED oat_hook.cpp)
66

77
OPTION(LSPLT_BUILD_SHARED OFF)
88
add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external)

dex2oat/src/main/cpp/dex2oat.cpp

Lines changed: 118 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,195 @@
1-
/*
2-
* This file is part of LSPosed.
3-
*
4-
* LSPosed is free software: you can redistribute it and/or modify
5-
* it under the terms of the GNU General Public License as published by
6-
* the Free Software Foundation, either version 3 of the License, or
7-
* (at your option) any later version.
8-
*
9-
* LSPosed is distributed in the hope that it will be useful,
10-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
* GNU General Public License for more details.
13-
*
14-
* You should have received a copy of the GNU General Public License
15-
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
16-
*
17-
* Copyright (C) 2022 LSPosed Contributors
18-
*/
19-
20-
//
21-
// Created by Nullptr on 2022/4/1.
22-
//
23-
24-
#include <stdio.h>
25-
#include <stdlib.h>
26-
#include <string.h>
271
#include <sys/socket.h>
282
#include <sys/un.h>
293
#include <unistd.h>
304

5+
#include <cstdio>
6+
#include <cstdlib>
7+
#include <cstring>
8+
#include <string>
9+
#include <vector>
10+
3111
#include "logging.h"
3212

13+
// Access to the process environment variables
14+
extern "C" char **environ;
15+
3316
#if defined(__LP64__)
3417
#define LP_SELECT(lp32, lp64) lp64
3518
#else
3619
#define LP_SELECT(lp32, lp64) lp32
3720
#endif
3821

39-
#define ID_VEC(is64, is_debug) (((is64) << 1) | (is_debug))
22+
namespace {
4023

41-
const char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac\0";
24+
constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac";
4225

43-
static ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
44-
int rec = recvmsg(sockfd, msg, flags);
26+
/**
27+
* Calculates a vector ID based on architecture and debug status.
28+
*/
29+
inline int get_id_vec(bool is64, bool is_debug) {
30+
return (static_cast<int>(is64) << 1) | static_cast<int>(is_debug);
31+
}
32+
33+
/**
34+
* Wraps recvmsg with error logging.
35+
*/
36+
ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
37+
ssize_t rec = recvmsg(sockfd, msg, flags);
4538
if (rec < 0) {
4639
PLOGE("recvmsg");
4740
}
4841
return rec;
4942
}
5043

51-
static void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
44+
/**
45+
* Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS.
46+
*
47+
* @return Pointer to the FD data on success, nullptr on failure.
48+
*/
49+
void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
5250
struct iovec iov = {
5351
.iov_base = &cnt,
5452
.iov_len = sizeof(cnt),
5553
};
56-
struct msghdr msg = {
57-
.msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz};
54+
struct msghdr msg = {.msg_name = nullptr,
55+
.msg_namelen = 0,
56+
.msg_iov = &iov,
57+
.msg_iovlen = 1,
58+
.msg_control = cmsgbuf,
59+
.msg_controllen = bufsz,
60+
.msg_flags = 0};
61+
62+
if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr;
5863

59-
xrecvmsg(sockfd, &msg, MSG_WAITALL);
6064
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
6165

62-
if (msg.msg_controllen != bufsz || cmsg == NULL ||
66+
if (msg.msg_controllen != bufsz || cmsg == nullptr ||
6367
cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET ||
6468
cmsg->cmsg_type != SCM_RIGHTS) {
65-
return NULL;
69+
return nullptr;
6670
}
6771

6872
return CMSG_DATA(cmsg);
6973
}
7074

71-
static int recv_fd(int sockfd) {
75+
/**
76+
* Helper to receive a single FD from the socket.
77+
*/
78+
int recv_fd(int sockfd) {
7279
char cmsgbuf[CMSG_SPACE(sizeof(int))];
73-
7480
void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1);
75-
if (data == NULL) return -1;
81+
if (data == nullptr) return -1;
7682

7783
int result;
78-
memcpy(&result, data, sizeof(int));
84+
std::memcpy(&result, data, sizeof(int));
7985
return result;
8086
}
8187

82-
static int read_int(int fd) {
88+
/**
89+
* Reads an integer acknowledgment from the socket.
90+
*/
91+
int read_int(int fd) {
8392
int val;
8493
if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1;
8594
return val;
8695
}
8796

88-
static void write_int(int fd, int val) {
97+
/**
98+
* Writes an integer command/ID to the socket.
99+
*/
100+
void write_int(int fd, int val) {
89101
if (fd < 0) return;
90-
write(fd, &val, sizeof(val));
102+
(void)write(fd, &val, sizeof(val));
91103
}
92104

105+
} // namespace
106+
93107
int main(int argc, char **argv) {
94108
LOGD("dex2oat wrapper ppid=%d", getppid());
109+
110+
// Prepare Unix domain socket address (Abstract Namespace)
95111
struct sockaddr_un sock = {};
96112
sock.sun_family = AF_UNIX;
97-
strlcpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 1);
113+
// sock.sun_path[0] is already \0, so we copy name into sun_path + 1
114+
std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2);
115+
116+
// Abstract socket length: family + leading \0 + string length
117+
socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1;
98118

119+
// 1. Get original dex2oat binary FD
99120
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
100-
size_t len = sizeof(sa_family_t) + strlen(sock.sun_path + 1) + 1;
101-
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
121+
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
102122
PLOGE("failed to connect to %s", sock.sun_path + 1);
103123
return 1;
104124
}
105-
write_int(sock_fd, ID_VEC(LP_SELECT(0, 1), strstr(argv[0], "dex2oatd") != NULL));
125+
126+
bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr);
127+
write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug));
128+
106129
int stock_fd = recv_fd(sock_fd);
107-
read_int(sock_fd);
130+
read_int(sock_fd); // Sync
108131
close(sock_fd);
109132

133+
// 2. Get liboat_hook.so FD
110134
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
111-
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
135+
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
112136
PLOGE("failed to connect to %s", sock.sun_path + 1);
113137
return 1;
114138
}
139+
115140
write_int(sock_fd, LP_SELECT(4, 5));
116141
int hooker_fd = recv_fd(sock_fd);
117-
read_int(sock_fd);
142+
read_int(sock_fd); // Sync
118143
close(sock_fd);
119144

120145
if (hooker_fd == -1) {
121-
PLOGE("failed to read liboat_hook.so");
146+
LOGE("failed to read liboat_hook.so");
147+
}
148+
LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd);
149+
150+
// Prepare arguments for execve
151+
// Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0]
152+
std::vector<const char *> exec_argv;
153+
154+
const char *linker_path =
155+
LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64");
156+
157+
char stock_fd_path[64];
158+
std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd);
159+
160+
exec_argv.push_back(linker_path);
161+
exec_argv.push_back(stock_fd_path);
162+
163+
// Append original arguments starting from argv[1]
164+
for (int i = 1; i < argc; ++i) {
165+
exec_argv.push_back(argv[i]);
122166
}
123-
LOGD("sock: %s %d", sock.sun_path + 1, stock_fd);
124-
125-
const char *new_argv[argc + 2];
126-
for (int i = 0; i < argc; i++) new_argv[i] = argv[i];
127-
new_argv[argc] = "--inline-max-code-units=0";
128-
new_argv[argc + 1] = NULL;
129-
130-
if (getenv("LD_LIBRARY_PATH") == NULL) {
131-
char const *libenv = LP_SELECT(
132-
"LD_LIBRARY_PATH=/apex/com.android.art/lib:/apex/com.android.os.statsd/lib",
133-
"LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64");
134-
putenv((char *)libenv);
167+
168+
// Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot
169+
// hook inlined target methods.
170+
exec_argv.push_back("--inline-max-code-units=0");
171+
exec_argv.push_back(nullptr);
172+
173+
// Setup Environment variables
174+
// Clear LD_LIBRARY_PATH to let the linker use internal config
175+
unsetenv("LD_LIBRARY_PATH");
176+
177+
// Set LD_PRELOAD to point to the hooker library FD
178+
std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd);
179+
setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1);
180+
181+
// Pass original argv[0] as DEX2OAT_CMD
182+
if (argv[0]) {
183+
setenv("DEX2OAT_CMD", argv[0], 1);
184+
LOGD("DEX2OAT_CMD set to %s", argv[0]);
135185
}
136186

137-
// Set LD_PRELOAD to load liboat_hook.so
138-
const int STRING_BUFFER = 50;
139-
char env_str[STRING_BUFFER];
140-
snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd);
141-
putenv(env_str);
142-
LOGD("Set env %s", env_str);
187+
LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path);
143188

144-
fexecve(stock_fd, (char **)new_argv, environ);
189+
// Perform the execution
190+
execve(linker_path, const_cast<char *const *>(exec_argv.data()), environ);
145191

146-
PLOGE("fexecve failed");
192+
// If we reach here, execve failed
193+
PLOGE("execve failed");
147194
return 2;
148195
}

dex2oat/src/main/cpp/include/logging.h

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#pragma once
22

3-
#include <errno.h>
43
#include <android/log.h>
4+
#include <errno.h>
55

66
#ifndef LOG_TAG
7-
#define LOG_TAG "LSPosedDex2Oat"
7+
#define LOG_TAG "VectorDex2Oat"
88
#endif
99

1010
#ifdef LOG_DISABLED
@@ -15,11 +15,7 @@
1515
#define LOGE(...) 0
1616
#else
1717
#ifndef NDEBUG
18-
#define LOGD(fmt, ...) \
19-
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \
20-
"%s:%d#%s" \
21-
": " fmt, \
22-
__FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__)
18+
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
2319
#define LOGV(fmt, ...) \
2420
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \
2521
"%s:%d#%s" \

0 commit comments

Comments
 (0)