(*Header).GetValidatorBytes panics with slice-bounds-out-of-range on short Extra (pre-Cancun branch)
Summary
(*Header).GetValidatorBytes(cfg *params.ChainConfig) in core/types/block.go panics with runtime error: slice bounds out of range whenever it is called on a *Header whose Extra is shorter than ExtraVanityLength + ExtraSealLength (< 97 bytes) AND cfg.IsCancun(h.Number) returns false.
The post-Cancun branch of the very same function already guards against this exact condition. The pre-Cancun branch does not, and the comment on consensus/bor/bor.go:670 openly acknowledges the function relies on upstream length validation:
// len(header.Extra) >= extraVanity+extraSeal has already been validated in
// validateHeaderExtraField, so this won't result in a panic
Because the function is exported, any out-of-tree consumer (indexers, archive-node frontends, light clients, block explorers, MEV tooling, custom RPC layers) that imports github.com/0xPolygon/bor/core/types and calls GetValidatorBytes directly, without first running validateHeaderExtraField or (c *Bor).VerifyHeader, will crash on attacker-controllable input.
In-tree (consensus engine) call sites are guarded; this is an API hardening / defense-in-depth issue, not a remote crash of a synced bor node.
System information
This is a source-code bug in the core/types package, not a runtime issue. The fields below from the issue template are therefore N/A in the usual sense:
- Bor client version: reproduced on
develop HEAD as of 2026-05-13 (core/types/block.go lines 503-523).
- Heimdall client version: N/A. Bug is in bor's
core/types, no Heimdall interaction involved.
- OS & Version: Linux x86_64 (Go 1.24); platform-independent (pure Go runtime panic on a slice op).
- Environment: N/A. Bug fires before any chain interaction.
- Type of node: N/A. Bug fires in library code, reachable from any package importing
core/types.
Overview of the problem
core/types/block.go:503-523 (verbatim):
// GetValidatorBytes extracts validator bytes from the header's Extra field.
// If you need multiple fields from BlockExtraData, prefer DecodeBlockExtraData
// to avoid redundant RLP decodes.
func (h *Header) GetValidatorBytes(chainConfig *params.ChainConfig) []byte {
if !chainConfig.IsCancun(h.Number) {
return h.Extra[ExtraVanityLength : len(h.Extra)-ExtraSealLength] // <-- no length check
}
if len(h.Extra) < ExtraVanityLength+ExtraSealLength { // <-- post-Cancun guard
log.Error("length of extra less is than vanity and seal")
return nil
}
var blockExtraData BlockExtraData
if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil {
log.Debug("error while decoding block extra data", "err", err)
return nil
}
return blockExtraData.ValidatorBytes
}
With ExtraVanityLength = 32 and ExtraSealLength = 65, the pre-Cancun slice operation h.Extra[32 : len(h.Extra)-65] panics for any len(h.Extra) < 97:
len(h.Extra) |
Slice op |
Panic |
| 1 |
Extra[32 : -64] |
runtime error: slice bounds out of range [:-64] |
| 65 |
Extra[32 : 0] |
runtime error: slice bounds out of range [32:0] |
| 96 |
Extra[32 : 31] |
runtime error: slice bounds out of range [32:31] |
| 97 |
Extra[32 : 32] |
OK (empty slice) |
The same body is reused (with the inner guard already present) in GetBaseFeeParams, DecodeBlockExtraData, and (*Block).GetTxDependency, so it is clear the maintainers know the length guard is required; it is just missing from one of the four sites.
Reproduction steps
Drop this test into core/types/block_test.go (inside the existing package types, after any existing function), then run go test -v -run TestGetValidatorBytesShortExtraVerbose ./core/types/. It must be placed inside the core/types package so it has access to the Header struct directly. It uses only packages already imported in block_test.go (math/big, testing, github.com/ethereum/go-ethereum/params) so no import changes are needed.
func TestGetValidatorBytesShortExtraVerbose(t *testing.T) {
// No CancunBlock set means IsCancun always returns false,
// so the vulnerable pre-Cancun branch is taken for every block number.
cfg := ¶ms.ChainConfig{ChainID: big.NewInt(137)}
for _, n := range []int{0, 1, 32, 64, 65, 80, 96} {
n := n
panicked := false
func() {
defer func() {
if r := recover(); r != nil {
panicked = true
t.Logf("len(Extra)=%2d --> PANIC: %v", n, r)
}
}()
h := &Header{Number: big.NewInt(1), Extra: make([]byte, n)}
_ = h.GetValidatorBytes(cfg)
}()
if !panicked {
t.Errorf("len(Extra)=%d: expected panic, got none (bug is already fixed?)", n)
}
}
}
Note on test semantics: recover() catches each panic so the test runner does not abort. The test passes when the bug is present (all 7 cases panic) and fails after the fix is applied (no panics, so !panicked triggers t.Errorf). The t.Logf line makes the individual panics visible in -v output.
Verified output on unpatched develop (run against github.com/ethereum/go-ethereum/core/types):
=== RUN TestGetValidatorBytesShortExtraVerbose
block_test.go:944: len(Extra)= 0 --> PANIC: runtime error: slice bounds out of range [:-65]
block_test.go:944: len(Extra)= 1 --> PANIC: runtime error: slice bounds out of range [:-64]
block_test.go:944: len(Extra)=32 --> PANIC: runtime error: slice bounds out of range [:-33]
block_test.go:944: len(Extra)=64 --> PANIC: runtime error: slice bounds out of range [:-1]
block_test.go:944: len(Extra)=65 --> PANIC: runtime error: slice bounds out of range [32:0]
block_test.go:944: len(Extra)=80 --> PANIC: runtime error: slice bounds out of range [32:15]
block_test.go:944: len(Extra)=96 --> PANIC: runtime error: slice bounds out of range [32:31]
--- PASS: TestGetValidatorBytesShortExtraVerbose (0.00s)
PASS
ok github.com/ethereum/go-ethereum/core/types 1.862s
Expected output after the fix from the linked PR is applied:
=== RUN TestGetValidatorBytesShortExtraVerbose
block_test.go:944: len(Extra)=0: expected panic, got none (bug is already fixed?)
...
--- FAIL: TestGetValidatorBytesShortExtraVerbose
Logs / Traces / Output / Error Messages
Native panic stack trace from a minimal harness inlining the upstream function verbatim (./harness "00"):
panic: runtime error: slice bounds out of range [:-64]
goroutine 1 [running]:
main.getValidatorBytesPreCancun({0xc0000a20b0, 0x1, 0x1})
core/types/block.go:508 +0xa5
main.exerciseGetValidatorBytesPreCancun({0xc0000a20b0, 0x1, 0x1})
main.go:156 +0x25
main.main()
main.go:181 +0x2b9
How this bug was found with Zorya
This bug was discovered with Zorya, a concolic-execution engine for Go binaries.
A minimal harness was built around the inlined-verbatim copy of (*Header).GetValidatorBytes and valset.ParseValidators, then analysed in function mode targeting exerciseGetValidatorBytesPreCancun for 300 s:
zorya /path/to/32_bor_parlia_valset/harness \
--lang go --compiler gc \
--thread-scheduling main-only \
--mode function 0x53d8a0 \
--arg "0000000000000000000000000000000000000000000000000000000000000000aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
The seed (a 99-byte hex blob: 32-byte vanity + 2-byte body + 65-byte seal) is a valid Extra so the concrete run takes the safe path. Zorya then negates the path constraints and explores nearby branches. The CBRANCH oracle flagged instruction 0x53d346 after 65.7 s with the following SAT witness:
[*] SATISFIABLE STATE FOUND
Elapsed since start: 65.739s
Instruction Address: 0x53d346
Panic Address: 0x53d39c
Opcode: CBRANCH
Detection method: Exploring the current path with a symbolic check on the pointer
The program can panic if its inputs are the following:
* The input 'extra.cap' must be 1
* The input 'extra.len' must be 1
* The input 'extra[0]' must be 0
The Z3-suggested extra.len = 1 was confirmed to natively reproduce the panic (see the table above). Zorya thus identified the missing length guard from binary alone, without any source-level annotation, and gave a minimal triggering input within ~1 minute.
To understand why extra.len = 1 is a valid witness: the vulnerable slice operation is h.Extra[32 : len(h.Extra)-65]. For this to succeed, Go's runtime requires len(h.Extra)-65 >= 32, which means len(h.Extra) >= 97. When len = 1, the high bound becomes 1-65 = -64, which is negative, violating the invariant and triggering the panic. Z3 was given the symbolic constraint len(h.Extra) < 97 (any value in that range makes the slice fail) and simply returned 1 as the smallest satisfying integer. The bug covers the entire range len(Extra) ∈ [0, 97) -- extra.len = 1 is just the concrete witness Z3 happened to produce first, and passing one zero byte ("00" in hex) to the harness confirms it instantly.
The same campaign also exercised the IsCancun-true branch (exerciseGetValidatorBytesPostCancun) and the GetTxDependency body (exerciseTxDependency); both returned no SAT state, confirming that their existing length guards correctly protect the slice operations.
Impact
-
In-tree path: Safe. All four call sites in consensus/bor/bor.go (verifyHeader calls GetValidatorBytes / GetBaseFeeParams; verifyCascadingFields calls GetValidatorBytes and parent.GetValidatorBytes) run after validateHeaderExtraField(header.Extra), which rejects len(Extra) < ExtraVanityLength + ExtraSealLength.
-
Out-of-tree consumers: Vulnerable. Any code that imports github.com/0xPolygon/bor/core/types and calls GetValidatorBytes on user-supplied / network-sourced headers without first invoking validateHeaderExtraField will panic on a short Extra. Concrete examples of consumer code patterns that may trip this:
- Block-explorer / indexer code paths that decode an RLP-encoded header and then immediately read validator data.
- Light-client implementations that consume headers from peers prior to running the full consensus engine.
- Off-chain analytics, MEV tooling, or snapshot exporters that enumerate validator sets per-block.
- Test fixtures or fuzzing harnesses that bypass
verifyHeader.
-
Production attacker reachability: low on a synced node (Cancun is long-active on mainnet, so the broken pre-Cancun branch is not hit on current blocks). Historical-sync paths walking pre-Cancun blocks plus any out-of-tree pre-validation hook expand the attack surface.
This is a defense-in-depth finding. Severity suggestion: Low.
Suggested fix
Mirror the post-Cancun guard into the pre-Cancun branch and document the postcondition on the godoc. Four-line patch:
// GetValidatorBytes extracts validator bytes from the header's Extra field.
-// If you need multiple fields from BlockExtraData, prefer DecodeBlockExtraData
-// to avoid redundant RLP decodes.
+// Returns nil if len(h.Extra) is shorter than ExtraVanityLength + ExtraSealLength.
+// If you need multiple fields from BlockExtraData, prefer DecodeBlockExtraData
+// to avoid redundant RLP decodes.
func (h *Header) GetValidatorBytes(chainConfig *params.ChainConfig) []byte {
+ if len(h.Extra) < ExtraVanityLength+ExtraSealLength {
+ return nil
+ }
if !chainConfig.IsCancun(h.Number) {
return h.Extra[ExtraVanityLength : len(h.Extra)-ExtraSealLength]
}
-
- if len(h.Extra) < ExtraVanityLength+ExtraSealLength {
- log.Error("length of extra less is than vanity and seal")
- return nil
- }
var blockExtraData BlockExtraData
if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil {
log.Debug("error while decoding block extra data", "err", err)
return nil
}
return blockExtraData.ValidatorBytes
}
Also recommend redundantly hoisting the same guard to the top of GetBaseFeeParams, DecodeBlockExtraData, and (*Block).GetTxDependency for consistency (they already perform the check inside their respective RLP-decode branches, but the early return makes the precondition uniform across the four functions).
The "len(header.Extra) >= extraVanity+extraSeal has already been validated" comment in consensus/bor/bor.go can then be removed; the invariant becomes intrinsic to the API, not an implicit caller obligation.
Additional Information
Not applicable in the usual start.sh / admin.peers / heimdall-config sense. This is a library API bug, not a runtime issue. Triggering it does not require a running bor node, a Heimdall sidecar, or any peer interaction; it is purely a precondition violation in core/types.
(*Header).GetValidatorBytespanics with slice-bounds-out-of-range on shortExtra(pre-Cancun branch)Summary
(*Header).GetValidatorBytes(cfg *params.ChainConfig)incore/types/block.gopanics withruntime error: slice bounds out of rangewhenever it is called on a*HeaderwhoseExtrais shorter thanExtraVanityLength + ExtraSealLength(< 97 bytes) ANDcfg.IsCancun(h.Number)returns false.The post-Cancun branch of the very same function already guards against this exact condition. The pre-Cancun branch does not, and the comment on
consensus/bor/bor.go:670openly acknowledges the function relies on upstream length validation:Because the function is exported, any out-of-tree consumer (indexers, archive-node frontends, light clients, block explorers, MEV tooling, custom RPC layers) that imports
github.com/0xPolygon/bor/core/typesand callsGetValidatorBytesdirectly, without first runningvalidateHeaderExtraFieldor(c *Bor).VerifyHeader, will crash on attacker-controllable input.In-tree (consensus engine) call sites are guarded; this is an API hardening / defense-in-depth issue, not a remote crash of a synced bor node.
System information
This is a source-code bug in the
core/typespackage, not a runtime issue. The fields below from the issue template are therefore N/A in the usual sense:developHEAD as of 2026-05-13 (core/types/block.golines 503-523).core/types, no Heimdall interaction involved.core/types.Overview of the problem
core/types/block.go:503-523(verbatim):With
ExtraVanityLength = 32andExtraSealLength = 65, the pre-Cancun slice operationh.Extra[32 : len(h.Extra)-65]panics for anylen(h.Extra) < 97:len(h.Extra)Extra[32 : -64]runtime error: slice bounds out of range [:-64]Extra[32 : 0]runtime error: slice bounds out of range [32:0]Extra[32 : 31]runtime error: slice bounds out of range [32:31]Extra[32 : 32]The same body is reused (with the inner guard already present) in
GetBaseFeeParams,DecodeBlockExtraData, and(*Block).GetTxDependency, so it is clear the maintainers know the length guard is required; it is just missing from one of the four sites.Reproduction steps
Drop this test into
core/types/block_test.go(inside the existingpackage types, after any existing function), then rungo test -v -run TestGetValidatorBytesShortExtraVerbose ./core/types/. It must be placed inside thecore/typespackage so it has access to theHeaderstruct directly. It uses only packages already imported inblock_test.go(math/big,testing,github.com/ethereum/go-ethereum/params) so no import changes are needed.Note on test semantics:
recover()catches each panic so the test runner does not abort. The test passes when the bug is present (all 7 cases panic) and fails after the fix is applied (no panics, so!panickedtriggerst.Errorf). Thet.Logfline makes the individual panics visible in-voutput.Verified output on unpatched
develop(run againstgithub.com/ethereum/go-ethereum/core/types):Expected output after the fix from the linked PR is applied:
Logs / Traces / Output / Error Messages
Native panic stack trace from a minimal harness inlining the upstream function verbatim (
./harness "00"):How this bug was found with Zorya
This bug was discovered with Zorya, a concolic-execution engine for Go binaries.
A minimal harness was built around the inlined-verbatim copy of
(*Header).GetValidatorBytesandvalset.ParseValidators, then analysed in function mode targetingexerciseGetValidatorBytesPreCancunfor 300 s:The seed (a 99-byte hex blob: 32-byte vanity + 2-byte body + 65-byte seal) is a valid
Extraso the concrete run takes the safe path. Zorya then negates the path constraints and explores nearby branches. The CBRANCH oracle flagged instruction0x53d346after 65.7 s with the following SAT witness:The Z3-suggested
extra.len = 1was confirmed to natively reproduce the panic (see the table above). Zorya thus identified the missing length guard from binary alone, without any source-level annotation, and gave a minimal triggering input within ~1 minute.To understand why
extra.len = 1is a valid witness: the vulnerable slice operation ish.Extra[32 : len(h.Extra)-65]. For this to succeed, Go's runtime requireslen(h.Extra)-65 >= 32, which meanslen(h.Extra) >= 97. Whenlen = 1, the high bound becomes1-65 = -64, which is negative, violating the invariant and triggering the panic. Z3 was given the symbolic constraintlen(h.Extra) < 97(any value in that range makes the slice fail) and simply returned 1 as the smallest satisfying integer. The bug covers the entire rangelen(Extra) ∈ [0, 97)--extra.len = 1is just the concrete witness Z3 happened to produce first, and passing one zero byte ("00"in hex) to the harness confirms it instantly.The same campaign also exercised the
IsCancun-true branch (exerciseGetValidatorBytesPostCancun) and theGetTxDependencybody (exerciseTxDependency); both returned no SAT state, confirming that their existing length guards correctly protect the slice operations.Impact
In-tree path: Safe. All four call sites in
consensus/bor/bor.go(verifyHeadercallsGetValidatorBytes/GetBaseFeeParams;verifyCascadingFieldscallsGetValidatorBytesandparent.GetValidatorBytes) run aftervalidateHeaderExtraField(header.Extra), which rejectslen(Extra) < ExtraVanityLength + ExtraSealLength.Out-of-tree consumers: Vulnerable. Any code that imports
github.com/0xPolygon/bor/core/typesand callsGetValidatorByteson user-supplied / network-sourced headers without first invokingvalidateHeaderExtraFieldwill panic on a shortExtra. Concrete examples of consumer code patterns that may trip this:verifyHeader.Production attacker reachability: low on a synced node (Cancun is long-active on mainnet, so the broken pre-Cancun branch is not hit on current blocks). Historical-sync paths walking pre-Cancun blocks plus any out-of-tree pre-validation hook expand the attack surface.
This is a defense-in-depth finding. Severity suggestion: Low.
Suggested fix
Mirror the post-Cancun guard into the pre-Cancun branch and document the postcondition on the godoc. Four-line patch:
Also recommend redundantly hoisting the same guard to the top of
GetBaseFeeParams,DecodeBlockExtraData, and(*Block).GetTxDependencyfor consistency (they already perform the check inside their respective RLP-decode branches, but the early return makes the precondition uniform across the four functions).The "len(header.Extra) >= extraVanity+extraSeal has already been validated" comment in
consensus/bor/bor.gocan then be removed; the invariant becomes intrinsic to the API, not an implicit caller obligation.Additional Information
Not applicable in the usual
start.sh/admin.peers/heimdall-configsense. This is a library API bug, not a runtime issue. Triggering it does not require a running bor node, a Heimdall sidecar, or any peer interaction; it is purely a precondition violation incore/types.