File: README.md
Introduction
Vector is a Zygisk module providing an ART hooking framework that maintains API consistency with the original Xposed. It is engineered on top of LSPlant to deliver a stable, native-level instrumentation environment.
The framework allows modules to modify system and application behavior in-memory. Because no APK files are modified, changes are non-destructive, easily reversible via reboot, and compatible across various ROMs and Android versions.
Compatibility
Vector supports devices running Android 8.1 through Android 17 Beta.
[!TIP]
This framework requires a recent installation of Magisk or KernelSU with Zygisk enabled.
Installation
- Download the latest release as a system module.
- Install the module via your root manager (Magisk/KernelSU).
- Ensure a Zygisk environment (e.g., NeoZygisk).
- Reboot the device.
- Access management settings via the system notification.
Downloads
| Channel | Source |
|---|---|
| Stable Releases | GitHub Releases |
| Canary (CI) Builds | GitHub Actions |
[!NOTE]
Debug builds are recommended for users encountering issues or performing troubleshooting.
We encourage users to test CI builds to help us identify bugs and accelerate development.
[!CAUTION]
GitHub requires users to be logged in to download CI artifacts.The link above is filtered to show only
masterbranch builds.
Please note that builds from Pull Requests (PRs) are often unstable and potentially unsafe (depending on the authors); we recommend staying on themasterbranch for verified builds, unless you are asked to help our debugging sessions.
Support and Contribution
If you encounter issues or wish to help improve the project, please refer to the resources below.
- Troubleshooting: Consult the guide before reporting bugs.
- Discussions: Join our community on GitHub Discussions.
- Localization: Help translate the project via Crowdin.
[!IMPORTANT]
Bug reports are only accepted if they are based on the latest debug build.Notice for Chinese speakers:
ไธบไบๆ้ซๆฒ้ๆ็๏ผๆฌ้กน็ฎไป ๆฅๅ่ฑๆ Issueใ่ฏทไฝฟ็จ DeepL ๆๅ ถไป็ฟป่ฏๅทฅๅ ทๆไบคๆจ็ๅ้ฆใ
Developer Resources
Vector supports both legacy and modern hooking standards to ensure broad module compatibility.
[!NOTE]
Vector supports thelibxposedAPI via two git submodules: the module API and the service API.A successful GitHub Actions build of the master branch indicates that Vector fully supports these APIs at those specific commits.
Developers are suggested to check out the same commits as Vector.
Credits
This project is made possible by the following open-source contributions:
- Magisk: The foundation of Android customization.
- LSPlant: The core ART hooking engine.
- XposedBridge: The standard Xposed APIs.
- Dobby: Inline hooking implementation.
- LSPosed: Upstream source.
- EdXposed: Upstream source, before LSPosed.
- xz-embedded: Library decompression utilities.
Legacy and Historical Dependencies
License
Vector is licensed under the GNU General Public License v3.
File: daemon/README.md
Vector Daemon Subsystem
The Vector daemon is a standalone, root-privileged Dalvik executable bootstrapped via app_process. Operating entirely outside the standard Android application sandbox, it serves as the central coordinator, state manager, and inter-process communication (IPC) asset server for the Vector framework.
Target processes operating under strict Android sandbox and SELinux constraints cannot safely access external configuration files or SQLite databases. The daemon offloads these operations, providing an IPC backend that serves memory-mapped resources, configuration states, and native file descriptors to target applications securely and efficiently.
Directory Structure
The daemon is organized into discrete packages handling IPC, state management, OS interfacing, and native environments.
src/main/
โโโ jni/ # Native C++ implementations (dex2oat wrapper, logcat parser)
โโโ kotlin/org/matrix/vector/daemon/
โโโ data/ # SQLite schema, immutable state cache, and file operations
โโโ env/ # UNIX domain socket servers and native process monitors
โโโ ipc/ # AIDL endpoints (Framework, Manager, ModuleApp, InjectedModule, SystemServer)
โโโ system/ # System binder delegates and Notification UI
โโโ utils/ # Context forgery, signature verification, and JNI bridges
โโโ Cli.kt # Command-line interface definitions
โโโ VectorDaemon.kt # Main entry point and looper initialization
โโโ VectorService.kt # Primary IVectorDaemon implementationConcurrency and State Management
To handle concurrent IPC requests without starving Android Binder thread pools, the daemon separates background I/O operations from state reads.
- Immutable State Container: The
DaemonStatedata class holds a frozen snapshot of all enabled modules and process scopes. IPC threads read from this object without acquiring locks. - Atomic Swaps: When the underlying SQLite database changes, the daemon triggers a conflated channel request. A background coroutine queries the database, computes the new module topology, instantiates a new
DaemonState, and atomically swaps the reference inConfigCache. - Preference Isolation: High-frequency module preference reads and writes are decoupled from the core state. Managed by
PreferenceStore, preferences are serialized as binary blobs and pushed as differential updates to modules, preventing unnecessary cache rebuilds.
IPC Architecture
The daemon implements a multi-layered IPC design utilizing Android's Binder mechanism and UNIX domain sockets. It avoids registering standard AIDL services with ServiceManager, relying instead on intercepting Binder transactions via the Zygisk module and actively pushing Binder references to target processes.
1. System Server Bootstrapping
During device boot, the daemon establishes a communication channel with the native Vector Zygisk module residing in system_server.
- The daemon registers an
IServiceCallbackto listen for the registration of a hardware proxy service (typically theserialservice). Once intercepted, the daemon replaces the proxy service with its own binder. - The Zygisk module queries this proxy service to retrieve the framework loader DEX via
SharedMemoryand the class obfuscation map. - Concurrently, the daemon sends a raw
ACTION_SEND_BINDERtransaction to theactivityservice. The Zygisk module's JNI hook intercepts this transaction before it reaches the Activity Manager, extracting and storing the daemon's primaryVectorServicebinder for future use.
2. Target Application Rendezvous
When a standard user application spawns, it requests framework access from the daemon.
- The target application queries the
activityservice. The Zygisk module insidesystem_serverintercepts this query. - The
system_serverforwards the application's UID, PID, process name, and a newly created heartbeatBBinderto the daemon using the previously storedVectorServicereference. - The daemon verifies the request against its
ConfigCacheto determine if the application is within the scope of any enabled modules. - If approved, the daemon returns an
FrameworkServicebinder, which thesystem_serverpasses back to the target application. - The daemon links a
DeathRecipientto the heartbeat binder to automatically clean up internal tracking maps when the application process dies. - The target application uses the
FrameworkServicebinder to fetch its specific module list, framework DEX, and obfuscation map.
3. Libxposed Module Injection
Unlike target applications which request access, the daemon actively pushes its API binder to module processes. This mechanism is strictly limited to modules utilizing the modern libxposed API.
- The daemon registers an
IUidObserverwith the Activity Manager to monitor process lifecycles. - When a UID becomes active,
ModuleAppServicechecks if the UID belongs to an enabled libxposed module. - The daemon retrieves an
IXposedServicebinder. To deliver it, the daemon callsIActivityManager.getContentProviderExternal, targeting a synthetic authority constructed from the module's package name. - The daemon executes
IContentProvider.callwith the actionSEND_BINDERand aBundlecontaining the binder. This injects the binder into the module's process space beforeApplication.onCreateexecutes, providing access to API verification, scope requests, and remote preferences.
4. Native Socket IPC
For native components that operate outside the Java Binder context, the daemon provisions two distinct types of UNIX domain sockets.
- Command-Line Interface: The
CliSocketServerexposes a filesystem-based socket at/data/adb/lspd/.cli_sock. The CLI client authenticates using a compiled-in UUID token and communicates using structured JSON. For live log streaming, the daemon attaches the log file's rawFileDescriptorto the socket reply payload, allowing the client to read directly from the OS-level stream buffer. - Dex2Oat Wrapper: The
Dex2OatServerlistens on an abstract UNIX domain socket. To prevent conflicts and detection, the exact name of this abstract socket is randomized during module installation. The C++dex2oatwrapper connects to this socket to receive necessary file descriptors viaSCM_RIGHTS.
Native Environment Subsystems
The daemon relies on native C++ subsystems to intercept Android's compilation pipeline and parse system log buffers directly, avoiding the overhead and limitations of standard shell utilities.
AOT Compilation Hijacking
Android's ART compiler aggressively inlines methods, which permanently prevents those methods from being hooked at runtime. To enforce the --inline-max-code-units=0 flag system-wide, Vector utilizes a C++ binary wrapper mounted over the system's dex2oat and dex2oat64 binaries.
The daemon manages this interception entirely through its native JNI layer. To ensure the replaced compiler binaries are globally visible to all newly spawned application processes, the daemon forks a privileged child process and uses setns with CLONE_NEWNS to enter the init (PID 1) mount namespace via /proc/1/ns/mnt. It then performs read-only bind mounts (MS_BIND | MS_REMOUNT | MS_RDONLY) over the target compiler binaries located in the /apex mount points.
When the wrapper executes, it connects to the daemon's abstract UNIX domain socket to retrieve the original compiler binary and the hooking library (liboat_hook.so) via SCM_RIGHTS. To guarantee the wrapper can connect without SELinux denials, the daemon dynamically writes to /proc/self/task/[tid]/attr/sockcreate before binding the socket. This instructs the kernel to label the abstract socket with a specific context, such as u:r:dex2oat:s0 or u:r:installd:s0, matching the strict domains under which the compiler operates.
If the wrapper is disabled or incompatible, the daemon unmounts the binaries and utilizes resetprop to inject the inline flag directly into the dalvik.vm.dex2oat-flags system property as a fallback. The Kotlin daemon continuously monitors SELinux states via a FileObserver on /sys/fs/selinux/enforce and its policy files. It dynamically remounts the wrappers if the system drops to permissive mode or alters policy, ensuring the interception persists across state changes.
Native Logcat Monitoring
Instead of relying on standard logcat shell execution, the daemon runs a native C++ process that interfaces directly with Android's liblog buffers (LOG_ID_MAIN and LOG_ID_CRASH).
The native parser performs zero-copy processing of log events, strictly filtering output by predefined exact tags (e.g., Magisk, KernelSU) and prefix tags (e.g., dex2oat, Vector, LSPosed). It writes the filtered output into two rotating log files: one for module frameworks and one for verbose system debugging, rotating them automatically when they reach 4MB.
To control this isolated native loop, the Kotlin daemon injects specific string triggers (such as !!refresh_modules!! or !!start_verbose!!) directly into the Android log stream. The C++ parser intercepts these specific messages originating from its own parent PID and dynamically rotates its file descriptors or alters its verbosity state without requiring additional IPC overhead.
File: dex2oat/README.md
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:
- 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.
- liboat_hook.so (Hooker): A shared library injected into the
dex2oatprocess viaLD_PRELOADthat utilizes PLT hooking to sanitize the OAT header's command-line metadata.
Key Features
- Inlining Suppression: Appends
--inline-max-code-units=0to the compiler arguments, ensuring all methods remain discrete and hookable. - FD-Based Execution: Executes the original
dex2oatvia the system linker using/proc/self/fd/paths, avoiding direct execution of files on the disk. - Metadata Spoofing: Intercepts
art::OatHeader::ComputeChecksumorart::OatHeader::GetKeyValueStoreto remove traces of the wrapper and its injected flags from the final.oatfile. - 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
- connects to a predefined Unix socket (the stub name
5291374ceda0...will be replaced during installation ofVector); - identifies the target architecture (32-bit vs 64-bit) and debug status;
- receives File Descriptors (FDs) for both the original
dex2oatbinary and theoat_hooklibrary; - reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags;
- clears
LD_LIBRARY_PATHand setsLD_PRELOADto the hooker library's FD; - 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 library to:
- Scan the memory map to find the
dex2oatbinary. - Locate and hook internal ART functions:
- 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.
File: external/README.md
External Dependencies
This directory contains all the external dependencies required to build the Vector framework.
They are included as git submodules to ensure version consistency and timely updating.
Native dependencies
Dobby:
A lightweight, multi-platform inline hooking framework. It serves as the backend for all native function hooking (HookInline).fmt:
A modern formatting library used for high-performance, type-safe logging throughout the native code.LSPlant:
A hooking framework for the Android Runtime (ART). It provides the core functionality for intercepting and modifying Java methods.xz-embedded:
A lightweight data compression library with a small footprint. It is used by the ELF parser to decompress the.gnu_debugdatasection of stripped native libraries.LSPlt:
A library for PLT (Procedure Linkage Table) hooking. It is used in thedex2oatsub-project to bypass a detection point. Note: This is included as a submodule for project convenience but is not compiled into theexternalC++ library itself.
Java libraries
apache/commons-lang:
A package of Java utility classes for the classes that are in java.lang's hierarchy. Some classes are renamed and then used to implement theXposedHelpersAPI.axml/manifest-editor:
A a tool used to modify Android Manifest binary file. It is to parse manifestation files of Xposed modules.
File: legacy/README.md
Legacy Xposed API Implementation
This document details the architecture and implementation of the legacy module within the Vector framework. The legacy subsystem provides a backward-compatibility layer, implementing the classic de.robv.android.xposed API namespace while routing execution to the modern native ART hooking engine.
Module Topology and Boundaries
The legacy compatibility subsystem spans multiple compilation boundaries to enforce strict separation between the API surface, the Dalvik/ART runtime, and the native execution environment.
- legacy: Contains the Java API surface (
de.robv.android.xposed.*), state translation handlers (LegacyDelegateImpl), reflection caching mechanisms, and resource/shared preference overrides. - xposed: Manages the isolated classloaders (
VectorModuleClassLoader), the Ahead-Of-Time (AOT) compiler deoptimizer (VectorDeopter), and the Dependency Injection (DI) framework. - native: Houses the JNI bridges (
hook_bridge.cpp,resources_hook.cpp), concurrent hook registries, stack-allocated invocation logic, in-memory DEX generation, and binary XML mutation routines. - daemon: Operates out-of-process with elevated privileges to provision SELinux-permissive directories and manage filesystem contexts for cross-process file sharing.
Dependency Injection and Bootstrap
During process startup, Startup.initXposed is invoked in the zygisk module. This routine establishes the DI (Dependency Injection) contract by instantiating LegacyDelegateImpl and injecting it into the xposed module via VectorBootstrap.INSTANCE.init().
The LegacyDelegateImpl satisfies the LegacyFrameworkDelegate interface, acting as the sole translation boundary for:
- Application package load events (
onPackageLoaded). - System server initialization (
onSystemServerLoaded). - Native hook execution routing (
processLegacyHook). - Resource directory tracking (
setPackageNameForResDir).
Module Initialization
Legacy modules are loaded during the initialization phase via XposedInit.loadLegacyModules(). The framework queries the daemon (VectorServiceClient.INSTANCE.getLegacyModules()) to retrieve the list of enabled APK paths.
Modules are not loaded using standard Android mechanism. To prevent detection via ClassLoader.getParent() chain-walking and to eliminate residual file descriptors, XposedInit.loadModule utilizes VectorModuleClassLoader. This classloader loads the module APK directly into memory, isolating the module's execution environment from the host application's classpath.
Once mapped into memory, the framework parses two specific manifest files within the APK to initialize Java and native execution hooks:
assets/xposed_init: Defines the Java entrypoint class, which is loaded viaVectorModuleClassLoaderand inspected forIXposedModimplementations to be registered with the internal callback arrays:IXposedHookZygoteInit: Invoked immediately with aStartupParamstructure containing the module path and system server status.IXposedHookLoadPackage: Wrapped inIXposedHookLoadPackage.Wrapperand appended to theXposedBridge.sLoadedPackageCallbacksarray.IXposedHookInitPackageResources: Appended toXposedBridge.sInitPackageResourcesCallbacksarray, which subsequently triggers the native resource hooking subsystem viaXposedInit.hookResources().
assets/native_init: Defines native library filenames that are treated as native hooking modules. These names are registered viaNativeAPI::recordNativeEntrypointto be intercepted during the dynamic linking process. Thenativemodule provides an infrastructure for native modules to perform inline hooking via native_api.cpp without direct access to the framework's core symbols.
Lifecycle Event Translation
The xposed module manages Android lifecycle events (e.g., LoadedApk.createOrUpdateClassLoaderLocked), and dispatches a LegacyPackageInfo payload to LegacyDelegateImpl.
LegacyDelegateImpl.onPackageLoaded translates the modern payload into the classic Xposed format. It constructs an XC_LoadPackage.LoadPackageParam object, mapping the following fields: packageName, processName, classLoader, appInfo, and isFirstApplication. The populated parameter object is then passed to XC_LoadPackage.callAll, which iterates over the sLoadedPackageCallbacks array and executes the registered module callbacks.
The system server represents a unique lifecycle edge case. LegacyDelegateImpl.onSystemServerLoaded manually registers android into the loadedPackagesInProcess set and constructs a hardcoded LoadPackageParam with the process name system_server.
Execution Routing and Method Hooking
The method hooking lifecycle involves identifying target executables via reflection, ensuring ART compliance by deoptimizing compiled code, registering JNI trampolines, and managing the execution state during invocation.
Structural Reflection Caching
Legacy modules rely heavily on reflection to locate target methods and fields (e.g., via XposedHelpers.findAndHookMethod). The legacy module implements a structural caching mechanism within XposedHelpers. Queries are encapsulated into MemberCacheKey subclasses (Method, Constructor, Field). These keys compute their hashes based on object identity and structural properties (class references, parameter array contents, and exactness flags) rather than strings. The keys are stored in ConcurrentHashMap instances (fieldCache, methodCache, constructorCache), enabling zero-allocation cache hits for repeated reflective lookups.
AOT Deoptimization
Modern Android Runtime (ART) environments utilize Ahead-Of-Time (AOT) compilation, which frequently inlines short methods into their callers. If an inlined method is hooked, the JNI trampoline will be bypassed because the caller executes the inlined machine code directly.
To mitigate this, the xposed module implements VectorDeopter. During initialization or application load, VectorDeopter.deoptMethods() queries a registry of known inlined methods VectorInlinedCallers. It iterates through these targets and issues a native command (HookBridge.deoptimizeMethod) to force ART to discard the compiled machine code for the target. This forces the method execution back into the ART interpreter (via ClassLinker::SetEntryPointsToInterpreter in lsplant), which strictly respects method boundaries and guarantees execution of the installed JNI trampolines.
Native Hook Registry and Execution State Translation
Hook registration is routed from XposedBridge to the native layer in hook_bridge.cpp. The native environment manages a concurrent global registry to track hooked executables and their associated callbacks.
When a hooked method executes, the native engine pauses standard execution and routes control back to xposed, which invokes the LegacyFrameworkDelegate.processLegacyHook interface.
The LegacyDelegateImpl translates the modern execution state (OriginalInvoker) into the legacy specification. It wraps the invocation state within LegacyApiSupport and performs the following execution loop:
- Iterates forward through all registered
XC_MethodHookinstances, invokingbeforeHookedMethod. - Checks if any module invoked
setResult()orsetThrowable(). If the execution was not skipped, it commands theOriginalInvokerto proceed with the native method execution. - Iterates backward through the callbacks, invoking
afterHookedMethod. If a downstream module threw an exception during execution,LegacyApiSupportcatches the exception and restores the original cached result or throwable. This protects the host process from unhandled module exceptions.
Resource Hooking Subsystem
The resource hooking implementation allows legacy modules to replace application assets, layout definitions, and string values at runtime. Because Android optimizes resource retrieval and XML parsing heavily in native C++ code, this subsystem requires a combination of framework-level injection, dynamic class generation, and direct memory manipulation of binary XML structures.
To intercept resource queries, the system replaces the default OS-provided Resources instance with a custom XResources subclass. During framework startup, XposedInit.hookResources() intercepts the Android android.app.ResourcesManager component, by applying hooks to the resource factory methods (createResources, createResourcesForActivity on Android 12+, and getOrCreateResources on older versions). When an application requests a new resource object, the hook callback executes cloneToXResources(). This method instantiates a new XResources object and copies the underlying OS implementation by extracting mResourcesImpl via HiddenApiBridge.Resources_setImpl. The newly constructed XResources instance is then injected back into the operating system's internal tracking arrays (e.g., mResourceReferences or the ActivityResource struct), encapsulated within a WeakReference to prevent memory leaks.
Dynamic Class Hierarchy Generation
To intercept resource queries effectively, the framework must replace the OS-provided resource instances with XResources and XTypedArray. However, hardcoding XResources to inherit directly from the base AOSP Resources class would result in fatal ClassCastException aborts when the heavily modified OEM framework attempts to cast the injected object back to its expected proprietary type. To resolve this runtime polymorphism requirement without triggering ClassNotFoundException during initial loading, the framework dynamically generates an intermediate class hierarchy.
During initialization, XposedBridge.initXResources within the legacy module inspects the exact runtime class of the system resources and typed arrays. It then invokes the native bridge ResourcesHook.makeInheritable to strip the final modifier from these OEM classes at the ART level, ensuring they can be subclassed. Subsequently, it invokes ResourcesHook.buildDummyClassLoader. The native implementation utilizes the dex_builder library to construct a DEX file directly within a memory buffer. It generates dummy classes named xposed.dummy.XResourcesSuperClass and xposed.dummy.XTypedArraySuperClass, dynamically setting their superclasses to the exact OEM classes detected earlier. This memory buffer is then loaded into the runtime via dalvik.system.InMemoryDexClassLoader. Finally, the legacy module manipulates the parent chain of its own classloader by overriding the parent field to point to the in-memory dummy classloader. At compile time, XResources is declared as extending the XResourcesSuperClass stub. At runtime, when Dalvik/ART resolves XResources, the manipulated classloader chain provides the dynamically generated dummy class. This mechanism guarantees that XResources safely inherits all OEM-specific methods and fields, successfully passing any internal type checks performed by the vendor framework.
On Lenovo ZUI devices, the OEM modified the obtainTypedArray implementation to query the device configuration from android.app.ActivityThread.sCurrentActivityThread. Accessing this field during Zygote startup returns null, resulting in a fatal NullPointerException that crashes the boot process. The legacy module circumvents this by reflecting an empty, uninitialized ActivityThread object, injecting it into the static sCurrentActivityThread field, invoking obtainTypedArray, and immediately nullifying the field in a finally block.
Replacement Caching and Native Binary XML Mutation
Intercepting high-frequency rendering paths (such as getDrawable or getColor) and querying a standard hash map for module replacements causes severe lock contention and degrades UI thread performance. To prevent this, XResources utilizes a lock-free bitmask cache to validate the existence of a replacement in O(1) time before querying the main sReplacements hash map.
sSystemReplacementsCache: A static 256-byte array tracking framework resource IDs (values below0x7f000000).mReplacementsCache: A 128-byte array tracking application-specific resource IDs (values greater than or equal to0x7f000000).
The bitmask acts as a high-performance fast-path rejection filter to prevent synchronized map lookups for unmodified resources. During registration, the framework maps the resource ID to an index in a byte arrayโharvesting entropy from the Type and Entry Index fieldsโand sets a specific bit using the ID's lowest three bits. Upon resource retrieval, getReplacement performs a bitwise check; if the bit is zero, the framework immediately returns null without acquiring a monitor. This O(1) check ensures that the majority of resource requests bypass the global sReplacements lock, maintaining UI thread performance.
When an application inflates a layout via LayoutInflater, the Android OS parses compiled AAPT binary XML files utilizing the native android::ResXMLParser C++ class. Standard Java hooks cannot intercept the internal ID resolution performed by this parser. To inject custom module layouts, the framework mutates the binary XML tree in memory.
In resources_hook.cpp, the
PrepareSymbolsfunction utilizes theElfImageutility to parselibandroidfw.soin memory. It resolves unexported, mangled C++ symbols forandroid::ResXMLParser::next,restart, andgetAttributeNameID, caching their memory addresses in global function pointers.If a requested layout is not already cached,
XResourcesextracts the native pointer (mParseState) and passes it to the JNI bridgerewriteXmlReferencesNative. The native code casts thejlongback to anandroid::ResXMLParser*and executes a loop, manually invokingResXMLParser_next.When the parser encounters an
android::ResXMLParser::START_TAGtoken, it extracts the attribute count and iterates over the tag's attributes. For each attribute, it resolves theattrNameIDvia the cachedgetAttributeNameIDpointer. If the ID belongs to the application package namespace (0x7f000000), it queries the Java layer via JNI (XResources.translateAttrId) to check if a module provided a replacement. If a replacement ID is returned, the native code performs an in-place mutation of the binary XML tree by directly overwriting the integer in the parser's memory allocation (mResIds[attrNameID] = attrResID). It repeats this evaluation and mutation logic for the attribute's value reference viaXResources.translateResId.Upon reaching the
END_DOCUMENTtoken, the native loop exits and invokesResXMLParser_restart. When the native bridge returns and the Android framework resumes the inflation process, it unknowingly parses the mutated binary XML tree, correctly resolving the module-provided layout IDs.
SharedPreferences and SELinux Boundaries
The classic Xposed API relied on the Context.MODE_WORLD_READABLE flag, allowing target applications to read configuration files directly from the module's /data/data//shared_prefs/ directory. Starting with Android 7.0, the operating system throws a SecurityException when this flag is used. Furthermore, modern SELinux policies enforce strict application data isolation, preventing cross-process directory traversal regardless of Unix file permissions.
To restore XSharedPreferences functionality without compromising system stability, the framework implements a coordinated bypass utilizing the out-of-process daemon and runtime path redirection.
The daemon module operates with elevated privileges and provisions a specialized safe-zone directory for module configuration sharing. When resolving the module directory, the daemon executes setSelinuxContextRecursive to apply the u:object_r:xposed_data:s0 SELinux context. This specific context is universally readable across standard application domains. The daemon subsequently invokes Os.chmod to enforce 755 Unix permissions and adjusts directory ownership. This creates a filesystem bridge that both the module and target applications can legally access without violating SELinux isolation.
Interception and Redirection
To utilize the safe-zone transparently, the legacy module intercepts the configuration saving mechanics within the module's own user interface process. During application load, LegacyDelegateImpl parses the module's APK metadata using VectorMetaDataReader. If the module declares an xposedminversion greater than 92 or contains the xposedsharedprefs flag, the framework triggers hookNewXSP. This routine applies two critical hooks to android.app.ContextImpl:
- Flag Stripping: It hooks
checkModeto intercept the mode integer. If theMODE_WORLD_READABLEbit is present, it suppresses the resultingSecurityExceptionby setting the hook throwable to null. - Path Redirection: It hooks
getPreferencesDirusing anXC_MethodReplacement. Instead of returning the standard isolated data directory, it returns the daemon-provisioned safe-zone path obtained viaVectorServiceClient.INSTANCE.getPrefsPath.
When the module attempts to save its standard SharedPreferences, the Android framework transparently writes the XML file into the SELinux-permissive bridge.
File I/O and IPC Bypass
When a target application is hooked and instantiates XSharedPreferences, the framework determines the path based on the target API level. For modern modules, it bypasses the legacy /data/data path entirely and maps directly to the safe-zone.
In the original Xposed framework, circumventing SELinux at read-time required synchronous IPC via BinderService or native root access via ZygoteService. In the Vector framework, these IPC mechanisms have been removed. Because the daemon pre-emptively assigns a permissive SELinux context to the safe-zone, the target application process possesses the necessary permissions to read the file directly. The SELinuxHelper component unconditionally returns DirectAccessService, an implementation of BaseService. This service acts purely as a structural API shim to maintain compatibility with the internal caching logic of XSharedPreferences, performing raw reads utilizing standard FileInputStream and BufferedInputStream operations without IPC overhead.
Since standard Android inter-process communication mechanisms (such as broadcast intents or content providers) are overly visible for cross-process preference tracking, XSharedPreferences implements an in-process filesystem watcher to handle live updates. When an OnSharedPreferenceChangeListener is registered, the framework spawns an internal daemon thread (sWatcherDaemon). This thread utilizes java.nio.file.WatchService (an abstraction over the Linux inotify subsystem) to monitor the safe-zone directory. The thread blocks on sWatcher.take(), and upon receiving an ENTRY_MODIFY or ENTRY_DELETE event for the target XML file, it validates the file hash and natively dispatches the legacy preference change callbacks to the registered listeners.
Developer References
For module developers building against or debugging the legacy Xposed API, the following external documentation provides historical context and usage guidelines:
File: manager/README.md
Vector Manager
The manager app: Jetpack Compose, one activity, configuring the root daemon over Binder. It holds
no privilege of its own โ everything it does to the device, it asks the daemon to do. It replaces:app, which is deleted.
This file covers what the code cannot tell you on its own: the constraints it is built around, and
the places where a mistake fails silently rather than loudly. The rest is in the code.
The parasitic model
The manager normally runs injected into com.android.shell rather than installed. ItsAndroidManifest.xml is never registered, so nothing declared there exists at runtime: noContentProvider, and therefore no androidx.startup and nothing that self-registers throughInitializationProvider; no FileProvider; and no per-app language API, sincesetApplicationLocales is keyed on an installed package. The language override is applied in
composition instead, which is why it takes effect without a restart.
Everything is therefore initialised explicitly, from the activity. The same APK also installs as an
ordinary app for development, and both modes have to work โ so anything that assumes one of them is
a bug waiting for the other.
Its memory is com.android.shell's memory. That is the reason behind decisions that would look
paranoid in a normal app: the log reader indexes byte offsets and pages a window rather than
holding a file, and the module scan is cached rather than repeated.
How the binder arrives
The framework loads <managerPackage>.Constants out of the injected dex by reflection and calls the
static setBinder(IBinder). Nothing in this APK calls it, so R8 is told to keep it inproguard-rules.pro. Rename that class or method and the handshake breaks at runtime with no
compile error anywhere โ the app simply comes up reporting no framework.
Order is not fixed. The binder can arrive before the activity exists, or the activity can start
before any binder does. ServiceLocator.attach() is idempotent and bind() is a plain assignment
to a StateFlow, so either order is safe, and repositories collect that flow rather than being
handed a binder โ a late arrival, or a reconnection, makes them re-read instead of leaving them
with whatever they managed to fetch before there was a daemon.
Talking to the daemon
ipc/DaemonClient wraps every AIDL call in runIpc, which moves it to Dispatchers.IO and returns
a Result. The interface isservices/manager-service/src/main/aidl/org/matrix/vector/ipc/IManagerService.aidl, and it is the
source of truth for what each call means: read the method's documentation there before calling it.
Two properties of Binder shape most of the mistakes made here, and the AIDL spells out what each
method does about them. A proxy returns a default for a transaction the daemon does not implement
rather than throwing, so 0, null and empty are indistinguishable from real answers โ seegetProtocolVersion and ROOT_UNKNOWN there. And a call that succeeded is not a call that did
anything: several of these return a boolean the daemon uses to refuse, and dropping it turns a
refusal into a silent success.
The daemon owns the truth. When a write and a read disagree, the read is usually coming from the
daemon's asynchronous cache while the write went to its database.
Logging
Log under Constants.TAG, and only under it. daemon/src/main/jni/logcat.cpp routes any tag
beginning Vector into the daemon's verbose stream, so those lines reach the Logs screen and travel
in the zip export a user attaches to a report. A file-local tag is ordinary Android practice and
would land nowhere. The conventions โ prefixes, levels, what never belongs in a message โ are onConstants.TAG itself.
Crashes are written to cacheDir/crash because that is where FileSystem.getLogs already collects
them from, in both of the manager's homes.
Strings
res/values/strings.xml, strings_logs.xml and strings_store.xml, translated into 18 locales.crowdin.yml points at this module and at the daemon's, and manager/build.gradle.kts merges../daemon/src/main/res, so a name collision between the two is a build error.
Nothing user-visible is hard-coded in a composable, and identifiers that must not be translated
carry translatable="false". The build scans values-* folders containing a strings.xml to
produce BuildConfig.TRANSLATIONS, so a locale Crowdin adds needs no code change.
Changing what a string means needs a new key. Reword it in place and eighteen translations go on
asserting the old meaning until someone notices, which can be a long time.
Building and running
./gradlew :manager:assembleDebug # the APK
./gradlew :zygisk:zipDebug # the module zip, which contains it
./gradlew ktfmtFormat # formatting is ktfmt; CI does not check itThe version code is git rev-list --count refs/remotes/origin/master, so a branch build and a
master build can share one; module.prop and the status page carry a build stamp, which is often
the only way to tell two builds apart on a device. It names where the build came from as well as
what it was built from. The commit leads and what follows says where: 93d66473-JingMatrix-Vector
for a CI build, the bare 93d66473 for a local one, and 93d66473+thinkpad โ the machine that made
it โ when the tree was not clean.
Debug builds add a second launcher activity โ a demo mode with scripted device states, insrc/debug and absent from release builds, so it cannot be used to make a release report a healthy
framework. It does mean monkey -c LAUNCHER picks one of the two at random:
adb shell am start -n org.matrix.vector.manager/.ui.MainActivityThere is no test source set anywhere in this repository, and CI runs zipAll and nothing else. A
green tick means it compiles and packages. Everything else is verified by running it against a real
daemon on a device.
File: native/README.md
Vector Native Library (`native`)
Purpose and Design Philosophy
This library provides low-level hooking and modification capabilities for the Android OS.
It is not a standalone application but a collection of components designed to be integrated into a larger loading mechanism, such as a Zygisk module.
Architectural Breakdown
The library is organized into distinct modules, each with a clear responsibility.
`core` - The Abstract Engine
This module defines the central abstractions and manages the runtime state. It's the conceptual heart of the library.
Context: An abstract base class that defines the injection lifecycle. It contains pure virtual methods likeLoadDexandSetupEntryClass. The consumer of this library (e.g., the Zygisk module) must inherit fromContextand provide the concrete implementations for these steps.ConfigBridge: A simple, native-side singleton that acts as a cache for configuration data (specifically, the obfuscation map) that is fetched and provided by the consumer.native_api: Implements the native module support system. It works by hooking the system'sdo_dlopenfunction. When it detects a registered module library being loaded, it calls that library'snative_initentry point, providing it with a set of APIs for creating its own native hooks.
`elf` - Symbol Resolution
This module is responsible for runtime symbol lookups in shared libraries, a critical function for native hooking.
ElfImage: A parser for ELF files mapped into the current process's memory. It can resolve symbols in stripped binaries by locating, decompressing (usingxz-embedded), and parsing the.gnu_debugdatasection. It applies a cascading lookup strategy: GNU hash -> ELF hash -> linear scan of the symbol table.ElfSymbolCache: A thread-safe, lazy-initialized cache forElfImageinstances, providing a safe way to access common libraries likelibart.soand thelinker.
`jni` - The Business Logic Interface
This is the most significant module and represents the library's primary service layer. It contains a set of JNI bridges that expose the core features to the injected Java framework code. The functionality here is the main product of the native library.
jni_bridge.h: Provides a set of helper macros (VECTOR_NATIVE_METHOD,REGISTER_VECTOR_NATIVE_METHODS, etc.) to standardize and simplify the tedious process of writing JNI boilerplate.HookBridge: The engine for ART method hooking. It maintains a thread-safe map of all active hooks. It also includes some stability controls, such as using atomic operations to set the backup method trampoline and throwing a Java exception instead of causing a native crash if a user tries to invoke the original method of a failed hook.ResourcesHook: Provides the functionality to intercept and rewrite Android's binary XML resources on the fly. It relies on non-public structures fromlibandroidfw.soand uses theelfmodule to find the necessary function symbols at runtime.NativeApiBridge: The JNI counterpart to thecore/native_api. It exposes a method for the Java framework to register the filenames of third-party native modules.
`common` & `framework`
common: A collection of basic utilities, including afmt-based logging system, global constants, and helper functions.framework: Contains minimal C++ structure definitions that mirror those inside Android's internallibandroidfw.so. These are necessary to correctly interpret resource data pointers.
3. Build System
The library is configured with CMake to be built as a static library (libnative.a). All external dependencies are also linked statically for maximum portability.
File: xposed/README.md
Xposed API implementation of the Vector framework
This module implements the libxposed API for the Vector framework. It serves as the primary bridge between the native ART hooking engine (lsplant) and module developers, providing a type-safe, OkHttp-style interceptor chain architecture.
Architectural Overview
The xposed module is designed with strict boundaries to ensure stability during the Android boot process and application lifecycles. It is written entirely in Kotlin and operates independently of the legacy Xposed API (de.robv.android.xposed).
It defines a Dependency Injection (DI) contract (LegacyFrameworkDelegate) which the legacy module must implement and inject during startup.
Core Components
1. The Hooking Engine
VectorHookBuilder: Implements theHookBuilderAPI. It validates the targetExecutable, bundles the module'sHooker,priority, andExceptionModeinto aVectorHookRecord, and registers it natively via JNI.VectorNativeHooker: The JNI trampoline target. When a hooked method is executed, the C++ layer invokescallback(Array<Any?>)on this class. It fetches the active hooks (both modern and legacy) from the native registry as globaljobjectreferences, constructs the rootVectorChain, and initiates execution.VectorChain: Implements the recursiveproceed()state machine.- Exception Handling: It implements the logic for
ExceptionMode. InPROTECTIVEmode, if an interceptor throws an exception before callingproceed(), the chain skips the interceptor. If it throws after callingproceed(), the chain catches the exception and restores the cached downstream result/throwable to protect the host process.
- Exception Handling: It implements the logic for
2. The Invocation System
The Invoker system allows modules to execute methods while bypassing standard JVM access checks, with granular control over hook execution.
Type.Origin: Dispatches directly to JNI (HookBridge.invokeOriginalMethod), bypassing all active hooks.Type.Chain: Constructs a localizedVectorChaincontaining only hooks with a priority less than or equal to the requestedmaxPriority, allowing modules to execute partial hook chains.VectorCtorInvoker: Handles constructor invocation. It separates memory allocation (HookBridge.allocateObject) from initialization (invokeOriginalMethod/invokeSpecialMethod) to support safenewInstanceSpeciallogic.
3. Dependency Injection Contract
To maintain the separation of concerns, the xposed module communicates with the legacy Xposed ecosystem via VectorBootstrap and LegacyFrameworkDelegate.
When xposed intercepts an Android lifecycle event (e.g., LoadedApk.createClassLoader), it dispatches the event internally via VectorLifecycleManager and then delegates the raw parameters to LegacyFrameworkDelegate so the legacy module can construct and dispatch the legacy XC_LoadPackage callbacks.
4. In-Memory ClassLoading & Isolation
Modules are executed strictly from memory using an isolated ClassLoader, ensuring zero disk footprint and maximum stealth against anti-cheat mechanisms.
- The module APK is loaded into
SharedMemory(ashmem) to bypass Java heap limitations. Once the Android Runtime (ART) ingests the DEX buffers, the ashmem is instantly unmapped, preventing memory leaks and leaving no residual file descriptors. - The
VectorModuleClassLoaderis attached exclusively to the Xposed Framework's classloader branch, preventing the target app from discovering the module via reflection orClassLoader.getParent()chain-walking. VectorURLStreamHandlerintercepts standardjar:requests, reading assets and resources natively from the module path without triggering Android's globalJarFilecache, preventing OS-level file locks.
File: zygisk/README.md
Vector Zygisk Module and Framework Loader
Overview
This subsystem constitutes the injection engine of the Vector framework. It acts as the bridge between the Android Zygote process and the high-level Java/Kotlin Xposed API. The architecture is designed to avoid standard Android service registration and disk-based class loading, operating entirely through in-memory execution, JNI-level Binder interception, and process identity transplantation.
The subsystem is divided into two discrete layers:
- Native Zygisk Layer (C++): Hooks process creation via Zygisk, filters target processes, establishes the initial IPC bridge, and bootstraps the Dalvik environment from memory.
- Framework Loader (Kotlin): The Java-world payload. It handles high-level Android framework manipulation, manages the custom Binder routing service, and orchestrates the Parasitic Manager execution.
IPC Architecture and Binder Relay
Vector utilizes a two-phase IPC routing mechanism to establish communication between injected applications and the root daemon. Instead of registering standard AIDL endpoints with ServiceManager, it intercepts the lowest level of Java Binder communication within the Dalvik VM.
The JNI Binder Trap
In ipc_bridge.cpp, the module uses the ART internal function SetTableOverride to replace the JNI function CallBooleanMethodV. This override intercepts all native calls to android.os.Binder.execTransact system-wide.
When a transaction occurs, the hook inspects the transaction code. If it matches the constant kBridgeTransactionCode (_VEC), the transaction is diverted to the static Kotlin method BridgeService.execTransact. All other transactions pass through to the original Android framework unmodified.
Phase 1: System Server Initialization
The system_server acts as the primary proxy router for the framework. During the postServerSpecialize callback, the following sequence occurs:
- The native module queries
ServiceManagerfor theserialservice (orserial_vectorfor late-inject scenarios). This service acts as a temporary rendezvous point. - The module sends a
_VECtransaction to retrieve a temporary binder, which it uses to fetch the framework DEX file descriptor and the obfuscation map. - The module installs the JNI Binder Trap (
HookBridge) and bootstraps the Kotlin layer viaMain.forkCommon. - Concurrently, the root daemon initiates a Binder transaction directly to the
system_server. The JNI trap intercepts this, and BridgeService processes theSEND_BINDERaction, storing the daemon's primaryIVectorDaemonbinder, sending backsystem_servercontext and linking aDeathRecipient.
Phase 2: User Application Rendezvous
Standard applications initialize their IPC connection by routing requests through the system_server.
- In
postAppSpecialize, the application queriesServiceManagerfor theactivityservice (which resides insystem_server). - The application sends a
_VECtransaction containing theGET_BINDERaction, its process name, and a newly allocated heartbeatBBinder. - The JNI trap inside
system_serverintercepts this transaction before the Activity Manager processes it. - The
system_server's BridgeService forwards the application's UID, PID, and heartbeat binder to the root daemon via theIVectorDaemonbinder acquired in Phase 1. - The daemon evaluates the request against its internal scope state. If approved, it generates an
IFrameworkServicebinder and returns it to thesystem_server, which writes it back to the waiting application's reply parcel. - The application uses this dedicated binder to fetch its specific framework DEX and obfuscation map.
The Heartbeat Mechanism
To manage process lifecycles without polling, the native module generates a dummy Binder object (heartbeat_binder) during both initialization phases. This object is passed to the daemon and kept alive in the application process via a JNI Global Reference (env->NewGlobalRef). If the application or system_server terminates normally or is killed by the kernel, the global reference is destroyed, the binder node is released, and the daemon's DeathRecipient triggers immediate resource cleanup.
Memory Execution and Obfuscation Synchronization
Vector does not write framework code to the /data partition.
- Asset Delivery: The root daemon provides the framework DEX via a
SharedMemoryfile descriptor, usingkDexTransactionCode. The C++ layer wraps this file descriptor in ajava.nio.DirectByteBufferand initializes adalvik.system.InMemoryDexClassLoader. - Dynamic Relinking: The daemon randomizes framework class names on each boot. The native module fetches a serialized dictionary over IPC, using
kObfuscationMapTransactionCode.SetupEntryClassuses this map to locate the randomized entry point (org.matrix.vector.core.Main) and BridgeService, enabling the framework to link correctly at runtime.
Parasitic Manager and Identity Transplantation
The Vector Manager application is not installed as a standard package. It runs by hollowing out a host process (e.g., com.android.shell) using a parasitic execution model.
System Server Intent Redirection
Within the system_server, ParasiticManagerSystemHooker intercepts ActivityTaskSupervisor.resolveActivity. When it detects an Intent tagged with the LAUNCH_MANAGER category, it dynamically modifies the returned ActivityInfo. It forces the system to launch the host package while setting the processName to the Manager's package name and adjusting theme and recents flags to mimic a standalone application.
Application Host Hijacking
When the native module detects the host package UID and the Manager process name during preAppSpecialize, it injects GID_INET (3003) into the process's GID array to ensure network access. Control is then passed to ParasiticManagerHooker.kt, which performs the identity transplantation:
- Code Injection: It intercepts
LoadedApk.getClassLoaderandActivityThread.handleBindApplication, swapping the host's ApplicationInfo with a hybrid object constructed from the manager's APK (provided via file descriptor). The manager's DEX is injected into the host'sPathClassLoader. - State Forgery: The system ActivityManager is unaware of the spoofed Manager activities. To prevent data loss during lifecycle transitions (e.g., screen rotation), the hooker intercepts
performStopActivityInnerto manually capture Bundle and PersistableBundle states into static concurrent maps. These states are reinjected duringscheduleLaunchActivity. - Context Spoofing: It intercepts
ActivityThread.installProviderandWebViewFactory.getProviderto construct forgedContextImplinstances, bypassing internal Android and Chromium package-name validation checks.