Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7526bc8
test binder
JingMatrix Mar 24, 2026
5c8bed1
Start refactoring daemon into Kotlin
JingMatrix Mar 25, 2026
635f739
Refactor phase 1
JingMatrix Mar 25, 2026
4e2900d
Refactor phase 2
JingMatrix Mar 25, 2026
cd4a36b
Refactor phase 3 and 4 (part done)
JingMatrix Mar 25, 2026
7747f07
Refactor phase 5
JingMatrix Mar 25, 2026
480e4f3
Fix compilation errors
JingMatrix Mar 25, 2026
ca26afb
Refactor phase 6
JingMatrix Mar 25, 2026
d855791
Complete implementation
JingMatrix Mar 26, 2026
5a9f0ef
Fix i18n support
JingMatrix Mar 26, 2026
ea23b5c
Correct JNI names
JingMatrix Mar 26, 2026
497af35
Fix databasehelper initilization
JingMatrix Mar 26, 2026
6a9bad4
Fix runtime bugs
JingMatrix Mar 26, 2026
73b4716
Fix notification
JingMatrix Mar 26, 2026
396f738
Fix manager
JingMatrix Mar 26, 2026
f67ef06
Fix manager status
JingMatrix Mar 26, 2026
7b848e1
Add docs
JingMatrix Mar 26, 2026
f915fcf
fix compilation
JingMatrix Mar 26, 2026
fec0f73
fix format
JingMatrix Mar 26, 2026
c97d519
Add cache permission fix
JingMatrix Mar 26, 2026
52022e1
Refactor CacheConfig
JingMatrix Mar 26, 2026
db53865
update readme
JingMatrix Mar 26, 2026
d46d7b5
catch package parsing error
JingMatrix Mar 26, 2026
914a1d8
[skip ci] improve doc
JingMatrix Mar 26, 2026
99c73e3
Remove Java code
JingMatrix Mar 26, 2026
e45ffa9
minor improvements
JingMatrix Mar 26, 2026
ce4af04
Support Android 17 beta3
JingMatrix Mar 27, 2026
c8b74c7
improve logging
JingMatrix Mar 27, 2026
7d3459b
Fix proguard rules
JingMatrix Mar 27, 2026
0da27d9
Fix enable modules
JingMatrix Mar 27, 2026
92453a1
Add cli feature
JingMatrix Mar 28, 2026
ced370e
Fix notification
JingMatrix Mar 28, 2026
d653e69
Fix package filtering
JingMatrix Mar 28, 2026
5fa6707
Fix modules loading
JingMatrix Mar 28, 2026
522f86f
Force socket listening
JingMatrix Mar 29, 2026
38e6c6d
Force IContentProvider.call signature
JingMatrix Mar 29, 2026
ec23fd8
Fix comments
JingMatrix Mar 29, 2026
a099efe
Force using readonly database for getModulePrefs
JingMatrix Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ plugins {
alias(libs.plugins.nav.safeargs)
alias(libs.plugins.autoresconfig)
alias(libs.plugins.materialthemebuilder)
alias(libs.plugins.lsplugin.resopt)
alias(libs.plugins.lsplugin.apksign)
}

Expand Down
9 changes: 0 additions & 9 deletions app/src/main/java/org/lsposed/manager/ConfigManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,6 @@ public static boolean setHiddenIcon(boolean hide) {
}
}

public static String getApi() {
try {
return LSPManagerServiceHolder.getService().getApi();
} catch (RemoteException e) {
Log.e(App.TAG, Log.getStackTraceString(e));
return e.toString();
}
}

