Riadh created PDFBOX-6236:
-----------------------------

             Summary: 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
         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