Skip to content

Bound heap usage when digesting signed attachments - #385

Merged
phax merged 1 commit into
phax:masterfrom
shunkica:issue-380-markable-file-input-stream
Aug 9, 2026
Merged

Bound heap usage when digesting signed attachments#385
phax merged 1 commit into
phax:masterfrom
shunkica:issue-380-markable-file-input-stream

Conversation

@shunkica

@shunkica shunkica commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #380

Problem: WSS4J digests signed attachments by calling mark()/reset() on their streams, forcing the entire attachment into heap memory even when using temporary files for large payloads.

Solution: Introduced MarkableFileInputStream, a new stream implementation that performs mark()/reset() via FileChannel positioning instead of heap buffering. This is used at all file-backed attachment sources: outgoing files, compressed temp files, and incoming decrypted attachments.

Small attachments (≤64 KB) continue using byte-array streams unchanged. Signatures remain identical, only the carrier changes.

Testing: All existing tests pass (129 in phase4-lib, 200 in phase4-test). New tests verify mark/reset behavior and round-trip integrity. Probe on a 250 MB attachment with -Xmx64m: previously failed with OutOfMemoryError, now completes in 181 ms with identical digests.

Notes: Encrypted-and-signed attachments still heap-buffer the plaintext during digestion (separate mechanism, out of scope). See PR body for limitations.

WSS4J's AttachmentContentSignatureTransform#processAttachment digests each
signed attachment (cid: reference) by calling mark(Integer.MAX_VALUE) on the
attachment source stream, reading it to the end and calling reset(), so the
attachment stays readable afterwards. The file backed streams handed out so
far (NonBlockingBufferedInputStream) honour that mark by buffering everything
read after it on the heap, so the peak heap scales with the attachment size
(a 250 MB attachment peaks at ~390 MB) although the content is already spilled
to a temporary file. This affects the signing and the verification side alike.

Add com.helger.phase4.util.MarkableFileInputStream, which implements
mark/reset by re-positioning the underlying FileChannel, and hand it out at
all the file backed provider sites:
- WSS4JAttachment.createOutgoingFileAttachment (File and byte[] overloads)
- WSS4JAttachment.createIncomingFileAttachment (both overloads)
- SoapHeaderElementProcessorWSS4J (decrypted attachment temporary file)
- AS4IncomingHandler._createReadMultipleISP

Digesting then runs in constant heap - a 250 MB attachment completes at
-Xmx64m, where the previous stream throws an OutOfMemoryError - and the 2 GB
limit of heap buffering mark/reset (see the WSS4J comment) no longer applies
to file backed attachments; a 2.5 GB attachment was verified at -Xmx64m. The
digest input is unchanged, only the carrier of the same bytes is swapped.
In-memory attachments (<= 64 KB) keep their byte array streams, which already
support constant heap mark/reset.

Not covered: incoming attachments that are encrypted as well as signed. There
WSS4J digests its own CipherInputStream, which does not support mark/reset, so
AttachmentContentSignatureTransform wraps it in a BufferedInputStream itself
and buffers the plaintext on the heap. That needs a separate change on the
decryption path.
@shunkica
shunkica force-pushed the issue-380-markable-file-input-stream branch from 586ed51 to a3c44e1 Compare August 9, 2026 12:13
@phax
phax merged commit be014c7 into phax:master Aug 9, 2026
0 of 3 checks passed
phax added a commit that referenced this pull request Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signature digesting of file-backed attachments buffers the whole attachment on the heap

2 participants