Home / Smart Contract Auditing / Critical Vulnerability Discovered in Solidity Code Generator: Memory Byte Array Deletion Bug Fixed in Version 0.8.37

Critical Vulnerability Discovered in Solidity Code Generator: Memory Byte Array Deletion Bug Fixed in Version 0.8.37

The Ethereum Foundation has released an important security advisory regarding a long-standing vulnerability in Solidity, the primary programming language for writing smart contracts on the Ethereum blockchain and other compatible networks. Discovered by researcher Shaheen Fazim on August 14, 2026, through the Ethereum Foundation’s bug bounty program, the defect affects the code generator’s handling of memory byte arrays. Specifically, when the delete operator is applied to an element within a bytes or string array located in memory, the generated code incorrectly writes 32 zero bytes starting at the targeted element instead of cleanly erasing just that single byte.

This unexpected behavior introduces potential data corruption risks, as the overflow write can wipe out up to 31 subsequent bytes. Depending on the array’s layout in memory, this overflow can either corrupt adjacent elements within the same data structure or bleed past the boundary of the array entirely, overwriting the beginning of whatever memory allocation happens to sit directly after it. Interestingly, the equivalent manual assignment operation (b[i] = 0) remains entirely unaffected by the bug and correctly clears only the single designated byte.

Following a thorough internal evaluation, the Solidity development team assigned the vulnerability a severity rating of low-to-medium. The flaw is strictly confined to the default evmasm compilation pipeline, while the newer IR (Intermediate Representation) pipeline remains completely untouched. Furthermore, the activation of the compiler optimizer is not required to trigger the defect, meaning standard, unoptimized builds are equally vulnerable.

Code analysis reveals that the affected code path actually predates the very first publicly released version of Solidity. Consequently, all compiler versions up to and including version 0.8.36 are susceptible to the bug. To address the issue, developers have patched the compiler in Solidity version 0.8.37, ensuring that the delete instruction correctly respects element boundaries.

To gauge the real-world impact of the discovery, the Solidity team performed a comprehensive scan of the Sourcify database, which contains roughly 870,000 verified smart contract compilations. The analysis searched specifically for the problematic source pattern across the ecosystem. The pattern was identified in a mere 17 compilations, encompassing only two distinct functions. Upon closer inspection, neither of those functions was found to be actively harmed by the flaw; in both cases, the bytes cleared within the arrays were either immediately discarded or deleted anyway, while adjacent memory regions either contained safe array lengths or remained entirely unused. At the time of the disclosure, no deployed smart contracts in the wild are known to have suffered malicious exploitation or unintended data corruption as a result of the bug.

Which Contracts Are Affected?

Understanding exposure to the vulnerability requires examining specific compiler configurations and source code patterns. A smart contract is only considered vulnerable if a precise combination of conditions is met simultaneously during the compilation and execution phases.

First, the contract must be compiled using an affected version of the Solidity compiler—specifically, version 0.8.36 or any earlier release. Second, the compilation must utilize the default evmasm code generation pipeline rather than the IR-based pipeline. Third, the source code must actively apply the delete operator to an individual element of a dynamic byte array or string residing in memory.

While the first two conditions can be verified easily by inspecting project configuration files and compiler settings, verifying the third condition requires a deeper understanding of how the compiler manages memory layouts. For development teams auditing existing codebases, security analysts recommend treating any contract that meets the first two criteria as potentially affected if it handles memory byte deletions, thereby avoiding the complexities of manually mapping out exact memory slots.

It is worth noting that if a smart contract never applies the delete operator to an individual element of a bytes or string variable in memory, it is completely immune to the issue. Neither the compiler optimizer nor the target Ethereum Virtual Machine (EVM) version alters whether the bug manifests, because the underlying flaw stems directly from the faulty bytecode emitted during the code generation stage. The optimizer simply preserves those incorrect semantics during its pass over the code.

Several important technical nuances govern whether a specific implementation falls victim to the bug. Crucially, only element-level deletions are vulnerable. Invoking delete b on an entire array routes through a completely separate code path and executes correctly without memory corruption. Additionally, the array in question must reside in memory. Byte arrays stored in contract storage utilize an entirely different handling mechanism and remain unaffected, while the delete operator cannot be applied to calldata arrays at all.

Data structures where every individual element occupies a full 32-word block in memory—such as bytes1[] memory—are also immune to the bug. The issue is uniquely isolated to bytes and string types, which pack their elements tightly back-to-back without word-boundary padding between individual indices.

Furthermore, string values share the exact same internal representation as bytes arrays. While direct syntax like delete s[i] will not compile for a string memory s, developers frequently cast strings to bytes to perform granular manipulations, writing delete bytes(s)[i]. Doing so invokes the identical faulty code path and modifies the underlying string data in place. Conversely, fixed-size byte arrays like bytes32 are unaffected because indexing them yields an isolated copy of the target byte rather than a mutable reference to the underlying memory block.

Technical Details

To comprehend the root cause of the memory corruption, it is necessary to examine how Solidity manages memory allocations under the hood. Most data types in Solidity memory occupy full 32-byte words. Dynamic byte arrays represent the primary exception: a memory byte array consists of an initial 32-byte length descriptor followed immediately by its raw data bytes, packed back-to-back without internal gaps.

Solidity allocates memory dynamically by incrementing a global free memory pointer, ensuring that each new allocation begins precisely where the previous one terminated. Consequently, consecutive allocations sit flush against one another, separated only by necessary word-alignment padding. If a write operation accidentally exceeds the bounds of a given allocation, it spills over directly into whatever data structure occupies the subsequent memory slot.

Most memory allocations created via new bytes(n) or initialized from literal values are padded with up to 31 zero bytes at the end, ensuring that any subsequent memory allocation cleanly starts on a fresh 32-byte word boundary. These are designated as padded arrays. However, the default evmasm pipeline omits padding for the results of bytes.concat, string.concat, and the various abi.encode family functions, including abi.encodePacked, abi.encodeWithSelector, and abi.encodeWithSignature. For these unpadded arrays, the underlying data ends precisely where the next allocation begins. Developers are strongly cautioned that whether an array is padded is a compiler implementation detail; code should never rely on specific padding behaviors, and inline assembly that manipulates padding directly violates official memory safety rules.

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

When a narrow value is stored in memory, its alignment depends heavily on its specific data type. Left-aligned types, such as fixed-size bytesN variables, anchor at the very first byte of a word. Right-aligned types—including standard integers, addresses, memory pointers, and array length indicators—sit at the far end of the word, padded with leading zero bytes. Because byte arrays store elements as single bytes, storing values to them is generally not equivalent to storing a full 32-byte word. The compiler normally handles this by issuing a single-byte store instruction (mstore8) whenever the element width is explicitly known, with the delete path serving as the sole exception.

The core defect lies in the code path generated for the delete keyword. Conceptually, executing delete x assigns the default zero value corresponding to that type, meaning delete b[i] should behave identically to an explicit assignment like b[i] = 0. While both operations ultimately conclude with a write operation targeting the element’s memory location, the legacy evmasm pipeline utilizes entirely different underlying store implementations for each.

For standard assignments, the compiler correctly recognizes the single-byte width of the array element and emits an mstore8 instruction. For the delete path, however, the code generator erroneously emits a full 32-byte zero-write starting directly at the index of the deleted element. Because this write initiates at the specific byte offset of the array element rather than aligning with a 32-byte word boundary, it spills far beyond the intended target.

Crucially, the 32-byte length header situated at the beginning of the array is never touched during this process, allowing the array to retain its original length indicator. Consequently, any subsequent operations relying on the array’s contents—such as computing cryptographic hashes via keccak256, performing ABI encoding, or returning data to an external caller—will read the silently corrupted byte sequence. The IR compilation pipeline, by contrast, routes delete b[i] through the same unified store logic used for standard assignments and remains entirely immune to the flaw.

The extent of the resulting memory corruption depends heavily on the index of the deleted element and how the array was originally allocated. When an element is deleted from the interior of an array, every subsequent element up to 31 indices forward is forcibly overwritten with zeros. Unless the array happens to contain entirely zero bytes past the deletion point, this alters the array’s contents immediately upon execution.

