Skip to content

rpc: fuzz the OOR and round proto parsers for panic safety#786

Open
Roasbeef wants to merge 2 commits into
mainfrom
fuzz-rpc-proto-parsers
Open

rpc: fuzz the OOR and round proto parsers for panic safety#786
Roasbeef wants to merge 2 commits into
mainfrom
fuzz-rpc-proto-parsers

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

Motivation

The darepo server's OOR and round RPC handlers parse client-controlled
proto bytes through helpers that live in this module (rpc/oorpb,
rpc/roundpb). A panic in one of those parsers is a server-crash vector
reachable by any client, so they deserve dedicated fuzz coverage.

This PR adds it. It is independent of the TLV-codec hardening in #785.

What this adds

Fuzz harnesses over the RPC proto parsers, driven both field-level
(fuzzer-controlled PSBT / descriptor / outpoint / signature / tree-node
bytes) and via the realistic proto.Unmarshal wire path:

  • rpc/oorpb: ParseSubmitPackageRequest,
    ParseFinalizePackageRequest, and the response parsers.
  • rpc/roundpb: OutpointFromProto, TxOutFromProto,
    PSBTFromBytes, SchnorrSigFromBytes, TxIDFromHex,
    MsgTxFromBytes, and TreeFromProto.

All targets assert the no-panic contract; seeds include valid values,
empty input, off-length hashes/sigs, and (for the tree) oversize child
indices and large node counts.

Findings

No bugs. These parsers already length/shape-check before fixed-size
copies, guard nil sub-messages, and feed btcd's PSBT/wire/schnorr
parsers only through a bounded reader. TreeFromProto is iterative (no
decode recursion) and enforces its WithMaxTreeNodes cap before any
per-node allocation, which bounds both node count and total allocation;
the pre-order childIdx > i invariant keeps the produced graph acyclic.
Fuzzing confirmed all of this holds on hostile input.

One out-of-scope note for a possible follow-up: lib/tree's
Node.Depth() / ForEach are recursive, so a valid ~50k-node linear
chain accepted by the parser would recurse deeply in downstream
traversal. That is a lib/tree concern (not these parsers) and is
bounded in practice by the node cap and Go's growable stacks.

Testing

  • go build ./rpc/... and go vet ./rpc/oorpb/ ./rpc/roundpb/ clean.
  • All fuzz targets run clean (no panic, no OOM).

Commits are atomic per package.

Roasbeef added 2 commits June 23, 2026 17:18
The server's OOR RPC handlers call ParseSubmitPackageRequest and
ParseFinalizePackageRequest on fully client-controlled proto bytes, so a
panic, slice OOB, or integer overflow in these decode paths is a remote
server-crash vector: one hostile client could take down the operator.

Add Go native fuzz targets that assert only the no-panic invariant (the
parsers may return errors freely). Each parser gets a field-level target
that builds the proto in-harness from fuzzer-controlled bytes (ark PSBT,
checkpoint PSBT, descriptor outpoint/policy/spend-path, recipient
script/value) and a wire-bytes target that runs proto.Unmarshal then the
parser to reach shapes the field fuzzer cannot construct. Seeds cover a
representative valid value, empty input, and 31/33-byte session-hash edge
cases that straddle the fixed-length copy gates. The response parsers are
fuzzed too: a compromised operator must not be able to panic the client.

No crashers surfaced across all targets.
The round RPC converters decode proto messages that originate from a
remote peer, so a panic, OOM, slice OOB, or integer overflow in any
decode helper is a process-crash vector. Add Go native fuzz targets
asserting only the no-panic invariant for OutpointFromProto,
OutpointsFromProto, TxOutFromProto, PSBTFromBytes, MsgTxFromBytes,
SchnorrSigFromBytes, TxIDFromHex, OutpointFromMapKey, and TreeFromProto
(both a field-level and a wire-bytes target each where useful).

TreeFromProto gets extra attention because it reconstructs the recursive
VTXO tree from a flattened node array. The wire target pins an explicit
WithMaxTreeNodes bound and seeds a many-node corpus so the fuzzer can
grow node counts toward and past the cap, proving the bound is enforced
rather than ignored and that hostile child-index maps cannot drive OOB or
unbounded work. Length-gate seeds straddle 31/33-byte hashes and
63/64/65-byte signatures.

