argotorg / argotorg/solidity

Duplicate setimmutable silently drops second write

Open
#16,811 4 comments 0 reactions 0 assignees View on GitHub
bug :bug: low effort medium impact must have eventually
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.