From ad3fd77cec6f979fd2e5c199cda71fde8df773bd Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 15 Oct 2026 01:45:29 +0100 Subject: [PATCH] PDFBOX-6200: Link the document state to substructures without recursion COSUpdateState.setOriginDocumentState() descended into the entries of dictionaries and arrays and into dereferenced indirect objects through recursive calls, so linking the document state to a deeply nested structure, such as the objects of large CAD documents being signed with an incremental save, failed with a StackOverflowError (also reported in PDFBOX-6039). Traverse the structure with an explicit stack instead, as suggested on PDFBOX-6039, rather than limiting the depth. --- .../org/apache/pdfbox/cos/COSUpdateState.java | 69 ++++++++++++------- .../apache/pdfbox/cos/TestCOSUpdateInfo.java | 43 ++++++++++++ 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java index 315c5708213..167c6b1193d 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/cos/COSUpdateState.java @@ -16,6 +16,9 @@ */ package org.apache.pdfbox.cos; +import java.util.ArrayDeque; +import java.util.Deque; + /** * A {@link COSUpdateState} instance manages update states for a {@link COSUpdateInfo}. Such states are used to create * a {@link COSIncrement} for the incremental saving of a {@link COSDocument}. @@ -117,51 +120,71 @@ public void setOriginDocumentState(COSDocumentState originDocumentState) */ private void setOriginDocumentState(COSDocumentState originDocumentState, boolean dereferencing) { - if(this.originDocumentState != null || originDocumentState == null) + if (originDocumentState == null) { return; } - this.originDocumentState = originDocumentState; - if(!dereferencing) + // The structure is traversed with an explicit stack instead of recursion, as it may be + // nested deeper than the call stack allows (PDFBOX-6200). + Deque pending = new ArrayDeque<>(); + pending.push(this); + while (!pending.isEmpty()) { - update(); + COSUpdateState state = pending.pop(); + if (state.originDocumentState != null) + { + continue; + } + state.originDocumentState = originDocumentState; + if (!dereferencing) + { + state.update(); + } + state.pushSubstructures(pending); } - - if(updateInfo instanceof COSDictionary) + } + + /** + * Pushes the {@link COSUpdateState}s of the substructures directly contained by the managed + * {@link #updateInfo} onto the given stack. + * + * @param pending The stack of {@link COSUpdateState}s that still have to be processed. + */ + private void pushSubstructures(Deque pending) + { + if (updateInfo instanceof COSDictionary) { COSDictionary dictionary = (COSDictionary) updateInfo; - for(COSBase entry : dictionary.getValues()) + for (COSBase entry : dictionary.getValues()) { - if (entry instanceof COSUpdateInfo) - { - ((COSUpdateInfo) entry).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, entry); } } - else if(updateInfo instanceof COSArray) + else if (updateInfo instanceof COSArray) { COSArray array = (COSArray) updateInfo; - for(COSBase entry : array) + for (COSBase entry : array) { - if (entry instanceof COSUpdateInfo) - { - ((COSUpdateInfo) entry).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, entry); } } - else if(updateInfo instanceof COSObject) + else if (updateInfo instanceof COSObject) { COSObject object = (COSObject) updateInfo; if (object.isDereferenced()) { - COSBase reference = object.getObject(); - if (reference instanceof COSUpdateInfo) - { - ((COSUpdateInfo) reference).getUpdateState().setOriginDocumentState(originDocumentState, dereferencing); - } + pushUpdateState(pending, object.getObject()); } } } + + private static void pushUpdateState(Deque pending, COSBase base) + { + if (base instanceof COSUpdateInfo) + { + pending.push(((COSUpdateInfo) base).getUpdateState()); + } + } /** *

diff --git a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java index 37b477d9836..054eb615680 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/cos/TestCOSUpdateInfo.java @@ -17,6 +17,7 @@ package org.apache.pdfbox.cos; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -58,4 +59,46 @@ void testIsSetNeedToBeUpdate() assertFalse(testCOSObject.isNeedToBeUpdated()); } + + /** + * PDFBOX-6200: linking a document state to a deeply nested structure must not overflow the + * stack. + */ + @Test + void testSetOriginDocumentStateOfDeeplyNestedStructure() + { + COSDocumentState origin = new COSDocumentState(); + origin.setParsing(false); + + // a chain of dictionaries, arrays and dereferenced indirect objects, built from the end + COSDictionary deepest = new COSDictionary(); + COSBase current = deepest; + for (int i = 0; i < 50_000; i++) + { + switch (i % 3) + { + case 0: + COSDictionary dictionary = new COSDictionary(); + dictionary.setItem(COSName.getPDFName("Next"), current); + current = dictionary; + break; + case 1: + COSArray array = new COSArray(); + array.add(current); + current = array; + break; + default: + current = new COSObject(current); + break; + } + } + COSUpdateInfo root = (COSUpdateInfo) current; + + root.getUpdateState().setOriginDocumentState(origin); + + assertSame(origin, deepest.getUpdateState().getOriginDocumentState()); + assertTrue(root.isNeedToBeUpdated()); + assertTrue(deepest.isNeedToBeUpdated()); + } + }