public static List<String> getDenyListPackages() {
List<String> list = new ArrayList<>();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp
binding.statusTitle.setText(R.string.activated);
binding.statusIcon.setImageResource(R.drawable.ic_round_check_circle_24);
}
binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s",
ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi()));
binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)",
ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
binding.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE);
} else {
boolean isMagiskInstalled = ConfigManager.isMagiskInstalled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
getChildFragmentManager().beginTransaction().add(R.id.setting_container, new PreferenceFragment()).commitNow();
}
if (ConfigManager.isBinderAlive()) {
binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi()));
binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
} else {
binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed)));
}
Expand Down
6 changes: 2 additions & 4 deletions app/src/main/java/org/lsposed/manager/util/UpdateUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,12 @@ public void onResponse(@NonNull Call call, @NonNull Response response) {
if (!response.isSuccessful()) return;
var body = response.body();
if (body == null) return;
String api = ConfigManager.isBinderAlive() ? ConfigManager.getApi() : "riru";
try {
var info = JsonParser.parseReader(body.charStream()).getAsJsonObject();
var notes = info.get("body").getAsString();
var assetsArray = info.getAsJsonArray("assets");
for (var assets : assetsArray) {
checkAssets(assets.getAsJsonObject(), notes, api.toLowerCase(Locale.ROOT));
checkAssets(assets.getAsJsonObject(), notes);
}
} catch (Throwable t) {
Log.e(App.TAG, t.getMessage(), t);
Expand All @@ -79,11 +78,10 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) {
App.getOkHttpClient().newCall(request).enqueue(callback);
}

private static void checkAssets(JsonObject assets, String releaseNotes, String api) {
private static void checkAssets(JsonObject assets, String releaseNotes) {
var pref = App.getPreferences();
var name = assets.get("name").getAsString();
var splitName = name.split("-");
if (!splitName[3].equals(api)) return;
pref.edit()
.putInt("latest_version", Integer.parseInt(splitName[2]))
.putLong("latest_check", Instant.now().getEpochSecond())
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ tasks.register<KtfmtFormatTask>("format") {
"hiddenapi/*/build.gradle.kts",
"services/*-service/build.gradle.kts",
)
dependsOn(":daemon:ktfmtFormat")
dependsOn(":xposed:ktfmtFormat")
dependsOn(":zygisk:ktfmtFormat")
}
Expand Down
63 changes: 63 additions & 0 deletions daemon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Vector Daemon

The Vector `daemon` is a highly privileged, standalone executable that runs as `root`.
It acts as the central coordinator and backend for the entire Vector framework.

Unlike the injected framework code, the daemon does not hook methods directly. Instead, it manages state, provides IPC endpoints to hooked apps and modules, handles AOT compilation evasion, and interacts safely with Android system services.

## Architecture Overview

The daemon relies on a dual-IPC architecture and extensive use of Android Binder mechanisms to orchestrate the framework lifecycle without triggering SELinux denials or breaking system stability.

1. _Bootstrapping & Bridge (`core/`)_: The daemon starts early in the boot process. It forces its primary Binder (`VectorService`) into `system_server` by hijacking transactions on the Android `activity` service.
2. _Privileged IPC Provider (`ipc/`)_: Android's sandbox prevents target processes from reading the framework APK, accessing SQLite databases, or resolving hidden ART symbols. The daemon exploits its root/system-level permissions to act as an asset server. It provides three critical components to hooked processes over Binder IPC:
* _Framework Loader DEX_: Dispatched via `SharedMemory` to avoid disk-based detection and bypass SELinux `exec` restrictions.
* _Obfuscation Maps_: Dictionaries provided over IPC when API protection is enabled, allowing the injected code to correctly resolve the randomized class names at runtime.
* _Dynamic Module Scopes_: Fast, lock-free lookups of which modules should be loaded into a specific UID/ProcessName.
3. _State Management (`data/`)_: To ensure IPC calls resolve in microseconds without race conditions, the daemon uses an _Immutable State Container_ (`DaemonState`). Module topology and scopes are built into a frozen snapshot in the background, which is atomically swapped into memory. High-volume module preference updates are isolated in a separate `PreferenceStore` to prevent state pollution.
4. _Native Environment (`env/` & JNI)_: Background threads (C++ and Kotlin Coroutines) handle low-level system subversion, including `dex2oat` compilation hijacking and logcat monitoring.

## Directory Layout

```text
src/main/
├── kotlin/org/matrix/vector/daemon/
│ ├── core/ # Entry point (Main), looper setup, and OS broadcast receivers
│ ├── ipc/ # AIDL implementations (Manager, Module, App, SystemServer endpoints)
│ ├── data/ # SQLite DB, Immutable State (DaemonState, ConfigCache), PreferenceStore, File & ZIP parsing
│ ├── system/ # System binder wrappers, UID observers, Notification UI
│ ├── env/ # Socket servers and monitors communicating with JNI (dex2oat, logcat)
│ └── utils/ # OEM-specific workarounds, FakeContext, JNI bindings
└── jni/ # Native C++ layer (dex2oat wrapper, logcat watcher, slicer obfuscation)
```

## Core Technical Mechanisms

### 1. IPC Routing (The Two Doors)
* _Door 1 (`SystemServerService`)_: A native-to-native entry point used exclusively for the _System-Level Initialization_ of `system_server`. By proxying the hardware `serial` service (via `IServiceCallback`), the daemon provides a rendezvous point accessible to the system before the Activity Manager is even initialized. It handles raw UID/PID/Heartbeat packets to authorize the base system framework hook.
* _Door 2 (`VectorService`)_: The _Application-Level Entrance_ used by user-space apps. Since user apps are forbidden by SELinux from accessing hardware services like `serial`, they use the "Activity Bridge" to reach the daemon. This door utilizes an action-based protocol allowing the daemon to perform _Scope Filtering_—matching the calling process against the current `DaemonState` before granting access to the framework.

### 2. AOT Compilation Hijacking (`dex2oat`)
To prevent Android's ART from inlining hooked methods (which makes them unhookable), Vector hijacks the Ahead-of-Time (AOT) compiler.
* _Mechanism_: The daemon (`Dex2OatServer`) mounts a C++ wrapper binary (`bin/dex2oatXX`) over the system's actual `dex2oat` binaries in the `/apex` mount namespace.
* _FD Passing_: When the wrapper executes, to read the original compiler or the `liboat_hook.so`, it opens a UNIX domain socket to the daemon. The daemon (running as root) opens the files and passes the File Descriptors (FDs) back to the wrapper via `SCM_RIGHTS`.
* _Execution_: The wrapper uses `memfd_create` and `sendfile` to load the hook, bypassing execute restrictions, and uses `LD_PRELOAD` to inject the hook into the real `dex2oat` process while appending `--inline-max-code-units=0`.

### 3. API Protection & DEX Obfuscation
To prevent unauthorized apps from detecting the framework or invoking the Xposed API, the daemon randomizes framework and loader class names on each boot. JNI maps the input `SharedMemory` via `MAP_SHARED` to gain direct, zero-copy access to the physical pages populated by Java. Using the [DexBuilder](https://github.com/JingMatrix/DexBuilder) library, the daemon mutates the DEX string pool in-place; this is highly efficient as the library's Intermediate Representation points directly to the mapped buffer, avoiding unnecessary heap allocations during the randomization process.

Once mutation is complete, the finalized DEX is written into a new `SharedMemory` region and the original plaintext handle is closed. Because signatures are now randomized, the daemon provides _Obfuscation Maps_ via Door 1 and Door 2. These dictionaries allow the injected code to correctly "re-link" and resolve the framework's internal classes at runtime despite their randomized names.

### 4. Lifecycle & Process Injection
Vector uses a proactive _Push Model_ to distribute the `IXposedService` binder. Upon detecting a process start via `IUidObserver`, the daemon utilizes `getContentProviderExternal` to obtain a direct line to the module's internal provider. It then executes a synchronous `IContentProvider.call()`, passing the control binder within a `Bundle`. This ensures the framework reference is injected into the target process’s memory before its `Application.onCreate()` executes, bypassing the detection and latency associated with standard `bindService` calls.

_Remote Preferences & Files_ are supported by a combination of the injected Binder and custom SELinux types. The daemon stores preferences and shared files in directories labeled `xposed_data`. Because the policy allows global access to this type, the injected binder simply provides the path or File Descriptor, and the target app can perform direct I/O, bypassing standard per-app sandbox restrictions.

## Development & Maintenance Guidelines

When modifying the daemon, strictly adhere to the following principles:

1. _Never Block IPC Threads_: AIDL `onTransact` methods are called synchronously by the Android framework and target apps. Blocking these threads (e.g., by executing raw SQL queries or heavy I/O directly) will cause Application Not Responding (ANR) crashes system-wide. Always read from the lock-free, immutable `DaemonState` snapshot exposed by `ConfigCache.state`.
2. _Resource Determinism_: The daemon runs indefinitely. Leaking a single `Cursor`, `ParcelFileDescriptor`, or `SharedMemory` instance will eventually exhaust system limits and crash the OS. Always use Kotlin's `.use { }` blocks or explicit C++ RAII wrappers for native resources.
3. _Isolate OEM Quirks_: Android OS behavior varies wildly between manufacturers (e.g., Lenovo hiding cloned apps in user IDs 900-909, MIUI killing background dual-apps). Place all OEM-specific logic in `utils/Workarounds.kt` to prevent core logic pollution.
4. _Context Forgery (`FakeContext`)_: The daemon does not have a real Android `Context`. To interact with system APIs that require one (like building Notifications or querying packages), use `FakeContext`. Be aware that standard `Context` methods may crash if not explicitly mocked.
157 changes: 81 additions & 76 deletions daemon/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import com.android.build.api.dsl.ApplicationExtension
import com.android.ide.common.signing.KeystoreHelper
import java.io.PrintStream
import java.util.UUID

val defaultManagerPackageName: String by rootProject.extra
val injectedPackageName: String by rootProject.extra
Expand All @@ -9,98 +10,102 @@ val versionCodeProvider: Provider<String> by rootProject.extra
val versionNameProvider: Provider<String> by rootProject.extra

plugins {
alias(libs.plugins.agp.app)
alias(libs.plugins.lsplugin.resopt)
alias(libs.plugins.agp.app)
alias(libs.plugins.kotlin)
alias(libs.plugins.ktfmt)
}

android {
buildFeatures {
prefab = true
buildConfig = true
}
defaultConfig {
buildConfigField(
"String",
"DEFAULT_MANAGER_PACKAGE_NAME",
""""$defaultManagerPackageName"""",
)
buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""")
buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""")
buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""")
buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""")
buildConfigField("long", "VERSION_CODE", versionCodeProvider.get())

defaultConfig {
applicationId = "org.lsposed.daemon"
val cliToken = UUID.randomUUID()
// Inject the MSB and LSB as Long constants
buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L")
buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L")
}

buildConfigField(
"String",
"DEFAULT_MANAGER_PACKAGE_NAME",
""""$defaultManagerPackageName"""",
)
buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""")
buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""")
buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""")
buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""")
buildConfigField("long", "VERSION_CODE", versionCodeProvider.get())
buildTypes {
all { externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } }
release {
isMinifyEnabled = true
proguardFiles("proguard-rules.pro")
}
}

buildTypes {
all {
externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } }
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-rules.pro")
}
}
externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } }

externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } }

namespace = "org.lsposed.daemon"
namespace = "org.matrix.vector.daemon"
}

android.applicationVariants.all {
val variantCapped = name.replaceFirstChar { it.uppercase() }
val variantLowered = name.lowercase()
val variantCapped = name.replaceFirstChar { it.uppercase() }
val variantLowered = name.lowercase()

val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get()
val signInfoTask =
tasks.register("generate${variantCapped}SignInfo") {
dependsOn(":app:validateSigning${variantCapped}")
val sign =
rootProject
.project(":app")
.extensions
.getByType(ApplicationExtension::class.java)
.buildTypes
.named(variantLowered)
.get()
.signingConfig
val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt")
outputs.file(outSrc)
doLast {
outSrc.parentFile.mkdirs()
val certificateInfo =
KeystoreHelper.getCertificateInfo(
sign?.storeType,
sign?.storeFile,
sign?.storePassword,
sign?.keyPassword,
sign?.keyAlias,
)

val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get()
val signInfoTask =
tasks.register("generate${variantCapped}SignInfo") {
dependsOn(":app:validateSigning${variantCapped}")
val sign =
rootProject
.project(":app")
.extensions
.getByType(ApplicationExtension::class.java)
.buildTypes
.named(variantLowered)
.get()
.signingConfig
val outSrc = file("$outSrcDir/org/lsposed/lspd/util/SignInfo.java")
outputs.file(outSrc)
doLast {
outSrc.parentFile.mkdirs()
val certificateInfo =
KeystoreHelper.getCertificateInfo(
sign?.storeType,
sign?.storeFile,
sign?.storePassword,
sign?.keyPassword,
sign?.keyAlias,
)
PrintStream(outSrc)
.print(
"""
|package org.lsposed.lspd.util;
|public final class SignInfo {
| public static final byte[] CERTIFICATE = {${
PrintStream(outSrc)
.print(
"""
|package org.matrix.vector.daemon.utils
|
|object SignInfo {
| @JvmField
| val CERTIFICATE = byteArrayOf(${
certificateInfo.certificate.encoded.joinToString(",")
}};
})
|}"""
.trimMargin()
)
}
.trimMargin())
}
registerJavaGeneratingTask(signInfoTask, outSrcDir.asFile)
}
// registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile)

kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) }
}

dependencies {
implementation(libs.agp.apksig)
implementation(projects.external.apache)
implementation(projects.hiddenapi.bridge)
implementation(projects.services.daemonService)
implementation(projects.services.managerService)
compileOnly(libs.androidx.annotation)
compileOnly(projects.hiddenapi.stubs)
implementation(libs.agp.apksig)
implementation(libs.gson)
implementation(libs.picocli)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
implementation(projects.external.apache)
implementation(projects.hiddenapi.bridge)
implementation(projects.services.daemonService)
implementation(projects.services.managerService)
compileOnly(libs.androidx.annotation)
compileOnly(projects.hiddenapi.stubs)
}
Loading
Loading