pulsar: fix mlen uint64 overflow → chain-halt (CRITICAL launch blocker)

verify parsed mlen from attacker's low-8-bytes, then guarded with
len(body) < mlen+sigSize — which OVERFLOWS uint64 when mlen is near MaxUint64,
slipping past the guard so body[:mlen] panics. A panic in geth's precompile
dispatch has no recover() → every validator halts on the tx. Fixed with a
non-overflowing subtraction guard (mlen > len(body) || len(body)-mlen < sigSize).
Regression test TestVerify_MlenOverflowRejected proves reject-not-panic.
Red-team finding #5.
This commit is contained in:
zeekay
2026-06-27 18:35:29 -07:00
parent 8c379b7b06
commit 3f95d00a49
2 changed files with 44 additions and 1 deletions
+6 -1
View File
@@ -193,7 +193,12 @@ func (p *pulsarVerifyPrecompile) Run(
mlen = (mlen << 8) | uint64(body[i])
}
body = body[32:]
if uint64(len(body)) < mlen+uint64(sigSize) {
// Bounds check WITHOUT overflow: mlen is attacker-controlled (low 8 bytes
// of a uint256), so mlen+sigSize can wrap uint64 and slip past a naive
// guard, making body[:mlen] panic — and a panic in the geth precompile
// dispatch (no recover) halts every validator on the tx. Compare with
// subtraction on the trusted len(body) instead.
if mlen > uint64(len(body)) || uint64(len(body))-mlen < uint64(sigSize) {
return nil, suppliedGas, ErrInvalidInputLength
}
msg := body[:mlen]
+38
View File
@@ -0,0 +1,38 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
import (
"encoding/binary"
"testing"
"github.com/luxfi/geth/common"
)
// TestVerify_MlenOverflowRejected proves the consensus-halt fix: an attacker
// who sets the message-length field near MaxUint64 must get a clean
// ErrInvalidInputLength, NOT a slice-out-of-range panic. A panic in geth's
// precompile dispatch has no recover() — it would halt every validator on the
// tx. Before the fix, mlen+sigSize wrapped uint64, the guard passed, and
// body[:mlen] panicked.
func TestVerify_MlenOverflowRejected(t *testing.T) {
p := PulsarVerifyPrecompile
input := []byte{ModePulsar44}
input = append(input, make([]byte, Pulsar44PublicKeySize)...)
lenField := make([]byte, 32)
binary.BigEndian.PutUint64(lenField[24:], ^uint64(0)-8)
input = append(input, lenField...)
input = append(input, make([]byte, 64)...)
defer func() {
if r := recover(); r != nil {
t.Fatalf("CONSENSUS-HALT: overflow mlen panicked instead of rejecting: %v", r)
}
}()
_, _, err := p.Run(nil, common.Address{}, common.Address{}, input, 100_000_000, true)
if err == nil {
t.Fatal("expected ErrInvalidInputLength for overflow mlen, got nil")
}
}