From f92304194cb5fa2aebefa345d4155e530ac732b5 Mon Sep 17 00:00:00 2001 From: Wes Smith Date: Sun, 7 Jun 2026 16:05:37 -0600 Subject: [PATCH] Fix Java bridge crash on Android 17 (API 37) arm64 Two independent bugs caused Java.perform() to SIGBUS the target on a Pixel 10 Pro XL running Android 17 (SDK 37, arm64). Both reproduce on a clean frida-server; neither is device-specific beyond the new ART build. 1. ArtMethod access-flags offset detection (_getArtMethodSpec) The scan that locates ArtMethod.access_flags_ masks off the runtime-only nterp flags before matching against the probe method (Process.getElapsedCpuTime). It stripped kAccNterpInvokeFastPathFlag but not kAccNterpEntryPointFastPathFlag. On API 37 the probe method carries the latter, so the real flags at offset 4 (0x50300119) no longer matched and the scan latched onto the next ArtMethod's flags one 32-byte stride over (offset 36), corrupting the flag offset for every method. Fix: also mask kAccNterpEntryPointFastPathFlag. 2. Unresolved literal pool in the thread-state-transition thunk (recompileExceptionClearForArm64) makeArtThreadStateTransitionImpl recompiles art::JNI::ExceptionClear and emits an invokeCallback subroutine whose putCallAddressWithArguments() loads `callback` via a PC-relative literal (1 MiB reach). The literal pool was only emitted at writer.dispose(), after the relocated ExceptionClear body. On Android 17 ExceptionClear and the following function sit ~2 MiB apart, so the discovered/relocated body pushes the pool out of the load's range and it is left as `ldr xN, #0`, which reads its own instruction bytes (0x...58000001) and branches to them, giving a PC alignment SIGBUS at the first withRunnableArtThread(). Fix: emit the invokeCallback subroutine first and flush the literal pool immediately after it (it sits after a ret, so it is never executed), then use the relocated transition body as the entrypoint. Verified on Pixel 10 Pro XL / Android 17: Java.use, static field reads, method invocation, and method replacement (.implementation) all work. --- lib/android.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/android.js b/lib/android.js index 11daebe..a87ec27 100644 --- a/lib/android.js +++ b/lib/android.js @@ -1187,7 +1187,11 @@ function _getArtMethodSpec (vm) { const entrypointFieldSize = (apiLevel <= 21) ? 8 : pointerSize; const expectedAccessFlags = kAccPublic | kAccStatic | kAccFinal | kAccNative; - const relevantAccessFlagsMask = ~(kAccFastInterpreterToInterpreterInvoke | kAccPublicApi | kAccNterpInvokeFastPathFlag) >>> 0; + // kAccNterpEntryPointFastPathFlag must be masked off too: on Android 17 the + // probe method (Process.getElapsedCpuTime) carries it, so leaving it set makes + // the offset-4 access_flags fail to match, and the scan latches onto the next + // ArtMethod's flags one stride over, corrupting every method's flag offset. + const relevantAccessFlagsMask = ~(kAccFastInterpreterToInterpreterInvoke | kAccPublicApi | kAccNterpInvokeFastPathFlag | kAccNterpEntryPointFastPathFlag) >>> 0; let jniCodeOffset = null; let accessFlagsOffset = null; @@ -4866,15 +4870,23 @@ function recompileExceptionClearForArm64 (buffer, pc, exceptionClearImpl, nextFu const writer = new Arm64Writer(buffer, { pc }); - writer.putBLabel('performTransition'); - + // Emit the invokeCallback subroutine first and flush the literal pool right + // after it. putCallAddressWithArguments() loads `callback` through a + // PC-relative literal whose pool entry has only ±1 MiB of reach. The relocated + // ExceptionClear body that follows can exceed that on Android 17 (ExceptionClear + // and the next function sit ~2 MiB apart), so deferring the pool to dispose() + // leaves the load as an unresolved `ldr xN, #0` that reads its own bytes and + // branches to garbage (PC-alignment SIGBUS). Flushing here keeps the pool in + // range; it sits after a ret, so it is never executed, and the transition body + // that follows becomes the entrypoint. const invokeCallback = pc.add(writer.offset); writer.putPushAllXRegisters(); writer.putCallAddressWithArguments(callback, ['x0']); writer.putPopAllXRegisters(); writer.putRet(); + writer.flush(); - writer.putLabel('performTransition'); + const performEntry = pc.add(writer.offset); let foundCore = false; let threadReg = null; @@ -5004,7 +5016,7 @@ function recompileExceptionClearForArm64 (buffer, pc, exceptionClearImpl, nextFu throwThreadStateTransitionParseError(); } - return new NativeFunction(pc, 'void', ['pointer'], nativeFunctionOptions); + return new NativeFunction(performEntry, 'void', ['pointer'], nativeFunctionOptions); } function throwThreadStateTransitionParseError () {