fix(consensus): bound OP_RETURN opretlen + clamp notary pubkeys array

Defensive-audit findings (adversarially verified + fleet stability-tested):

#4 (CRITICAL) hush_voutupdate trusted an attacker-decoded OP_RETURN length
(opretlen, up to 65535 via OP_PUSHDATA2) with no check against the real script
length, driving up to ~64KB out-of-bounds reads through hush_stateupdate ->
hush_eventadd_opreturn -> hush_kvupdate (persisted to disk, leaked via kvsearch
RPC, reliable crash on block connect). Reject any opret claiming more bytes than
remain in the script, at the single taint source.

#5 (HIGH) notary-ratification loop did memcpy(pubkeys[numvalid++],..) into a
fixed uint8_t[64][33] with no bound; >64 crafted vouts smashed the stack. Clamp
numvalid < 64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:21:28 -05:00
parent 7e99311210
commit fc06a43dd7

View File

@@ -591,6 +591,14 @@ int32_t hush_voutupdate(bool fJustCheck,int32_t *isratificationp,int32_t notaryi
opretlen += (scriptbuf[len++] << 8); opretlen += (scriptbuf[len++] << 8);
} }
opoffset = len; opoffset = len;
// SECURITY (Finding #4): opretlen is attacker-controlled (up to 65535 via OP_PUSHDATA2)
// and was previously used with no bounds check. scriptbuf is a fixed DRAGON_MAXSCRIPTSIZE
// stack buffer in hush_connectblock, so an oversized opretlen drives out-of-bounds reads in
// the downstream 'K'/KV and notarization paths (persisted to disk, leaked via kvsearch RPC,
// reliable crash on block connect). Reject any opret claiming more bytes than actually
// remain in the real script; this mirrors the no-OP_RETURN fall-through so nothing valid changes.
if ( opretlen < 0 || opretlen > scriptlen - len )
return(notaryid);
matched = 0; matched = 0;
if ( SMART_CHAIN_SYMBOL[0] == 0 ) if ( SMART_CHAIN_SYMBOL[0] == 0 )
{ {
@@ -933,7 +941,7 @@ int32_t hush_connectblock(bool fJustCheck, CBlockIndex *pindex,CBlock& block)
if ( len >= sizeof(uint32_t) && len <= sizeof(scriptbuf) ) if ( len >= sizeof(uint32_t) && len <= sizeof(scriptbuf) )
{ {
memcpy(scriptbuf,(uint8_t *)&block.vtx[i].vout[j].scriptPubKey[0],len); memcpy(scriptbuf,(uint8_t *)&block.vtx[i].vout[j].scriptPubKey[0],len);
if ( len == 35 && scriptbuf[0] == 33 && scriptbuf[34] == 0xac ) if ( len == 35 && scriptbuf[0] == 33 && scriptbuf[34] == 0xac && numvalid < (int32_t)(sizeof(pubkeys)/sizeof(pubkeys[0])) )
{ {
memcpy(pubkeys[numvalid++],scriptbuf+1,33); memcpy(pubkeys[numvalid++],scriptbuf+1,33);
for (k=0; k<33; k++) for (k=0; k<33; k++)