fix: Three process-abort paths in the Android injecting adapter - #759
Open
AdrianEddy wants to merge 3 commits into
Open
fix: Three process-abort paths in the Android injecting adapter#759AdrianEddy wants to merge 3 commits into
AdrianEddy wants to merge 3 commits into
Conversation
An event raise posted with `View.post` runs even after the host view has been removed from its parent; `getParent()` is then null and the `.unwrap()` on the method call panicked out of an `extern "system"` frame, aborting the process. Drop the event instead — there is nothing to send it to.
Rust aborts the process when a panic reaches an `extern "system"` frame, and the code under the five native entry points surfaces every JNI error as a panic via `.unwrap()` — so a detached host view, a missing method, or any other JNI error aborted the process. Wrap each entry point in an unwind boundary that logs, clears any pending Java exception, and returns null / `JNI_FALSE`. A contained panic poisons the adapter mutex, so accessibility degrades rather than resuming on half-applied state; `update_if_active` skips its work when the mutex is poisoned instead of propagating the poison panic into the caller.
`update_if_active` created two local references per call — the upgraded host view and the posted `Runnable` — and freed neither, and `Drop` leaked one more upgraded host reference. The type is documented as callable from any thread, but on a thread that is not executing a JNI native method nothing pops a local frame, so the references accumulate until ART hits its local reference table limit (a hard abort on older Android versions; unbounded growth on newer ones). Register all three with `auto_local`.
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.
Using
accesskit_android(embedded-dex) with anInjectingAdapterover a hostViewwhose lifetime tracksAccessibilityManager.isEnabled(), there are three distinct ways to abort the process. All three live ininject.rs/event.rs, and each fix is small — one commit per fix.1.
update_if_activeleaks two JNI local references per call (andDropleaks a third).self.host.upgrade_local(&env)is a bareNewLocalRef, and theRunnablecreated inpost_to_ui_threadis another; jni 0.21'sJObjecthas noDropglue, so neither is freed — and the adapter'sDropimpl upgrades the host the same way. The type documents that its functions make no assumptions about being called from the Android UI thread — but on a thread that is not executing a JNI native method, nothing pops a local frame, so the references accumulate until ART aborts (API ≤ 25, fixed 512-entry table) or grows the table without bound (API 26+). For a toolkit pushing a tree update per accessibility-relevant frame, the fixed table fills in seconds. Fix: register all three references withauto_local.2. No unwind boundary at the five
extern "system"entry points.runCallback,createAccessibilityNodeInfo,findFocus,performActionandonHoverEventrun.unwrap()-based code directly in anextern "system"frame, where Rust aborts on unwind — so any JNI error underneath (a detached view, a genuinely missing method, an OOM) is a SIGABRT rather than a recoverable failure. Nothing in the crate documents an API floor either —getAccessibilityDelegate()in the install closure, for instance, is only public API since 29 (hidden before that), and any lookup that does fail becomes an abort. Fix: a smallguard_ffihelper that catches, logs, clears any pending Java exception (returning to Java with one pending rethrows it in the Java caller — inside the UILooperforrunCallback), and returns null /JNI_FALSE. A contained panic poisons the adapter mutex, so later delegate calls fail contained too, andupdate_if_activeskips its work on a poisoned mutex instead of propagating the poison panic into the caller's thread: accessibility degrades instead of the process dying or resuming on half-applied state.3.
send_completed_eventcalls a method on a nullViewParent. Event raises run from aView.postclosure, and Android runs an enqueuedRunnableeven after the view has been detached — at which pointgetParent()returns null, jni'scall_methodrejects the null receiver, and the.unwrap()panics (and, per 2, aborts). Reachable on every "remove the accessibility host view when the screen reader is disabled" edge. Fix: return early — there is nothing to send the event to.Note that fix 2's
catch_unwindis inert in a release profile built withpanic = "abort"— which is exactly why fixes 1 and 3, which remove the panic and the leak rather than catching them, are load-bearing on their own.This PR was generated by Claude