Skip to content

fix(android): defer PdfFile dispose to the rendering thread to fix SIGSEGV in libpdfium - #1045

Open
GaelCO wants to merge 2 commits into
wonday:masterfrom
GaelCO:fix-Native-SIGSEGV
Open

GaelCO wants to merge 2 commits into
wonday:masterfrom
GaelCO:fix-Native-SIGSEGV

Conversation

@GaelCO

@GaelCO GaelCO commented Oct 1, 2026 •

Copy link
Copy Markdown

Fixes #882
Fixes #976
Fixes #987
Fixes #989
Fixes #1024
Fixes #1041

Likely fixes #847

Summary

On Android, closing a <Pdf> viewer (or changing its source) while a page is still being rendered can crash the whole app with a native SIGSEGV in libpdfium.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 dedicated HandlerThread ("PDF renderer", RenderingHandler). PDFView.recycle() runs on the main thread and does, in this order:

  1. renderingHandler.stop() + removeMessages(MSG_RENDER_TASK). This drops queued render tasks, but cannot interrupt the one currently running.
  2. pdfFile.dispose(), which synchronously closes the native pdfium document.

Neither PdfFile.dispose() nor PdfFile.renderPageBitmap() take a lock (checked in the 4.0.1 bytecode: no monitorenter). So if a render is in progress, the document is freed under the renderer → use-after-free in pdfium:

recycle() is reached from two paths, both affected:

  • unmount: onDetachedFromWindow() → recycle()
  • source change on a mounted view: 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:

  1. detach pdfFile from the view (set the field to null), so super.recycle() skips its synchronous dispose(),
  2. call super.recycle() as before (stop rendering, clear the cache, reset state…),
  3. post pdfFile.dispose() on the view's RenderingHandler.

RenderingHandler is a single-threaded looper, so the posted dispose can only run after the render currently in progress (queued render tasks have already been removed by recycle()). Close and render can no longer overlap.

The helper PdfFileDisposer lives in the com.github.barteksc.pdfviewer package because PDFView.pdfFile, PDFView.renderingHandler and the PdfFile class are package-private.

Why it is safe

  • The dispose always runs, no leak. On unmount, onDetachedFromWindow() calls recycle() before renderingHandlerThread.quitSafely(), and quitSafely() still processes messages already queued. If the looper has already quit, post() returns false: nothing can be rendering any more, so the document is disposed directly on the calling thread.
  • Nothing reads the detached pdfFile. The steps of super.recycle() that run before its original dispose (animation stop, gesture disable, cache recycle, scroll handle) don't touch pdfFile, and recycle() runs atomically on the main thread, so no gesture callback can interleave.
  • No deadlock / no overlap on reload. On a source change, the old document may now close on the render thread while DecodingAsyncTask opens the new one. In pdfiumandroid 1.0.32, both PdfiumCore.newDocument() and PdfDocument.close() synchronize on the same global PdfiumCore.lock, with no nested lock. The new document gets a new RenderingHandler / PdfFile, so the two never mix.
  • Thread-agnostic dispose. PdfFile.dispose() only calls PdfiumCore.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.
  • Fails loudly on dependency changes. If a future AndroidPdfViewer renames these fields, the build breaks at compile time rather than misbehaving at runtime. The version stays pinned in android/build.gradle.

Why not upgrade io.legere:pdfiumandroid?

pdfiumandroid ≥ 2.0.0 adds a configurable LockManager that addresses this kind of race. But its bundled libpdfium.so needs API 26 at the native-link level, even though the module declares minSdk 24. On API < 26 the library fails to load:

java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "strtof_l"

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 local file:// PDF and tears the viewer down 0–150 ms after onLoadComplete (20 % of iterations before load completes), in two modes:

Mode Without this PR With this PR
unmount (onDetachedFromWindow → recycle()) crash after ~114 loads: RuntimeException: Get page pdf document null on "PDF renderer" 500 iterations, 0 crash
source switch (Configurator.load() → recycle()) SIGSEGV in libpdfium.so, nativeRenderPageBitmap+668 (same as #1041) after ~89 loads 500 iterations, 0 crash

Memory: 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.

, 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>
@GaelCO GaelCO changed the title fix(android): defer PdfFile dispose to the rendering thread to fix SIG fix(android): defer PdfFile dispose to the rendering thread to fix SIGSEGV in libpdfium Oct 1, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment