From 4918314444c3f336e19c837a474a0c6953e63b9f Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Sun, 4 Oct 2026 16:06:20 -0500 Subject: [PATCH] fix: stop leaking a JNI global ref per ArrayBuffer passed to Java The first time a JS ArrayBuffer, SharedArrayBuffer or typed array crosses to Java, JsArgConverter and JsArgToArrayConverter wrap it in a direct ByteBuffer and take a JNI global reference to it that is never deleted. GetOrCreateObjectId already keeps the buffer strongly reachable from Java for as long as the JS object lives, so the extra reference only pins it: every distinct buffer stays in the global reference table for the life of the process. Because JSObjectFinalizer keeps a JS object alive while its Java counterpart is, the ArrayBuffer's memory is never freed either. An app that hands Java a fresh buffer per frame overflows ART's 51200-entry table within hours and aborts with "JNI ERROR (app bug): global reference table overflow". Co-Authored-By: Claude Opus 5.5 --- .../main/assets/app/tests/byte-buffer-test.js | 29 ++++++++++++++++++- .../runtime/src/main/cpp/JsArgConverter.cpp | 7 +++-- .../src/main/cpp/JsArgToArrayConverter.cpp | 7 +++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/test-app/app/src/main/assets/app/tests/byte-buffer-test.js b/test-app/app/src/main/assets/app/tests/byte-buffer-test.js index 8ed0e592f..675559f56 100644 --- a/test-app/app/src/main/assets/app/tests/byte-buffer-test.js +++ b/test-app/app/src/main/assets/app/tests/byte-buffer-test.js @@ -80,4 +80,31 @@ describe("Tests mapped ByteBuffer conversion", function () { } expect(exceptionCaught).toBe(true); }); -}); \ No newline at end of file +}); + +describe("Tests JNI references of ArrayBuffers passed to Java", function () { + // ART aborts the process once its JNI global reference table holds 51200 + // entries. Each ArrayBuffer or typed array crossing to Java for the first + // time used to take a global reference that was never deleted, so these + // loops cross more distinct buffers than the table can hold. + var MORE_THAN_THE_GLOBAL_REF_TABLE = 60000; + + it("should not keep a global reference per buffer passed as a method argument", function () { + var holder = new com.tns.tests.ByteBufferHolder(); + for (var i = 0; i < MORE_THAN_THE_GLOBAL_REF_TABLE; i++) { + var buffer = i % 2 ? new ArrayBuffer(1) : new Uint8Array(1); + new Uint8Array(buffer.buffer || buffer)[0] = i & 0x7f; + holder.hold(buffer); + } + expect(holder.get(0)).toBe((MORE_THAN_THE_GLOBAL_REF_TABLE - 1) & 0x7f); + }); + + it("should not keep a global reference per buffer stored in a Java array", function () { + var array = Array.create(java.nio.ByteBuffer, 1); + for (var i = 0; i < MORE_THAN_THE_GLOBAL_REF_TABLE; i++) { + array[0] = new ArrayBuffer(1); + } + // reaching here without the runtime aborting is the real assertion + expect(array[0]).toBeTruthy(); + }); +}); diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index e5d82f0bd..14ac72260 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -331,8 +331,11 @@ bool JsArgConverter::ConvertArg(const Local &arg, int index) { buffer = directBuffer; } - buffer = env.NewGlobalRef(buffer); - + // The buffer needs no global ref of its own: GetOrCreateObjectId keeps + // it strongly reachable from Java until the JS object is collected. + // One taken here would never be deleted, pinning the buffer (and, via + // JSObjectFinalizer, its ArrayBuffer) and filling the JNI global + // reference table. int id = objectManager->GetOrCreateObjectId(buffer); auto clazz = env.GetObjectClass(buffer); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 63216a06f..b702b8474 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -334,8 +334,11 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalGetOrCreateObjectId(buffer); auto clazz = env.GetObjectClass(buffer); objectManager->Link(jsObj, id, clazz);