diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 7f96d0a8c..2349e5cc8 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -71,6 +71,7 @@ require("./tests/testTypedArrayArgs"); require("./tests/dex-interface-implementation"); require("./tests/testClassForNameDiscovery"); require("./tests/testPrebuiltExtendClasses"); +require("./tests/testProxyRegistrationGc"); require("./tests/testInterfaceImplementation"); require("./tests/testPrimitiveCallbackReturns"); require("./tests/testRuntimeImplementedAPIs"); diff --git a/test-app/app/src/main/assets/app/tests/testProxyRegistrationGc.js b/test-app/app/src/main/assets/app/tests/testProxyRegistrationGc.js new file mode 100644 index 000000000..3fcab8ef7 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testProxyRegistrationGc.js @@ -0,0 +1,82 @@ +describe("Proxy registration GC guard", function () { + var Guard = com.tns.ProxyRegistrationGc; + + function maps(names) { + var text = names.map(function (name, i) { + return "7f0000" + i + "000-7f0000" + i + "fff rw-p 00000000 00:00 0 " + name; + }).join("\n"); + return new java.io.BufferedReader(new java.io.StringReader(text)); + } + + var markCompact = [ + "[anon:dalvik-main space]", + "[anon:dalvik-Concurrent mark-compact compaction buffers]", + "[anon:dalvik-Concurrent Mark Compact live words bitmap]", + "/apex/com.android.art/lib64/libart.so" + ]; + var concurrentCopying = [ + "[anon:dalvik-main space (region space)]", + "[anon:dalvik-concurrent copying gc mark stack]", + "[anon:dalvik-region space live bitmap]", + "/apex/com.android.art/lib64/libart.so" + ]; + + it("collects under the mark-compact collector", function () { + expect(Guard.isRequired(36, maps(markCompact))).toBe(true); + }); + + it("skips the collection under the concurrent-copying collector", function () { + expect(Guard.isRequired(33, maps(concurrentCopying))).toBe(false); + }); + + it("never collects before ART became updatable", function () { + expect(Guard.isRequired(30, maps(markCompact))).toBe(false); + expect(Guard.isRequired(21, maps([]))).toBe(false); + }); + + it("collects when the heap mappings are unnamed", function () { + expect(Guard.isRequired(33, maps(["/apex/com.android.art/lib64/libart.so", "[stack]"]))).toBe(true); + }); + + it("collects when no known collector is recognized", function () { + expect(Guard.isRequired(36, maps(["[anon:dalvik-main space]", "[anon:dalvik-card table]"]))).toBe(true); + }); + + it("collects when mark-compact and region space names both appear", function () { + expect(Guard.isRequired(36, maps(concurrentCopying.concat(markCompact)))).toBe(true); + }); + + it("matches the collector this process is running", function () { + var reader = new java.io.BufferedReader(new java.io.FileReader("/proc/self/maps")); + var sawMarkCompact = false; + var sawConcurrentCopying = false; + try { + var line; + while ((line = reader.readLine()) !== null) { + var lower = String(line).toLowerCase(); + if (lower.indexOf("[anon:dalvik-") < 0) { + continue; + } + if (lower.indexOf("mark-compact") >= 0 || lower.indexOf("mark compact") >= 0) { + sawMarkCompact = true; + } + if (lower.indexOf("concurrent copying") >= 0 || lower.indexOf("(region space)") >= 0) { + sawConcurrentCopying = true; + } + } + } finally { + reader.close(); + } + + var sdk = android.os.Build.VERSION.SDK_INT; + if (sdk < 31) { + expect(Guard.isRequired()).toBe(false); + } else if (sawMarkCompact) { + expect(Guard.isRequired()).toBe(true); + } else if (sawConcurrentCopying) { + expect(Guard.isRequired()).toBe(false); + } else { + expect(Guard.isRequired()).toBe(true); + } + }); +}); diff --git a/test-app/runtime/src/main/java/com/tns/DexFactory.java b/test-app/runtime/src/main/java/com/tns/DexFactory.java index cd71f13b3..d913a023b 100644 --- a/test-app/runtime/src/main/java/com/tns/DexFactory.java +++ b/test-app/runtime/src/main/java/com/tns/DexFactory.java @@ -177,8 +177,14 @@ public Class resolveClass(String baseClassName, String name, String className Class result = null; String classNameToLoad = isInterface ? fullClassName : desiredDexClassName; - if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader - && injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath)) { + boolean injectIntoParent = injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader; + if (injectIntoParent && ProxyRegistrationGc.isRequired()) { + // The dex is registered by the first loadClass below, so the blocking + // collection has to stay immediately before it. + java.lang.Runtime.getRuntime().gc(); + } + + if (injectIntoParent && injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath)) { try { result = classLoader.loadClass(classNameToLoad); } catch (ClassNotFoundException e) { diff --git a/test-app/runtime/src/main/java/com/tns/ProxyRegistrationGc.java b/test-app/runtime/src/main/java/com/tns/ProxyRegistrationGc.java new file mode 100644 index 000000000..91a7d7e4e --- /dev/null +++ b/test-app/runtime/src/main/java/com/tns/ProxyRegistrationGc.java @@ -0,0 +1,77 @@ +package com.tns; + +import android.os.Build; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.Locale; + +/** + * Whether DexFactory has to run a blocking collection before it registers a + * generated dex with the app's long-lived class loader. + * + * ART's concurrent mark-compact collector can lose the DexCache location store + * when a collection overlaps ClassLinker::RegisterDexFile, and the app then dies + * with a SIGSEGV in RegisterDexFileLocked. That collector only reaches devices + * through the ART module, which is updatable from API 31. + * + * The collection is skipped only when the process's heap mappings positively + * identify the concurrent-copying collector. A collector that can't be + * identified, including future ART builds with renamed mappings, is treated as + * affected. + */ +public final class ProxyRegistrationGc { + private static final int FIRST_UPDATABLE_ART_SDK = 31; + + private static volatile Boolean required; + + private ProxyRegistrationGc() { + } + + public static boolean isRequired() { + Boolean result = required; + if (result == null) { + result = detect(); + required = result; + } + return result; + } + + private static boolean detect() { + if (Build.VERSION.SDK_INT < FIRST_UPDATABLE_ART_SDK) { + return false; + } + try (BufferedReader maps = new BufferedReader(new FileReader("/proc/self/maps"))) { + return isRequired(Build.VERSION.SDK_INT, maps); + } catch (IOException | RuntimeException e) { + return true; + } + } + + /** + * @param maps the contents of /proc/self/maps; ART names its heap mappings + * "[anon:dalvik-...]" on kernels that support anonymous VMA names + */ + public static boolean isRequired(int sdkInt, BufferedReader maps) throws IOException { + if (sdkInt < FIRST_UPDATABLE_ART_SDK) { + return false; + } + boolean concurrentCopying = false; + String line; + while ((line = maps.readLine()) != null) { + int nameStart = line.indexOf("[anon:dalvik-"); + if (nameStart < 0) { + continue; + } + String name = line.substring(nameStart).toLowerCase(Locale.ROOT); + if (name.contains("mark-compact") || name.contains("mark compact")) { + return true; + } + if (name.contains("concurrent copying") || name.contains("(region space)")) { + concurrentCopying = true; + } + } + return !concurrentCopying; + } +}