Home / Smart Contract Auditing / Critical Solidity Memory Bug Discovered: Deleting Elements in Byte Arrays Can Overwrite Adjacent Data

Critical Solidity Memory Bug Discovered: Deleting Elements in Byte Arrays Can Overwrite Adjacent Data

A newly discovered vulnerability in the Solidity programming language has brought to light an unexpected code generation flaw affecting memory byte arrays. Reported on August 14, 2026, by researcher Shaheen Fazim through the Ethereum Foundation’s official bug bounty program, the vulnerability causes memory corruption when the delete operator is applied to an individual element of a bytes array located in memory. Rather than clearing only the specific byte targeted by the developer, the generated bytecode writes a full 32-byte word of zeroes starting directly at that element. This unintended behavior overwrites up to 31 subsequent bytes, threatening either other data residing within the same array or, when the deletion occurs near the end of the array, adjacent variables allocated directly afterward in EVM memory.

Assigned a low-to-medium severity rating on the Ethereum Foundation’s internal scale, the defect impacts the default evmasm compilation pipeline, while the newer IR pipeline remains entirely unaffected. Crucially, triggering the bug does not require the use of the compiler optimizer, as the flaw stems directly from the code generation phase itself. The affected code path is deeply embedded in the compiler’s history, predating the very first public release of Solidity. Consequently, every single version of the compiler up to and including version 0.8.36 is affected. The issue has been formally addressed and patched in Solidity version 0.8.37.

To gauge the real-world impact of the discovery, developers scanned the Sourcify database, which houses approximately 870,000 verified smart contract compilations, searching specifically for the vulnerable source code pattern. The analysis revealed that the pattern appears in a mere 17 compilations, encompassing only two distinct functions. Furthermore, a deeper inspection confirmed that neither of those functions is actually vulnerable in practice. In one instance, the overwritten bytes within the array are subsequently discarded or deleted anyway, while in the other, the array never grows large enough to reach the overwritten memory, or the write lands safely in unused space. As a result, no deployed smart contracts are currently known to have suffered malicious exploitation or unintended state corruption due to this bug.

Which Contracts Are Affected?

Understanding the precise conditions under which the bug manifests is essential for smart contract auditors and developers reviewing legacy codebases. A contract is only vulnerable if several distinct conditions align during compilation and execution. First, the source code must be compiled using an affected version of Solidity—specifically, any version from the inception of the language up to 0.8.37. Second, the compilation must utilize the default evmasm code generation pipeline rather than the IR pipeline. Finally, the code logic must explicitly apply the delete operator to an element of a dynamic bytes or string array that resides in memory.

While verifying the first two conditions is straightforward by inspecting compiler settings and source code configurations, evaluating the third condition requires careful reasoning about the underlying memory layout produced by the compiler. Because memory layout intricacies can be difficult to predict manually, security teams recommend that if the first two conditions are met, developers should treat their contracts with caution or update their compiler version rather than attempting to definitively rule out the third condition.

It is important to emphasize that if a smart contract never applies the delete operator to an element of a bytes or string value stored in memory, it is completely immune to this vulnerability. Neither the activation of the optimizer nor the targeted Ethereum Virtual Machine version alters whether the bug appears, because the defect resides fundamentally in the instructions emitted by the code generator. The optimizer merely preserves these faulty semantics rather than correcting or worsening them.

A few technical nuances further clarify the scope of the vulnerability. Only element-level deletions are affected; executing a delete operation on an entire array—such as calling delete b on a whole byte array—invokes a completely different and correct code path. Furthermore, the array must reside in memory. Byte arrays stored in persistent storage utilize an entirely separate code path and remain unaffected, while the delete operator cannot be applied to calldata at all. Similarly, arrays whose elements each occupy a full 32-byte word in memory, such as bytes1[] memory, are safe because only bytes and string types pack their elements back-to-back without gaps.

Although string values use the exact same underlying memory representation as bytes, standard string indexing is not natively supported in Solidity. However, developers frequently convert strings to bytes typecasts—such as bytes(s)—to inspect or modify individual characters. Applying the delete operator to an indexed cast like delete bytes(s)[i] triggers the identical vulnerability, modifying the string’s internal data in place. Conversely, fixed-byte types like bytes32 are unaffected because indexing them yields an isolated copy of the byte rather than a mutable reference into the underlying memory buffer.

Technical Details

To understand the root cause of the bug, one must examine how Solidity manages memory allocation and storage for variable-length byte arrays. Most values in EVM memory occupy full 32-byte words. The primary exception is the bytes type: a memory byte array consists of an initial 32-byte length field followed immediately by its raw data bytes, packed continuously without internal padding.

Solidity allocates memory by advancing a global free memory pointer, ensuring that each new allocation begins exactly where the previous one terminated. Consequently, consecutive memory allocations sit flush against one another, separated only by necessary word-alignment padding. When a write operation overruns the intended boundaries of an allocation, it spills directly into the memory space assigned to the next variable.

In standard operations—such as arrays created via new bytes(n) or initialized from literal byte strings—the compiler appends up to 31 zero bytes of padding so that subsequent allocations always begin on a clean 32-byte word boundary. These are classified as padded arrays. However, the legacy evmasm pipeline omits this padding for the results of certain native functions, including bytes.concat, string.concat, and the entire suite of ABI encoding functions like abi.encode, abi.encodePacked, and abi.encodeCall. The data produced by these functions ends precisely where the next memory allocation begins, creating unpadded arrays.

