ANT-2026-Z3GGGKM5 · ava-labs/libevm
denial-of-service high
Severity Claude high · Security research firm high · Maintainer -
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Anvil Security.
ANT-2026-Z3GGGKM5: Reentrancy guard dereferences nil StateDB under static call
In libevm's reentrancy package, Guard() fetches env.StateDB() and immediately calls sdb.GetTransientState() with no nil check. The PrecompileEnvironment contract and implementation explicitly return a nil StateDB interface when the interpreter is read-only, which is the case for any stateful precompile reached via STATICCALL or from within an existing static frame. An attacker who can submit a transaction deploys a contract that STATICCALLs a guarded precompile; the nil-interface method call panics, and because there is no recover() on the block-import path, every validating node processing the canonical block crashes in lockstep. The result is a deterministic, chain-wide halt requiring a patched binary to resume.
Target
Project: ava-labs/libevm
Location: libevm/reentrancy/guard.go:45
Technical Details
Guard() assumes env.StateDB() is always non-nil, but core/vm/environment.libevm.go:83-88 returns nil in read-only mode by design. Invoking GetTransientState() on a nil interface value panics, and the block-processing path (core/state_processor.go, core/blockchain.go) has no recover(), so the panic propagates to process termination. reentrancy.Guard is the library's officially recommended wrapper for stateful precompiles that use env.Call(), and its tests cover only the non-static CALL path.
Crash signature: Go runtime panic: nil pointer / nil interface method call in reentrancy.Guard → StateDB().GetTransientState()
Reproduction
- Deploy a trivial contract containing staticcall(gas(), P, 0, 0, 0, 0) where P is the guarded precompile address
- Submit a single transaction invoking that contract
- During block processing, env.StateDB() returns nil under the static frame and GetTransientState() panics
- Panic propagates past block import (no recover()), terminating the node process on every validator
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Make the guard safe in read-only context: check env.ReadOnly() and return a revert error before touching StateDB(), or read transient state via an accessor guaranteed non-nil under static calls.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-Z3GGGKM5.
Reference: ANT-2026-Z3GGGKM5
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Anvil Security.
- Verdict
- true positive
- Severity
- high
The change that resolved this finding.
diff --git a/libevm/reentrancy/guard.go b/libevm/reentrancy/guard.go
index 8a782bcf95f6..ac5206359fcc 100644
--- a/libevm/reentrancy/guard.go
+++ b/libevm/reentrancy/guard.go
@@ -33,11 +33,16 @@ var slotPreimagePrefix = []byte("libevm-reentrancy-guard-")
// Guard returns [vm.ErrExecutionReverted] i.f.f. it has already been called
// with the same `key`, by the same contract, in the same transaction. It
-// otherwise returns nil. The `key` MAY be nil.
+// otherwise returns nil unless in a read-only context, in which case it always
+// returns [vm.ErrWriteProtection]. The `key` MAY be nil.
//
// Contract equality is defined as the [libevm.AddressContext] "self" address
// being the same under EVM semantics.
func Guard(env vm.PrecompileEnvironment, key []byte) error {
+ if env.ReadOnly() {
+ return vm.ErrWriteProtection
+ }
+
self := env.Addresses().EVMSemantic.Self
slot := crypto.Keccak256Hash(slotPreimagePrefix, key)
diff --git a/libevm/reentrancy/guard_test.go b/libevm/reentrancy/guard_test.go
index 83e0de4a44bf..ac8ec8cd8225 100644
--- a/libevm/reentrancy/guard_test.go
+++ b/libevm/reentrancy/guard_test.go
@@ -76,6 +76,11 @@ func TestGuardIntegration(t *testing.T) {
// This MUST NOT be [assert.ErrorIs] as such errors are never wrapped in geth.
assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted")
assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data")
+
+ t.Run("static_call", func(t *testing.T) {
+ _, _, err := evm.StaticCall(vm.AccountRef{}, sut, []byte{}, 1e6)
+ require.Equal(t, vm.ErrWriteProtection, err, "StaticCall()")
+ })
}
type envStub struct {
@@ -92,6 +97,10 @@ func (s *envStub) Addresses() *libevm.AddressContext {
}
}
+func (*envStub) ReadOnly() bool {
+ return false
+}
+
func (s *envStub) StateDB() vm.StateDB {
return s.db
}https://github.com/ava-labs/libevm/commit/63a0b4c93a12787bfaa60da768b176d3a8a6cd79
Dates from discovery through public reveal.
- 2026-04-21 Reported to tracker
- 2026-04-29 Patch released
- 2026-05-28 Sent to maintainer
- 2026-05-28 Maintainer acknowledged
- 2026-08-17 Publicly revealed
SHA-3-512 hash:
25cafbdb7f56b02677dd08c704a1e05a3d7d3a69fbfab1130950e27249e4a437e1d5762056c9597a6c59a7b25385f70b25aa2a26c24e9a4843eb383b6d4afaff
Committed 2026-04-21 15:42 PT
Revealed 2026-08-17 13:01 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-Z3GGGKM5",
"bug_class": "Denial of Service / Nil Pointer Dereference",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-21T16:54:30+00:00",
"description": "In libevm's reentrancy package, Guard() fetches env.StateDB() and immediately calls sdb.GetTransientState() with no nil check. The PrecompileEnvironment contract and implementation explicitly return a nil StateDB interface when the interpreter is read-only, which is the case for any stateful precompile reached via STATICCALL or from within an existing static frame. An attacker who can submit a transaction deploys a contract that STATICCALLs a guarded precompile; the nil-interface method call panics, and because there is no recover() on the block-import path, every validating node processing the canonical block crashes in lockstep. The result is a deterministic, chain-wide halt requiring a patched binary to resume.",
"discovered_at": "2026-04-19T00:00:00+00:00",
"location": "libevm/reentrancy/guard.go:45",
"poc_sha256": null,
"preimage_version": 1,
"project": "ava-labs/libevm",
"reproduction": [
"1. Deploy a trivial contract containing staticcall(gas(), P, 0, 0, 0, 0) where P is the guarded precompile address",
"2. Submit a single transaction invoking that contract",
"3. During block processing, env.StateDB() returns nil under the static frame and GetTransientState() panics",
"4. Panic propagates past block import (no recover()), terminating the node process on every validator"
],
"technical_details": "Guard() assumes env.StateDB() is always non-nil, but core/vm/environment.libevm.go:83-88 returns nil in read-only mode by design. Invoking GetTransientState() on a nil interface value panics, and the block-processing path (core/state_processor.go, core/blockchain.go) has no recover(), so the panic propagates to process termination. reentrancy.Guard is the library's officially recommended wrapper for stateful precompiles that use env.Call(), and its tests cover only the non-static CALL path.",
"title": "Reentrancy guard dereferences nil StateDB under static call",
"vendor_severity": null
}