fix(network-driver): bound tcc query request size and item count (#2050) - #2264
Draft
AkramBitar wants to merge 1 commit into
Draft
fix(network-driver): bound tcc query request size and item count (#2050)#2264AkramBitar wants to merge 1 commit into
AkramBitar wants to merge 1 commit into
Conversation
AkramBitar
force-pushed
the
fix-2050-tcc-query-limits
branch
from
August 18, 2026 21:58
73a8bac to
6ae8269
Compare
The chaincode's read-only queries — queryStates, queryTokens, areTokensSpent — performed one ledger read per element of an untrusted JSON array, with no cap on array length or payload size. Unlike invoke, bounded by driver.ResourceLimits, they are not reached through the size-limited transaction-submission flow, so any client able to call them could drive an unbounded number of GetState calls from a single request. Add tcc.QueryLimits: MaxQueryRequestBytes (1 MiB, checked before the JSON decode) and MaxQueryItems (4096, checked before the first ledger read). Defaults replace any field left unset or negative, so an unconfigured chaincode is still bounded; the standalone chaincode process overrides them via TOKEN_QUERY_MAX_REQUEST_BYTES / TOKEN_QUERY_MAX_ITEMS. These limits are not consensus-relevant: the query path performs no writes and is not an endorsement boundary. Also fix a nil-pointer dereference the new fuzzing found: a `null` element in a queryTokens array decodes to a nil *token.ID that translator.QueryTokens dereferenced. Tests cover the exact boundaries, rejection with zero GetState calls for each of the three query functions, and one fuzz target per function (persisted corpora plus nightly matrix entries). Fixes #2050 Signed-off-by: AkramBitar <akram@il.ibm.com>
AkramBitar
force-pushed
the
fix-2050-tcc-query-limits
branch
from
August 18, 2026 22:12
6ae8269 to
6f832b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The chaincode's read-only queries —
queryStates,queryTokens,areTokensSpent— perform oneledger read per element of a caller-supplied JSON array, with no cap on array length or payload
size.
invokeis bounded bydriver.ResourceLimits; the query path is not, and it doesn't gothrough the size-limited transaction-submission flow either.
Today: attacker sends
queryStatesan array of 10M keys (no signature, wallet or privilegesneeded) → the whole array is unmarshalled → 10M
GetStatecalls in one invocation → the peer istied up. Repeat concurrently for resource exhaustion. Same for the other two.
Fix
Two limits, both applied before any work proportional to the input:
MaxQueryRequestBytesjson.UnmarshalMaxQueryItemsAfter: oversized payload → rejected before decoding, zero
GetStatecalls. Retry with 5,000short keys → item cap rejects it before the read loop, zero reads. A legitimate 4,096-key request
is served exactly as before.
WithDefaults()replaces any field left unset or negative, so an unconfigured chaincode is stillbounded and a config typo can't disable a limit. The standalone process overrides via
TOKEN_QUERY_MAX_REQUEST_BYTES/TOKEN_QUERY_MAX_ITEMS. Not consensus-relevant — the query pathwrites nothing and isn't an endorsement boundary — so no lockstep rollout needed.
Bonus: a panic the new fuzzing found
["queryTokens", "[null]"]— 8 bytes, inside every limit — panicked: a JSONnulldecodes to a nil*token.IDthattranslator.QueryTokensdereferenced.Invoke'srecover()meant no peer crash,but every such request logged a full stack trace, and direct translator callers have no recovery at
all. Fixed at the root; both triggering payloads are kept in the fuzz corpus.
Tests
GetStatecalls; at exactlyMaxQueryItemsserved with one read per element; unconfigured chaincode still bounded. Verifiedthese fail with the guards removed.
queryStatesusesthe string as a key verbatim;
areTokensSpentadds validator init and, with graph hiding, thecomposite-key builder;
queryTokensdecodes structs). Nightly matrix entries for all three.f.Addcarries the seed shapes;testdata/fuzz/holds only the 8 files that benefit from being ondisk — the
MaxQueryItemsboundary pair per target and the two crash reproducers, which report asnamed subtests and outlive the build cache the generated corpus lives in.
make checksandmake lint-auto-fixclean;./token/services/network/...green,tccalso under-race.Docs
New
docs/security/tcc_query_limits.md, linked fromdocs/services/network-fabric.mdanddocs/configuration.md.Fixes #2050
🤖 Generated with Claude Code