Duplicate setimmutable silently drops second write
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Description
Calling the Yul builtin `setimmutable(offset, name, value)` twice with the same `name` in a single object silently drops the SECOND call. No error and no warning are produced. Only the FIRST value reaches the immutable slot; the second (and any subsequent) call collapses to a 5-byte `POP; POP;` no-op in the deployed bytecode.
Root cause: in `libevmasm/Assembly.cpp` (the `AssignImmutable` arm of `Assembly::assemble()`, lines 1403–1435), after the first `AssignImmutable` item for a given name is assembled, line 1434 calls `immutableReferencesBySub.erase(item.data())`, wiping the slot offsets from the map. When a second `AssignImmutable` for the same name arrives, the map lookup returns an empty offsets vector, the loop body runs zero times, and execution falls into the empty-offsets branch at lines 1427–1432 which only emits `POP; POP;`. The user's second value is silently discarded.
Relevant assembler arm:
```cpp
// libevmasm/Assembly.cpp:1403-1435 — AssignImmutable assembler arm
case AssignImmutable:
{
auto const& offsets = immutableReferencesBySub[item.data()].second;
for (size_t i = 0; i < offsets.size(); ++i)
{
if (i != offsets.size() - 1)
{
ret.bytecode.push_back(static_cast(Instruction::DUP2));
instructionLocationEmitter.emit();
ret.bytecode.push_back(static_cast(Instruction::DUP2));
instructionLocationEmitter.emit();
}
// ... PUSH offset ADD MSTORE ...
}
if (offsets.empty())
{
ret.bytecode.push_back(static_cast(Instruction::POP));
instructionLocationEmitter.emit();
ret.bytecode.push_back(static_cast(Instruction::POP));
}
immutableReferencesBySub.erase(item.data()); // ← line 1434, the silent overwrite
break;
}
```
Expected: either a diagnostic ("immutable assigned twice") raised in `AsmAnalysis`, or at minimum an `AssemblyException` at assembly time — comparable to the sibling case where `setimmutable` and `loadimmutable` appear in the same flat block, which IS caught by an `assertThrow` at `Assembly.cpp:1296-1300`. The Yul docs for `setimmutable(offset, "name", value)` do not specify behavior for duplicate-name calls; Solidity's own surface enforces exactly-one-assignment via `ImmutableValidator`, but at the Yul level no analyzer rejects the duplicate.
## Environment
- Compiler version: 0.8.35-develop.2026.5.5+commit.47b9dedd.Linux.g++
- Operating system: Linux Ubuntu Jammy
## Steps to Reproduce
Minimal Yul reproducer (compile with `solc --strict-assembly --bin `):
```yul
object "C" {
code {
setimmutable(0, "x", 1)
setimmutable(0, "x", 2) // silently dropped
let s := datasize("C_runtime")
let o := dataoffset("C_runtime")
codecopy(0, o, s)
return(0, s)
}
object "C_runtime" {
code {
mstore(0, loadimmutable("x"))
return(0, 32)
}
}
}
```
Output (`solc --strict-assembly --bin`):
```
Binary representation:
60015f6001015260025f505060278060165f395ff3fe7f<32 zero bytes>5f5260205ff3
```
The deployed contract returns `1`, not `2`. The 5-byte `60025f5050` fingerprint corresponds to the second `setimmutable("x", 2)` call collapsing to `PUSH1 0x02; PUSH0; POP; POP;`.
Removing the second line yields:
```
60015f6001015260278060115f395ff3fe7f<32 zero bytes>5f5260205ff3
```
— identical save for the absent 5-byte `60025f5050` insertion (and the constructor offsets shifted by 5 to accommodate the no-op). The runtime tail is unchanged, confirming the second value never reaches any slot.
Compilation produces no `Error` and no `Warning` in either case.
Contributor guide
Research direction
Start in libevmasm/Assembly.cpp at the AssignImmutable arm around lines 1403–1435, then inspect the sibling setimmutable/loadimmutable validation at lines 1296–1300 and the related AsmAnalysis path. Make duplicate assignments produce a diagnostic or AssemblyException instead of silently emitting POP; POP;, and verify the supplied Yul reproducer no longer compiles without an error or warning.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100