Repository navigation
Conversation
, wonday#1041) PDFView.recycle() disposed the PdfFile synchronously on the main thread while RenderingHandler could still be rendering a page on the "PDF renderer" thread, freeing the native pdfium document under the renderer (SIGSEGV in libpdfium.so). PdfView.recycle() now detaches the PdfFile before super.recycle() and posts its dispose on the rendering handler, so it runs after any in-flight render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderingHandler.proceed() reads PDFView.pdfFile when it dequeues a render task. PdfFileDisposer cleared the field before super.recycle() had stopped the handler and removed the queued tasks, so a task dequeued in between hit a NullPointerException on the "PDF renderer" thread. Stop the handler and drop the queued tasks before clearing the field, as PDFView.recycle() itself does before its own dispose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for this PR. Your root-cause analysis matches what we see in production. We applied it (head NPE at
|
| Build | Reloads | Fatal crashes | Renderer thread logs D/PdfiumCore: Already closed |
|---|---|---|---|
| 7.0.4 | 1,113 (3 runs) | 1: IllegalStateException: Already closed at PdfDocument.openPage ← RenderingHandler.proceed:109 |
on 60–82% of reloads |
| 7.0.4 + this PR | 109 (2 runs) | 2: the NPE above, after 45 and 64 reloads | 0 |
| 7.0.4 + drain (below) | 2,280 (3 runs) | 0 | 0 |
The deferred dispose does remove the overlap (0 "Already closed" lines). But clearing pdfFile ahead of it turns the dequeued-task window into the dominant crash at this reload rate.
What we're using instead: drain the renderer before recycle()
It uses the same package trick, but waits for the in-flight task instead of deferring the close:
package com.github.barteksc.pdfviewer;
import android.os.Looper;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public final class RenderingDrain {
private static final long TIMEOUT_MS = 1000;
private RenderingDrain() {}
public static void drain(PDFView view) {
final RenderingHandler renderingHandler = view.renderingHandler;
if (renderingHandler == null) {
return;
}
renderingHandler.stop();
renderingHandler.removeMessages(RenderingHandler.MSG_RENDER_TASK);
if (Looper.myLooper() == renderingHandler.getLooper()) {
return;
}
final CountDownLatch idle = new CountDownLatch(1);
if (!renderingHandler.postAtFrontOfQueue(idle::countDown)) {
return;
}
try {
idle.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}// org/wonday/pdf/PdfView.java
@Override
public void recycle() {
RenderingDrain.drain(this);
super.recycle();
}- Why it's safe: once the latch fires, the renderer is idle and its queue is empty. So
super.recycle()'s synchronousdispose()can't overlap a render, and no task can start afterpdfFileis cleared. - UI-thread cost: the UI thread waits for at most the in-flight tile. That's roughly what
PdfDocument.close()already does today, since it blocks onPdfiumCore.lockduring a native render. - The 1 s timeout only matters if the renderer is stuck. In that case the in-flight task is already past
openPage, so the close just waits for the lock as before. - Unmount:
onDetachedFromWindow()drains inrecycle()beforequitSafely(). On unmount, pdfium's open-document count returned to 0 every time in our tests.
Native memory: on 7.0.4, every renderer-side "Already closed" also leaked a pdfium page, because PdfPage.close() returns early under IGNORE once the document is closed. That's roughly 0.3 MB each, about +187 MB of native heap over 760 reloads. With the drain, native heap stayed flat across all three runs.
Happy to open this as a PR, or contribute it as a commit to this branch, if that's useful.
Fixes #882
Fixes #976
Fixes #987
Fixes #989
Fixes #1024
Fixes #1041
Likely fixes #847
Summary
On Android, closing a
<Pdf>viewer (or changing itssource) while a page is still being rendered can crash the whole app with a nativeSIGSEGVinlibpdfium.so. This PR makes the native document close wait until any in-flight render has finished.Root cause
PDFView(com.github.zacharee:AndroidPdfViewer:4.0.1) renders pages on a dedicatedHandlerThread("PDF renderer",RenderingHandler).PDFView.recycle()runs on the main thread and does, in this order:renderingHandler.stop()+removeMessages(MSG_RENDER_TASK). This drops queued render tasks, but cannot interrupt the one currently running.pdfFile.dispose(), which synchronously closes the native pdfium document.Neither
PdfFile.dispose()norPdfFile.renderPageBitmap()take a lock (checked in the 4.0.1 bytecode: nomonitorenter). So if a render is in progress, the document is freed under the renderer → use-after-free in pdfium:FPDF_FFLDraw←PdfPage.nativeRenderPageBitmap+668←PdfFile.renderPageBitmap←RenderingHandler.proceedlibpdfium.so(FT_Done_Face) still occurring on `react-native-pdf@7.0.4 #1024:FT_Done_Face←PdfPage.nativeClosePage←PdfiumCore.renderPageBitmap←RenderingHandler.proceedrecycle()is reached from two paths, both affected:onDetachedFromWindow()→recycle()PDFView.Configurator.load()→recycle()AlreadyClosedBehavior.IGNORE(#989) only covers the case where pdfiumandroid sees the document is already closed before entering native code. It cannot protect a native call that is already running when the document is freed.This also explains why #989 was still reported on 7.0.4 after #999: IGNORE hides the Java-side IllegalStateException, but the document can still be closed while a render is in progress
Fix
PdfView.recycle()is overridden to:pdfFilefrom the view (set the field tonull), sosuper.recycle()skips its synchronousdispose(),super.recycle()as before (stop rendering, clear the cache, reset state…),pdfFile.dispose()on the view'sRenderingHandler.RenderingHandleris a single-threaded looper, so the posted dispose can only run after the render currently in progress (queued render tasks have already been removed byrecycle()). Close and render can no longer overlap.The helper
PdfFileDisposerlives in thecom.github.barteksc.pdfviewerpackage becausePDFView.pdfFile,PDFView.renderingHandlerand thePdfFileclass are package-private.Why it is safe
onDetachedFromWindow()callsrecycle()beforerenderingHandlerThread.quitSafely(), andquitSafely()still processes messages already queued. If the looper has already quit,post()returnsfalse: nothing can be rendering any more, so the document is disposed directly on the calling thread.pdfFile. The steps ofsuper.recycle()that run before its original dispose (animation stop, gesture disable, cache recycle, scroll handle) don't touchpdfFile, andrecycle()runs atomically on the main thread, so no gesture callback can interleave.DecodingAsyncTaskopens the new one. In pdfiumandroid 1.0.32, bothPdfiumCore.newDocument()andPdfDocument.close()synchronize on the same globalPdfiumCore.lock, with no nested lock. The new document gets a newRenderingHandler/PdfFile, so the two never mix.PdfFile.dispose()only callsPdfiumCore.closeDocument()and clears fields; nothing in it needs the main thread. As a side benefit, the main thread no longer blocks on the native close.android/build.gradle.Why not upgrade
io.legere:pdfiumandroid?pdfiumandroid ≥ 2.0.0 adds a configurable
LockManagerthat addresses this kind of race. But its bundledlibpdfium.soneeds API 26 at the native-link level, even though the module declaresminSdk 24. On API < 26 the library fails to load:This is the crash reported in #979 when this project moved to 1.0.34, and why it was reverted to 1.0.32 (c1879d0). I reproduced it again with 2.0.3 on an API 25 emulator and reported it upstream: johngray1965/PdfiumAndroidKt#55.
react-native-pdf supports
minSdkVersion 21, so upgrading would mean either raising that to 26 (breaking for apps that still support Android 5–7) or shipping a library that crashes at load on those devices. This fix stays on 1.0.32, needs no native rebuild and changes no public API. If upstream ships an API-21-compatible binary, the dependency can be upgraded later, and this workaround stays harmless.Testing
Stress test on a physical device: Samsung Galaxy Tab Active5 (SM-X306B), Android 16 / API 36, arm64, debug build of
FabricExample. A test screen mounts a localfile://PDF and tears the viewer down 0–150 ms afteronLoadComplete(20 % of iterations before load completes), in two modes:onDetachedFromWindow→recycle())RuntimeException: Get page pdf document nullon "PDF renderer"Configurator.load()→recycle())SIGSEGVinlibpdfium.so,nativeRenderPageBitmap+668(same as #1041) after ~89 loadsMemory: same native heap growth per mount/unmount with and without this PR (≈ 3.0 MB per load in both cases, measured outside the race window). The PR doesn't introduce a leak, and every load has a matching
PdfDocument.close. That growth already exists on master and is unrelated to this change. I'll report it separately.