ANT-2026-Z3GGGKM5 · ava-labs/libevm

denial-of-service high

Severity Claude high · Security research firm high · Maintainer -

Discovered by Claude Mythos Preview

REPORT

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

  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

[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

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Anvil Security.

Verdict
true positive
Severity
high
UPSTREAM FIX

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

TIMELINE

Dates from discovery through public reveal.

  1. 2026-04-21 Reported to tracker
  2. 2026-04-29 Patch released
  3. 2026-05-28 Sent to maintainer
  4. 2026-05-28 Maintainer acknowledged
  5. 2026-08-17 Publicly revealed
PROVENANCE

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
}