Guard queryByID against oversized peer responses to prevent node crash - #2153
Conversation
da28c96 to
7dbd051
Compare
|
@adecaro , Any suggestions regarding these changes? |
24c31e1 to
9127cfc
Compare
26bf4e4 to
7765c05
Compare
There was a problem hiding this comment.
hi @Effi-S , I spent some time in this function recently (#1426, #1999) so I read this one closely. The panic looks real to me: the loop ranges values but indexes keys, and since for ns, keys := range keysByNS shadows the outer parameter, the check is comparing against that namespace's keys, which is the right thing to compare against. Using != rather than > also seems right, since a short response would index in range but pair values with the wrong keys.
One thing I checked in case it was worth widening the PR: the finality twin at network/fabric/finality/deliveryqs.go does not have this bug, it walks the key set and fetches per txID with no positional indexing. So Fix A really is specific to the lookup path.
Ran the package at -count=2 -race, green. Three notes below, all non-blocking, and the first is more of a question than a suggestion.
3781ea1 to
96b6866
Compare
|
I'm going to push the out of bounds check only. |
Signed-off-by: Effi-S <effi.szt@gmail.com>
Fixes #2055
Summary
queryByIDunmarshals a single peer's raw chaincode-query response directly intovalues, then ranges over it while indexing the originalkeysslice by position — with nothing validating that the response returned exactlylen(keys)elements. This function runs inside a goroutine with norecover()anywhere in the call chain, so an oversized response crashes the whole process, not just the request.Where
token/services/network/fabric/lookup/deliveryqs.go:109-132:resis the raw response from a single peer's chaincode query (ChannelStateQuerier.QueryStates→Channel.Chaincode(ns).Query(...).Query()).Impact
queryByIDruns inside a goroutine spawned byQueryByID(go q.queryByID(...)) with norecover()anywhere in the call chain. A byzantine, buggy, or simply out-of-sync peer that answers aQueryStatesrequest with more elements than were requested for a given namespace drivesipast the end ofkeys, panicking that goroutine — which is unrecovered and therefore crashes the entire process, not just the one request. This is a full-availability attack surface reachable by anything capable of influencing or replacing a single peer's chaincode-query response.Fix
Two independent, complementary defenses:
A — Validate the response length before the loop. Never trust the peer to return the right number of values. A mismatch is treated like the other failure cases already handled in this function (bad marshal, query error): log it and fall back to the slower block scan instead of trusting the response.
B — Wrap the goroutine body in
recover()(defense in depth). Even after Fix A, any future unforeseen panic in this background path must degrade to a failed request rather than crashing the node.Tests
TestQueryByID_OversizedResponseindeliveryqs_test.gofeeds a crafted response with more values (2) than keys requested (1). Before the fix this panics; after Fix A it delivers nothing for the oversized namespace and falls back to the block scan: