Skip to content

Commit 9e623e9

Browse files
committed
Address PR review comments.
1 parent 3b0d9ea commit 9e623e9

5 files changed

Lines changed: 19 additions & 30 deletions

File tree

Changelog.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
Language Features:
44
* General: Add a builtin that computes the base slot of a storage namespace using the `erc7201` formula from ERC-7201.
55

6+
Important Bugfixes:
7+
* Yul Optimizer: Fix `UnusedStoreEliminator` incorrectly removing `returndatacopy` operations when the length comes from a stale `returndatasize()` call that was invalidated by subsequent call opcodes.
8+
69
Compiler Features:
710
* Commandline Interface: Disallow selecting the deprecated assembly input mode that was only accessible via `--assemble` instead of treating it as equivalent to `--strict-assembly`.
811
* Commandline Interface: Introduce `--experimental` flag required for enabling the experimental mode.
@@ -12,12 +15,11 @@ Compiler Features:
1215
* Metadata: Store the state of the experimental mode in JSON and CBOR metadata. In CBOR this broadens the meaning of the existing `experimental` field, which used to indicate only the presence of certain experimental pragmas in the source.
1316
* Standard JSON Interface: Introduce `settings.experimental` setting required for enabling the experimental mode.
1417
* Yul Optimizer: Improve performance of control flow side effects collector and function references resolver.
15-
* Yul Optimizer: Remove optimization that eliminated `returndatacopy` operations. This optimization was intentionally removed because it is very rarely used and complicates the implementation of `UnusedStoreEliminator`.
18+
* Yul Optimizer: Remove optimization that eliminated `returndatacopy` operations.
1619

1720
Bugfixes:
1821
* Yul: Fix incorrect serialization of Yul object names containing double quotes and escape sequences, producing output that could not be parsed as valid Yul.
1922
* Yul EVM Code Transform: Improve stack shuffler performance by fixing a BFS deduplication issue.
20-
* Yul Optimizer: Fix `UnusedStoreEliminator` incorrectly removing `returndatacopy` operations when the length comes from a stale `returndatasize()` call that was invalidated by subsequent call opcodes.
2123

2224

2325
### 0.8.34 (2026-02-18)

docs/bugs.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
{
33
"uid": "SOL-2026-2",
44
"name": "UnusedStoreEliminatorStaleReturnDataSize",
5-
"summary": "The Yul optimizer's ``UnusedStoreEliminator`` may incorrectly remove ``returndatacopy`` operations when using a stale value from ``returndatasize()`` that was invalidated by subsequent call operations.",
6-
"description": "The ``UnusedStoreEliminator`` is a Yul optimizer step that removes redundant memory and storage writes. It has a special optimization for ``returndatacopy(0, 0, returndatasize())``, which copies the entire return data buffer to memory. This operation can be removed if the start offset is zero and the length equals `returndatasize()`, similar to other memory write operations. However, the check for this pattern did not account for `returndatasize()` values becoming stale. The size of the return data buffer is updated by ``CALL``, ``STATICCALL``, ``DELEGATECALL``, and ``CALLCODE`` opcodes. If a ``returndatasize()`` value is stored in a variable before such an operation and then used in a subsequent ``returndatacopy``, the stored size no longer reflects the actual return data buffer size. It should revert, if the stale return data size is greater than actual return data size. The optimizer would still consider it safe to remove, even though it may revert and should not be removed. This could lead to incorrect behavior when the code should revert but it does not. The bug only affects inline assembly or handwritten Yul code that uses optimizer sequences including the ``UnusedStoreEliminator`` step, which is a part of default optimizer sequence. The fix removes this optimisation as it is very rarely used.",
5+
"summary": "The Yul optimizer's ``UnusedStoreEliminator`` may incorrectly remove ``returndatacopy(...)`` operations when using a stale value from ``returndatasize()`` that was invalidated by subsequent call operations.",
6+
"description": "The ``UnusedStoreEliminator`` is a Yul optimizer step that removes redundant memory and storage writes. One of the operations eligible for removal is ``returndatacopy(...)``. This particular operation has a quirk - unlike any other instruction for bulk memory copying it reverts on out-of-bounds access. A revert is one of the side-effects that the optimizer guarantees to preserve so the operation can only be removed when it is certain that it cannot revert. This is the case when the entire return data buffer is copied to memory, i.e. when the start offset is zero and the length equals ``returndatasize()``. The optimizer was special-cased to detect and optimize only this specific pattern, since it matches the code produced by the code generator for external calls. However, the check did not account for the possibility of ``returndatasize()`` values becoming stale. The size of the return data buffer is updated by ``call()``, ``staticcall()``, ``delegatecall()``, and ``callcode()``. If a ``returndatasize()`` value is stored in a variable before such an operation and then used in a subsequent ``returndatacopy(...)``, the stored size may no longer reflect the actual return data buffer size. Despite this, the optimizer would consider it safe to remove, bypassing the revert and allowing the code to continue, possibly leading to unexpected behavior. Since the code generator never produces code that interleaves multiple calls and access to their return data, the bug only affected inline assembly or handwritten Yul code. The necessary condition is the use of an optimizer sequence including the ``UnusedStoreEliminator`` step (which is the default).",
77
"link": "https://blog.soliditylang.org/2026/X/Y/Z/",
88
"introduced": "0.8.13",
99
"fixed": "0.8.35",
10-
"severity": "low",
10+
"severity": "very low",
1111
"conditions": {
1212
"yulOptimizer": true
1313
}

libyul/optimiser/UnusedStoreEliminator.cpp

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,23 +158,28 @@ void UnusedStoreEliminator::visit(Statement const& _statement)
158158
// both by querying a combination of semantic information and by listing the instructions.
159159
// This way the assert below should be triggered on any change.
160160
using evmasm::SemanticInformation;
161-
bool isStorageWrite = (*instruction == Instruction::SSTORE);
162-
bool isMemoryWrite =
161+
bool isStorageOnlyWrite = (*instruction == Instruction::SSTORE);
162+
bool isMemoryOnlyWrite =
163163
*instruction == Instruction::EXTCODECOPY ||
164164
*instruction == Instruction::CODECOPY ||
165165
*instruction == Instruction::CALLDATACOPY ||
166-
// TODO: Removing MCOPY is complicated because it's not just a store but also a load.
167-
//*instruction == Instruction::MCOPY ||
168166
*instruction == Instruction::MSTORE ||
169167
*instruction == Instruction::MSTORE8;
168+
bool isBlacklisted =
169+
// TODO: Removing MCOPY is complicated because it's not just a store but also a load.
170+
*instruction == Instruction::MCOPY ||
171+
// The optimization that eliminated `returndatacopy` operations
172+
// was intentionally removed because it is very rarely used and complicates the implementation
173+
// of UnusedStoreEliminator. I.e. a variable which is initialized to `returndatasize()` becomes
174+
// stale when a call changing the size is used in between the variable initialization and `returndatacopy`.
175+
*instruction == Instruction::RETURNDATACOPY;
170176
bool isCandidateForRemoval =
171-
*instruction != Instruction::MCOPY &&
172-
*instruction != Instruction::RETURNDATACOPY &&
177+
!isBlacklisted &&
173178
SemanticInformation::otherState(*instruction) != SemanticInformation::Write && (
174179
SemanticInformation::storage(*instruction) == SemanticInformation::Write ||
175180
(!m_ignoreMemory && SemanticInformation::memory(*instruction) == SemanticInformation::Write)
176181
);
177-
yulAssert(isCandidateForRemoval == (isStorageWrite || (!m_ignoreMemory && isMemoryWrite)));
182+
yulAssert(isCandidateForRemoval == (isStorageOnlyWrite || (!m_ignoreMemory && isMemoryOnlyWrite)));
178183
if (isCandidateForRemoval)
179184
{
180185
m_allStores.insert(&_statement);

test/libyul/yulOptimizerTests/unusedStoreEliminator/returndatacopy_returndatasize.yul

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
11
// This test ensures that `returndatacopy` is NOT optimized away.
2-
// The optimization that eliminated `returndatacopy` operations
3-
// was intentionally removed because it is very rarely used and complicates the implementation
4-
// of UnusedStoreEliminator. I.e. a variable which is initialized to `returndatasize()` becomes
5-
// stale when a call changing the size is used in between the variable initialization and `returndatacopy`.
6-
// ```
7-
// let x = returndatasize()
8-
// staticcall(...)
9-
// returndatacopy(0, 0, x) // Cannot be optimized away, because it can revert.
10-
// ```
112
{
123
returndatacopy(0,0,returndatasize())
134
}

test/libyul/yulOptimizerTests/unusedStoreEliminator/returndatacopy_returndatasize_var.yul

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
11
// This test ensures that `returndatacopy` is NOT optimized away.
2-
// The optimization that eliminated `returndatacopy` operations
3-
// was intentionally removed because it is very rarely used and complicates the implementation
4-
// of UnusedStoreEliminator. I.e. a variable which is initialized to `returndatasize()` becomes
5-
// stale when a call changing the size is used in between the variable initialization and `returndatacopy`.
6-
// ```
7-
// let x = returndatasize()
8-
// staticcall(...)
9-
// returndatacopy(0, 0, x) // Cannot be optimized away, because it can revert.
10-
// ```
112
{
123
let s := returndatasize()
134
returndatacopy(0,0,s)

0 commit comments

Comments
 (0)