[ 
https://issues.apache.org/jira/browse/PDFBOX-6236?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18111308#comment-18111308
 ] 

Gustavo A. commented on PDFBOX-6236:
------------------------------------

That remainder is worth a number: on the attached {{already-signed.pdf}} it 
takes no signing and
no DSS, just {{addPage}} and {{saveIncremental}}, and 43 still comes out.

{noformat}
getHighestXRefObjectNumber() = 42   but that file's newest trailer says /Size 44
new page written as            43   = the number revision 4's /XRef stream 
occupies at offset 76040
PDFBox reads 2 pages; pypdf 6.16.2 reads 1, resolving 43 0 R to the /XRef 
dictionary
{noformat}

Same on 3.0.6, 3.0.7, 3.0.8 and on today's 3.0.9-SNAPSHOT and 4.0.0-SNAPSHOT. 
So PDFBOX-6176
stopped PDFBox from producing files in this shape, but not from reusing the 
number in one it is
handed.

The seed is in {{COSWriter#write}}: {{number = getHighestXRefObjectNumber()}}, 
the highest number
with an *in-use* entry. 43 has none, so it looks unallocated even though the 
trailer being
extended already claims 0..43. {{max(that, previous trailer /Size - 1)}} would 
cover it.

Tilman, that is also why the warnings stop at 32: {{checkCrossReferenceTable}} 
walks
{{getXrefTable().keySet()}} and stops at its last key, 42, so it never reaches 
the one number that
actually collided.

The same condition arrives from free entries rather than missing ones, with 
reproducers that need
no signature and no prior PDFBox: PDFBOX-6252.


> PDDocument.saveIncremental() adds a new field object whose number collides 
> with the increment's own XRef stream object, on a PDF that already contains a 
> prior signature
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: PDFBOX-6236
>                 URL: https://issues.apache.org/jira/browse/PDFBOX-6236
>             Project: PDFBox
>          Issue Type: Bug
>    Affects Versions: 3.0.6 PDFBox
>         Environment: - Java 25 (Eclipse Temurin 25+36)
> - OS: Linux
>            Reporter: Riadh
>            Priority: Major
>         Attachments: PDFBOX-BUG-REPORT.md, ReproPdfBoxOnly.java, 
> already-signed.pdf, with-empty-field.pdf
>
>
> ## Summary
> Adding a new AcroForm field (a `PDSignatureField`, but the field type does 
> not appear to matter) to a PDF that already contains one prior signature, 
> then saving it with `PDDocument#saveIncremental(OutputStream)`, produces a 
> document in which the new field's indirect object number is ambiguous: 
> PDFBox's own reader resolves it to the expected field dictionary, but any 
> reader that builds a flat cross-reference map across the full `/Prev` 
> revision chain (e.g. pypdf, PyPDF2 — tested with both, same result) resolves 
> the very same object number to the incremental update's own `/XRef` stream 
> dictionary instead. No signing is actually performed in this reproduction — 
> this is a pure `saveIncremental()` issue, independent of any 
> digital-signature or third-party library (e.g. it does **not** require the EU 
> DSS library; the attached reproduction uses only `org.apache.pdfbox.*` 
> classes).
> ## Steps to reproduce
> 1. Start from any PDF that already has one prior signature (any signature 
> type/level). In the attached `already-signed.pdf`, the highest existing 
> object number is 42 (`COSDocument#getHighestXRefObjectNumber() == 42`), the 
> AcroForm has 2 fields: a `/Sig` field at object 9 and a text field at object 
> 10. 2. Add a new signature field and save incrementally:
> {code:java}
> try (PDDocument doc = Loader.loadPDF(new File("already-signed.pdf"))) {
>     PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
>     PDPage page = doc.getPage(0);
>     PDSignatureField signatureField = new PDSignatureField(acroForm);
>     signatureField.setPartialName("signature_signed_pdfbox_only");
>     PDAnnotationWidget widget = signatureField.getWidgets().get(0);
>     widget.setRectangle(new PDRectangle(50, 50, 100, 50));
>     widget.setPage(page);
>     page.getAnnotations().add(widget);
>     acroForm.getFields().add(signatureField);
>     try (FileOutputStream fos = new FileOutputStream("with-empty-field.pdf")) 
> {
>         doc.saveIncremental(fos);
>     }
> } {code}
> (Full source in the attached `ReproPdfBoxOnly.java`.)
> 3. Inspect the resulting `with-empty-field.pdf`.
> ## Expected result
> The AcroForm's `/Fields` array has 3 entries, and the new entry (object 
> number 43 in this reproduction) resolves — consistently, from any conformant 
> PDF reader — to the new field's `/Widget` dictionary.
> ## Actual result
> - Re-opening the file **with PDFBox itself** resolves object `43 0 R` 
> correctly:
> {code:java}
> field[2] objNum=43 /Type=Annot /FT=Sig /T=signature_signed_pdfbox_only 
> hasV=false {code}
> - Reading the **same file** with `pypdf` (6.16.1) or `PyPDF2` (both tested, 
> identical result) resolves the very same reference (`43 0 R`) in the 
> AcroForm's `/Fields` array to the increment's own `/XRef` stream dictionary 
> instead:
> {code:java}
> >>> fields[2].get_object()
>   {'/ID': [...], '/Info': ..., '/Root': ..., '/Prev': 55240, '/Type': '/XRef',
>    '/Size': 44, '/Index': [0, 2, 33, 10], '/W': [1, 3, 0], '/Filter': 
> '/FlateDecode'}
>   >>> pdf.get_fields().keys()
>   dict_keys(['signature', 'prénom'])   # the new field is invisible {code}
> Object number `43` is therefore used ambiguously within the same incremental 
> update: PDFBox's writer appears to reuse/collide the number allocated to the 
> new field object with the number allocated to the revision's own `/XRef` 
> stream object. Since a standards-compliant reader merging the `/Prev` xref 
> chain keeps only the *last* xref entry seen per object number, and PDFBox 
> itself resolves it one way while other libraries resolve it the other way, 
> this indicates PDFBox's `COSWriter` writes conflicting/duplicate xref 
> information for object `43` within this single incremental revision.
>  
> ## Suggested area to investigate
> `org.apache.pdfbox.pdfwriter.COSWriter`:
> - The `number` counter used both to allocate new object keys while writing 
> the body (`getObjectKey(COSBase)`) and to allocate the xref-stream's own key 
> in `doWriteXRefInc()` (`COSObjectKey xrefStreamKey = new 
> COSObjectKey(++number, 0)`).
> - This counter is seeded from `COSDocument#getHighestXRefObjectNumber()`, 
> itself computed by `COSParser` by merging the `/Prev` xref chain at load time 
> (`org/apache/pdfbox/pdfparser/COSParser.java`, around the code that does 
> `document.setHighestXRefObjectNumber(...)` after building 
> `document.getXrefTable()`).
> - When the input document already has objects from a prior 
> signature/incremental revision, this next-object-number computation appears 
> not to always fully account for the existing revision chain, so the number 
> handed out for the new field object collides with the number subsequently 
> used (or already used) for the increment's own xref-stream object. 
> This looks adjacent to — but distinct from — PDFBOX-4997 (unmodified 
> `COSName`-only objects being needlessly rewritten during incremental 
> updates), which was found while investigating this issue but does not by 
> itself explain the object-number collision described here.
> ## Impact
> Any workflow that adds a new form/signature field to a PDF that already 
> carries a prior signature, then saves incrementally (a common pattern for 
> sequential multi-signer PAdES workflows, e.g. via the EU DSS library's 
> `PAdESService#addNewSignatureField`, which is how we first found this) 
> produces a PDF where the newly added field is not reliably discoverable by 
> other PDF libraries, even though PDFBox's own reader can still find it — a 
> real interoperability/validation risk for any consumer not using this exact 
> PDFBox version and reading strategy.
> ## Attachments
> - `already-signed.pdf` — input document with 1 existing signature. 
> - `with-empty-field.pdf` — output of the reproduction, already exhibiting the 
> corrupted `/Fields` entry. 
> - `ReproPdfBoxOnly.java` — full minimal reproduction source (pure PDFBox, no 
> external dependency beyond PDFBox itself).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to