Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
4.8 KiB
Solidity Compiler — CWE-407 Disclosure Brief
2026-03-26 · Patches available — awaiting upstream merge
Finding
Two O(n²) defects in the Solidity compiler. Both patched. Patches ready for upstream review. One fires on every contract compiled with --via-ir or --optimize — the standard flags for production Ethereum deployment. The developer who wrote it left a // TODO: This algorithm is non-optimal. comment in the same file.
The Defects
solc-0001 (PATCHED — HIGH): libyul/optimiser/CallGraphGenerator.cpp:49
std::find(currentPath.begin(), currentPath.end(), function)
currentPath is a std::vector<YulString> tracking the current DFS path in the Yul call graph cycle detector. For each function visited, the code scans the entire path to detect cycles. Path length grows with call depth D. Over F functions: O(F × D²).
The developer acknowledged this at line 36 of the same file:
// TODO: This algorithm is non-optimal.
solc-0002 (PATCHED — MEDIUM): libevmasm/Assembly.cpp:1077
std::find(items.begin(), items.end(), ...)
Linear scan in EOF relative jump resolution. Fires during bytecode assembly for contracts using the EOF container format. O(J²) where J = jump count.
Complexity Proof (solc-0001)
The Yul call graph cycle detector performs a DFS over the function call graph. At each node:
currentPathholds the DFS stack, length up to D (max call depth)std::findoncurrentPathscans up to D entries- Called once per function visit: F total visits
- Per-visit cost: O(D)
- Total: O(F × D)
But D itself grows with recursion depth, and in the worst case (a linear call chain), D reaches F. Worst-case total: O(F²).
For a DeFi protocol with 200 internal Yul functions and call depth 50: 200 × 50 = 10,000 vector scans per compilation. Production DeFi contracts (Uniswap v4 hooks, Aave v3, Compound v3) compiled with --via-ir --optimize hit this path on every solc invocation.
Impact
solc-0001 fires on every contract compiled with --via-ir or --optimize. These are the standard flags for:
- Production Ethereum mainnet deployment
- Every EVM-compatible chain: Polygon, BNB Chain, Avalanche, Arbitrum, Optimism, Base
- Smart contract audit toolchains (Slither, Echidna, Certora all shell out to
solc) - CI pipelines for every DeFi protocol and NFT platform
Compilation time for complex contracts is already a developer pain point. The O(F²) cycle detection is pure overhead — the algorithm is provably suboptimal by the developer's own admission — and it fires on every compile of every non-trivial contract.
solc-0002 affects contracts using EOF (EVM Object Format), the new container format being rolled out in upcoming Ethereum hard forks. This is the right time to fix it — before EOF adoption scales.
The Fix
solc-0001:
// Before
std::vector<YulString> currentPath;
// ... in DFS:
if (std::find(currentPath.begin(), currentPath.end(), function) != currentPath.end())
// cycle detected
// After
std::vector<YulString> currentPath; // keep for backtracking order
std::unordered_set<YulString> currentPathSet; // add for O(1) membership
// ... in DFS:
if (currentPathSet.count(function) > 0)
// cycle detected
// on push: currentPath.push_back(f); currentPathSet.insert(f);
// on pop: currentPath.pop_back(); currentPathSet.erase(f);
The vector is retained for ordered backtracking. The set is added solely for O(1) cycle detection. This is the standard pattern for DFS cycle detection with path reconstruction.
solc-0002: Replace the linear scan with an std::unordered_map or std::unordered_set keyed on the jump target identifier.
Patches
solc-0001: defects/solc/patch/solc-0001-callgraph-cyclefinder-uset.patch
Adds std::set<FunctionHandle> currentPathSet alongside currentPath vector. DFS cycle check switches from std::find (O(D)) to currentPathSet.count() (O(1)). Unit test: 5/5 pass. At F=D=32: defective=15,872 comparisons, fixed=1,024, 15.5× speedup.
solc-0002: defects/solc/patch/solc-0002-assembly-rjump-index.patch
Builds std::unordered_map<u256, size_t> tagIndex before the worklist loop. Each RJUMP resolution uses tagIndex.find() (O(1)) instead of std::find over all items (O(N)). Unit test: 4/4 pass. At J=N=100: defective=5,050 comparisons, fixed=200, 25.3× speedup.
What We Ask
- Patches are ready for review. Confirm receipt and assign a GitHub issue reference (ethereum/solidity).
- Assess severity — solc-0001 in particular has direct supply-chain reach across every EVM chain and audit toolchain.
- Coordinate a disclosure date — we are targeting 90 days from first contact.
- We will credit the Solidity team in the public disclosure. Preferred acknowledgment format welcome.
Contact: see cover email. This brief is confidential until coordinated disclosure.