Stop leaving JNI exceptions pending across the native boundary - #853
Merged
Conversation
A Java method that throws does not unwind into C++. The exception is left pending on the thread, and the runtime asserts on that at the next transition: on a device, a throwing framework entry aborted system_server with "No pending exception expected" and the phone boot looped. CheckJNI is not needed for this; the assert is always on. Six places let that happen. FindAndCall handed the framework entry to Java and looked at nothing afterwards, so the abort was all anyone got, while the log line above it still said the framework had been injected. Two lookups in resources_hook returned JNI_FALSE to Java with NoSuchMethodError pending, so a caller that asked for a boolean got a throw. RegisterNatives, LogcatMonitor's refreshFd lookup and dex2oat's string read did the same on their failure paths, and the obfuscation map builder returned null on a failed FindClass without clearing, then fed two unchecked method ids to NewObject. Most of them are now the lsplant JNI wrappers, which clear the exception, log the Java stack behind it, and return scoped references -- that last part also releases the local reference the obfuscation map leaked per entry. Where the caller has to know the outcome, the check stays explicit, because a wrapper clears the exception before anyone can ask. The trace is rendered through Log.getStackTraceString rather than ExceptionDescribe. ExceptionDescribe writes to stderr, which in a process forked from the zygote goes nowhere: measured on a device, it produced no output at all, which would have traded an aborting-but-informative tombstone for a survivable process and no stack. SetAllowUnload(false) deliberately stays unconditional: the ART and JNI hooks are installed before the entry runs and their trampolines point into this library, so a failed entry is not a reason to let it be unloaded. hook_bridge is untouched. It implements Method.invoke semantics and has to leave a target's exception pending so it can wrap it in InvocationTargetException.
JingMatrix
force-pushed
the
jni-pending-exceptions
branch
from
August 2, 2026 21:49
b71e6fb to
4e6cc3e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
JNI has a rule that is easy to forget: when a Java method throws, the exception does not become a C++ exception. It stays pending on the thread, and the runtime is entitled to abort at the next transition rather than let you carry on. So every call into Java, and every lookup that can fail, owes an answer to "did that throw" — and six files never asked.
The worst of them is
FindAndCall, which hands a whole process to the framework's Java entry and then inspected nothing. A throwing entry left that process without Xposed, while the line printed immediately afterwards said the framework had been injected. It now reports the failure and returns whether the call arrived, and both callers say which happened. The rest are smaller versions of the same thing: two lookups inresources_hookreturnedJNI_FALSEto Java withNoSuchMethodErrorstill pending, so a caller that asked for a boolean got a throw instead;RegisterNatives,LogcatMonitor'srefreshFdlookup anddex2oat's string read did the same on their failure paths; and the obfuscation map builder returned null on a failedFindClasswithout clearing, then fed two unchecked method ids toNewObject.Most of this is not new code but the lsplant wrappers we already have. They clear the exception, log the Java stack behind it, and hand back scoped references — which incidentally disposes of a local reference the obfuscation map leaked per entry. An explicit check survives only where the caller has to know what happened, because a wrapper clears the exception before anyone can ask.
Two decisions worth recording. The stack is rendered with
Log.getStackTraceStringrather thanExceptionDescribe, because the latter writes to stderr and a process forked from the zygote has nowhere for stderr to go — the trace would simply vanish. AndSetAllowUnload(false)stays unconditional: the ART and JNI hooks are installed before the entry runs and their trampolines point into this library, so a failed entry is no reason to let it be unloaded.hook_bridgeis deliberately untouched. It implementsMethod.invokesemantics and has to leave a target's exception pending so it can wrap it inInvocationTargetException.