[via-IR][optimizer] Infinite hang in StackLayoutGenerator stack shuffler with unchecked narrow arithmetic + console.log + assert

Open
#17,134 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
cpp, solidity
Domain
compilers

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

bug :bug: low impact medium effort performance :racehorse: viair

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) and 0.8.37 (official release) — both affected; also the
    ZKsync-patched 0.8.30-1.0.2.
  • Compilation pipeline: via-IR + optimizer (with optimizer.enabled = false the same file compiles in ~46 s).
  • Target EVM version: default (no --evm-version passed).
  • Framework/IDE: originally Foundry (forge test -vv, via zksolc); reproducer below is plain solc CLI.
  • 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 = false compiles the same file in ~46 s; the hang requires the Yul optimization sequence.
  • No single optimizer step reproduces it — optimizerSteps set to f, I, j, c or U alone all compile; the full default sequence is needed.
  • console.log and assert are each necessary. Removing either kind makes the file compile in 1–3 s; a file with 200 console.log calls only compiles in 1 s.
  • The console implementation matters. Replacing forge-std's _sendLogPayload (which uses assembly ("memory-safe") plus a _castToPure function-pointer cast) with an otherwise equivalent minimal version that drops the memory-safe annotation makes the same program compile cleanly, or fail with Stack too deep — instead of hanging.
  • Empty loop bodies / empty else survive the reduction — deleting for (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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from argotorg/solidity

All issues in argotorg/solidity

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.