Skip to content

fix(certifier): check wire message size before JSON deserialisation #1523

Closed
atharrva01 wants to merge 4 commits intohyperledger-labs:mainfrom
atharrva01:fix/certifier-comm-stack-size-guard
Closed

fix(certifier): check wire message size before JSON deserialisation #1523
atharrva01 wants to merge 4 commits intohyperledger-labs:mainfrom
atharrva01:fix/certifier-comm-stack-size-guard

Conversation

@atharrva01
Copy link
Copy Markdown
Contributor

@atharrva01 atharrva01 commented Apr 10, 2026

Closes #1557

Follow-on to #1498 based on your @adecaro's review feedback.

The MaxRequestBytes check I added in #1498 ran after full JSON deserialization , so a 500 MiB payload would still spike the heap before getting rejected. The fix needs to happen earlier.

I can't touch the transport layer directly, but ReceiveRaw() gets us close enough: now we check len(raw) > MaxWireMessageBytes before ever calling json.Unmarshal, so an oversized message gets dropped before any allocations for the IDs slice or Request field.

To keep this unit-testable without a real FSC session, I added a sessionFactory field (defaults to session.JSON) so tests can inject a fake that returns preset raw bytes.

Changes:

  • config.go , adds MaxWireMessageBytes = MaxRequestBytes * 2
  • service.go , switches to ReceiveRaw + size guard + manual unmarshal; adds sessionFactory
  • service_test.go , 4 new tests: oversized reject, at-limit pass-through, transport errors, constant relationship

@atharrva01
Copy link
Copy Markdown
Contributor Author

caught oversized payloads before deserialization using ReceiveRaw + size guard, cc @adecaro

@AkramBitar
Copy link
Copy Markdown
Contributor

@atharrva01 thanks a lot for opening the PR.
Could you please open an issue and link it to this PR?

Also, could you please have a look at the failing tests?

@adecaro
Copy link
Copy Markdown
Contributor

adecaro commented Apr 15, 2026

Hi @atharrva01 , thanks for the effort.
I'm not convinced by this one. I need we need a more systematic way to address these kind of issues without having to modify each view. For instance, a way to inject this limit in the comm stack of the smart client could be very helpful.
What do you think?

@atharrva01
Copy link
Copy Markdown
Contributor Author

hi @adecaro you are right, per-view patching doesn't scale.

In d5feb20 I've extracted the size guard into a reusable SizeLimitedJsonSession wrapper at token/services/utils/json/session/limited.go. It decorates any JsonSession and enforces a configurable byte limit before deserialization on every Receive / ReceiveRaw / ReceiveWithTimeout call. Two constructors: session.NewSizeLimitedSession(inner, maxBytes) for wrapping an existing session, and session.JSONWithLimit(ctx, maxBytes) as a drop-in for session.JSON(ctx).

CertificationService now uses JSONWithLimit as its default sessionFactory, so Call() goes back to a plain s.Receive() the guard is fully transparent to view logic. Any other service that wants the same protection just swaps session.JSON(ctx)session.JSONWithLimit(ctx, limit), no per-view boilerplate needed.

This stops short of injecting into the FSC comm stack directly (that would need upstream changes), but it gives a single, reusable session-layer abstraction any view in this repo can opt into. Happy to discuss if a deeper comm-stack hook makes more sense.

@atharrva01 atharrva01 force-pushed the fix/certifier-comm-stack-size-guard branch 2 times, most recently from 1d4acb2 to 2ba1638 Compare April 16, 2026 09:06
atharrva01 and others added 4 commits April 29, 2026 09:46
Adds MaxWireMessageBytes (2 MiB) and uses ReceiveRaw() to reject
oversized messages before json.Unmarshal, preventing heap spikes from
allocate-then-reject. Injects sessionFactory for unit testability.

Signed-off-by: atharrva01 <atharvaborade568@gamil.com>
Signed-off-by: atharrva01 <atharvaborade568@gamil.com>
…sion

Create session.SizeLimitedJsonSession, a JsonSession wrapper that enforces
a configurable byte limit on every received message before JSON decoding.
Expose it via session.NewSizeLimitedSession and session.JSONWithLimit so
any view can opt in without duplicating the size-check logic.

CertificationService now defaults its sessionFactory to JSONWithLimit, and
Call() reverts to the simpler s.Receive() call — the guard is transparent.

Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
@adecaro adecaro force-pushed the fix/certifier-comm-stack-size-guard branch from 0b0a346 to 0054c64 Compare April 29, 2026 07:46
@adecaro
Copy link
Copy Markdown
Contributor

adecaro commented Apr 29, 2026

Hi @atharrva01 , I think this check needs to be handled as earlier as possible. If we check at the application level, we risk resource exhaustion anyway.

So, I would close this PR, but I would like you to ask to check the fabric smart client (platform/view/services/comm/host) to double check that:

  • they have a maximum message size limit, and
  • this limit is configurable.

What do you think? Thanks for your effort in any case 🙏

@atharrva01
Copy link
Copy Markdown
Contributor Author

atharrva01 commented Apr 29, 2026

Hi @adecaro investigated platform/view/services/comm/host in FSC (at the version pinned in this repo: v0.10.2-0.20260428094934-a70a13e26c74). Here's what I found:

Does FSC have a max message size limit?

Yes both backends enforce a 10 MiB cap at the transport layer, before any application-level deserialization:

Backend File Constant Enforcement point
libp2p platform/view/services/comm/io/reader.go:46 maxMessageSize = 10 * 1024 * 1024 varintReader.ReadData() reads the varint length header, rejects before allocating the body buffer
WebSocket platform/view/services/comm/host/websocket/ws/streamreader.go:17 maxDelimitedPayloadSize = 10 * 1024 * 1024 delimitedReader.Read() rejects before accumulating the payload

Both checks happen as early as possible: the varint/length header is read first, and if it exceeds 10 MiB the message is rejected immediately no heap spike from body allocation.

Is the limit configurable?

No. Both constants are hardcoded. The comm config.go only exposes fsc.p2p.incomingMessagesBufferSize and fsc.p2p.streamReaderBufferSize there is no fsc.p2p.maxMessageSize or equivalent config key.

Conclusion

The protection the maintainer was looking for already exists at the FSC comm layer. The only missing piece is configurability that would require an upstream FSC change to expose the limit via fsc.p2p.maxMessageSize (or similar). This is out of scope for this repo.

Closing this PR as requested. Thanks for the guidance 🙏

@atharrva01 atharrva01 closed this Apr 29, 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.

bug(certifier): wire message deserialized before size guard fires, enabling heap exhaustion

3 participants