CVE-2026-91737
Overview
Background
- `PDFiumPage::PopulateAnnotationLinks()`
- A method in Chrome’s PDF plugin that walks a page’s link annotations to build the plugin’s list of clickable link targets.
- `FPDF_PAGE` / `GetPage()`
FPDF_PAGEis PDFium’s opaque handle to a loaded page;GetPage()returns the currently loaded handle, which becomes dangling once the page is unloaded.- `ScopedPageUnloadPreventer`
- An RAII guard that, while alive, pins a
PDFiumPageas loaded so PDFium cannot unload (and free) the underlying page object. - Embedded-PDF JavaScript
- Script embedded in a PDF (via actions/annotations) that PDFium can execute during document processing and that can re-enter the plugin and request a page unload.
Root Cause Analysis
PDFiumPage::PopulateAnnotationLinks() caches a raw FPDF_PAGE page = GetPage() and then iterates over link annotations with FPDFLink_Enumerate(page, &start_pos, &link_annot), resolving each via GetLinkTarget(). Per the commit, processing links during this loop can trigger JavaScript embedded in the PDF, which can re-enter the plugin and cause the page to be unloaded — freeing the object that page still points at. Because the cached page handle is used across every loop iteration, the next FPDFLink_Enumerate() call (and its downstream use) dereferences a freed page, a use-after-free. The invariant violated is that page must remain a valid, loaded handle for the entire duration of the enumeration loop.
The fix constructs a ScopedPageUnloadPreventer scoped_unload_preventer(this) immediately after obtaining page, so any unload attempt triggered by embedded JS is deferred until after the loop (and the guard’s scope) ends, keeping page valid throughout.
FPDF_PAGE handle across a loop whose body can synchronously run embedded JavaScript that unloads and frees that very page; the fix pins the page alive for the loop’s lifetime with a ScopedPageUnloadPreventer so re-entrant unloads cannot free the handle mid-iteration.Attack Path
- Craft a malicious PDF Build a PDF with link annotations plus embedded JavaScript wired to run during annotation/link processing.
- Trigger annotation-link population
Cause the PDF plugin to call
PDFiumPage::PopulateAnnotationLinks(), which cachespage = GetPage()and begins theFPDFLink_Enumerate()loop. - Force a page unload from within the loop
During link processing the embedded JS re-enters the plugin and requests unloading of the page, freeing the object that the cached
pagehandle references. - Reuse the freed handle
The loop’s next
FPDFLink_Enumerate()/GetLinkTarget()use dereferences the now-danglingpage, producing the use-after-free. - Exploit the freed allocation With control over reallocation of the freed page memory, an attacker may groom the heap to corrupt state and gain a read/write primitive or code execution in the PDF/renderer process (inferred).
Impact Assessment
Files Changed
pdf/pdfium/pdfium_page.cc
Audit Directions
- Cached page/handle across re-entrant loopsAudit any PDFium code that stores an
FPDF_PAGE(or similar raw handle) fromGetPage()and then loops calling PDFium APIs, since intervening embedded-JS execution can unload the page; require aScopedPageUnloadPreventer(or equivalent) for the handle’s lifetime. - Annotation/link/form processing entry pointsReview sibling methods that iterate annotations, links, or form fields (e.g. other
Populate*/Enumerate-driven loops) for the same missing unload guard. - JS-reentrancy boundariesTrace every plugin operation that can synchronously invoke embedded PDF JavaScript and verify no raw page/object pointer captured before the call is used after it without a lifetime guard.
Patch
From 8ff0627e42eba19d9b72207b0b22dcfc60f3860c Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Fri, 11 Sep 2026 10:06:41 -0700
Subject: [PATCH] [PDF] Prevent page unloading in PDFiumPage::PopulateAnnotationLinks()
PDFiumPage::PopulateAnnotationLinks() can trigger JS embedded in PDFs,
which can in turn trigger page unloads. Avoid this with a
ScopedPageUnloadPreventer.
Bug: 558036280
Change-Id: I8944a1ea5879029be30051edb1a80d1d557de3a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393364
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1696369}
---
diff --git a/pdf/pdfium/pdfium_page.cc b/pdf/pdfium/pdfium_page.cc
index 9543e49..7d8fd62 100644
--- a/pdf/pdfium/pdfium_page.cc
+++ b/pdf/pdfium/pdfium_page.cc
@@ -1544,6 +1544,8 @@
int start_pos = 0;
FPDF_LINK link_annot;
FPDF_PAGE page = GetPage();
+ // Make sure `page` stays valid for the duration of the loop.
+ ScopedPageUnloadPreventer scoped_unload_preventer(this);
while (FPDFLink_Enumerate(page, &start_pos, &link_annot)) {
Link link;
Area area = GetLinkTarget(link_annot, &link.target);