No crashers surfaced across all targets.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive fuzz tests for RPC payload parsers and converters in both the oorpb and roundpb packages to ensure they are resilient against remote crash vectors like panics, OOMs, and out-of-bounds accesses. The feedback suggests expanding the fuzzing coverage to include additional nil pointer edge cases, specifically by testing nil nodes and outputs in VTXOTree and a nil outpoint within the signing descriptors, ensuring the parser logic is fully robust against nil-dereference panics.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +165 to +199
pt := &VTXOTree{
Nodes: []*TreeNode{
{
Input: &Outpoint{
TxHash: txHash,
OutputIndex: 0,
},
Outputs: []*TxOut{
{
Value: amount,
PkScript: pkScript,
},
},
CoSigners: [][]byte{coSigner},
Children: map[uint32]uint32{
childOut: childIdx,
},
Amount: amount,
Signature: sig,
},
{
Input: &Outpoint{
TxHash: txHash,
OutputIndex: 1,
},
Outputs: []*TxOut{
{
Value: amount,
PkScript: pkScript,
},
},
Children: map[uint32]uint32{},
Amount: amount,
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To ensure the parser is fully panic-safe against nil pointers, we should include nil elements in the Nodes and Outputs slices of the VTXOTree struct, similar to how nil elements are tested in FuzzParseSubmitPackageRequestFields. Currently, TreeFromProto does not check if a node in pt.Nodes is nil before dereferencing it in treeNodeFromProto, which will cause a panic.

		pt := &VTXOTree{
			Nodes: []*TreeNode{
				{
					Input: &Outpoint{
						TxHash:      txHash,
						OutputIndex: 0,
					},
					Outputs: []*TxOut{
						{
							Value:    amount,
							PkScript: pkScript,
						},
						nil,
					},
					CoSigners: [][]byte{coSigner},
					Children: map[uint32]uint32{
						childOut: childIdx,
					},
					Amount:    amount,
					Signature: sig,
				},
				nil,
				{
					Input: &Outpoint{
						TxHash:      txHash,
						OutputIndex: 1,
					},
					Outputs: []*TxOut{
						{
							Value:    amount,
							PkScript: pkScript,
						},
					},
					Children: map[uint32]uint32{},
					Amount:   amount,
				},
			},

Comment on lines +71 to +84
SigningDescriptors: []*OORSigningDescriptor{
{
Outpoint: &OOROutPoint{
Txid: descTxid,
Vout: descVout,
},
VtxoPolicyTemplate: descPolicy,
SpendPath: descSpendPath,
OwnerLeafPolicy: descPolicy,
},
// A nil descriptor entry must be rejected, not
// dereferenced.
nil,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure comprehensive fuzz coverage for nil nested fields, we should also include a signing descriptor with a nil Outpoint in the SigningDescriptors slice. This will verify that decodeSigningDescriptor and decodeOutPoint correctly handle nil outpoints without panicking.

			SigningDescriptors: []*OORSigningDescriptor{
				{
					Outpoint: &OOROutPoint{
						Txid: descTxid,
						Vout: descVout,
					},
					VtxoPolicyTemplate: descPolicy,
					SpendPath:          descSpendPath,
					OwnerLeafPolicy:    descPolicy,
				},
				{
					Outpoint:           nil,
					VtxoPolicyTemplate: descPolicy,
					SpendPath:          descSpendPath,
					OwnerLeafPolicy:    descPolicy,
				},
				// A nil descriptor entry must be rejected, not
				// dereferenced.
				nil,
			}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0436b6a10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +43
[]byte("psbt"), []byte("checkpoint"), validSessionID,
[]byte("policy"), []byte("spendpath"), uint32(0),
[]byte{0x51}, int64(1000), []byte("vtxopolicy"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed submit fuzzing past PSBT decoding

Every seed in this target supplies invalid Ark/checkpoint PSBT bytes, so ParseSubmitPackageRequest returns from psbtutil.Parse or decodePSBTSlice before it reaches the descriptor and recipient fields this harness is trying to fuzz. Since the request also always includes a nil descriptor before RecipientOutputs are processed, even a future valid-PSBT mutation would still return before the recipient script/value/policy parameters are read; seed with serialized PSBTs and split nil-entry cases into separate parser calls so these fields actually get coverage.

Useful? React with 👍 / 👎.

PkScript: pkScript,
},
},
CoSigners: [][]byte{coSigner},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let tree fuzzing reach child validation

This field-level tree harness always adds exactly one co-signer entry, but the seed co-signer values are empty or all-zero encodings, and TreeFromProto parses co-signers in treeNodeFromProto before it validates the Children map. Generating a valid compressed secp256k1 key by mutation is impractical, so the childOut/childIdx parameters and the child-index guards are effectively unreachable from the seed corpus; add a no-cosigner/no-signature path or seed a known-valid public key before fuzzing child indices.

Useful? React with 👍 / 👎.

@levmi levmi added enhancement New feature or request oor P1 Priority 1 — high round rpc RPC transport and protobuf security Security hardening and access control tests labels Jul 6, 2026
@lightninglabs-deploy

Copy link
Copy Markdown
Collaborator

@Roasbeef, remember to re-request review from reviewers when ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request oor P1 Priority 1 — high round rpc RPC transport and protobuf security Security hardening and access control tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants