Home / Smart Contract Auditing / Critical Memory Corruption Bug Discovered in Solidity Code Generator Affecting Versions up to v0.8.36

Critical Memory Corruption Bug Discovered in Solidity Code Generator Affecting Versions up to v0.8.36

On August 14, 2026, developer Shaheen Fazim identified and responsibly reported a significant security vulnerability in the Solidity compiler’s code generator through the Ethereum Foundation’s bug bounty program. The defect relates to how the compiler handles the delete operator when applied to an element of a bytes or string array located in memory. Instead of correctly clearing only the targeted element, the generated machine code writes 32 zero bytes starting precisely at that element. This unintended memory footprint inadvertently wipes out up to 31 subsequent bytes, either further down within the same array or, when the deleted element is positioned near the end of the array’s allocation boundary, into whatever data structures reside immediately after it in memory.

According to the vulnerability assessment conducted by the Solidity core team, this discrepancy is strictly confined to the default evmasm pipeline. The newer IR (Intermediate Representation) pipeline remains entirely unaffected by the flaw. Furthermore, invoking the Solidity optimizer is not a prerequisite for triggering the bug, meaning developers cannot bypass the issue simply by adjusting optimization flags. Because the underlying code path responsible for this behavior predates the very first official release version of Solidity, the vulnerability spans a vast range of historical compiler iterations. All versions up to and including Solidity 0.8.37 are impacted, with the bug officially patched in version 0.8.37.

To gauge the real-world exposure of smart contracts deployed on public networks, the engineering team conducted an automated scan of the Sourcify database. Examining roughly 870,000 verified compilations for the specific source pattern known to trigger the defect, the search returned positive matches in only 17 distinct compilations. These compilations contained merely two unique functions, neither of which ultimately suffered harmful consequences upon closer inspection. A detailed severity assessment revealed that in one of the functions, the affected bytes were immediately discarded or overwritten anyway, while the other function allocated its memory in a region where no vital data resided past the boundary. Consequently, no deployed smart contract in the wild is currently known to have suffered malicious exploitation or unintended data loss due to this vulnerability.

Which Contracts Are Affected?

Understanding exposure to this memory corruption vulnerability requires looking closely at how and where data types are declared and manipulated within a smart contract. A contract is only susceptible to the defect if a specific set of conditions aligns during compilation and execution. First, the project must be compiled using an affected version of the Solidity compiler—specifically version 0.8.36 or any earlier iteration. Second, the compilation must rely on the default evmasm code generation pipeline rather than the alternative IR-based pipeline. Finally, the contract logic must actively invoke the delete operator on an individual element of a bytes or string array residing in memory.

While the first two conditions can be quickly verified by examining compiler configuration settings and source code files, verifying the third condition demands an intricate understanding of how the compiler structures and manages memory layouts. For developers who confirm the presence of the first two criteria, security best practices suggest treating the codebase as potentially affected rather than attempting to prove absolute safety regarding memory positioning.

It is important to emphasize that if a smart contract never applies the delete operator to an element of a memory-based bytes or string value, it is completely immune to the bug. Neither the choice of the EVM target version nor the activation of the optimizer alters whether the defect manifests. The core issue lies fundamentally within the instructions emitted during the code generation phase, and the optimizer blindly preserves these faulty execution semantics.

Several technical clarifications help define the exact boundaries of the vulnerability. Only individual element deletes are affected; applying delete to an entire array via delete b follows a completely different code path and functions correctly. Additionally, the array must reside in memory. Byte arrays stored in contract storage utilize an entirely separate code path and remain unaffected, while the delete operator cannot be applied to calldata arrays under any circumstances. Furthermore, arrays whose individual elements each occupy a full 32-byte word in memory—such as bytes1[] memory—are untouched by the issue. Only bytes and string types pack their underlying elements tightly back-to-back without word-boundary spacing.

Variable-length string values are also affected by extension. While direct syntax such as delete s[i] will fail to compile for a string memory s, developers frequently convert strings into byte arrays using explicit type casting like bytes(s). Executing delete bytes(s)[i] on that cast reference triggers the exact same underlying compiler bug. Depending on where the deleted element sits within the array and how the array itself was allocated by the runtime environment, the reach and impact of the resulting memory corruption can vary significantly.

Technical Details

To comprehend the root cause of this vulnerability at the implementation level, it is helpful to examine how Solidity manages memory allocation. Most data types in Solidity memory consume full 32-byte words. The primary exception to this rule is the bytes type, where a byte array consists of an initial 32-byte length header followed immediately by the raw data bytes packed continuously without structural gaps.

Solidity allocates memory linearly by advancing a global free memory pointer, meaning every new allocation starts precisely where the preceding allocation ended. Aside from necessary padding, consecutive memory allocations sit flush against one another. As a consequence, any write operation that inadvertently runs past the allocated footprint of one data structure bleeds directly into the beginning of the next.

For most standard memory allocations—such as arrays created using new bytes(n) or initialized directly from hex literals—the raw data is followed by up to 31 zero bytes of padding. This padding ensures that subsequent memory allocations always align neatly on a 32-byte word boundary. These allocations are referred to as padded arrays. Conversely, the default evmasm pipeline fails to append padding to the output of certain operations, including bytes.concat, string.concat, and the entire family of abi.encode functions. In these cases, the array data ends abruptly where the next memory allocation begins, creating unpadded arrays. Relying on the presence or absence of this padding is dangerous, as internal pipeline behaviors can shift across compiler versions, and inline assembly that manipulates padding directly violates official memory safety rules.

When a value narrower than a word is stored in memory, its alignment depends heavily on its specific type. Left-aligned types, such as bytesN variants, begin at the very first byte of the word. Right-aligned types, such as standard integers, addresses, memory pointers, and array length headers, sit flush against the end of the word with zero bytes padding the front.

Memory Byte Array Element Delete Clears Whole Word Bug | Solidity Programming Language

Strings share the exact same underlying memory representation as bytes, containing variable-length UTF-8 characters which traditionally prevent direct index access. Type-casting a string to a bytes array does not instantiate a duplicate in memory; rather, bytes(s) points directly to the original string memory. Consequently, invoking delete bytes(s)[i] mutates the string data in place. Fixed-size bytesN values remain unaffected because indexing them returns an isolated copy of the target byte rather than a mutable reference to the underlying memory structure.

Because their constituent elements are single bytes, dynamically packed byte arrays represent the sole scenario where storing a value to memory deviates from standard full-word operations. The compiler handles this by issuing a single-byte store instruction (mstore8) whenever it recognizes the precise element width, except for the specific code path triggered by the delete operator.

The Delete Path and Corruption Reach

When a developer executes delete x, the intention is to assign the zero value native to that variable’s type. Logically, executing delete b[i] should produce identical outcomes to performing an explicit assignment via b[i] = 0. Both operations ultimately conclude with a write operation targeting the memory location of the array element. However, within the vulnerable evmasm pipeline, these two actions diverge sharply in their underlying implementation.

While a standard assignment correctly consults the element width and issues a single-byte store (mstore8), the delete code path mistakenly treats the target as a full-word operation. This results in a massive 32-byte write of zeroes starting at the exact memory offset of the deleted element and extending forward. Because this write initiates at the specific index rather than aligning to a clean word boundary, it stretches far beyond the intended single byte.

Crucially, the array’s length header—which resides immediately before the data payload—is never touched, meaning the array retains its original reported length. Any subsequent operations that rely on the array’s contents, such as computing cryptographic hashes via keccak256, performing ABI encodings, or returning data to an external caller, silently process the corrupted byte sequence. Meanwhile, the IR pipeline correctly routes delete b[i] through the same unified store mechanism used by standard assignments, entirely bypassing the flaw.

The extent of the memory corruption depends heavily on the index of the deleted element and the presence of allocation padding. When deleting an element inside a padded array, the trailing padding bytes absorb a portion of the rogue write. Deleting elements near the beginning of a padded array confines the damage entirely within the array’s own data allocation, wiping out up to 31 subsequent elements depending on the initial index.

However, when developers delete elements positioned within the final 31 bytes of a padded array, the 32-byte zero write overflows past the array’s boundary and corrupts whatever data structure resides immediately afterward in memory. For unpadded arrays generated via concatenation or ABI encoding functions, deleting any of the final 31 elements universally spills over into subsequent memory allocations, regardless of the overall array length. For smaller unpadded arrays consisting of 31 bytes or fewer, this overflow risk applies to every single index, including the very first element.

Example Scenario

To illustrate how both internal and external memory corruption manifest in practice, consider a smart contract featuring adjacent variable declarations designed to leverage predictable memory layouts. Within a test contract containing a helper function to clear an element of a short byte array, invoking delete d[0] on a five-byte array allocates a single padded word. The flawed delete operation clears the entire word starting from index zero, inadvertently wiping out the remaining four bytes of the array alongside the target element.

In a separate test function demonstrating corruption beyond array boundaries, a 32-byte padded array is allocated immediately alongside a struct containing a maximum-value 256-bit integer. When the code executes delete b[31] to clear the final element of the byte array, the 32-byte zero write spills over into the adjacent memory location occupied by the integer. Because integers are stored in a right-aligned format with their most significant bytes placed first, the rogue write zeroes out the 31 most significant bytes of the integer. As a result, an expected maximum integer value collapses down to 255, completely altering the application state without triggering any runtime exceptions or compiler warnings. Replacing these vulnerable delete statements with standard explicit assignments successfully prevents the unintended memory corruption across all tested compiler versions.

Security auditors note that because the compiler emits no warnings and the EVM executes the instructions without reverting, diagnosing this behavior can prove exceptionally difficult during standard testing phases. Although internal array corruption is frequently caught by unit tests inspecting returned array values, memory corruption affecting adjacent variables often escapes notice until anomalous application behavior occurs in production environments.

Following the public disclosure and confirmation of the bug, the Solidity engineering team implemented a robust patch in version 0.8.37. The fix forces the compiler’s delete code path to properly consult element widths in the exact same manner as standard assignment operations. When clearing packed byte-array elements, the compiler now correctly emits a single-byte store instruction rather than a full-word memory write, ensuring absolute parity between delete b[i] and b[i] = 0 across all compilation pipelines. Developers maintaining active codebases utilizing affected compiler versions are strongly encouraged to upgrade their tooling chains to Solidity 0.8.37 or later, and to audit their codebases for instances where the delete keyword is applied to elements of memory-based bytes or string arrays.

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *