[via-IR][optimizer] Infinite hang in StackLayoutGenerator stack shuffler with unchecked narrow arithmetic + console.log + assert
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
Research direction
Start with libyul/backends/evm/StackLayoutGenerator.cpp and the StackHelpers.h shuffler named in the report. Reproduce with the supplied Solidity source using solc --via-ir --optimize --bin source.sol, then investigate why stack shuffling does not terminate; the report also provides a version that compiles when optimization is disabled for comparison. Done when the reproducer terminates and compilation produces a diagnostic or output instead of hanging.
Written by the indexing model from the issue text.
Description
Description
solc --via-ir --optimize spins forever on a single function that mixes unchecked narrow-integer
arithmetic, forge-std-style console.log calls and assert statements: 100 % of one core, flat memory,
and zero output — no StackTooDeepError, no YulAssertion, no diagnostic of any kind. The process has
to be killed externally. GDB shows the time being spent in
libyul/backends/evm/StackLayoutGenerator.cpp (the propagateStackThroughOperation /
StackHelpers.h Shuffler<ShuffleOperations>::shuffle stack-shuffle solver).
Reduced from a machine-generated Foundry test contract by line-level delta debugging (greedy deletion,
accepting a reduction only while a 10 s timeout was still hit). Two independent reductions — 78 and
91 lines, starting from different files — reproduce the same hang.
The sub-system matches #16704 / #16745–#16748, but the symptom does not: those either abort
(YulAssertion) or finish within 12–43 s, whereas this one emits nothing and keeps running.
Environment
- Compiler version:
0.8.34(svm) and0.8.37(official release) — both affected; also the
ZKsync-patched0.8.30-1.0.2. - Compilation pipeline: via-IR + optimizer (with
optimizer.enabled = falsethe same file compiles in ~46 s). - Target EVM version: default (no
--evm-versionpassed). - Framework/IDE: originally Foundry (
forge test -vv, viazksolc); reproducer below is plainsolcCLI. - EVM execution environment: n/a (compile-time only).
- Operating system: Linux
7.0.0-34-generic, x86-64, 6 cores.
Steps to Reproduce
The reproducer is self-contained (the minimal console library is inlined, no forge-std needed):
solc --via-ir --optimize --bin source.sol
$ timeout 10 solc-0.8.34 --via-ir --optimize --bin source.sol ; echo "exit=$?"
exit=124 # killed by timeout — 0 bytes on stdout, 0 bytes on stderr
timeout 2400 was killed after 40 minutes with 0 bytes on both streams. Process state throughout:
R, ~100 % of one core, and VmSize=247.3 MB / VmRSS=242.6 MB sampled every 3 s for 18 s — completely flat.
Reproducer A (78 lines) — click to expand
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library console {
address constant CONSOLE_ADDRESS = 0x000000000000000000636F6e736F6c652e6c6f67;
function _sendLogPayloadImplementation(bytes memory payload) internal view {
address consoleAddress = CONSOLE_ADDRESS;
assembly ("memory-safe") {
pop(staticcall(gas(), consoleAddress, add(payload, 32), mload(payload), 0, 0))
}
}
function _castToPure(function(bytes memory) internal view fnIn) internal pure
returns (function(bytes memory) pure fnOut)
{
assembly { fnOut := fnIn }
}
function _sendLogPayload(bytes memory payload) internal pure {
_castToPure(_sendLogPayloadImplementation)(payload);
}
function log(string memory p0, uint256 p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,uint256)", p0, p1));
}
function log(string memory p0, int256 p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,int256)", p0, p1));
}
}
contract GeneratedTest{
// ========== Fixture arrays (value=1) for forge fuzz testing ==========
uint256[] public fixtureSeedUint256 = [uint256(1)];
uint32[] public fixtureSeedUint32 = [uint32(1)];
uint64[] public fixtureSeedUint64 = [uint64(1)];
int64[] public fixtureSeedInt64 = [int64(1)];
int32[] public fixtureSeedInt32 = [int32(1)];
function testFuzz_run(uint256 seedUint256, uint32 seedUint32, uint64 seedUint64, int64 seedInt64, int32 seedInt32) public{
unchecked {
int64 var0 = ((int64(8356521789746546688) + (int64(7488455986218573824) * fixtureSeedInt64[0])) | (int64(2336277531640645632) ^ (int64(2816076421627660288) * fixtureSeedInt64[0])));
uint64 var1 = ((uint64(11002395062264868864) + (uint64(1777921714135719936) * fixtureSeedUint64[0])) % (uint64(12119725343812202496) * (uint64(9331666172782899200) * fixtureSeedUint64[0])));
assert((var1 == var1));
int32 var2 = ((int32(1544679149) | int32(-232285414)) & (int32(-478697375) | (int32(-1794086546) * fixtureSeedInt32[0])));
for (uint32 i = uint32(650609376); i < uint32(650609377); i++) {
uint32 var3 = (uint32(3966023184) * fixtureSeedUint32[0]);
int32 var4 = (int32(-873082394) * fixtureSeedInt32[0]);
for (uint32 j = uint32(2511881017); j < uint32(2511881020); j++) {
}
uint32 var6 = ((var3 + (uint32(838445771) * fixtureSeedUint32[0])) % (uint32(449846147) & var3));
int64 var7 = (~(((var0 >= (var0 * fixtureSeedInt64[0])) ? int64((int64(-2699486544584349696) * fixtureSeedInt64[0])) : int64((var0 * fixtureSeedInt64[0])))));
if (((false && (var7 != (var0 * fixtureSeedInt64[0]))) || ((var2 > (int32(2118252708) * fixtureSeedInt32[0])) || true))) {
uint64 var8 = ((var1 - (uint64(13007521369530060800) * fixtureSeedUint64[0])) + (~(var1)));
var3 = ((false || (var1 <= (var1 * fixtureSeedUint64[0]))) ? uint32(((var8 == (var8 * fixtureSeedUint64[0])) ? uint32(var6) : uint32((uint32(866843478) * fixtureSeedUint32[0])))) : uint32((uint32(3544590767) ^ var3)));
assert(((((var3 == (var3 - (uint32(0) * fixtureSeedUint32[0]))) && (var3 <= (uint32(94251739) * fixtureSeedUint32[0]))) && (!((var3 != (var3 / (uint32(1) * fixtureSeedUint32[0])))))) || (!((i == (uint32(650609376) * fixtureSeedUint32[0]))))));
var7 = ((var7 | (int64(1019382045118269440) * fixtureSeedInt64[0])) - (var7 % (int64(6768255337194600448) * fixtureSeedInt64[0])));
for (uint32 j = uint32(236595508); j < uint32(236595511); j++) {
uint32 var9 = ((var6 * (var6 * fixtureSeedUint32[0])) - (uint32(817585025) - (var3 * fixtureSeedUint32[0])));
int64 var10 = ((var7 ^ (var0 * fixtureSeedInt64[0])) & (int64(-4589272784087918592) % (int64(5353105587992414208) * fixtureSeedInt64[0])));
for (uint32 k = uint32(1653486608); k < uint32(1653486611); k++) {
int64 var11 = ((var10 ^ (int64(-7095492279848376320) * fixtureSeedInt64[0])) | (int64(-7978974020914317312) * fixtureSeedInt64[0]));
assert(((((!((var11 < int64(-2463750541153601536)))) && (var11 >= (int64(-2463750541153601536) * fixtureSeedInt64[0]))) && (var11 >= (int64(-9223372036854775808) * fixtureSeedInt64[0]))) || (!((((i == (uint32(650609376) * fixtureSeedUint32[0])) && (j == (uint32(236595510) * fixtureSeedUint32[0]))) && (k == (uint32(1653486608) * fixtureSeedUint32[0])))))));
uint64 var12 = ((var1 % (var8 * fixtureSeedUint64[0])) | (var8 & (uint64(11743156967922180096) * fixtureSeedUint64[0])));
assert(((((!((var12 != (var12 + (uint64(0) * fixtureSeedUint64[0]))))) && (!((var12 > uint64(251765810976239616))))) && (var12 <= (uint64(251765810976239616) * fixtureSeedUint64[0]))) || (!((((i == (uint32(650609376) * fixtureSeedUint32[0])) && (j == (uint32(236595510) * fixtureSeedUint32[0]))) && (k == (uint32(1653486608) * fixtureSeedUint32[0])))))));
uint32 var13 = (var6 | (uint32(1921773189) * fixtureSeedUint32[0]));
console.log("Value of 0022:", var13);
assert((((var13 == (var13 * (uint32(1) * fixtureSeedUint32[0]))) && (var13 == (var13 * (uint32(1) * fixtureSeedUint32[0])))) || (!((((i == (uint32(650609376) * fixtureSeedUint32[0])) && (j == (uint32(236595510) * fixtureSeedUint32[0]))) && (k == uint32(1653486610)))))));
}
uint32 var14 = ((false ? uint32((uint32(4068883520) * fixtureSeedUint32[0])) : uint32(var3)) * (var9 * fixtureSeedUint32[0]));
var2 = ((false ? (var10 >= (int64(-9219989110020784128) * fixtureSeedInt64[0])) : (var3 != var14)) ? int32((var4 - var2)) : int32((-((int32(-1227297892) * fixtureSeedInt32[0])))));
}
} else {
}
int32 var17 = (((var2 < (var2 * fixtureSeedInt32[0])) || (var6 >= (uint32(721445180) * fixtureSeedUint32[0]))) ? int32((int32(1427346509) * fixtureSeedInt32[0])) : int32((-((int32(-777416749) * fixtureSeedInt32[0])))));
console.log("Value of 0027:", int256(var17));
assert(((((var17 == (var17 - (int32(0) * fixtureSeedInt32[0]))) && (var17 == (int32(777416749) * fixtureSeedInt32[0]))) || (var17 >= (int32(0) * fixtureSeedInt32[0]))) || (!((i == (uint32(650609376) * fixtureSeedUint32[0]))))));
}
}
}
}
Reproducer A (91 lines) — click to expand
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library console {
address constant CONSOLE_ADDRESS = 0x000000000000000000636F6e736F6c652e6c6f67;
function _sendLogPayloadImplementation(bytes memory payload) internal view {
address consoleAddress = CONSOLE_ADDRESS;
assembly ("memory-safe") {
pop(staticcall(gas(), consoleAddress, add(payload, 32), mload(payload), 0, 0))
}
}
function _castToPure(function(bytes memory) internal view fnIn) internal pure
returns (function(bytes memory) pure fnOut)
{
assembly { fnOut := fnIn }
}
function _sendLogPayload(bytes memory payload) internal pure {
_castToPure(_sendLogPayloadImplementation)(payload);
}
function log(string memory p0, uint256 p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,uint256)", p0, p1));
}
function log(string memory p0, int256 p1) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(string,int256)", p0, p1));
}
}
contract GeneratedTest{
// ========== Fixture arrays (value=1) for forge fuzz testing ==========
uint256[] public fixtureSeedUint256 = [uint256(1)];
uint32[] public fixtureSeedUint32 = [uint32(1)];
uint64[] public fixtureSeedUint64 = [uint64(1)];
int64[] public fixtureSeedInt64 = [int64(1)];
int32[] public fixtureSeedInt32 = [int32(1)];
function testFuzz_run(uint256 seedUint256, uint32 seedUint32, uint64 seedUint64, int64 seedInt64, int32 seedInt32) public{
unchecked {
int64 var0 = (-((~((int64(-2763102931801964544) * fixtureSeedInt64[0])))));
int64 var1 = ((int64(8173482772302186496) % var0) ^ var0);
assert(((var1 == (var1 * (int64(1) * fixtureSeedInt64[0]))) && (!((var1 > (int64(-208721565807327229) * fixtureSeedInt64[0]))))));
int64 var2 = ((true ? int64((var1 * fixtureSeedInt64[0])) : int64((var0 * fixtureSeedInt64[0]))) & (int64(-1417940872579362816) * var0));
uint32 var3 = ((uint32(4000644646) + (uint32(1403407272) * fixtureSeedUint32[0])) + (uint32(304248353) / (uint32(3387840218) * fixtureSeedUint32[0])));
uint64 var4 = ((uint64(14050382574896549888) % (uint64(15031498991544598528) * fixtureSeedUint64[0])) & (uint64(4952709372258801664) | (uint64(15255950899278555136) * fixtureSeedUint64[0])));
for (uint32 i = uint32(3925702497); i < uint32(3925702499); i++) {
for (uint32 j = uint32(3159460707); j < uint32(3159460709); j++) {
}
var0 = (((var0 > (var0 * fixtureSeedInt64[0])) ? int64(var0) : int64((var1 * fixtureSeedInt64[0]))) * (var0 / (int64(-4739048724704026624) * fixtureSeedInt64[0])));
uint64 var7 = ((uint64(271797758403119104) - (var4 * fixtureSeedUint64[0])) / (~(uint64(2798216410646654976))));
uint64 var8 = var4;
for (uint32 j = uint32(3167636122); j < uint32(3167636123); j++) {
var8 = ((var7 | (var7 * fixtureSeedUint64[0])) | (uint64(13734554319099854848) | (var4 * fixtureSeedUint64[0])));
for (uint32 k = uint32(4134190084); k < uint32(4134190085); k++) {
var3 = ((var3 ^ (uint32(3230740930) * fixtureSeedUint32[0])) * (false ? uint32((var3 * fixtureSeedUint32[0])) : uint32((uint32(2206688981) * fixtureSeedUint32[0]))));
int64 var9 = ((var2 + (int64(6707815657213417472) * fixtureSeedInt64[0])) & ((var8 == (uint64(2564440415635787776) * fixtureSeedUint64[0])) ? int64((int64(-1472872193194233856) * fixtureSeedInt64[0])) : int64((var2 * fixtureSeedInt64[0]))));
var7 = ((uint64(142820929974403072) | uint64(10624796512804882432)) / (uint64(11457589582471944192) * (var8 * fixtureSeedUint64[0])));
var4 = ((true ? uint64((uint64(15110167477684905984) * fixtureSeedUint64[0])) : uint64(var4)) - (uint64(14706087199221549056) + (var4 * fixtureSeedUint64[0])));
console.log("Value of 0020:", (var4 * fixtureSeedUint64[0]));
assert(((var4 >= (uint64(4819705220994994176) * fixtureSeedUint64[0])) || (!((((i == (uint32(3925702497) * fixtureSeedUint32[0])) && (j == (uint32(3167636122) * fixtureSeedUint32[0]))) && (k == (uint32(4134190084) * fixtureSeedUint32[0])))))));
}
var2 = (((var8 <= var7) ? (var2 == (int64(6553941962661181440) * fixtureSeedInt64[0])) : (var2 >= int64(-2758028069496836096))) ? int64((int64(-6522011674105337856) + (int64(-5284834009716621312) * fixtureSeedInt64[0]))) : int64((~(var1))));
}
var3 = ((var3 & uint32(1145225934)) - (uint32(3683574007) / (var3 * fixtureSeedUint32[0])));
uint64 var12 = ((!(false)) ? uint64((var4 - (uint64(572547607247581184) * fixtureSeedUint64[0]))) : uint64((uint64(1605336039612958720) / (var4 * fixtureSeedUint64[0]))));
if (((true && true) && (!(false)))) {
for (uint32 j = uint32(271752428); j < uint32(271752430); j++) {
uint64 var13 = (((var3 <= (uint32(2656884339) * fixtureSeedUint32[0])) || (var4 == (uint64(13506352667432898560) * fixtureSeedUint64[0]))) ? uint64((var12 & (uint64(4474182229412237312) * fixtureSeedUint64[0]))) : uint64(((var12 <= (uint64(12275686143124940800) * fixtureSeedUint64[0])) ? uint64((uint64(17742358935690551296) * fixtureSeedUint64[0])) : uint64((uint64(3380383043229827072) * fixtureSeedUint64[0])))));
console.log("Value of 0032:", (var13 * fixtureSeedUint64[0]));
}
var4 = ((uint64(13338779488892360704) | (uint64(10887467816390090752) * fixtureSeedUint64[0])) | (~((uint64(17335086555759179776) * fixtureSeedUint64[0]))));
} else {
var4 = (uint64(10362987781740544000) * fixtureSeedUint64[0]);
}
var2 = ((int64(8627440958569992192) - int64(4828991399201058816)) + ((var12 >= (var12 * fixtureSeedUint64[0])) ? int64((var2 * fixtureSeedInt64[0])) : int64(var1)));
}
if ((!((false || true)))) {
for (uint32 i = uint32(2144075452); i < uint32(2144075453); i++) {
for (uint32 j = uint32(413699562); j < uint32(413699565); j++) {
var3 = (~((uint32(3064546739) / (uint32(3453410155) * fixtureSeedUint32[0]))));
}
var3 = (uint32(3945901023) % (true ? uint32((uint32(3740513322) * fixtureSeedUint32[0])) : uint32(var3)));
}
var4 = (uint64(3185230421226708992) * fixtureSeedUint64[0]);
} else {
for (uint32 i = uint32(2369558910); i < uint32(2369558913); i++) {
int32 var21 = (((var1 > (var0 * fixtureSeedInt64[0])) ? int32((int32(551190112) * fixtureSeedInt32[0])) : int32(int32(-397764103))) & (int32(1935070791) * (int32(1055854501) * fixtureSeedInt32[0])));
}
}
}
}
}
Expected Behavior
Compilation terminates — either successfully, or with a user-facing diagnostic such as StackTooDeepError.
Actual Behavior
solc never returns and prints nothing.
Analysis
optimizer.enabled = falsecompiles the same file in ~46 s; the hang requires the Yul optimization sequence.- No single optimizer step reproduces it —
optimizerStepsset tof,I,j,corUalone all compile; the full default sequence is needed. console.logandassertare each necessary. Removing either kind makes the file compile in 1–3 s; a file with 200console.logcalls only compiles in 1 s.- The
consoleimplementation matters. Replacing forge-std's_sendLogPayload(which usesassembly ("memory-safe")plus a_castToPurefunction-pointer cast) with an otherwise equivalent minimal version that drops thememory-safeannotation makes the same program compile cleanly, or fail withStack too deep— instead of hanging. - Empty loop bodies / empty
elsesurvive the reduction — deletingfor (uint32 j = …) { }or} else { }makes the file compile, so the CFG's cyclic structure is part of the trigger. - Raw size is not sufficient: a hand-written 54-line file with 91 storage reads and 12 variables in the same style compiles in 2 s.
Stuck stack (recovered from RIP-relative string references in .rodata, since the binary is stripped and
ptrace attach was unavailable):
#4 StackLayoutGenerator.cpp (builds a stack-local std::map, calls into the solver)
#5 'std::visit: variant is valueless'
#7 'std::get: wrong index for variant'
#9 ObjectOptimizer::optimize libyul/ObjectOptimizer.cpp
#11 YulStack::analyzeParsed libyul/YulStack.cpp
#16 StandardCompiler::compile libsolidity/interface/StandardCompiler.cpp
'Stack solidity::yul::StackLayoutGenerator::propagateStackThroughOperation(Stack, const CFG::Operation &, bool)'
and StackHelpers.h (containing "Could not create stack layout after 1000 iterations.") resolve to the
hot frames on 0.8.37. Three samples taken at different times gave different #0 PCs but an identical
call chain from frame #4 upward.
Relation to existing issues
| Issue | Trigger shape | Terminating behaviour |
|---|---|---|
| #16704 | ≥ 998 chained internal-library calls in one expression | aborts, YulAssertion from StackHelpers.h(122) |
| #16746 | 600-term | chain over a diamond inheritance graph |
~12 s budget exceeded, then finishes |
| #16747 | ~3000-term + chain |
~12 s; ~1.87 GB peak RSS without the optimizer |
| #16748 | 190 nested while (j < ++j) loops |
~12 s; CPU-bound, 42–53 MB peak RSS |
| #16697 | deeply nested try/catch |
~115× slower, finishes in ~12 s |
| this report | flat body — many independent statements in one function, no long chain, no deep nesting | no abort, no output, ≥ 40 min, flat RSS |
The hot sub-system is the same, but the reproducer shape and the symptom (indefinite hang with zero output,
vs. abort or 12–43 s) don't seem to be covered above — reporting so it can be triaged against them.
Caveat
The process was observed to run ≥ 40 minutes without output; that is not a proof that it never
terminates, only that it does not abort the way #16704 does and does not finish within two orders of
magnitude of the times in #16745–#16748. No perf profile could be taken (perf_event_paranoid = 4 in this
environment), so caller attribution relies on the recovered symbol strings shown above.
- Dominant language
- C++
- Stars
- 25.8k
- Forks
- 6.2k
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 43
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from argotorg/solidity
-
AI tool policy: add AGENTS.mdMay be free again @cameel claimed this 47 days ago, and no pull request is open. Opendocumentation :book:
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
argotorg/solidity#16996 · 2 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
`fitsPrecisionExp` charges the base's bit *length* per multiplication instead of its bit *position*, so valid constant powers such as `2 ** 2049` are rejectedPossibly taken @clonker claimed this 54 days ago. Openbug :bug:
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
argotorg/solidity#16984 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
`0 ** (-1)` is silently evaluated to `0` in a constant expression instead of being rejected as division by zeroMay be free again @matheusaaguiar claimed this 54 days ago, and no pull request is open. Openbug :bug: low impact
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
argotorg/solidity#16982 · 3 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
AssemblyItem AssemblyItem::toSubAssemblyTag assumes size_t is at least 64bPossibly taken @riba2534 claimed this 67 days ago. Openbug :bug: low impact
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
FileReader: Duplicate search paths cause false ambiguous importsPossibly taken @zjuncal claimed this 3 days ago. Openbug :bug:
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
Maintainers usually reply within 1 day
All issues in argotorg/solidity
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
KLayout/klayout#2554 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
LaneDibello/Kotor-Patch-Manager#375 ·
Maintainers usually reply within 2 days