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]