Skip to content

multi: reject trailing characters when parsing inputs#2558

Merged
Roasbeef merged 27 commits into
btcsuite:masterfrom
starius:trailing
Jul 1, 2026
Merged

multi: reject trailing characters when parsing inputs#2558
Roasbeef merged 27 commits into
btcsuite:masterfrom
starius:trailing

Conversation

@starius

@starius starius commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Change Description

Some functions accept []byte inputs with trailing characters. Many such cases were fixed and covered with regression tests.

This PR replaces there PRs:

Fixes #2424
Fixes #2195

Note 1. In commit "multi: use local submodules in root" I replaced btcutil/v2 and wire/v2 with local versions to make the fixed changes available in rpc: * commits.

Note 2. PSBT parsing accepted \n and \r\n in the end and in the middle, if base64 parsing is enabled. This is not aligned with BIP 174/RFC4648 and Bitcoin Core, which reject such inputs. I added this validation in a separate commit. Now base64 PSBT string must have only base64 commot charset + = characters in the end, but no whitespace, new line or tabs.

Steps to Test

Run the tests. Each fix has a regression test which can be tried with the fix commit reverted. A regression test compiles, but fails with a symptomatic error.

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

@Lrifton92 Lrifton92 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.

Went through the full series (27 commits) @starius — happy to see all the trailing-byte cases consolidated in one place, and thanks for extending it well beyond the original three (wire v2 payloads, musig2 partial sigs, btcutil byte constructors, block proposals, DB block loading).

I built the root module + every touched submodule and ran their suites locally (go1.26.4):

  • psbt, wire, btcutil (+ bloom), btcec/schnorr/musig2, blockchain, and root . (rpcserver) — all green.

I also validated the "revert the fix, keep the test → red" property on a few representative pairs, since that's the whole point of splitting each fix from its regression test:

  • rpc: restoring rpcserver.go to pre-59db835TestHandleSendRawTransactionRejectsTrailingBytes and TestHandleDecodeRawTransactionRejectsTrailingBytes fail. ✅
  • btcutil: restoring tx.go/block.go to pre-41d537dTestNewTxFromBytesRejectsTrailingData and TestNewBlockFromBytesRejectsTrailingData fail ("expected error for … with trailing data"). ✅
  • psbt: restoring psbt.go to pre-70e8cebTestRejectsNonCanonicalBase64Packet fails (raw illegal base64 data instead of ErrInvalidPsbtFormat). ✅

One heads-up so it doesn't get blamed on this PR: netsync TestSyncStateMachine (handleHeadersMsg should update lastProgressTime) fails for me locally — but it fails identically on a clean master (280d4d2) and this PR doesn't touch netsync, so it looks like a pre-existing timing flake, unrelated here.

LGTM. This correctly supersedes #2541 / #2550 / #2556 — glad to close those in favor of this consolidated version whenever you'd like.

@Roasbeef Roasbeef left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM 🐧

Comment thread psbt/utils.go
// exported.
// assertFullyConsumed returns ErrInvalidPsbtFormat if r still has bytes
// available after parsing.
func assertFullyConsumed(r io.Reader) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

Was originally thinking we could just do it unilaterally at the wire level, but this is better as we can't assume too much about the context the reader/writer impls are used within.

Comment thread go.mod

replace (
github.com/btcsuite/btcd/btcutil/v2 => ./btcutil
github.com/btcsuite/btcd/wire/v2 => ./wire

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reminder to push a tag and then make a PR to update after.

@Roasbeef Roasbeef merged commit 6cfd717 into btcsuite:master Jul 1, 2026
4 checks passed
Comment thread psbt/utils.go
txOut := &wire.TxOut{}
reader := bytes.NewReader(txout)

if err := wire.ReadTxOut(reader, 0, 0, txOut); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This introduces a memory-retention regression for parsed WitnessUtxo values. wire.ReadTxOut returns TxOut.PkScript as a slice into a 4 MiB wire.scriptSlab; psbt/partial_input.go then stores the returned TxOut directly as pi.WitnessUtxo, so each witness input can retain the whole slab even when the script is tiny.

Suggested fix: compact/copy the script after wire.ReadTxOut before returning it from readTxOut:

script := make([]byte, len(txOut.PkScript))
copy(script, txOut.PkScript)
txOut.PkScript = script

A regression test can assert that parsed witness scripts do not retain the slab, e.g. cap(packet.Inputs[0].WitnessUtxo.PkScript) == len(packet.Inputs[0].WitnessUtxo.PkScript).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch! Fixed in #2563

@starius

starius commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

One heads-up so it doesn't get blamed on this PR: netsync TestSyncStateMachine (handleHeadersMsg should update lastProgressTime) fails for me locally — but it fails identically on a clean master (280d4d2) and this PR doesn't touch netsync, so it looks like a pre-existing timing flake, unrelated here.

Fixed in #2564

@Lrifton92

Copy link
Copy Markdown

Went back through the merged series once more — no concerns on correctness. Two non-blocking follow-ups worth tracking:

  1. The replace directives in root go.mod (btcutil/v2 => ./btcutil, wire/v2 => ./wire) are fine for wiring the submodule fixes into the rpc:* commits, but they'll need to come back out once btcutil/wire are tagged, otherwise go get of the root module breaks for downstream. Might be worth a checklist note on the release so it doesn't get missed.
  2. NewTxFromBytes/NewBlockFromBytes are now strict about trailing bytes — that's exactly the fix, and every in-tree caller passes exact-length input, but since btcutil is imported pretty broadly it might deserve a line in the release notes for anyone who was (accidentally) relying on the old lenient behavior.

Also nice bonus catch on the readTxOut rewrite — switching to wire.ReadTxOut fixes the multi-byte CompactSize script-length case the old txout[9:] slice got wrong, not just trailing bytes. The 0xfd 300-byte-script test makes that explicit. 👍

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

Labels

None yet

Projects

None yet

4 participants