When the deletion targets one of the final elements of an array, the 32-byte zero-write can easily breach the array’s boundaries and corrupt the beginning of the next adjacent memory allocation. For padded arrays, the trailing padding absorbs a portion of the write. Deletions near the end of the array will only bleed into subsequent memory if the padding is insufficient to contain the overflow. The number of vulnerable trailing elements is dictated by the array’s length: arrays whose lengths leave a full 31 bytes of padding experience no overflow, whereas arrays whose lengths are exact multiples of 32 have zero padding, leaving their final 31 elements vulnerable to spilling over into neighboring memory blocks.

When memory corruption spills past a padded array, it overwrites the header or leading bytes of the subsequent allocation. For left-aligned memory structures, the most significant bytes are wiped clean. For right-aligned values such as integers and memory pointers, the overwrite clears the leading bytes of the word, with the survival of the stored value depending entirely on the magnitude of the overflow. A minor spill of 12 bytes or fewer may leave an Ethereum address intact, whereas a maximal write of 31 bytes strips away everything except the least significant byte. For unpadded arrays, deleting any of the final 31 elements invariably breaches the adjacent memory allocation regardless of the array’s overall length. For short unpadded arrays consisting of 31 bytes or fewer, this vulnerability applies to every index, including the very first element.

Example Scenario

To illustrate how both forms of corruption manifest in practice, security researchers constructed a demonstration contract designed to leverage adjacent memory allocations produced by the compiler:

contract C 
    struct S  uint256 x; 

    // Corruption inside the array.
    function clearElement() public pure returns (bytes memory) 
        bytes memory d = hex"0102030405";
        delete d[0];
        return d;
    

    // Corruption beyond the array.
    function clearLastElement() public pure returns (uint256) 
        bytes memory b = new bytes(32);
        S memory s = S(type(uint256).max);
        delete b[31];
        return s.x;
    

When compiled using default settings via the vulnerable evmasm pipeline, both functions produce corrupted outputs. In the clearElement function, the five-byte array occupies a portion of a single padded word. Executing delete d[0] triggers a 32-byte zero write starting from index zero, wiping out the targeted element along with the remaining four bytes of the array, causing the function to return an array of all zeros rather than the expected single-byte erasure.

In the clearLastElement function, the 32-byte array occupies exactly one word with zero padding. Deleting the final element at index 31 emits a 32-byte zero write that spills 31 bytes directly into the memory allocated for structure s, which sits immediately following the array. Because integer values are stored in memory with their most significant bytes first, the 31 overwritten bytes correspond to the most significant portions of s.x. As a result, the maximum possible 256-bit integer value is stripped of its leading bytes, leaving only the lowest byte intact, causing the function to return 255 instead of the expected maximum integer value. Replacing these delete statements with explicit assignments like d[0] = 0 and b[31] = 0 produces correct results across all tested compiler versions.

Severity Assessment and Recommended Actions

The compiler emits no warning when compiling vulnerable code, and the generated bytecode does not trigger runtime reverts. Instead, the defect manifests silently as unexpected data corruption, complicating debugging efforts. However, intra-array corruption—the more frequent of the two manifestations—is typically exposed during standard unit testing when developers inspect the resulting array values.

Although the flaw can be triggered entirely through high-level Solidity code without invoking inline assembly, its practical likelihood of occurrence in production environments is low. The bug remained undetected throughout the history of the language, and ecosystem scans identified the vulnerable pattern in only two distinct deployed functions, neither of which suffered active functional damage.

The fix implemented in Solidity version 0.8.37 ensures that the delete path correctly queries the element width in the exact same manner as standard assignments. When clearing a packed byte array element, the compiler now emits a precise single-byte store (mstore8) instead of a full-word store, aligning the behavior of delete b[i] and b[i] = 0 across all compilation pipelines.

Development teams maintaining smart contracts written in Solidity are advised to upgrade their compiler toolchains to version 0.8.37 or later. Projects unable to upgrade immediately should review their codebases to ensure that the delete operator is never applied to individual elements of memory-resident bytes or string arrays, opting instead for explicit zero assignments where granular clearing is required.

Tagged:

Leave a Reply

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