fix(pdfium): don't double-free the buffer when PDFium refuses a document - #12
Open
mikepenz wants to merge 2 commits into
Open
fix(pdfium): don't double-free the buffer when PDFium refuses a document#12mikepenz wants to merge 2 commits into
mikepenz wants to merge 2 commits into
Conversation
`openPdfDocument` frees the native buffer in the `h == 0L` branch and again
in the enclosing `catch`, which the `error(...)` on the next line always
reaches. Any bytes PDFium refuses — a 401/404 HTML body, a JPEG served in
place of the PDF, a corrupt file — therefore abort the host process instead
of throwing:
signal 6 (SIGABRT), code -1 (SI_QUEUE)
Abort message: 'Scudo ERROR: invalid chunk state when deallocating address ...'
name: pdfium-shared >>> com.example.app <<<
NucleusFramework#5 scudo::Allocator<...>::deallocate(void*, ...)
NucleusFramework#8 dev.nucleusframework.pdfium.PdfDocument_androidKt$openPdfDocument$2.invokeSuspend
On the JVM the same path exits 134. Callers can't defend against it —
`PdfReaderState.open` already catches `Throwable`, but the free happens
below the JNI boundary.
Drop the inner `nFreeBuffer`; the `catch` owns it. The document handles
opened before the failure are still closed in the branch, as before.
androidMain and jvmMain only — iosMain pins/unpins the ByteArray and
webMain transfers an ArrayBuffer, so neither has a second free.
First test source set in the repo, so `:pdfium:check` (already run by
Pre Merge Checks) now exercises the JNI open path on the JVM.
`refusedBytesThrowInsteadOfAbortingTheProcess` is a regression test for the
double free: on the parent commit it does not fail, it takes the whole test
JVM down —
Process 'Gradle Test Executor 2' finished with non-zero exit value 134
(this value may indicate that the process was terminated with the SIGABRT signal)
Reaching the assertion at all is the signal. `aValidDocumentStillOpens`
pairs with it so the buffer's remaining single free stays exercised on the
success path too.
mikepenz
commented
Aug 3, 2026
| } | ||
|
|
||
| /** A minimal, valid one-page PDF (built-in /Helvetica, no embedded font program). */ | ||
| private fun minimalPdf(): ByteArray = java.util.Base64.getDecoder().decode( |
Author
There was a problem hiding this comment.
Feel free to replace this with a different PDF test file that you control.
There was a problem hiding this comment.
Pull request overview
Fixes a native crash in openPdfDocument (Android + JVM) caused by freeing the same native buffer twice when PDFium rejects input bytes, and adds a JVM regression test to ensure the failure path throws rather than aborting the process.
Changes:
- Remove the redundant
nFreeBuffer(bufferAddr)in theh == 0Lfailure branch on Android and JVM. - Add
jvmTestcoverage for both the refused-bytes path and a minimal valid PDF success path. - Wire
kotlin.testinto thejvmTestsource set dependencies.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt | Stops double-free on open failure (JVM actual). |
| pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt | Stops double-free on open failure (Android actual). |
| pdfium/src/jvmTest/kotlin/dev/nucleusframework/pdfium/OpenPdfDocumentTest.kt | Adds regression + success-path tests for JVM open. |
| pdfium/build.gradle.kts | Adds kotlin.test dependency for jvmTest. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
226
to
230
| if (h == 0L) { | ||
| // Cleanup previously opened handles + buffer on failure. | ||
| // Cleanup previously opened handles. The buffer is freed by the catch | ||
| // below — freeing it here too double-frees it (native abort). | ||
| for (j in 0 until i) PdfiumBridge.nCloseDocument(handles[j]) | ||
| PdfiumBridge.nFreeBuffer(bufferAddr) | ||
| error("PDFium refused to open document (err=${PdfiumBridge.nGetLastError()})") |
Comment on lines
201
to
205
| if (h == 0L) { | ||
| // NOTE: the buffer is freed by the catch below — freeing it here too | ||
| // double-frees it (native abort, not an exception). | ||
| for (j in 0 until i) PdfiumBridge.nCloseDocument(handles[j]) | ||
| PdfiumBridge.nFreeBuffer(bufferAddr) | ||
| error("PDFium refused to open document (err=${PdfiumBridge.nGetLastError()})") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
openPdfDocumentfrees the native buffer twice when PDFium refuses to open the bytes — once in theh == 0Lbranch, then again in the enclosingcatch, which theerror(...)on the very next line always reaches:Any bytes PDFium rejects hit it: a 401/404 HTML body served in place of the file, the wrong variant of an attachment, a corrupt or truncated download. The app doesn't get an exception — the process dies.
Android:
Desktop/JVM is affected identically (
PdfDocument.jvm.kthas the same shape) and exits134.This isn't defensible from the caller's side:
PdfReaderState.openalready wraps the call incatch (Throwable), but the double free happens below the JNI boundary, so there is nothing to catch. Validating bytes before calling is only a partial workaround — it can't cover encrypted or subtly corrupt PDFs that PDFium alone can decide on.The fix
Drop the inner
nFreeBuffer; thecatchalready owns the buffer. Document handles opened before the failure are still closed in the branch, unchanged.androidMain+jvmMainonly —iosMainpins/unpins theByteArrayandwebMaintransfers anArrayBuffer, so neither has a second free.Test
Adds
pdfium/src/jvmTest/— the first test source set in the repo, so:pdfium:check(already run by Pre Merge Checks, no workflow change needed) now covers the JNI open path.refusedBytesThrowInsteadOfAbortingTheProcess— on the parent commit this does not fail, it takes the test JVM down (finished with non-zero exit value 134 … SIGABRT). Reaching the assertion at all is the signal.aValidDocumentStillOpens— keeps the success path's single free exercised.Verified both directions locally on
darwin-aarch64::pdfium:jvmTestBUILD SUCCESSFULBUILD FAILED—Gradle Test Executor 2 finished with non-zero exit value 134One line of wiring:
jvmTest.dependencies { implementation(libs.kotlin.test) }(libs.kotlin.testwas already in the catalog).The four
configureCMakeDebug[<abi>]tasks fail on my machine for want of an NDK/CMake toolchain, unrelated to this change; CI has them.