Fix crash: avoid wrapping normal exceptions via reflection in Settings/PackageManager hooks

The target app crashed on launch after the root/manager-app and Settings
hooks landed. The actual bug: the pass-through branch of both
hookSettingsClass and hookPackageManager used XC_MethodReplacement and
manually called XposedBridge.invokeOriginalMethod() when a call wasn't one
we wanted to intercept. That routes through Method.invoke(), which wraps
*any* exception the real method throws in InvocationTargetException — including
completely normal ones like NameNotFoundException for a not-installed
package, or SettingNotFoundException for an unrecognized key, unrelated to
anything we hide. The target app's own root-detection code checks a list of
known manager packages and expects to catch NameNotFoundException directly;
the wrapped exception type broke that on the very first miss.

Fixed by switching both to XC_MethodHook.beforeHookedMethod, setting
param.result/param.throwable only for the keys/packages we actually want to
affect and leaving param untouched otherwise. That lets the real method run
through its normal, non-reflective path for everything else, so its
exceptions propagate correctly.

Confirmed on-device: the target app launches and stays in the foreground
with all six features enabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Claude Sonnet 5
2026-08-06 21:46:16 +06:00
parent 328237d722
commit dbd93b821c
+27 -21
View File
@@ -10,7 +10,6 @@ import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
@@ -167,24 +166,27 @@ class VeilHook : IXposedHookLoadPackage {
) { ) {
val clazz = XposedHelpers.findClassIfExists(className, lpparam.classLoader) ?: return val clazz = XposedHelpers.findClassIfExists(className, lpparam.classLoader) ?: return
fun callOriginal(param: XC_MethodHook.MethodHookParam): Any? = // beforeHookedMethod + param.result, not XC_MethodReplacement: some overloads (e.g. the
XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args) // 2-arg Settings.Global.getInt) throw a checked SettingNotFoundException for keys they
// don't recognize. Manually calling XposedBridge.invokeOriginalMethod for the pass-through
val stringReplacement = object : XC_MethodReplacement() { // case routes through Method.invoke(), which wraps ANY exception the real method throws
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { // in InvocationTargetException — breaking callers that catch the real exception type.
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param) // Leaving param untouched for the pass-through case lets the original run through its
return when (name) { // normal (non-reflective) path instead, so its real exceptions propagate correctly.
in nullStringKeys -> null val stringHook = object : XC_MethodHook() {
in zeroValueKeys -> "0" override fun beforeHookedMethod(param: MethodHookParam) {
else -> callOriginal(param) val name = param.args.getOrNull(1) as? String ?: return
when (name) {
in nullStringKeys -> param.result = null
in zeroValueKeys -> param.result = "0"
} }
} }
} }
val intReplacement = object : XC_MethodReplacement() { val intHook = object : XC_MethodHook() {
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { override fun beforeHookedMethod(param: MethodHookParam) {
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param) val name = param.args.getOrNull(1) as? String ?: return
return if (name in zeroValueKeys) 0 else callOriginal(param) if (name in zeroValueKeys) param.result = 0
} }
} }
@@ -200,7 +202,7 @@ class VeilHook : IXposedHookLoadPackage {
for (methodName in listOf("getString", "getStringForUser")) { for (methodName in listOf("getString", "getStringForUser")) {
for (signature in signatures) { for (signature in signatures) {
try { try {
XposedHelpers.findAndHookMethod(clazz, methodName, *signature, stringReplacement) XposedHelpers.findAndHookMethod(clazz, methodName, *signature, stringHook)
} catch (t: Throwable) { } catch (t: Throwable) {
// Not present with this signature on this API level. // Not present with this signature on this API level.
} }
@@ -210,7 +212,7 @@ class VeilHook : IXposedHookLoadPackage {
for (methodName in listOf("getInt", "getIntForUser")) { for (methodName in listOf("getInt", "getIntForUser")) {
for (signature in signatures) { for (signature in signatures) {
try { try {
XposedHelpers.findAndHookMethod(clazz, methodName, *signature, intReplacement) XposedHelpers.findAndHookMethod(clazz, methodName, *signature, intHook)
} catch (t: Throwable) { } catch (t: Throwable) {
// Not present with this signature on this API level. // Not present with this signature on this API level.
} }
@@ -224,13 +226,17 @@ class VeilHook : IXposedHookLoadPackage {
lpparam.classLoader, lpparam.classLoader,
) ?: return ) ?: return
val throwIfHidden = object : XC_MethodReplacement() { // See hookSettingsClass for why this is beforeHookedMethod + param.throwable rather than
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { // XC_MethodReplacement: getPackageInfo/getApplicationInfo throw NameNotFoundException for
// ANY not-installed package, not just ones we hide. Calling invokeOriginalMethod ourselves
// for the pass-through case would wrap that entirely normal exception in an
// InvocationTargetException via Method.invoke(), which callers don't expect and crash on.
val throwIfHidden = object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
val name = param.args.getOrNull(0) as? String val name = param.args.getOrNull(0) as? String
if (name != null && name in hiddenPackages) { if (name != null && name in hiddenPackages) {
throw PackageManager.NameNotFoundException(name) param.throwable = PackageManager.NameNotFoundException(name)
} }
return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args)
} }
} }