Whether an allocation includes padding is an internal implementation detail that can vary between compilation pipelines and future compiler versions. Smart contract code must never rely on specific memory layouts, and inline assembly that manipulates padding violates official memory safety guidelines.

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

When a value narrower than 32 bytes is stored in memory, its alignment depends entirely on its data type. Left-aligned values, such as fixed bytesN types, begin at the very first byte of the word. Right-aligned values, including integers, addresses, memory pointers, and dynamic array length fields, sit at the far right of the word with leading zero bytes filling the front. Because byte array elements are single bytes, storing values into them is normally the one scenario where a memory write does not occupy a full word. The compiler normally handles this by issuing a single-byte store instruction (mstore8) whenever the element width is known—with the single exception of the flawed delete path.

When developers execute delete x, Solidity assigns the zero value corresponding to x‘s data type. Logically, executing delete b[i] should behave identically to an explicit assignment like b[i] = 0. While both operations ultimately target the memory location of the array element, the legacy evmasm pipeline routes them through completely different store implementations.

While a standard assignment correctly identifies the element’s width and invokes a single-byte store, the delete path fails to check the element width. Instead, it applies a full 32-byte word store initialized with zeroes. Because this write begins at the specific index of the deleted element rather than aligning with a 32-byte word boundary, it spills forward across multiple bytes. The array’s length field, which sits safely ahead of the data buffer, remains untouched, meaning the array preserves its original length. However, any subsequent computations relying on the array’s contents—such as computing a keccak256 hash, performing ABI encoding, or returning the byte array to an external caller—will process the corrupted data. The IR pipeline utilizes a unified store mechanism for both assignments and deletions, which is why it remains completely immune to the bug.

How Far the Write Reaches

The extent of the data corruption depends heavily on the index of the deleted element and how the array was initially allocated in memory. For padded arrays, the trailing padding bytes absorb a portion of the rogue write. Deletions affecting elements far from the end of the array stay entirely within the array’s allocated memory buffer. However, deleting one of the final 31 elements of a padded array causes the 32-byte zero write to bleed past the end of the array and into whatever variable happens to be allocated immediately afterward.

The exact number of vulnerable ending elements depends on the array’s total length. Arrays with lengths that leave a full 31 bytes of padding—such as lengths 1, 33, or 65—do not leak writes past their boundaries. Conversely, arrays whose lengths are exact multiples of 32 have zero padding, meaning deletions on any of their final 31 elements will overwrite adjacent memory.

When a write spills into the next memory allocation, its destructive impact depends on the alignment of the adjacent variable. For left-aligned values, the most significant bytes are cleared to zero. For right-aligned variables such as integers, addresses, or pointers, the write wipes out the leading bytes of the word. A small spill of up to 12 bytes might leave an Ethereum address intact, while a maximal spill of 31 bytes strips away nearly the entire numerical magnitude of a large integer. For unpadded arrays generated by concatenation or encoding functions, deleting any of the final 31 elements will always spill into the subsequent allocation, regardless of the array’s overall length. For unpadded arrays shorter than 31 bytes, this vulnerability applies to every single index, including the very first element.

Example Demonstrating the Flaw

To illustrate how both forms of corruption manifest in real-world code, security researchers provided a demonstration contract. The example relies on the standard behavior of the Solidity compiler, which allocates consecutively declared local variables in adjacent memory slots.

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 the default evmasm pipeline, both functions produce corrupted execution results. In the clearElement function, the small five-byte array occupies part of a single padded memory word. Executing delete d[0] triggers a 32-byte zero write starting at index zero. Instead of clearing only the first byte, the operation wipes out the entire word, inadvertently clearing the remaining four bytes of the array as well. The function returns an all-zero byte array instead of the expected partial clearance.

In the clearLastElement function, the 32-byte array occupies exactly one full word with zero padding. Deleting the final element at index 31 causes the 32-byte zero write to spill directly into the memory location of structure s, which sits immediately following the array. Because 256-bit unsigned integers are stored right-aligned with their most significant bytes placed first, the 31 overwritten bytes destroy the high-order data of type(uint256).max. Only the lowest byte survives intact, causing the function to return 255 instead of the maximum possible integer value. Replacing these delete statements with explicit assignments like d[0] = 0 and b[31] = 0 produces the correct, expected behavior across all tested compiler versions.

Severity Assessment and Remediation

While the compiler emits no warnings and executed bytecode does not revert at runtime, the silent nature of the corruption makes debugging exceptionally difficult. Internal array corruption is the more frequently encountered manifestation and is readily caught by rigorous unit tests that inspect the resulting byte arrays. Fortunately, the practical risk in deployed smart contracts remains low, as demonstrated by the extensive Sourcify code scans.

The official remedy implemented by the Solidity development team in version 0.8.37 ensures consistency between assignment and deletion logic. The compiler’s delete code path now explicitly consults the element width, emitting a precise single-byte store (mstore8) when clearing packed byte array elements rather than defaulting to a full-word store. Consequently, delete b[i] and b[i] = 0 now behave identically across both compilation pipelines.

Developers maintaining codebases utilizing affected compiler versions are strongly advised to audit their smart contracts for any instances where the delete operator is applied to individual elements of memory-based bytes or string arrays. Upgrading the project to Solidity 0.8.37 or later resolves the underlying code generation defect entirely, restoring expected memory safety guarantees across all execution pathways.

Tagged:

Leave a Reply

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