Repository navigation
fix(DexFactory): GC before registering injected proxies - #2110
Conversation
…s loader ART's mark-compact GC can lose the store of DexCache::location_ when a collection overlaps ClassLinker::RegisterDexFile. RegisterDexFileLocked then dereferences the null location and the app dies with SIGSEGV (fault addr 0x8). Registering into the app's long-lived PathClassLoader, as DexFactory does for main-runtime proxies since #1968, is the case that reproduces it; a fresh DexClassLoader per jar never crashed. A blocking Runtime.gc() right before the injected dex is registered leaves no collection in flight. In a stress harness and a standalone repro (no NativeScript code) it took the crash rate from 22-73% of runs to 0 when the registering thread is the main allocator. It still crashed once when another thread allocated non-stop, so it narrows the window rather than closing it. Cost is one explicit GC (about 4-14 ms on test-suite heaps) on the first use of each runtime-generated proxy.
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
The collection before registering an injected proxy only matters under
ART's concurrent mark-compact collector. ProxyRegistrationGc skips it when
the process's heap mappings positively identify the concurrent-copying
collector ("concurrent copying" / "(region space)" names, no mark-compact
names) and below API 31, where ART can't have been updated to a version
with mark-compact. Anything it can't identify, including renamed mappings
in future ART builds or kernels without anonymous VMA names, still
collects. The decision is made once per process, on the first injection.
On the API 33 emulator (concurrent copying) the suite's explicit GCs drop
from 134 to the tests' own 20; on API 36 (mark-compact) they stay at 134.
Description
The crash. Apps intermittently die with a native crash when the main runtime generates a proxy class at runtime, i.e. an
extend()the static binding generator didn't see:It shows up in our test suite on the API 36 emulator, a few times a day across runs.
Cause: an ART GC bug.
ClassLinker::AllocDexCachestores the dex location into the newDexCache, thenRegisterDexFilewaits in aScopedGCCriticalSectionfor an in-flight collection. After that wait,DexCache::location_reads back as null, whileclass_loader_anddex_file_(stored after the wait) are intact.RegisterDexFileLockedthen dereferences the null location. The tombstone heap dumps show this directly, and in some crashes the locationStringis still on the heap right after theDexCache.What we found out:
DexFactorydoes for main-runtime proxies since fix(DexFactory): register injected proxy dex with a single class loader #1968 (addDexPathinto the app'sPathClassLoader, soClass.forNamefinds them). With a freshDexClassLoaderper jar it never crashed, but that would loseClass.forNamevisibility.The workaround. A blocking
Runtime.getRuntime().gc()immediately before the injected dex is registered, so no collection is in flight whenRegisterDexFileruns. Results:gc()It isn't a guarantee. When another thread allocates non-stop, a background GC queued behind the explicit one can start right after it. That still cut crashes about tenfold (1 crash in ~27k registrations).
Cost. One explicit GC, about 4–14 ms on test-suite-sized heaps and more on large heaps, on the first use of each runtime-generated proxy in a process. Since #2101 the prebuilt classes are found, so runtime generation only happens for extends the generator can't see: rare in production builds, but every
extendin HMR dev mode. The full test suite ran in 116 s with the change, against 117–121 s without.Only where the collector may be mark-compact.
ProxyRegistrationGcdecides once per process, on the first injection, whether the collection is needed:/proc/self/mapspositively identifies the concurrent-copying collector. That means[anon:dalvik-...]mappings containingconcurrent copyingor(region space), and none containingmark-compact/Mark Compact. Both sets of names come from ART's collector setup; I checked them on API 33 (ART module 331711080) and API 36 (361153460) processes.Does your commit message include the wording below to reference a specific issue in this repo?
No issue.
Related Pull Requests
PathClassLoader.Does your pull request have unit tests?
Yes, for the guard.
testProxyRegistrationGc.jschecks the decision on synthetic maps:The GC race itself needs hundreds of registrations under allocation pressure to show up, so it's covered by the stress numbers above rather than a suite spec.
./gradlew runtestsAndVerifyResults -Pabis=arm64-v8a, full suite:System.gc()calls)Summary by CodeRabbit