From fbf3b860b8c15910535d251798e52cce83cc1b78 Mon Sep 17 00:00:00 2001 From: bdeshi Date: Thu, 6 Aug 2026 21:07:24 +0600 Subject: [PATCH] Fix crash: avoid wrapping normal exceptions via reflection in Settings/PackageManager hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../main/java/space/bdeshi/veil/VeilHook.kt | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/space/bdeshi/veil/VeilHook.kt b/app/src/main/java/space/bdeshi/veil/VeilHook.kt index ca8d2d5..d1eaed3 100644 --- a/app/src/main/java/space/bdeshi/veil/VeilHook.kt +++ b/app/src/main/java/space/bdeshi/veil/VeilHook.kt @@ -10,7 +10,6 @@ import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XSharedPreferences -import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam @@ -167,24 +166,27 @@ class VeilHook : IXposedHookLoadPackage { ) { val clazz = XposedHelpers.findClassIfExists(className, lpparam.classLoader) ?: return - fun callOriginal(param: XC_MethodHook.MethodHookParam): Any? = - XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args) - - val stringReplacement = object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { - val name = param.args.getOrNull(1) as? String ?: return callOriginal(param) - return when (name) { - in nullStringKeys -> null - in zeroValueKeys -> "0" - else -> callOriginal(param) + // beforeHookedMethod + param.result, not XC_MethodReplacement: some overloads (e.g. the + // 2-arg Settings.Global.getInt) throw a checked SettingNotFoundException for keys they + // don't recognize. Manually calling XposedBridge.invokeOriginalMethod for the pass-through + // case routes through Method.invoke(), which wraps ANY exception the real method throws + // in InvocationTargetException — breaking callers that catch the real exception type. + // Leaving param untouched for the pass-through case lets the original run through its + // normal (non-reflective) path instead, so its real exceptions propagate correctly. + val stringHook = object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + 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() { - override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { - val name = param.args.getOrNull(1) as? String ?: return callOriginal(param) - return if (name in zeroValueKeys) 0 else callOriginal(param) + val intHook = object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val name = param.args.getOrNull(1) as? String ?: return + if (name in zeroValueKeys) param.result = 0 } } @@ -200,7 +202,7 @@ class VeilHook : IXposedHookLoadPackage { for (methodName in listOf("getString", "getStringForUser")) { for (signature in signatures) { try { - XposedHelpers.findAndHookMethod(clazz, methodName, *signature, stringReplacement) + XposedHelpers.findAndHookMethod(clazz, methodName, *signature, stringHook) } catch (t: Throwable) { // Not present with this signature on this API level. } @@ -210,7 +212,7 @@ class VeilHook : IXposedHookLoadPackage { for (methodName in listOf("getInt", "getIntForUser")) { for (signature in signatures) { try { - XposedHelpers.findAndHookMethod(clazz, methodName, *signature, intReplacement) + XposedHelpers.findAndHookMethod(clazz, methodName, *signature, intHook) } catch (t: Throwable) { // Not present with this signature on this API level. } @@ -224,13 +226,17 @@ class VeilHook : IXposedHookLoadPackage { lpparam.classLoader, ) ?: return - val throwIfHidden = object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { + // See hookSettingsClass for why this is beforeHookedMethod + param.throwable rather than + // 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 if (name != null && name in hiddenPackages) { - throw PackageManager.NameNotFoundException(name) + param.throwable = PackageManager.NameNotFoundException(name) } - return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args) } }