mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-20 18:56:17 +00:00
The new Yul optimizer toolkit (#519)
This PR introduces powerful code size optimizations. This is achieved with an additional IR layer in between Yul and LLVM IR. It works quite well (see below for a comparison) because it captures optimization gains that neither solc nor LLVM can realize. The feature is gated behind `--newyork` or a corresponding standard JSON input setting and **off by default**. ### How it works We implemented an SSA IR modelled after or inspired by the [MLIR SCF dialect](https://mlir.llvm.org/docs/Dialects/SCFDialect/). Yul is translated into this IR and the IR implements mainly the following optimizations: - **Type narrowing**: narrows 256-bit values to I1..I160 where provable (bidirectional + interprocedural), so LLVM emits native 32/64-bit ops instead of software 256-bit arithmetic. - **FMP range proof**: exposes the `mload(0x40) < heap_size` bound to LLVM IPSCCP, eliminating trailing overflow checks on FMP-derived offsets. Single largest code-size contributor. - **Heap analysis**: skips big-endian byte-swaps where native little-endian layout is safe. - **mem_opt / keccak fusion+folding**, **compound outlining** (mapping sload/sstore), **guard narrowing**, **inlining** tuned for PolkaVM, and **exact + fuzzy function dedup**. ### Results The optimizations implemented with the newyork IR bring substantial code size improvements. - Integration contracts: approximately **-20% to -50%** code size. - OpenZeppelin Wizard corpus: around **-47%** code size. This table shows the code size improvements on various OpenZeppelin ERC implementations, taken from their [contracts wizard](https://wizard.openzeppelin.com/), with almost all features enabled. | Contract | Default: (bytes) | With `--newyork`: (bytes) | Reduction | |---|---:|---:|---:| | erc1155 | 59,206 | 30,649 | -48.2% | | erc20 | 81,697 | 42,893 | -47.5% | | erc721 | 92,052 | 49,423 | -46.3% | | oz_gov | 141,861 | 81,159 | -42.8% | | oz_rwa | 77,274 | 37,975 | -50.9% | | oz_simple_erc20 | 29,950 | 16,554 | -44.7% | | oz_stable | 82,117 | 39,032 | -52.5% | | **Total** | **564,157** | **297,685** | **-47.2%** | We believe this to be a reasonably representative dataset and conclude that the newyork IR achieves drastic code size reductions on real world dApp implementations. ### Methodology, security and runtime performance Since the main contributors to these improvements are the FMP range proofs and the type narrowing, we don't expect big runtime performance regressions (it may even benefit runtime performance in some cases). The PR also includes docs (`book/src/developer_guide/newyork_{optimizer,ir}.md`) and many new integration tests, including regression tests for known FMP soundness traps. The PR was authored collaboratively and reviewed extensively by the `revive` maintainers, as well as Claude Opus, Claude Fable, Qwen, Minimax and Deepseek LLMs, over a span of many months. The `newyork` compiler pipeline introduces no new regressions over our integration and differential test suites (containing over 30000 test cases). This was achieved by careful manual reviews and many LLM bughunt loops. Additionally, a final security review by Anthropic's Fable 5 LLM found no remaining soundness issues. Nonetheless, like any brand new software, please consider this an experimental feature upon release. --------- Signed-off-by: xermicus <bigcyrill@hotmail.com> Signed-off-by: xermicus <cyrill@parity.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: neo <me@90f9.org> Co-authored-by: LJ <81748770+elle-j@users.noreply.github.com>
This commit is contained in:
183 files changed
+49503
-561
No files matched your search
@@ -43,7 +43,13 @@ where
|
||||
// not running the optimization passes.
|
||||
let optimizer_settings = OptimizerSettings::none();
|
||||
|
||||
initialize_llvm(PolkaVMTarget::PVM, "resolc", Default::default());
|
||||
initialize_llvm(
|
||||
PolkaVMTarget::PVM,
|
||||
"resolc",
|
||||
optimizer_settings.level_middle_end_size,
|
||||
false,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
group
|
||||
.sample_size(90)
|
||||
|
||||
@@ -12,6 +12,9 @@ pub static EXTENSION_ABI: &str = "abi";
|
||||
/// The Yul IR file extension.
|
||||
pub static EXTENSION_YUL: &str = "yul";
|
||||
|
||||
/// The newyork IR debug dump file extension.
|
||||
pub static EXTENSION_NEWYORK: &str = "newyork";
|
||||
|
||||
/// The EVM file extension.
|
||||
pub static EXTENSION_EVM: &str = "evm";
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(crate) mod extension;
|
||||
pub(crate) mod keccak256;
|
||||
pub(crate) mod metadata;
|
||||
pub(crate) mod object;
|
||||
pub(crate) mod solidity_selector;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub use self::base::*;
|
||||
@@ -21,5 +22,6 @@ pub use self::extension::*;
|
||||
pub use self::keccak256::*;
|
||||
pub use self::metadata::*;
|
||||
pub use self::object::*;
|
||||
pub use self::solidity_selector::*;
|
||||
pub use self::utils::*;
|
||||
pub use contract_identifier::*;
|
||||
@@ -0,0 +1,22 @@
|
||||
//! Solidity ABI selector words used when emitting reverts.
|
||||
//!
|
||||
//! Each constant is the 32-byte EVM memory word that stores the corresponding
|
||||
//! Solidity selector at memory offset 0. The 4-byte selector occupies the
|
||||
//! high-order bytes (memory addresses 0..4) and the remaining 28 bytes are
|
||||
//! zero. The hex strings are consumed verbatim by 256-bit integer constructors
|
||||
//! such as `Context::word_const_str_hex`, so the selector ends up in the most
|
||||
//! significant bits of the resulting `i256`.
|
||||
|
||||
/// 32-byte EVM word holding the Solidity `Panic(uint256)` ABI selector.
|
||||
///
|
||||
/// `keccak256("Panic(uint256)")[..4] == 0x4e487b71`, placed in the high-order
|
||||
/// bytes of a 32-byte word followed by 28 zero bytes.
|
||||
pub const PANIC_UINT256_SELECTOR_WORD_HEX: &str =
|
||||
"4e487b7100000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
/// 32-byte EVM word holding the Solidity `Error(string)` ABI selector.
|
||||
///
|
||||
/// `keccak256("Error(string)")[..4] == 0x08c379a0`, placed in the high-order
|
||||
/// bytes of a 32-byte word followed by 28 zero bytes.
|
||||
pub const ERROR_STRING_SELECTOR_WORD_HEX: &str =
|
||||
"08c379a000000000000000000000000000000000000000000000000000000000";
|
||||
@@ -7,6 +7,9 @@ repository.workspace = true
|
||||
authors.workspace = true
|
||||
description = "revive compiler integration test cases"
|
||||
|
||||
[features]
|
||||
newyork = ["resolc/newyork"]
|
||||
|
||||
[dependencies]
|
||||
alloy-primitives = { workspace = true }
|
||||
alloy-sol-types = { workspace = true }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"Baseline": 870,
|
||||
"Computation": 2418,
|
||||
"DivisionArithmetics": 9368,
|
||||
"ERC20": 17090,
|
||||
"Events": 1662,
|
||||
"FibonacciIterative": 1427,
|
||||
"Flipper": 2240,
|
||||
"SHA1": 8056
|
||||
"Baseline": 838,
|
||||
"Computation": 2368,
|
||||
"DivisionArithmetics": 11444,
|
||||
"ERC20": 18057,
|
||||
"Events": 1614,
|
||||
"FibonacciIterative": 1373,
|
||||
"Flipper": 2205,
|
||||
"SHA1": 7830
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Baseline": 493,
|
||||
"Computation": 1217,
|
||||
"DivisionArithmetics": 7370,
|
||||
"ERC20": 8726,
|
||||
"Events": 909,
|
||||
"FibonacciIterative": 969,
|
||||
"Flipper": 1058,
|
||||
"SHA1": 6264
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract ArrayProbe {
|
||||
uint256[8] fixedArr;
|
||||
uint256[] dynArr;
|
||||
mapping(uint256 => uint256) m;
|
||||
|
||||
function run(uint256 a, uint256 b, uint256 c) public returns (uint256) {
|
||||
for (uint256 i = 0; i < 8; i++) { fixedArr[i] = i * a + 1; }
|
||||
// attacker-controlled index: out-of-bounds must revert (panic 0x32) identically
|
||||
uint256 r = fixedArr[a & 7];
|
||||
r = r + fixedArr[b & 7];
|
||||
// dynamic array push/index
|
||||
uint256 n = c & 15;
|
||||
for (uint256 i = 0; i < n; i++) { dynArr.push(i + a); }
|
||||
if (dynArr.length > 0) { r = r ^ dynArr[b % dynArr.length]; }
|
||||
// mapping
|
||||
m[a] = b;
|
||||
m[b] = c;
|
||||
r = r + m[a] + m[b];
|
||||
// explicit bounds hit (reverts when a >= 8)
|
||||
r = r + fixedArr[a];
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/// A copy with a STATIC destination in [0x40, 0x60) but a DYNAMIC length
|
||||
/// clobbers the free-memory-pointer slot with arbitrary bytes. A subsequent
|
||||
/// mload(0x40) must observe the full 256-bit word (EVM), but newyork applies
|
||||
/// its FMP range proof and truncates it because `fmp_could_be_unbounded` is
|
||||
/// not set for the dynamic-length copy case (only the constant-length case).
|
||||
object "CalldataCopyFmp" {
|
||||
code { datacopy(0, dataoffset("CalldataCopyFmp_deployed"), datasize("CalldataCopyFmp_deployed")) return(0, datasize("CalldataCopyFmp_deployed")) }
|
||||
object "CalldataCopyFmp_deployed" {
|
||||
code {
|
||||
let len := calldataload(0)
|
||||
calldatacopy(0x40, 32, len)
|
||||
let r := mload(0x40)
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/// Soundness PoC (newyork-specific): the `offx` parameter of `probe` is used as
|
||||
/// an mload offset (case 0) which makes newyork narrow it to i64, and as a
|
||||
/// `calldatacopy` SOURCE offset (case 1). newyork's CallDataCopy lowering treats
|
||||
/// the calldata source offset as a heap pointer (`narrow_offset_for_pointer`),
|
||||
/// so a large source offset traps on the PVM heap-bounds check / mis-sized copy
|
||||
/// instead of zero-filling like EVM. 4 call sites keep `probe` non-inlined.
|
||||
/// Reproduces only under RESOLC_USE_NEWYORK=1 (the Yul path is correct).
|
||||
object "CalldataCopySrcNarrow" {
|
||||
code { let s := datasize("CalldataCopySrcNarrow_deployed") codecopy(0, dataoffset("CalldataCopySrcNarrow_deployed"), s) return(0, s) }
|
||||
object "CalldataCopySrcNarrow_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let off := calldataload(32)
|
||||
let r0 := probe(op, off)
|
||||
let r1 := probe(op, off)
|
||||
let r2 := probe(op, off)
|
||||
let r3 := probe(op, off)
|
||||
mstore(0x200, r0)
|
||||
mstore(0x220, add(r1, add(r2, r3)))
|
||||
return(0x200, 64)
|
||||
function probe(opx, offx) -> r {
|
||||
switch opx
|
||||
case 0 { r := mload(offx) }
|
||||
case 1 { calldatacopy(0x80, offx, 32) r := mload(0x80) }
|
||||
case 2 { r := keccak256(offx, 32) }
|
||||
default { r := offx }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
object "CalldataloadOOB" {
|
||||
code { datacopy(0, dataoffset("CalldataloadOOB_deployed"), datasize("CalldataloadOOB_deployed")) return(0, datasize("CalldataloadOOB_deployed")) }
|
||||
object "CalldataloadOOB_deployed" {
|
||||
code {
|
||||
let off := calldataload(0) // the offset to test (from calldata[0])
|
||||
let x := calldataload(off) // EVM: zero-pads beyond calldatasize
|
||||
mstore(0, x)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Differential guard against a latent soundness footgun in the codegen for
|
||||
// __revive_caller: before the fix the helper wrote its result through the
|
||||
// shared @__address_spill_buffer LLVM global while declaring
|
||||
// memory(inaccessiblemem: read). The same global is also written by the
|
||||
// inlined tx.origin and address(this) patterns, so the function attribute
|
||||
// was a contract violation from LLVM's point of view (globals are Other
|
||||
// memory, not inaccessiblemem). Any optimizer pass that exploited the wrong
|
||||
// attribute to CSE / hoist a load of the spill buffer across a
|
||||
// __revive_caller() call would have miscompiled the surrounding tx.origin /
|
||||
// address(this) read.
|
||||
//
|
||||
// The fix moves __revive_caller's output to a function-local alloca so the
|
||||
// attribute matches the body. This contract exercises every shape that
|
||||
// previously shared the spill buffer.
|
||||
contract CallerOriginAliasing {
|
||||
function caller_then_origin() external view returns (address, address) {
|
||||
return (msg.sender, tx.origin);
|
||||
}
|
||||
|
||||
function origin_then_caller() external view returns (address, address) {
|
||||
return (tx.origin, msg.sender);
|
||||
}
|
||||
|
||||
function caller_address_origin() external view returns (address, address, address) {
|
||||
return (msg.sender, address(this), tx.origin);
|
||||
}
|
||||
|
||||
function repeated_caller() external view returns (address, address, address) {
|
||||
return (msg.sender, msg.sender, msg.sender);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
object "CallvalueRevertData" {
|
||||
code { let s := datasize("CallvalueRevertData_deployed") codecopy(0, dataoffset("CallvalueRevertData_deployed"), s) return(0, s) }
|
||||
object "CallvalueRevertData_deployed" {
|
||||
code {
|
||||
let sel := calldataload(0)
|
||||
// One callvalue read per case (separate regions, so CSE keeps them distinct): three reads
|
||||
// enable the callvalue-check outline.
|
||||
switch sel
|
||||
case 1 { if callvalue() { revert(0, 0) } sstore(0, 0x11) }
|
||||
case 2 { if callvalue() { revert(0, 0) } sstore(0, 0x22) }
|
||||
case 3 {
|
||||
// A data-carrying revert whose operands are bound in the enclosing (case) scope. The outline
|
||||
// must not treat this as an empty revert and drop the returned memory[off, off+len).
|
||||
mstore(0x80, 0xdeadbeef)
|
||||
let off := calldataload(32)
|
||||
let len := calldataload(64)
|
||||
if callvalue() { revert(off, len) }
|
||||
sstore(0, 0x33)
|
||||
}
|
||||
default { sstore(0, 0xff) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `to_llvm.rs::get_or_create_return_block` (and the
|
||||
/// emit-revert sibling) call `context.xlen_type().const_int(const_offset,
|
||||
/// false)` to build the constant offset/length for the
|
||||
/// `emit_exit_unchecked` path. `const_int` takes a `u64` and truncates
|
||||
/// silently to `xlen_type`'s width (`i32`), so any constant
|
||||
/// offset/length above `u32::MAX` (but still within `u64::MAX` so
|
||||
/// `try_extract_const_u64` returns `Some`) is wrapped mod `2^32` and the
|
||||
/// resulting return/revert reads from / aliases the truncated heap
|
||||
/// offset. EVM expands memory and OOGs.
|
||||
contract ConstReturnOverflowBug {
|
||||
function bug() external pure returns (uint256 r) {
|
||||
assembly {
|
||||
// Constant offset 2^56 — fits in u64, doesn't fit in u32. With
|
||||
// the current codegen, the shared `return_shared_…` block
|
||||
// truncates to i32 (=0) and reads/writes at heap[0..32]
|
||||
// instead of trapping.
|
||||
mstore(0x100000000000000, mload(0x100000000000000))
|
||||
return(0x100000000000000, 32)
|
||||
// `r` is just to satisfy the signature; control flow exits
|
||||
// via the inline-assembly `return` above.
|
||||
r := 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `mem_opt::optimize_statements` `*Copy` handler
|
||||
/// (CodeCopy / ExtCodeCopy / ReturnDataCopy / DataCopy /
|
||||
/// CallDataCopy) only invalidates `memory_state[word(dest)]` when
|
||||
/// `dest` is statically known. It ignores the *length* entirely, so
|
||||
/// additional words inside `[dest, dest + length)` keep their stale
|
||||
/// tracked entries. A subsequent `mload` at the exact tracked offset
|
||||
/// is forwarded to the pre-overwrite value while EVM reads the
|
||||
/// actually-overwritten bytes from the copy.
|
||||
///
|
||||
/// The dynamic `length` parameter defeats solc's dead-store
|
||||
/// elimination of the sentinel — solc can't prove that
|
||||
/// `calldatacopy(0xc0, 0, length)` overwrites bytes `0xe0..0x100`
|
||||
/// without knowing `length >= 0x40`.
|
||||
contract CopyOverlapBug {
|
||||
function bug(uint256 length) external pure returns (bytes32 r) {
|
||||
assembly {
|
||||
mstore(0xe0, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
|
||||
calldatacopy(0xc0, 0, length)
|
||||
r := mload(0xe0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Probe: custom error revert data must match EVM (selector + ABI-encoded args)
|
||||
/// for edge-case argument values. Targets newyork's custom-error outlining.
|
||||
contract CustomErrorArgs {
|
||||
error E1(uint256 a);
|
||||
error E2(uint256 a, uint256 b, address c);
|
||||
error E3(uint8 x, bytes32 y);
|
||||
function f(uint256 sel, uint256 a, uint256 b) external pure {
|
||||
if (sel == 0) revert E1(a);
|
||||
if (sel == 1) revert E2(a, b, address(uint160(a)));
|
||||
if (sel == 2) revert E3(uint8(a), bytes32(b));
|
||||
revert E1(~a);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
object "CustomErrorDupArg" {
|
||||
code { let s := datasize("CustomErrorDupArg_deployed") codecopy(0, dataoffset("CustomErrorDupArg_deployed"), s) return(0, s) }
|
||||
object "CustomErrorDupArg_deployed" {
|
||||
code {
|
||||
// Custom-error revert shape (selector + one uint256 argument), but the argument word is written
|
||||
// twice. EVM last-write-wins, so the revert argument is 0xbbbb. A reverse scan that keeps the
|
||||
// first (earliest) match would collapse this to an error carrying 0xaaaa.
|
||||
mstore(0, 0x1234567800000000000000000000000000000000000000000000000000000000)
|
||||
mstore(4, 0xaaaa)
|
||||
mstore(4, 0xbbbb)
|
||||
revert(0, 0x24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,20 @@ contract DivisionArithmetics {
|
||||
r := smod(n, d)
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression / soundness PoC: newyork simplifier folds `div(x, x) → 1`
|
||||
/// and `sdiv(x, x) → 1` for any runtime value `x`, but EVM defines
|
||||
/// division by zero to return 0, so `div(0, 0) = 0`. The fold is
|
||||
/// unsound when `x == 0`.
|
||||
///
|
||||
/// `mod(x, x) → 0` is sound because EVM also returns 0 for mod by zero.
|
||||
function divSelf(uint x) external pure returns (uint r) {
|
||||
assembly { r := div(x, x) }
|
||||
}
|
||||
function sdivSelf(int x) external pure returns (int r) {
|
||||
assembly { r := sdiv(x, x) }
|
||||
}
|
||||
function modSelf(uint x) external pure returns (uint r) {
|
||||
assembly { r := mod(x, x) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Probe: abi.encodePacked tight-packing of mixed-width values + keccak256
|
||||
/// (signature/CREATE2/mapping-key hashing). Must match EVM exactly.
|
||||
contract EncodePackedHash {
|
||||
function h(uint8 a, uint256 b, address c, uint16 d, bytes memory e) external pure returns (bytes32) {
|
||||
return keccak256(abi.encodePacked(a, b, c, d, e));
|
||||
}
|
||||
function h2(uint256 a, uint256 b) external pure returns (bytes32) {
|
||||
return keccak256(abi.encodePacked(a, b));
|
||||
}
|
||||
function h3(string memory s, uint8 n) external pure returns (bytes32) {
|
||||
return keccak256(abi.encodePacked(s, n));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
object "EscapeProbe" {
|
||||
code {
|
||||
datacopy(0, dataoffset("EscapeProbe_deployed"), datasize("EscapeProbe_deployed"))
|
||||
return(0, datasize("EscapeProbe_deployed"))
|
||||
}
|
||||
object "EscapeProbe_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let a := calldataload(32)
|
||||
switch op
|
||||
case 0 { mstore(0, a) log0(0, 32) return(0, 0) }
|
||||
case 1 { mstore(0, a) log1(0, 32, 0x42) return(0, 0) }
|
||||
case 2 { mstore(0x40, a) log0(0x40, 32) return(0, 0) }
|
||||
case 3 { let p := mload(0x40) mstore(p, a) log0(p, 64) return(0, 0) }
|
||||
case 4 { mstore(0, a) mstore(32, not(a)) log0(0, 64) return(0, 0) }
|
||||
case 5 { mstore(0, a) revert(0, 32) }
|
||||
case 6 { mstore(0x40, a) mstore8(0x60, 0xCD) log0(0x40, 33) return(0, 0) }
|
||||
case 7 { mstore(0, a) return(0, 32) }
|
||||
case 8 { mstore(0x40, a) return(0x40, 32) }
|
||||
case 9 { mstore(0, a) mstore8(7, 0x99) revert(0, 32) }
|
||||
case 10 { mstore(0, a) log2(0, 32, a, 0x11) return(0, 0) }
|
||||
case 11 { let p := mload(0x40) mstore(p, a) mstore(add(p, 32), a) return(p, 64) }
|
||||
default { return(0, 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,15 @@ contract ExtCode {
|
||||
}
|
||||
}
|
||||
|
||||
// Two distinct addresses queried in a single function body: regression guard
|
||||
// for the `code_size` import memory-attribute bug where LLVM CSE'd the two
|
||||
// syscalls into one and returned `2 * extcodesize(a)`.
|
||||
function ExtCodeSizeSum(address a, address b) public view returns (uint ret) {
|
||||
assembly {
|
||||
ret := add(extcodesize(a), extcodesize(b))
|
||||
}
|
||||
}
|
||||
|
||||
function CodeSize() public pure returns (uint ret) {
|
||||
assembly {
|
||||
ret := codesize()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Regression test: factorial with uint8 parameter and uint64 result.
|
||||
// Bug: newyork pipeline doesn't revert for out-of-range uint8 parameter,
|
||||
// causing the function to return 1 (factorial of truncated 0) instead of
|
||||
// reverting.
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": true,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {
|
||||
"code": { "Solidity": { "contract": "Factorial", "solc_optimizer": false } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "ebbee3910000000000000000000000000000000000000000000000000000000000000005"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "ebbee3910000000000000000000000000000000000000000000000000000000100000000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
contract Factorial {
|
||||
function main(uint8 n) public pure returns (uint64) {
|
||||
uint64 fact = 1;
|
||||
for (uint8 i = 1; i <= n; i++) {
|
||||
fact *= i;
|
||||
}
|
||||
return fact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
object "FmpBig" {
|
||||
code { datacopy(0, dataoffset("FmpBig_deployed"), datasize("FmpBig_deployed")) return(0, datasize("FmpBig_deployed")) }
|
||||
object "FmpBig_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let v := calldataload(32)
|
||||
let r := 0
|
||||
switch op
|
||||
// direct untrusted store to FMP then load
|
||||
case 0 { mstore(0x40, v) r := mload(0x40) }
|
||||
// allocator-looking store (add(mload(0x40), v)) then load
|
||||
case 1 { mstore(0x40, add(mload(0x40), v)) r := mload(0x40) }
|
||||
// store big, use as offset
|
||||
case 2 { mstore(0x40, v) let p := mload(0x40) r := p }
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract FmpCrossObjectBug {
|
||||
// Recursive so solc can't inline; the function survives to
|
||||
// newyork IR as `fun_rec` with parameter ValueIds starting at 0,
|
||||
// colliding with the deploy (parent) object's `v0 := 0x80`.
|
||||
function rec(uint256 value, uint256 depth) internal view returns (uint256 r) {
|
||||
if (depth == 0) {
|
||||
assembly {
|
||||
mstore(0x40, value)
|
||||
// Force the same byte-swap-mode path as FmpRangeProofBug
|
||||
// so the range proof is the only optimization controlling
|
||||
// the loaded value.
|
||||
let _success := staticcall(gas(), origin(), 0, 0x80, 0, 0)
|
||||
r := mload(0x40)
|
||||
}
|
||||
return r;
|
||||
}
|
||||
return rec(value, depth - 1);
|
||||
}
|
||||
|
||||
fallback() external {
|
||||
uint256 value;
|
||||
uint256 depth;
|
||||
assembly {
|
||||
value := calldataload(0)
|
||||
depth := calldataload(0x20)
|
||||
}
|
||||
uint256 result = rec(value, depth);
|
||||
assembly {
|
||||
mstore(0x100, result)
|
||||
return(0x100, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: same shape as `FmpRevertBug` but with *dynamic*
|
||||
/// offset and length on the revert. The original `mark_escaping_range`
|
||||
/// only sets `has_dynamic_escapes` for fully-dynamic escapes
|
||||
/// (`(None, _)` arm) — it does not lower `min_dynamic_escape_start`,
|
||||
/// so `fmp_native_safe()` doesn't reject and `mstore(0x40, …)` still
|
||||
/// encodes as a 4-byte i32 LE native store. With the caller passing
|
||||
/// `offset = 0` and `length = 96`, the revert covers the FMP slot
|
||||
/// and the BE/LE encoding mismatch is observed.
|
||||
contract FmpDynRevertBug {
|
||||
fallback() external {
|
||||
uint256 offset;
|
||||
uint256 length;
|
||||
assembly {
|
||||
mstore(0x40, 0xdeadbeef)
|
||||
offset := calldataload(0)
|
||||
length := calldataload(32)
|
||||
revert(offset, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `mem_opt::FmpPropagation::propagate_statements`
|
||||
/// tracks the known FMP value across statements. Its MStore handler
|
||||
/// detects FMP writes by `region == FreePointerSlot ||
|
||||
/// resolved_offset == Some(0x40)`. For an mstore with a *dynamic*
|
||||
/// offset whose runtime value happens to be 0x40 (but the simplifier
|
||||
/// can't see that — e.g. `calldataload(...)`), neither condition
|
||||
/// fires: the simplifier leaves `region == Unknown` because the
|
||||
/// offset isn't a literal, and `resolve_offset` returns None because
|
||||
/// the value isn't tracked in the local constants map. The
|
||||
/// `fmp_value` stays stale.
|
||||
///
|
||||
/// A subsequent `mload(0x40)` IS recognized as an FMP load (because
|
||||
/// 0x40 is a literal). FmpPropagation folds it to the stale literal
|
||||
/// `fmp_value` at IR level — diverging from EVM which reads the
|
||||
/// overwritten memory.
|
||||
///
|
||||
/// Both values (`0x100`, `0x200`) fit in the heap-size range so the
|
||||
/// FMP-load range proof (`to_llvm.rs::apply_range_proof`) doesn't
|
||||
/// affect this test.
|
||||
contract FmpDynStoreBug {
|
||||
fallback() external {
|
||||
uint256 offset;
|
||||
assembly {
|
||||
// Set FMP via a recognized FMP-slot mstore.
|
||||
mstore(0x40, 0x100)
|
||||
// Caller passes 0x40 as calldata so offset = 0x40 at
|
||||
// runtime, but the simplifier sees an opaque calldataload.
|
||||
offset := calldataload(0)
|
||||
// Dynamic-offset mstore. FmpPropagation can't tell this
|
||||
// hits 0x40, so it doesn't invalidate the known fmp_value.
|
||||
mstore(offset, 0x200)
|
||||
let v := mload(0x40)
|
||||
mstore(0x100, v)
|
||||
return(0x100, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract FmpNativeStoreBug {
|
||||
// Recursive — solc can't inline.
|
||||
function loop(uint depth) internal pure returns (uint) {
|
||||
if (depth == 0) return 0;
|
||||
return loop(depth - 1) + depth;
|
||||
}
|
||||
|
||||
fallback() external {
|
||||
uint256 v;
|
||||
uint256 depth;
|
||||
assembly {
|
||||
v := calldataload(0)
|
||||
depth := calldataload(0x20)
|
||||
mstore(0x40, v)
|
||||
}
|
||||
// Recursive call: mem_opt clears memory_state, no escape so
|
||||
// fmp_word_escapes stays false, fmp_native_safe stays true.
|
||||
uint256 unused = loop(depth);
|
||||
uint256 r;
|
||||
assembly {
|
||||
r := mload(0x40)
|
||||
// use unused so solc doesn't DCE the call
|
||||
r := add(r, mul(unused, 0))
|
||||
mstore(0x100, r)
|
||||
return(0x100, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Track an FMP value, then overwrite the FMP slot via a calldatacopy whose
|
||||
/// source is beyond calldatasize (zero-fill). A stale FMP-propagation would
|
||||
/// forward the tracked value; EVM reads the (now zeroed) slot.
|
||||
object "FmpPropCopy" {
|
||||
code { datacopy(0, dataoffset("FmpPropCopy_deployed"), datasize("FmpPropCopy_deployed")) return(0, datasize("FmpPropCopy_deployed")) }
|
||||
object "FmpPropCopy_deployed" {
|
||||
code {
|
||||
mstore(0x40, 0x1234)
|
||||
calldatacopy(0x40, 1000, 32) // src 1000 >= calldatasize -> zero-fills heap[0x40..0x60]
|
||||
let p := mload(0x40) // EVM: 0; stale FmpProp would give 0x1234
|
||||
mstore(0x80, p)
|
||||
return(0x80, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract FmpRangeProofBug {
|
||||
fallback() external {
|
||||
assembly {
|
||||
let v := calldataload(0)
|
||||
mstore(0x40, v)
|
||||
// Force fmp_word_escapes via a staticcall whose args cover
|
||||
// memory[0..0x80] (so 0x40 is in the escape range). That
|
||||
// disables FMP-native-mode storage and forces ByteSwap mode
|
||||
// where the range-proof bug lives.
|
||||
let _success := staticcall(gas(), origin(), 0, 0x80, 0, 0)
|
||||
let r := mload(0x40)
|
||||
mstore(0x100, r)
|
||||
return(0x100, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `heap_opt.rs::fmp_native_safe()` does NOT check
|
||||
/// `tainted_regions` or `escaping_regions`. A `revert(0, 96)` that
|
||||
/// covers the FMP slot (0x40) only calls `mark_escaping_range`
|
||||
/// (line 241-243), which adds the FMP word to `escaping_regions` /
|
||||
/// `tainted_regions` — but `fmp_native_safe()` only looks at four flags
|
||||
/// (variable_accessed_offsets, has_dynamic_accesses,
|
||||
/// has_return_covering_fmp, min_dynamic_escape_start) and returns
|
||||
/// true when the static `revert(0, 96)` set none of them.
|
||||
///
|
||||
/// `Statement::Return` correctly sets `has_return_covering_fmp` in this
|
||||
/// case (line 252-254). `Statement::Revert` does not, so a contract
|
||||
/// that reverts with data covering the FMP gets the FMP slot stored
|
||||
/// as native i32 little-endian (4 bytes at heap[0x40..0x44]) instead
|
||||
/// of the big-endian 32-byte word EVM produces.
|
||||
///
|
||||
/// EVM revert data [0x40..0x60) = 32-byte BE encoding of FMP value.
|
||||
/// PVM revert data [0x40..0x60) = 4 bytes LE i32, then 28 bytes
|
||||
/// of uninitialized/zero heap memory.
|
||||
contract FmpRevertBug {
|
||||
fallback() external {
|
||||
assembly {
|
||||
// Set FMP to 0xdeadbeef. With newyork's InlineNative mode,
|
||||
// this becomes a 4-byte i32 LE store at heap[0x40..0x44].
|
||||
mstore(0x40, 0xdeadbeef)
|
||||
// Revert with 96 bytes covering [0x00..0x60). EVM caller
|
||||
// sees 32-byte BE encoding of FMP at bytes [0x40..0x60).
|
||||
// PVM caller sees the 4-byte LE encoding at [0x40..0x44)
|
||||
// followed by 28 uninitialized bytes — different bytes.
|
||||
revert(0, 96)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/// Soundness PoC candidate: store a meaningful value into the free-memory-
|
||||
/// pointer slot (0x40) and return a region covering it. If heap native-mode
|
||||
/// stores 0x40 little-endian while the `return` reads it big-endian, the
|
||||
/// returned bytes are byte-swapped relative to EVM.
|
||||
object "FmpSlotReturnByteOrder" {
|
||||
code {
|
||||
let size := datasize("FmpSlotReturnByteOrder_deployed")
|
||||
codecopy(0, dataoffset("FmpSlotReturnByteOrder_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "FmpSlotReturnByteOrder_deployed" {
|
||||
code {
|
||||
let v := calldataload(0)
|
||||
mstore(0x40, v)
|
||||
return(0x40, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Soundness PoC: newyork forward inference gives a `FreePointerSlot` `mload`
|
||||
/// (the free-memory pointer at 0x40) the width I32, sound only while the FMP
|
||||
/// holds a Solidity-allocator pointer (`< heap_size`). When a write to 0x40 is
|
||||
/// not provably sbrk-bounded, `heap_opt` sets `fmp_could_be_unbounded` and
|
||||
/// codegen loads the *full* FMP word (no `FMP < heap_size` range proof) — but
|
||||
/// inference still said I32, so `gt(v, 0xffffffff)` was evaluated at 32 bits and
|
||||
/// truncated the live value.
|
||||
///
|
||||
/// `mstore(0x40, calldataload(0))` taints the FMP with an arbitrary value; the
|
||||
/// `staticcall` is a memory barrier so the following `mload(0x40)` is a real load
|
||||
/// rather than store-forwarded. With `v = 2^40` (> 2^32) the comparison must be
|
||||
/// `1`; the bug truncates `v` to its low 32 bits (`0`) and yields `0`.
|
||||
object "FmpUnboundedCompare" {
|
||||
code {
|
||||
let size := datasize("FmpUnboundedCompare_deployed")
|
||||
codecopy(0, dataoffset("FmpUnboundedCompare_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "FmpUnboundedCompare_deployed" {
|
||||
code {
|
||||
let taint := calldataload(0)
|
||||
mstore(0x40, taint)
|
||||
let ok := staticcall(gas(), 0, 0, 0, 0, 0)
|
||||
let v := mload(0x40)
|
||||
let out := 0
|
||||
if gt(v, 0xffffffff) {
|
||||
out := 1
|
||||
}
|
||||
mstore(0, out)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
object "GF" {
|
||||
code { datacopy(0, dataoffset("GF_deployed"), datasize("GF_deployed")) return(0, datasize("GF_deployed")) }
|
||||
object "GF_deployed" {
|
||||
code {
|
||||
function f(p0, p1) -> r0, r1 {
|
||||
r0 := 0 r1 := 0
|
||||
if xor(4294967295, 32) { r0 := p1 leave }
|
||||
if shr(p1, p1) { r0 := p1 leave }
|
||||
if 32 { r1 := and(sub(340282366920938463463374607431768211456, 1461501637330902918203684832716283019655932542975), 0xFF) leave }
|
||||
if sar(255, p1) { r1 := and(smod(5315799647723455265, p1), 0xFF) leave }
|
||||
}
|
||||
let a := calldataload(0)
|
||||
let b := calldataload(32)
|
||||
let c := calldataload(64)
|
||||
let d := calldataload(96)
|
||||
let s0, s1 := f(a, b)
|
||||
let t0, t1 := f(c, d)
|
||||
let r := xor(xor(s0, s1), xor(t0, t1))
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Soundness/robustness PoC: newyork's SSA builder (`ssa.rs`) does not place a
|
||||
/// variable declared in a `for`-loop INIT block into the scope visible to the
|
||||
/// loop BODY and POST blocks. In Yul, the init-block declarations are scoped to
|
||||
/// the entire `for` statement (condition, body, post). The canonical Yul loop
|
||||
///
|
||||
/// for { let j := 0 } lt(j, a) { j := add(j, 1) } { ... use j ... }
|
||||
///
|
||||
/// therefore panics with `ICE: SsaBuilder::assign called for undeclared
|
||||
/// variable 'j'` when the body/post references `j`.
|
||||
///
|
||||
/// solc's ForLoopInitRewriter hoists init declarations out of the init block
|
||||
/// (emitting an empty init), so the Solidity frontend — even inline assembly —
|
||||
/// never produces this shape. Direct Yul input (`resolc --yul` / `Code::Yul`)
|
||||
/// does, and fails to compile. Fail-safe (ICE, not a silent miscompile), but a
|
||||
/// valid program the newyork pipeline cannot compile.
|
||||
object "ForInitScopeIce" {
|
||||
code {
|
||||
let size := datasize("ForInitScopeIce_deployed")
|
||||
codecopy(0, dataoffset("ForInitScopeIce_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "ForInitScopeIce_deployed" {
|
||||
code {
|
||||
let a := calldataload(0)
|
||||
let sum := 0
|
||||
for { let j := 0 } lt(j, a) { j := add(j, 1) } {
|
||||
sum := add(sum, j)
|
||||
}
|
||||
mstore(0x80, sum)
|
||||
return(0x80, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
object "ForLoopFmp" {
|
||||
code { let s := datasize("ForLoopFmp_deployed") codecopy(0, dataoffset("ForLoopFmp_deployed"), s) return(0, s) }
|
||||
object "ForLoopFmp_deployed" {
|
||||
code {
|
||||
// Establish a known free-memory pointer so FMP propagation tracks it as a constant.
|
||||
mstore(0x40, 0x80)
|
||||
// Dynamic trip count from calldata keeps the loop from being unrolled (which would hide the
|
||||
// per-iteration re-read of the FMP).
|
||||
let n := calldataload(0)
|
||||
// Each iteration allocates a fresh 32-byte slot at the current FMP and bumps the pointer.
|
||||
// The loop-top `mload(0x40)` must observe the bump from the previous iteration; if FMP
|
||||
// propagation rewrites it to the pre-loop constant, every iteration aliases the first slot.
|
||||
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
|
||||
let p := mload(0x40)
|
||||
mstore(p, add(i, 1))
|
||||
mstore(0x40, add(p, 0x20))
|
||||
}
|
||||
// Read back the first three allocated slots. Correct: [1, 2, 3] for n >= 3.
|
||||
// With the aliasing bug: [n, 0, 0].
|
||||
mstore(0x200, mload(0x80))
|
||||
mstore(0x220, mload(0xa0))
|
||||
mstore(0x240, mload(0xc0))
|
||||
return(0x200, 0x60)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/// Soundness PoC: newyork's fuzzy function deduplication
|
||||
/// (`simplify.rs::deduplicate_functions_fuzzy`) abstracts `switch` case
|
||||
/// match values into parameterizable literal positions
|
||||
/// (`fuzzy_encode_statement` emits a placeholder per case value), so two
|
||||
/// functions that are structurally identical *except for their switch case
|
||||
/// match values* hash to the same fuzzy form and are merged.
|
||||
///
|
||||
/// However `replace_literals_with_params` advances the literal counter for
|
||||
/// each switch case but NEVER substitutes `case.value`. The merged canonical
|
||||
/// function therefore keeps ITS OWN case values; the removed function's call
|
||||
/// sites are redirected to the canonical function with the (now ignored)
|
||||
/// differing values appended as dead extra arguments.
|
||||
///
|
||||
/// Result: `g`'s callers silently execute `f`'s switch dispatch. `g(111)`
|
||||
/// should hit `g`'s `case 111` and return 1111, but after the merge it falls
|
||||
/// through to the default of `f` (whose cases are 100/200/300) and returns
|
||||
/// the default value. EVM (and unoptimized semantics) returns 1111.
|
||||
object "FuzzySwitchBug" {
|
||||
code {
|
||||
let size := datasize("FuzzySwitchBug_deployed")
|
||||
codecopy(0, dataoffset("FuzzySwitchBug_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "FuzzySwitchBug_deployed" {
|
||||
code {
|
||||
let sel := calldataload(0)
|
||||
let x := calldataload(32)
|
||||
let r := 0
|
||||
switch sel
|
||||
case 0 { r := f(x) }
|
||||
case 1 { r := f(x) }
|
||||
case 2 { r := g(x) }
|
||||
case 3 { r := g(x) }
|
||||
default { r := 0 }
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
|
||||
function f(a) -> out {
|
||||
let t := add(a, 1)
|
||||
t := mul(t, 2)
|
||||
t := add(t, 3)
|
||||
t := xor(t, 4)
|
||||
t := add(t, 5)
|
||||
out := 0
|
||||
switch a
|
||||
case 100 { out := add(t, 1000) }
|
||||
case 200 { out := add(t, 2000) }
|
||||
case 300 { out := add(t, 3000) }
|
||||
default { out := 9999 }
|
||||
}
|
||||
function g(a) -> out {
|
||||
let t := add(a, 1)
|
||||
t := mul(t, 2)
|
||||
t := add(t, 3)
|
||||
t := xor(t, 4)
|
||||
t := add(t, 5)
|
||||
out := 0
|
||||
switch a
|
||||
case 111 { out := add(t, 1000) }
|
||||
case 222 { out := add(t, 2000) }
|
||||
case 333 { out := add(t, 3000) }
|
||||
default { out := 9999 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
object "HugeConstOffsetStore" {
|
||||
code { let s := datasize("HugeConstOffsetStore_deployed") codecopy(0, dataoffset("HugeConstOffsetStore_deployed"), s) return(0, s) }
|
||||
object "HugeConstOffsetStore_deployed" {
|
||||
code {
|
||||
let x := calldataload(0)
|
||||
// A dynamic store keeps the object out of all-native mode, so the huge CONSTANT-offset store
|
||||
// below takes the inline (unchecked) path rather than the bounds-checked AllNative path.
|
||||
mstore(calldataload(32), 7)
|
||||
// Huge constant offset: far past the fixed heap. EVM charges astronomical memory-expansion gas
|
||||
// and runs out of gas; the inline unchecked GEP would instead write out of the heap global. It
|
||||
// must trap (out-of-gas), not corrupt memory and fall through to the sstore.
|
||||
mstore(0xFFFFFFF0, x)
|
||||
sstore(0, 1)
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
object "IfCallFmp" {
|
||||
code { let s := datasize("IfCallFmp_deployed") codecopy(0, dataoffset("IfCallFmp_deployed"), s) return(0, s) }
|
||||
object "IfCallFmp_deployed" {
|
||||
code {
|
||||
// Known free-memory pointer, tracked as a constant by FMP propagation.
|
||||
mstore(0x40, 0x80)
|
||||
let cnt := calldataload(0)
|
||||
// The branch calls an internal allocator that bumps the FMP. `allocate` is recursive, so it is
|
||||
// never inlined: the `mstore(0x40, ...)` lives inside the callee, not inside this `if` body.
|
||||
// Region FMP invalidation must therefore consult `fmp_writers` (the call), not just look for a
|
||||
// direct FMP store in the branch — otherwise the post-`if` `mload(0x40)` is forwarded the stale
|
||||
// pre-branch constant 0x80 even when `cnt > 0` bumped the pointer.
|
||||
if cnt { allocate(cnt) }
|
||||
let p := mload(0x40)
|
||||
mstore(0x200, p)
|
||||
return(0x200, 0x20)
|
||||
function allocate(n) {
|
||||
if n {
|
||||
let f := mload(0x40)
|
||||
mstore(0x40, add(f, 0x20))
|
||||
allocate(sub(n, 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Regression test: contracts whose Yul IR generates internal helper functions
|
||||
// (e.g. abi_encode_string, extract_byte_array_length) that survive the newyork
|
||||
// inliner require a function scope in the LLVM context during code generation.
|
||||
// Without it, declaring frontend functions in generate_object triggers an ICE:
|
||||
// "function scope must be pushed before declaring frontend functions".
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": false,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "e942b51600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"VerifyCall": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "6d4ce63c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"VerifyCall": {
|
||||
"success": true,
|
||||
"output": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
contract InternalFn {
|
||||
string public a;
|
||||
string public b;
|
||||
|
||||
function set(string calldata _a, string calldata _b) external {
|
||||
a = _a;
|
||||
b = _b;
|
||||
}
|
||||
|
||||
function get() external view returns (string memory, string memory) {
|
||||
return (a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract KeccakFuseBug {
|
||||
address public target;
|
||||
|
||||
constructor() {
|
||||
target = address(this);
|
||||
}
|
||||
|
||||
function probe(uint256[8] calldata seeds)
|
||||
external view returns (uint256 r, bytes32 sink_out)
|
||||
{
|
||||
bytes32 sink;
|
||||
uint256 s0 = seeds[0]; uint256 s1 = seeds[1]; uint256 s2 = seeds[2];
|
||||
uint256 s3 = seeds[3]; uint256 s4 = seeds[4]; uint256 s5 = seeds[5];
|
||||
uint256 s6 = seeds[6]; uint256 s7 = seeds[7];
|
||||
assembly {
|
||||
mstore(0, s0) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s1) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s2) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s3) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s4) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s5) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s6) sink := xor(sink, keccak256(0, 32))
|
||||
mstore(0, s7) sink := xor(sink, keccak256(0, 32))
|
||||
}
|
||||
(bool ok,) = target.staticcall("");
|
||||
ok;
|
||||
assembly {
|
||||
r := mload(0)
|
||||
}
|
||||
sink_out = sink;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/// Soundness PoC: newyork forward type inference widens an `if`/`switch`
|
||||
/// output only from the then/else region *yields*, never from `inputs`. But on
|
||||
/// a missing `else`/`default` edge codegen routes `inputs` straight through to
|
||||
/// `outputs` (`to_llvm` phi construction; the IR `If` "defaults to yielding
|
||||
/// inputs unchanged" contract). The `leave`-elimination wrapper
|
||||
/// (`inline.rs::wrap_remaining_in_guard`) builds exactly that shape:
|
||||
/// `else_region: None`, non-empty `outputs`, where `inputs` are the
|
||||
/// pre-`leave` accumulators and the then-region yields the fall-through values.
|
||||
///
|
||||
/// `f(v)` sets `ret := v` (full width), takes `leave` for `v > 1000` (carrying
|
||||
/// the wide `ret`), and otherwise falls through to `ret := 7` (narrow). Inference
|
||||
/// widened the output only from the `7` yield → i8, so codegen truncates the
|
||||
/// leave-edge value to its low byte. `from_yul` always emits an explicit else,
|
||||
/// so only the post-inlining `leave` shape is affected.
|
||||
///
|
||||
/// Called with `v = 2^200` (> 1000): EVM returns `2^200`; the bug returns
|
||||
/// `2^200 mod 256 == 0`.
|
||||
object "LeaveWideOutput" {
|
||||
code {
|
||||
let size := datasize("LeaveWideOutput_deployed")
|
||||
codecopy(0, dataoffset("LeaveWideOutput_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "LeaveWideOutput_deployed" {
|
||||
code {
|
||||
let x := calldataload(0)
|
||||
let r := f(x)
|
||||
let out := 0
|
||||
if gt(r, 5) {
|
||||
out := 1
|
||||
}
|
||||
mstore(0, out)
|
||||
return(0, 32)
|
||||
|
||||
function f(v) -> ret {
|
||||
ret := v
|
||||
if gt(v, 1000) {
|
||||
leave
|
||||
}
|
||||
ret := 7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/// Store a value at 0x80, then overwrite [0x80,0xA0) with calldata via calldatacopy.
|
||||
/// A stale load-forward would return the original stored value.
|
||||
object "LoadFwdCopy" {
|
||||
code { datacopy(0, dataoffset("LoadFwdCopy_deployed"), datasize("LoadFwdCopy_deployed")) return(0, datasize("LoadFwdCopy_deployed")) }
|
||||
object "LoadFwdCopy_deployed" {
|
||||
code {
|
||||
mstore(0x80, 0x1234)
|
||||
calldatacopy(0x80, 0, 32)
|
||||
let p := mload(0x80) // EVM: calldata[0..32]; stale fwd: 0x1234
|
||||
mstore(0x100, p)
|
||||
return(0x100, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ pragma solidity ^0.8;
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": true,
|
||||
"differential": false,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {
|
||||
@@ -24,6 +24,11 @@ pragma solidity ^0.8;
|
||||
"data": "f016832c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"VerifyCall": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": {
|
||||
@@ -31,6 +36,11 @@ pragma solidity ^0.8;
|
||||
},
|
||||
"data": "f4a63aa5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"VerifyCall": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
object "Mem27" {
|
||||
code { datacopy(0, dataoffset("Mem27_deployed"), datasize("Mem27_deployed")) return(0, datasize("Mem27_deployed")) }
|
||||
object "Mem27_deployed" {
|
||||
code {
|
||||
let r := 0
|
||||
r := xor(r, mload(16))
|
||||
r := xor(r, keccak256(16, 29))
|
||||
r := xor(r, mload(16))
|
||||
r := add(r, sload(13))
|
||||
r := xor(r, keccak256(0, 15))
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Regression fixture for the dead-store-elimination key-mismatch bug in the
|
||||
/// newyork mem_opt pass. `MemoryOptimizer.pending_stores` is keyed by the exact
|
||||
/// store offset, but the load handler removed entries by the word-aligned offset.
|
||||
///
|
||||
/// For a store at the non-word-aligned offset `0x104`, the exact `mload(0x104)`
|
||||
/// forwards the stored value but (with the bug) removes the word-aligned key
|
||||
/// `0x100` instead of `0x104`, so the store is never marked read. The later
|
||||
/// same-offset `mstore(0x104, v2)` then finds the still-pending entry and
|
||||
/// dead-eliminates the first store — even though `mload(0x108)` (overlapping,
|
||||
/// non-forwarded) read its bytes. The eliminated store makes `y` observe fresh
|
||||
/// memory instead of `v1`, diverging from the EVM reference.
|
||||
///
|
||||
/// The `keccak256(0x104, 0x40)` forces the region to big-endian (escaping)
|
||||
/// memory, so the only behavioural divergence under the bug is the dropped
|
||||
/// store, not memory byte order. Compiled directly through revive's Yul path so
|
||||
/// the exact statement shape reaches the newyork mem_opt pass intact.
|
||||
object "MemOptOverlapDeadStore" {
|
||||
code {
|
||||
let size := datasize("MemOptOverlapDeadStore_deployed")
|
||||
codecopy(0, dataoffset("MemOptOverlapDeadStore_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "MemOptOverlapDeadStore_deployed" {
|
||||
code {
|
||||
let v1 := calldataload(0)
|
||||
let v2 := calldataload(32)
|
||||
mstore(0x104, v1)
|
||||
let x := mload(0x104)
|
||||
let y := mload(0x108)
|
||||
mstore(0x104, v2)
|
||||
let h := keccak256(0x104, 0x40)
|
||||
mstore(0, xor(xor(x, y), h))
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
object "MemProbe" {
|
||||
code {
|
||||
datacopy(0, dataoffset("MemProbe_deployed"), datasize("MemProbe_deployed"))
|
||||
return(0, datasize("MemProbe_deployed"))
|
||||
}
|
||||
object "MemProbe_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let a := calldataload(32)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { mstore(0, a) r := keccak256(0, 32) }
|
||||
case 1 { mstore(0, a) mstore(32, a) r := keccak256(0, 64) }
|
||||
case 2 { mstore(0, a) r := mload(0) }
|
||||
case 3 { mstore(0, a) mstore(16, a) r := mload(0) }
|
||||
case 4 { mstore8(0, a) r := mload(0) }
|
||||
case 5 { mstore(0, a) r := keccak256(0, 1) }
|
||||
case 6 { mstore(0, a) calldatacopy(32, calldatasize(), 32) r := keccak256(0, 64) }
|
||||
case 7 { mstore(0, a) mcopy(32, 0, 32) r := keccak256(32, 32) }
|
||||
case 8 { mstore(0, a) r := msize() }
|
||||
case 9 { mstore(0x80, a) r := msize() }
|
||||
case 10 { mstore8(100, 0xAB) r := msize() }
|
||||
case 11 { mstore(0, a) mstore(32, a) mcopy(8, 0, 40) r := keccak256(0, 72) }
|
||||
case 12 { mstore(0, a) r := keccak256(0, 0) }
|
||||
case 13 { let p := mload(0x40) mstore(p, a) r := keccak256(p, 32) }
|
||||
case 14 { mstore(0, a) r := keccak256(3, 17) }
|
||||
case 15 { mstore(0, a) mstore8(5, 0xEE) r := mload(0) }
|
||||
case 16 { mstore(0, a) r := keccak256(31, 1) }
|
||||
case 17 { mstore(10, a) r := mload(0) }
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
object "MsizeCopy" {
|
||||
code { let s := datasize("MsizeCopy_deployed") codecopy(0, dataoffset("MsizeCopy_deployed"), s) return(0, s) }
|
||||
object "MsizeCopy_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { calldatacopy(0x81, 0, 5) r := msize() } // dest 0x81 len 5 -> end 0x86 -> EVM 0xA0
|
||||
case 1 { mstore(0x80, 1) mcopy(0x141, 0x80, 7) r := msize() } // dest 0x141 len 7 -> 0x148 -> 0x160
|
||||
case 2 { calldatacopy(0x100, 0, 33) r := msize() } // 0x100+33=0x121 -> 0x140
|
||||
default { r := 0 }
|
||||
mstore(0x400, r)
|
||||
return(0x400, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
object "MsizeForCondition" {
|
||||
code { let s := datasize("MsizeForCondition_deployed") codecopy(0, dataoffset("MsizeForCondition_deployed"), s) return(0, s) }
|
||||
object "MsizeForCondition_deployed" {
|
||||
code {
|
||||
let x := calldataload(0)
|
||||
// Touch memory at 0x80 so the EVM free-memory watermark (msize) is a
|
||||
// nonzero 0xA0, regardless of `x`.
|
||||
mstore(0x80, x)
|
||||
let ran := 0
|
||||
// A *bare* `msize()` for-loop condition: this is the only expression
|
||||
// position the IR does not materialize into a preceding `let`. The body
|
||||
// runs once iff `msize()` is truthy. If a native store fails to update
|
||||
// the heap-size watermark (because `has_msize` missed this position),
|
||||
// `msize()` reads stale 0, the body is skipped, and `ran` diverges.
|
||||
for {} msize() {} {
|
||||
ran := add(ran, 1)
|
||||
break
|
||||
}
|
||||
mstore(0x400, ran)
|
||||
return(0x400, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Probe: msize after various memory-touching ops. EVM msize = highest accessed
|
||||
/// byte rounded up to a word. newyork tracks msize via ensure_heap_size; any
|
||||
/// divergence corrupts Solidity's allocator (which reads msize/FMP).
|
||||
object "MsizeProbe" {
|
||||
code { let s := datasize("MsizeProbe_deployed") codecopy(0, dataoffset("MsizeProbe_deployed"), s) return(0, s) }
|
||||
object "MsizeProbe_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let x := calldataload(32)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { mstore(0x80, x) r := msize() }
|
||||
case 1 { mstore8(0x95, x) r := msize() }
|
||||
case 2 { let _v := mload(0x100) r := msize() }
|
||||
case 3 { mstore(0x80, x) calldatacopy(0x200, 0, 16) r := msize() }
|
||||
case 4 { r := msize() }
|
||||
case 5 { mstore(0x80, x) let _v := mload(0x81) r := msize() }
|
||||
case 6 { mstore8(0x100, x) r := msize() }
|
||||
case 7 { mstore(and(x, 0xfe0), x) r := msize() }
|
||||
default { r := 0 }
|
||||
mstore(0x400, r)
|
||||
return(0x400, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
object "MsizeReadOps" {
|
||||
code { let s := datasize("MsizeReadOps_deployed") codecopy(0, dataoffset("MsizeReadOps_deployed"), s) return(0, s) }
|
||||
object "MsizeReadOps_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { mstore(0,0) let h := keccak256(0x81, 5) r := msize() } // reads 0x81..0x86 -> EVM 0xA0
|
||||
case 1 { log0(0x81, 7) r := msize() } // 0x81..0x88 -> EVM 0xA0
|
||||
case 2 { let h := keccak256(0xc1, 33) r := msize() } // 0xc1..0xe2 -> EVM 0x100
|
||||
default { r := 0 }
|
||||
mstore(0x400, r)
|
||||
return(0x400, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
object "MsizeUnaligned" {
|
||||
code { let s := datasize("MsizeUnaligned_deployed") codecopy(0, dataoffset("MsizeUnaligned_deployed"), s) return(0, s) }
|
||||
object "MsizeUnaligned_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let x := calldataload(32)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { mstore(0x81, x) r := msize() } // EVM: ceil(0xA1)->0xC0 (192)
|
||||
case 1 { mstore8(0x85, x) r := msize() } // EVM: ceil(0x86)->0xA0 (160)
|
||||
case 2 { mstore(0x80, x) r := msize() } // aligned: 0xA0 (160)
|
||||
case 3 { mstore(0xc1, x) r := msize() } // EVM: ceil(0xE1)->0x100 (256)
|
||||
default { r := 0 }
|
||||
mstore(0x400, r)
|
||||
return(0x400, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: newyork's `narrow_function_returns` only inspects the
|
||||
/// function's fall-through `return_values`, ignoring values returned by EARLY
|
||||
/// `leave` (return) statements. When the fall-through returns a small constant
|
||||
/// (here `return 0`, min_width I1) but earlier branches return full-width
|
||||
/// values, the function's return type is narrowed to i32 and EVERY early-leave
|
||||
/// result is truncated to 32 bits. `op(a,b)` for a = 2^256-1 returns
|
||||
/// 0xffffffff instead of 2^256-1.
|
||||
contract MultiLeaveReturnNarrow {
|
||||
function run(uint256 op, uint256 a, uint256 b) external pure returns (uint256) {
|
||||
if (op == 0) return a + b;
|
||||
if (op == 1) return a - b;
|
||||
if (op == 2) return a * b;
|
||||
if (op == 3) return a / b;
|
||||
if (op == 4) return a % b;
|
||||
return 0; // small-constant fall-through
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
object "NarrowProbe" {
|
||||
code {
|
||||
datacopy(0, dataoffset("NarrowProbe_deployed"), datasize("NarrowProbe_deployed"))
|
||||
return(0, datasize("NarrowProbe_deployed"))
|
||||
}
|
||||
object "NarrowProbe_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let a := calldataload(32)
|
||||
let r := 0
|
||||
switch op
|
||||
// high-bit round trip: narrow via shr then widen back via shl
|
||||
case 0 { r := shl(200, shr(200, a)) }
|
||||
// narrow to i32 then square (needs up to 64 bits)
|
||||
case 1 { let t := and(a, 0xFFFFFFFF) r := mul(t, t) }
|
||||
// narrow to i64 then shift into the high half
|
||||
case 2 { let t := shr(192, a) r := shl(192, t) }
|
||||
// narrow store then load round trip (i16)
|
||||
case 3 { let t := and(a, 0xFFFF) mstore(0, t) r := mload(0) }
|
||||
// top byte (i8) shifted back to the top
|
||||
case 4 { let t := byte(0, a) r := shl(248, t) }
|
||||
// narrow to i8 then storage round trip
|
||||
case 5 { let t := and(a, 0xFF) sstore(5, t) r := sload(5) }
|
||||
// narrow then negate: 0 - small => full-width large value
|
||||
case 6 { let t := shr(128, a) r := sub(0, t) }
|
||||
// not of a narrow value => high bits all set (full width)
|
||||
case 7 { let t := and(a, 0xFFFFFFFF) r := not(t) }
|
||||
// i64 + i64 => up to 65 bits
|
||||
case 8 { let t := and(a, 0xFFFFFFFFFFFFFFFF) r := add(t, t) }
|
||||
// signextend over a narrowed value
|
||||
case 9 { let t := shr(64, a) r := signextend(7, t) }
|
||||
// narrow then compare-against-large: lt(small, big) must be 1
|
||||
case 10 { let t := and(a, 0xFF) r := lt(t, 0x100000000) }
|
||||
// div of a narrowed dividend by 1 then widen-add a big constant
|
||||
case 11 { let t := and(a, 0xFFFF) r := add(div(t, 1), shl(200, 1)) }
|
||||
// mulmod where two operands are narrow but the product overflows 256
|
||||
case 12 { let t := and(a, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) r := mulmod(t, t, 0x100000000000000000000000000000000) }
|
||||
// narrow value used as exp base, large exponent
|
||||
case 13 { let t := and(a, 0xFF) r := exp(t, 64) }
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
object "OpProbe" {
|
||||
code {
|
||||
datacopy(0, dataoffset("OpProbe_deployed"), datasize("OpProbe_deployed"))
|
||||
return(0, datasize("OpProbe_deployed"))
|
||||
}
|
||||
object "OpProbe_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let a := calldataload(32)
|
||||
let b := calldataload(64)
|
||||
let c := calldataload(96)
|
||||
let r := 0
|
||||
switch op
|
||||
case 0 { r := shl(a, b) }
|
||||
case 1 { r := shr(a, b) }
|
||||
case 2 { r := sar(a, b) }
|
||||
case 3 { r := byte(a, b) }
|
||||
case 4 { r := signextend(a, b) }
|
||||
case 5 { r := sdiv(a, b) }
|
||||
case 6 { r := smod(a, b) }
|
||||
case 7 { r := exp(a, b) }
|
||||
case 8 { r := addmod(a, b, c) }
|
||||
case 9 { r := mulmod(a, b, c) }
|
||||
case 10 { r := div(a, b) }
|
||||
case 11 { r := mod(a, b) }
|
||||
case 12 { r := slt(a, b) }
|
||||
case 13 { r := sgt(a, b) }
|
||||
case 14 { r := lt(a, b) }
|
||||
case 15 { r := gt(a, b) }
|
||||
case 16 { r := not(a) }
|
||||
case 17 { r := addmod(mul(a, b), c, a) }
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: the panic-pattern outliner in
|
||||
/// `simplify.rs::find_panic_pattern_backwards` extracts the panic code
|
||||
/// from `mstore(4, code)` via `code_val.to_u64_digits().first()` and a
|
||||
/// `<= 0xFF` check. That returns the LEAST-significant u64 digit, so any
|
||||
/// 256-bit `code` value whose low byte is in `[0, 0xFF]` is mis-classified
|
||||
/// as a small canonical Solidity panic code regardless of its higher bits.
|
||||
///
|
||||
/// Codegen for `Statement::PanicRevert { code }` emits the canonical
|
||||
/// 36-byte Solidity panic format (selector + zero-padded `code`), so the
|
||||
/// high bits of the original mstored value are silently dropped from the
|
||||
/// revert data observed by the caller. EVM emits the original bytes.
|
||||
contract PanicCodeBug {
|
||||
fallback() external {
|
||||
assembly {
|
||||
// canonical panic selector at offset 0
|
||||
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
|
||||
// "code" value 0x10_00000000_00000000_00000000_00000042 — its low
|
||||
// byte (0x42) passes the simplifier's `<= 0xFF` check, but byte
|
||||
// position 20 of the revert encoding holds the 0x10 nibble.
|
||||
mstore(4, 0x10000000000000000000000000000042)
|
||||
revert(0, 0x24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Probe: less-common panic codes' revert data must match EVM.
|
||||
contract PanicCodes {
|
||||
enum E { A, B, C }
|
||||
function enumConv(uint256 x) external pure returns (E) { return E(x); } // 0x21 if x>2
|
||||
function uninitFp() external pure returns (uint256) {
|
||||
function(uint256) internal pure returns (uint256) fp;
|
||||
return fp(5); // 0x51
|
||||
}
|
||||
function overAlloc(uint256 n) external pure returns (uint256) {
|
||||
uint256[] memory a = new uint256[](n); // 0x41 if huge
|
||||
return a.length;
|
||||
}
|
||||
function shiftEdge(uint256 a, uint256 s) external pure returns (uint256) {
|
||||
return a << s; // no panic, edge
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `simplify.rs::find_panic_pattern_backwards` walks
|
||||
/// backward from `revert(0, 0x24)` skipping every mstore whose offset
|
||||
/// is not exactly 0 or 4, and the final `only_safe` gate only checks
|
||||
/// that intermediate statements are `Let`/`MStore`/`Expression`. An
|
||||
/// `mstore(p, …)` with `p ∈ (0, 4) ∪ (4, 0x24)` partially overwrites
|
||||
/// the panic-encoded revert data EVM emits, but the simplifier still
|
||||
/// matches the canonical panic shape and replaces the whole sequence
|
||||
/// with `Statement::PanicRevert { code }` — dropping the corrupting
|
||||
/// mstore from the revert data observed by the caller.
|
||||
contract PanicInterveneBug {
|
||||
fallback() external {
|
||||
assembly {
|
||||
// Canonical panic selector + code prelude.
|
||||
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
|
||||
mstore(4, 0x01)
|
||||
// Unaligned mstore overwriting bytes 7..38 — bytes 7..35 of
|
||||
// this end up in the revert(0, 0x24) data on EVM. PVM emits
|
||||
// canonical PanicRevert(0x01) instead, dropping these bytes.
|
||||
mstore(7, 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef)
|
||||
revert(0, 0x24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
object "PanicOutlineCall" {
|
||||
code { let s := datasize("PanicOutlineCall_deployed") codecopy(0, dataoffset("PanicOutlineCall_deployed"), s) return(0, s) }
|
||||
object "PanicOutlineCall_deployed" {
|
||||
code {
|
||||
// A canonical Panic(uint256) shape, but with an internal call between the selector store and
|
||||
// the revert. `inner` is recursive so it is never inlined; it reverts with distinct data before
|
||||
// the panic revert is reached. Collapsing this window into a single PanicRevert would drop the
|
||||
// call and revert with the wrong (panic) payload.
|
||||
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
|
||||
inner(0)
|
||||
mstore(4, 0x11)
|
||||
revert(0, 0x24)
|
||||
function inner(n) {
|
||||
if n { inner(sub(n, 1)) }
|
||||
mstore(0x80, 0xdeadbeef)
|
||||
revert(0x80, 0x20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
object "PanicOutlineYield" {
|
||||
code { let s := datasize("PanicOutlineYield_deployed") codecopy(0, dataoffset("PanicOutlineYield_deployed"), s) return(0, s) }
|
||||
object "PanicOutlineYield_deployed" {
|
||||
code {
|
||||
let cond := calldataload(0)
|
||||
let r := 0
|
||||
// `r` is reassigned inside the branch, between the panic selector store and the revert, and is
|
||||
// read after the branch — so the branch region yields that in-window definition of `r`.
|
||||
// Collapsing the window into a PanicRevert drops the definition; without a zero-binding rescue
|
||||
// the surviving yield references an undefined value and the SSA validator panics.
|
||||
if cond {
|
||||
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
|
||||
r := 7
|
||||
mstore(4, 0x11)
|
||||
revert(0, 0x24)
|
||||
}
|
||||
sstore(0, r)
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
object "PanicOverwriteSelector" {
|
||||
code { let s := datasize("PanicOverwriteSelector_deployed") codecopy(0, dataoffset("PanicOverwriteSelector_deployed"), s) return(0, s) }
|
||||
object "PanicOverwriteSelector_deployed" {
|
||||
code {
|
||||
// Canonical panic selector + code, but the selector word is then overwritten before the
|
||||
// revert. Last-write-wins means the EVM revert data starts with 0xdeadbeef, not the panic
|
||||
// selector — so this must NOT be collapsed into a canonical Panic(0x11).
|
||||
mstore(0, 0x4e487b7100000000000000000000000000000000000000000000000000000000)
|
||||
mstore(4, 0x11)
|
||||
mstore(0, 0xdeadbeef)
|
||||
revert(0, 0x24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/// Soundness PoC: newyork parameter narrowing
|
||||
/// (`type_inference.rs::narrow_function_params`) narrows a parameter to I64/I32
|
||||
/// whenever its `max_width` is lowered below I256 by a use that re-validates the
|
||||
/// value (a memory offset bounded by `safe_truncate_int_to_xlen`). The call
|
||||
/// boundary then emits a *checked* truncation that traps on an out-of-range
|
||||
/// argument (`to_llvm.rs::checked_truncate_to`).
|
||||
///
|
||||
/// The bug: `max_width` was lowered by ANY narrowing use, including one reached
|
||||
/// only on a conditional path. `store_if(p, c)` uses `p` as a memory offset only
|
||||
/// inside `if c`. EVM skips the store (and never touches `p`) when `c == 0`, so a
|
||||
/// `p >= 2^64` argument is harmless. But the narrowed `i64` signature truncates
|
||||
/// `p` unconditionally at every call site, trapping before `c` is even tested.
|
||||
///
|
||||
/// Every caller passes a `calldataload`-derived `p` (forward width I256, so the
|
||||
/// caller-driven narrowing path cannot narrow it) and four distinct call sites
|
||||
/// plus the body's arithmetic chain keep `store_if` out of the inliner, so the
|
||||
/// narrowed parameter reaches codegen.
|
||||
///
|
||||
/// Called with `c == 0` and `p == 2^64`: EVM returns `4` (each `store_if`
|
||||
/// returns its else-path value); the buggy narrowing traps at the first call.
|
||||
object "ParamCondNarrow" {
|
||||
code {
|
||||
let size := datasize("ParamCondNarrow_deployed")
|
||||
codecopy(0, dataoffset("ParamCondNarrow_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "ParamCondNarrow_deployed" {
|
||||
code {
|
||||
let c := calldataload(0)
|
||||
let acc := 0
|
||||
acc := add(acc, store_if(calldataload(32), c))
|
||||
acc := add(acc, store_if(calldataload(64), c))
|
||||
acc := add(acc, store_if(calldataload(96), c))
|
||||
acc := add(acc, store_if(calldataload(128), c))
|
||||
mstore(0, acc)
|
||||
return(0, 32)
|
||||
|
||||
function store_if(p, g) -> r {
|
||||
r := 1
|
||||
if g {
|
||||
// `p` is used as a memory offset only on this conditional path.
|
||||
mstore(p, 0x42)
|
||||
r := mload(p)
|
||||
}
|
||||
// Live arithmetic chain on the condition keeps the body over the
|
||||
// always-inline size so the narrowed parameter reaches codegen.
|
||||
let a1 := add(g, 1)
|
||||
let a2 := mul(a1, 3)
|
||||
let a3 := xor(a2, 7)
|
||||
let a4 := add(a3, 9)
|
||||
let a5 := or(a4, 13)
|
||||
let a6 := add(a5, 17)
|
||||
let a7 := mul(a6, 19)
|
||||
let a8 := xor(a7, 23)
|
||||
r := add(r, a8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: function parameter used only as `mload` offset gets
|
||||
/// silently narrowed from i256 to i64 by `narrow_function_params`. The
|
||||
/// call-site bare truncate drops the upper 192 bits BEFORE the use-site
|
||||
/// `safe_truncate_int_to_xlen` can observe them, so `mload(2^64)` aliases
|
||||
/// to `mload(0)` and returns the zero-initialised scratch slot instead of
|
||||
/// OOGing on memory expansion the way EVM does.
|
||||
///
|
||||
/// `fetch` is intentionally recursive so the early newyork inliner refuses
|
||||
/// to inline it, preserving the narrowable `mload(x)`-only parameter shape.
|
||||
contract ParamMload {
|
||||
function fetch(uint depth, uint x) internal pure returns (uint r) {
|
||||
if (depth == 0) {
|
||||
assembly { r := mload(x) }
|
||||
return r;
|
||||
}
|
||||
return fetch(depth - 1, x);
|
||||
}
|
||||
|
||||
function tryFetch(uint x) external pure returns (uint) {
|
||||
return fetch(1, x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
object "PastHeapConstReturn" {
|
||||
code { let s := datasize("PastHeapConstReturn_deployed") codecopy(0, dataoffset("PastHeapConstReturn_deployed"), s) return(0, s) }
|
||||
object "PastHeapConstReturn_deployed" {
|
||||
code {
|
||||
// Return a constant 32-byte range starting exactly at the heap end (heap is 128 KiB = 0x20000),
|
||||
// with no prior store. The inline unchecked seal_return would read one-past-heap into the
|
||||
// returndata (information leak); it must instead trap via the bounds-checked path.
|
||||
return(0x20000, 0x20)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
contract SDivNarrowBug {
|
||||
/// Forces LLVM IR of the form
|
||||
/// sdiv i256 (and i256 %a, 0xff..ff_u64), (and i256 %b, 0xff..ff_u64)
|
||||
///
|
||||
/// `provable_bit_width` accepts the AND-mask proof and lets revive's
|
||||
/// `narrow_divrem_instructions` rewrite this to `sdiv i64 (trunc..), (trunc..)`
|
||||
/// followed by `sext` back to i256. The truncated low-64 view of an i256
|
||||
/// whose bit 63 is set looks negative under i64, but the i256 value is
|
||||
/// non-negative (high 192 bits are zero by the AND), so the narrowed
|
||||
/// signed division returns the wrong sign.
|
||||
function sdiv_masked(uint256 a, uint256 b) external pure returns (int256 q) {
|
||||
assembly {
|
||||
let am := and(a, 0xffffffffffffffff)
|
||||
let bm := and(b, 0xffffffffffffffff)
|
||||
q := sdiv(am, bm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/// Soundness PoC: newyork forward-width rule for `sar` with a constant shift
|
||||
/// (`type_inference.rs::infer_expression_width`, the `Shr | Sar` arm):
|
||||
///
|
||||
/// if let Some(shift) = known_constants[lhs] {
|
||||
/// ... else { BitWidth::from_bits((256 - shift).max(1)) }
|
||||
/// } else { rhs_width }
|
||||
///
|
||||
/// For logical `shr` this is correct (the shifted-in high bits are zero, so the
|
||||
/// result fits in `256 - shift` bits). For ARITHMETIC `sar` it is NOT: when the
|
||||
/// shifted value is negative, `sar` sign-extends, so the high bits stay set and
|
||||
/// the result is full-width (~2^256). The rule ignores the operand's sign and
|
||||
/// caps the width at `256 - shift`. `sar` also never marks the value
|
||||
/// `is_signed`, so return-type narrowing then truncates the sign bits.
|
||||
///
|
||||
/// `doSar` returns `sar(250, value)`. For `value = -1` (2^256-1) the result is
|
||||
/// `-1` (all ones) on EVM, but the width rule reports `from_bits(6) = I8`
|
||||
/// (clamped to I32), so the narrowed return drops everything above 32 bits and
|
||||
/// yields `0xffffffff`.
|
||||
object "SarReturnNarrow" {
|
||||
code {
|
||||
let size := datasize("SarReturnNarrow_deployed")
|
||||
codecopy(0, dataoffset("SarReturnNarrow_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "SarReturnNarrow_deployed" {
|
||||
code {
|
||||
let z0, w0 := doSar(calldataload(0), calldataload(32))
|
||||
let z1, w1 := doSar(calldataload(64), calldataload(96))
|
||||
let z2, w2 := doSar(calldataload(128), calldataload(160))
|
||||
let z3, w3 := doSar(calldataload(192), calldataload(224))
|
||||
mstore(0, z0)
|
||||
mstore(32, w0)
|
||||
mstore(64, z1)
|
||||
mstore(96, w1)
|
||||
mstore(128, z2)
|
||||
mstore(160, w2)
|
||||
mstore(192, z3)
|
||||
mstore(224, w3)
|
||||
return(0, 256)
|
||||
|
||||
function doSar(value, y) -> z, w {
|
||||
z := sar(250, value)
|
||||
let a1 := add(y, 1)
|
||||
let a2 := mul(a1, 3)
|
||||
let a3 := xor(a2, 7)
|
||||
let a4 := add(a3, 9)
|
||||
let a5 := or(a4, 13)
|
||||
let a6 := add(a5, 17)
|
||||
let a7 := mul(a6, 19)
|
||||
let a8 := xor(a7, 23)
|
||||
w := add(a8, 29)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Register 0x20 (< 0x60, exempt from dynamic-escape guard) as an aligned native candidate.
|
||||
object "ScratchCopyTaint" {
|
||||
code { datacopy(0, dataoffset("ScratchCopyTaint_deployed"), datasize("ScratchCopyTaint_deployed")) return(0, datasize("ScratchCopyTaint_deployed")) }
|
||||
object "ScratchCopyTaint_deployed" {
|
||||
code {
|
||||
mstore(0x20, 0)
|
||||
// Overwrite scratch [0x00,0x40) with big-endian calldata. Taints only word_align(0)=0.
|
||||
calldatacopy(0, 0, 64)
|
||||
// Read 0x20: if native-LE, byte-reverses the BE calldata word.
|
||||
let b := mload(0x20)
|
||||
mstore(0x80, b)
|
||||
return(0x80, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Soundness PoC: the under-estimated forward width of `sdiv` (Bug #7) is also
|
||||
/// consumed by codegen's comparison-operand narrowing
|
||||
/// (`to_llvm.rs::try_narrow_comparison`), a separate amplifier from return-type
|
||||
/// narrowing. When a comparison's operand has an (incorrectly) narrow inferred
|
||||
/// width, both operands are truncated to that width before comparing — so a
|
||||
/// full-width value masquerading as i32 is silently truncated, giving the wrong
|
||||
/// comparison result and hence wrong control flow.
|
||||
///
|
||||
/// `doCmp` returns `lt(sdiv(x, y), 0xffffffff)`. With `x` narrowed (callers pass
|
||||
/// `and(calldataload, 0xff)`) and `y = -1`, `sdiv(5, -1) = -5 = 2^256-5`, which
|
||||
/// is NOT `< 0xffffffff` (EVM result 0). But the comparison truncates the
|
||||
/// quotient to i32 (`0xfffffffb`) and computes `lt(0xfffffffb, 0xffffffff) = 1`.
|
||||
object "SdivCompareNarrow" {
|
||||
code {
|
||||
let size := datasize("SdivCompareNarrow_deployed")
|
||||
codecopy(0, dataoffset("SdivCompareNarrow_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "SdivCompareNarrow_deployed" {
|
||||
code {
|
||||
let r0, w0 := doCmp(and(calldataload(0), 0xff), calldataload(32))
|
||||
let r1, w1 := doCmp(and(calldataload(64), 0xff), calldataload(96))
|
||||
let r2, w2 := doCmp(and(calldataload(128), 0xff), calldataload(160))
|
||||
let r3, w3 := doCmp(and(calldataload(192), 0xff), calldataload(224))
|
||||
mstore(0x80, r0)
|
||||
mstore(0xa0, w0)
|
||||
mstore(0xc0, r1)
|
||||
mstore(0xe0, w1)
|
||||
mstore(0x100, r2)
|
||||
mstore(0x120, w2)
|
||||
mstore(0x140, r3)
|
||||
mstore(0x160, w3)
|
||||
return(0x80, 256)
|
||||
|
||||
function doCmp(x, y) -> r, w {
|
||||
let s := sdiv(x, y)
|
||||
r := lt(s, 0xffffffff)
|
||||
let a1 := add(y, 1)
|
||||
let a2 := mul(a1, 3)
|
||||
let a3 := xor(a2, 7)
|
||||
let a4 := add(a3, 9)
|
||||
let a5 := or(a4, 13)
|
||||
let a6 := add(a5, 17)
|
||||
let a7 := mul(a6, 19)
|
||||
let a8 := xor(a7, 23)
|
||||
w := add(a8, 29)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/// Soundness PoC: newyork return-type narrowing
|
||||
/// (`type_inference.rs::narrow_function_returns`) narrows a function's return
|
||||
/// type to its forward `min_width` whenever `min_width < I256` and the return
|
||||
/// value is not flagged `is_signed`.
|
||||
///
|
||||
/// The forward width rule for `sdiv` is `lhs_width` (the dividend's width)
|
||||
/// (`infer_expression_width`: `Div | SDiv | Mod | SMod => lhs_width`). For
|
||||
/// *unsigned* div/mod and for `smod` (result takes the dividend's sign) this is
|
||||
/// a correct upper bound. For **signed division `sdiv`** it is NOT: when the
|
||||
/// dividend is a small non-negative value (narrow width) but the divisor is
|
||||
/// negative, the quotient is negative — a full-width two's-complement value
|
||||
/// (~2^256). `sdiv` never marks its result `is_signed`, so the guard misses it.
|
||||
///
|
||||
/// `doSdiv` returns `sdiv(x, y)`; every caller passes `and(calldataload,0xff)`
|
||||
/// so the dividend param `x` narrows, the return min_width is computed as
|
||||
/// `x`'s width, and the return type is clamped/narrowed — truncating the
|
||||
/// negative quotient. Four distinct call sites + body size keep the function
|
||||
/// out of the inliner (CostBenefit -> LLVM noinline), so the narrowed return
|
||||
/// survives to codegen.
|
||||
///
|
||||
/// `sdiv(5, -1) = -5 = 2^256 - 5` on EVM; the narrowed return drops the high
|
||||
/// bits, returning 0x...fffffffb instead.
|
||||
object "SdivReturnNarrow" {
|
||||
code {
|
||||
let size := datasize("SdivReturnNarrow_deployed")
|
||||
codecopy(0, dataoffset("SdivReturnNarrow_deployed"), size)
|
||||
return(0, size)
|
||||
}
|
||||
object "SdivReturnNarrow_deployed" {
|
||||
code {
|
||||
let z0, w0 := doSdiv(and(calldataload(0), 0xff), calldataload(32))
|
||||
let z1, w1 := doSdiv(and(calldataload(64), 0xff), calldataload(96))
|
||||
let z2, w2 := doSdiv(and(calldataload(128), 0xff), calldataload(160))
|
||||
let z3, w3 := doSdiv(and(calldataload(192), 0xff), calldataload(224))
|
||||
mstore(0, z0)
|
||||
mstore(32, w0)
|
||||
mstore(64, z1)
|
||||
mstore(96, w1)
|
||||
mstore(128, z2)
|
||||
mstore(160, w2)
|
||||
mstore(192, z3)
|
||||
mstore(224, w3)
|
||||
return(0, 256)
|
||||
|
||||
function doSdiv(x, y) -> z, w {
|
||||
z := sdiv(x, y)
|
||||
// Live chain on the divisor `y` (kept observable via `w`) to
|
||||
// push the function body over the always-inline size, so the
|
||||
// narrowed return type reaches codegen.
|
||||
let a1 := add(y, 1)
|
||||
let a2 := mul(a1, 3)
|
||||
let a3 := xor(a2, 7)
|
||||
let a4 := add(a3, 9)
|
||||
let a5 := or(a4, 13)
|
||||
let a6 := add(a5, 17)
|
||||
let a7 := mul(a6, 19)
|
||||
let a8 := xor(a7, 23)
|
||||
w := add(a8, 29)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
object "SelfCall" {
|
||||
code { datacopy(0, dataoffset("SelfCall_deployed"), datasize("SelfCall_deployed")) return(0, datasize("SelfCall_deployed")) }
|
||||
object "SelfCall_deployed" {
|
||||
code {
|
||||
let flag := calldataload(0)
|
||||
let value := calldataload(32)
|
||||
switch flag
|
||||
// leaf: echo the value back as return data
|
||||
case 0 {
|
||||
mstore(0, value)
|
||||
return(0, 32)
|
||||
}
|
||||
// recurse: build [0, value] calldata, self-call, return the returndata word
|
||||
default {
|
||||
mstore(0x80, 0)
|
||||
mstore(0xA0, value)
|
||||
let ok := call(gas(), address(), 0, 0x80, 64, 0x100, 32)
|
||||
// mix in returndatasize and a returndatacopy round-trip
|
||||
returndatacopy(0x200, 0, returndatasize())
|
||||
let r := mload(0x100)
|
||||
r := xor(r, mload(0x200))
|
||||
r := add(r, ok)
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
object "SelfCallRd" {
|
||||
code { datacopy(0, dataoffset("SelfCallRd_deployed"), datasize("SelfCallRd_deployed")) return(0, datasize("SelfCallRd_deployed")) }
|
||||
object "SelfCallRd_deployed" {
|
||||
code {
|
||||
let flag := calldataload(0)
|
||||
switch flag
|
||||
// leaf: return a chunk of size = calldataload(32) (bounded), filled with calldataload(64)
|
||||
case 0 {
|
||||
let sz := and(calldataload(32), 0xFF)
|
||||
mstore(0, calldataload(64))
|
||||
mstore(32, not(calldataload(64)))
|
||||
mstore(64, calldataload(64))
|
||||
return(0, sz)
|
||||
}
|
||||
// caller: self-call leaf, then returndatacopy(dest, off, len) with bounded params
|
||||
default {
|
||||
mstore(0x80, 0)
|
||||
mstore(0xA0, calldataload(32))
|
||||
mstore(0xC0, calldataload(64))
|
||||
let ok := call(gas(), address(), 0, 0x80, 96, 0x100, 0)
|
||||
let rds := returndatasize()
|
||||
let off := and(calldataload(96), 0xFF)
|
||||
let len := and(calldataload(128), 0xFF)
|
||||
returndatacopy(0x200, off, len)
|
||||
let r := xor(mload(0x200), add(rds, ok))
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
object "ShiftParamNarrow" {
|
||||
code { datacopy(0, dataoffset("ShiftParamNarrow_deployed"), datasize("ShiftParamNarrow_deployed")) return(0, datasize("ShiftParamNarrow_deployed")) }
|
||||
object "ShiftParamNarrow_deployed" {
|
||||
code {
|
||||
let opx := calldataload(0)
|
||||
let shamt := calldataload(32)
|
||||
let xv := calldataload(64)
|
||||
let r0 := f(opx, shamt, xv)
|
||||
let r1 := f(opx, shamt, xv)
|
||||
let r2 := f(opx, shamt, xv)
|
||||
let r3 := f(opx, shamt, xv)
|
||||
mstore(0x200, r0)
|
||||
mstore(0x220, add(r1, add(r2, r3)))
|
||||
return(0x200, 64)
|
||||
function f(op, s, v) -> r {
|
||||
switch op
|
||||
case 0 { r := mload(s) } // narrows s to i64
|
||||
case 1 { r := shl(s, v) }
|
||||
case 2 { r := shr(s, v) }
|
||||
case 3 { r := sar(s, v) }
|
||||
case 4 {
|
||||
// bulk to push f over the inline threshold (keep f a real function)
|
||||
mstore(0x300, s) mstore(0x320, v) mstore(0x340, op)
|
||||
r := keccak256(0x300, 0x60)
|
||||
r := add(r, keccak256(0x300, 0x40))
|
||||
r := add(r, keccak256(0x300, 0x20))
|
||||
}
|
||||
default { r := s }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
object "SignedCmpNarrow" {
|
||||
code { datacopy(0, dataoffset("SignedCmpNarrow_deployed"), datasize("SignedCmpNarrow_deployed")) return(0, datasize("SignedCmpNarrow_deployed")) }
|
||||
object "SignedCmpNarrow_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let a := calldataload(32)
|
||||
let b := calldataload(64)
|
||||
let r := 0
|
||||
switch op
|
||||
// i1 operands: sgt(0, 1) must be 0 (EVM), not 1
|
||||
case 0 { r := sgt(eq(a, b), gt(a, b)) }
|
||||
case 1 { r := slt(gt(a, b), eq(a, b)) }
|
||||
// i8 operands with top bit set: sgt(0xC8, 0x05) must be 1 (both positive)
|
||||
case 2 { r := sgt(and(a, 0xFF), and(b, 0xFF)) }
|
||||
case 3 { r := slt(and(a, 0xFF), and(b, 0xFF)) }
|
||||
mstore(0, r) return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/// Probe: if newyork narrows the signextend BYTE-INDEX parameter to i64, a huge
|
||||
/// index (>=31, which EVM treats as "leave value unchanged") truncated to a
|
||||
/// small index would sign-extend from a low byte instead, changing the value.
|
||||
/// `pp` is used as mload offset (case 0) to drive narrowing, and as the
|
||||
/// signextend index (case 1). value 0x80 has bit 7 set, so signextend(0, 0x80)
|
||||
/// = 0xff..ff80 while signextend(huge, 0x80) = 0x80.
|
||||
object "SignextendIndexNarrow" {
|
||||
code { let s := datasize("SignextendIndexNarrow_deployed") codecopy(0, dataoffset("SignextendIndexNarrow_deployed"), s) return(0, s) }
|
||||
object "SignextendIndexNarrow_deployed" {
|
||||
code {
|
||||
let op := calldataload(0)
|
||||
let p := calldataload(32)
|
||||
let r0 := probe(op, p)
|
||||
let r1 := probe(op, p)
|
||||
let r2 := probe(op, p)
|
||||
let r3 := probe(op, p)
|
||||
mstore(0x200, r0) mstore(0x220, add(r1, add(r2, r3)))
|
||||
return(0x200, 64)
|
||||
function probe(opx, pp) -> r {
|
||||
switch opx
|
||||
case 0 { r := mload(pp) }
|
||||
case 1 { r := signextend(pp, 0x80) }
|
||||
case 2 { r := keccak256(pp, 32) }
|
||||
default { r := pp }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
object "StorageProbe" {
|
||||
code {
|
||||
datacopy(0, dataoffset("StorageProbe_deployed"), datasize("StorageProbe_deployed"))
|
||||
return(0, datasize("StorageProbe_deployed"))
|
||||
}
|
||||
object "StorageProbe_deployed" {
|
||||
code {
|
||||
let k := calldataload(0)
|
||||
let v := calldataload(32)
|
||||
|
||||
// >= 9 mapping-style sstores in the `let h := keccak256(..); sstore(h, _)`
|
||||
// form so compound_outlining's keccak256_pair + sstore -> mapping_sstore
|
||||
// fusion (T9, total mapping ops >= 9) triggers. Distinct base slots so
|
||||
// the slots do not collide. The differential runner compares the full
|
||||
// storage state between newyork-PVM and solc-EVM.
|
||||
mstore(0, k) mstore(0x20, 0) let s0 := keccak256(0, 64) sstore(s0, v)
|
||||
mstore(0, k) mstore(0x20, 1) let s1 := keccak256(0, 64) sstore(s1, add(v, 1))
|
||||
mstore(0, k) mstore(0x20, 2) let s2 := keccak256(0, 64) sstore(s2, not(v))
|
||||
mstore(0, k) mstore(0x20, 3) let s3 := keccak256(0, 64) sstore(s3, shl(200, v))
|
||||
mstore(0, k) mstore(0x20, 4) let s4 := keccak256(0, 64) sstore(s4, and(v, 0xFF))
|
||||
mstore(0, add(k, 1)) mstore(0x20, 0) let s5 := keccak256(0, 64) sstore(s5, v)
|
||||
mstore(0, add(k, 2)) mstore(0x20, 0) let s6 := keccak256(0, 64) sstore(s6, sub(0, v))
|
||||
mstore(0, not(k)) mstore(0x20, 7) let s7 := keccak256(0, 64) sstore(s7, v)
|
||||
mstore(0, k) mstore(0x20, 8) let s8 := keccak256(0, 64) sstore(s8, v)
|
||||
mstore(0, k) mstore(0x20, 9) let s9 := keccak256(0, 64) sstore(s9, exp(v, 3))
|
||||
|
||||
// also read one back through the fused mapping_sload path and store it plainly
|
||||
mstore(0, k) mstore(0x20, 0) let r := sload(keccak256(0, 64))
|
||||
sstore(0xFFFF, r)
|
||||
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
object "Store8Fmp" {
|
||||
code { datacopy(0, dataoffset("Store8Fmp_deployed"), datasize("Store8Fmp_deployed")) return(0, datasize("Store8Fmp_deployed")) }
|
||||
object "Store8Fmp_deployed" {
|
||||
code {
|
||||
mstore8(0x40, 0xAB)
|
||||
let r := mload(0x40)
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Regression test: ABI parameter validation for sub-256-bit types.
|
||||
// Solidity 0.8+ reverts when a calldataload value exceeds the range of its
|
||||
// declared type (e.g., passing 256 to a uint8 parameter).
|
||||
// Bug: the newyork pipeline silently truncates instead of reverting.
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": true,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {
|
||||
"code": { "Solidity": { "contract": "SubTypeValidation", "solc_optimizer": false } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "d552145b0000000000000000000000000000000000000000000000000000000100000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "7440c86b0000000000000000000000000000000000000000000000010000000000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "0850cee20000000000000000000000000000000100000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "d552145b0000000000000000000000000000000000000000000000000000000000000005"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "7440c86b0000000000000000000000000000000000000000000000000000000000000005"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "0850cee20000000000000000000000000000000000000000000000000000000000000005"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
contract SubTypeValidation {
|
||||
// Should revert when called with value > 0xFF
|
||||
function narrow_uint8(uint8 x) public pure returns (uint8) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// Should revert when called with value > 0xFFFFFFFFFFFFFFFF
|
||||
function narrow_uint64(uint64 x) public pure returns (uint64) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// Should revert when called with value > 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
function narrow_uint128(uint128 x) public pure returns (uint128) {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Regression test: sub-underflow result widened to uint256 must preserve EVM
|
||||
// modular semantics.
|
||||
//
|
||||
// Bug (crates/newyork/src/to_llvm.rs ~5977-5999): when `BinaryOperation::Sub`
|
||||
// is codegen'd with both operands LLVM-narrowed to <= 64 bits (via
|
||||
// `try_narrow_let_binding` Strategy 1 — structural proof from `and value, mask`)
|
||||
// and the consumer's demand for the let binding holding the sub result is None
|
||||
// (here: the result is mstored as a uint256), codegen takes the i128 fast-path:
|
||||
// sub at i128 of (0, 1) → 2^128 - 1 (high bit set)
|
||||
// Later widening via ensure_word_type → build_int_z_extend produces
|
||||
// 0x...0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF instead of 2^256 - 1.
|
||||
//
|
||||
// Inline assembly is used so the masks land as IR constants (Solidity's
|
||||
// non-asm path wraps integer literals through a `convert_rational_*` function,
|
||||
// which defeats the structural narrowness proof).
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": true,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {
|
||||
"code": { "Solidity": { "contract": "SubUnderflowZext", "solc_optimizer": false } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "0aa7a67700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
contract SubUnderflowZext {
|
||||
function sub_underflow(uint256 a, uint256 b) external pure returns (uint256 r) {
|
||||
assembly {
|
||||
let x := and(a, 0xff)
|
||||
let y := and(b, 0xff)
|
||||
r := sub(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
object "SwitchCvCseDangling" {
|
||||
code { let s := datasize("SwitchCvCseDangling_deployed") codecopy(0, dataoffset("SwitchCvCseDangling_deployed"), s) return(0, s) }
|
||||
object "SwitchCvCseDangling_deployed" {
|
||||
code {
|
||||
let sel := calldataload(0)
|
||||
// Every branch (cases + default) starts with an equivalent non-payable callvalue check, so the
|
||||
// check is fully hoisted above the switch and dropped from each branch. The cases also read
|
||||
// `callvalue()` a SECOND time; environment CSE rewrites that read to reuse the first binding.
|
||||
// Hoisting must redirect that reuse to the hoisted binding, otherwise it references the
|
||||
// dropped definition and trips the SSA validator.
|
||||
switch sel
|
||||
case 1 { let cv := callvalue() if cv { revert(0, 0) } let x := callvalue() sstore(0, add(x, 0x100)) }
|
||||
case 2 { let cv := callvalue() if cv { revert(0, 0) } let y := callvalue() sstore(0, add(y, 0x200)) }
|
||||
default { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0xdd) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
object "SwitchCvFallthrough" {
|
||||
code { let s := datasize("SwitchCvFallthrough_deployed") codecopy(0, dataoffset("SwitchCvFallthrough_deployed"), s) return(0, s) }
|
||||
object "SwitchCvFallthrough_deployed" {
|
||||
code {
|
||||
let sel := calldataload(0)
|
||||
switch sel
|
||||
// Each selector is non-payable: it reverts if value was sent.
|
||||
case 1 { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0x11) return(0, 0) }
|
||||
case 2 { let cv := callvalue() if cv { revert(0, 0) } sstore(0, 0x22) return(0, 0) }
|
||||
// There is NO default. A no-match selector must fall through to here. Hoisting the cases'
|
||||
// callvalue check above the switch would make this fall-through path revert on a nonzero
|
||||
// value, even though it never had a callvalue check of its own.
|
||||
sstore(0, 0xff)
|
||||
return(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/// Force scrutinee to i64 via an explicit 64-bit mask.
|
||||
object "SwitchWideLabel" {
|
||||
code { datacopy(0, dataoffset("SwitchWideLabel_deployed"), datasize("SwitchWideLabel_deployed")) return(0, datasize("SwitchWideLabel_deployed")) }
|
||||
object "SwitchWideLabel_deployed" {
|
||||
code {
|
||||
let x := and(calldataload(0), 0xffffffffffffffff)
|
||||
switch x
|
||||
case 0 { mstore(0, 0xAA) return(0, 32) }
|
||||
case 0x10000000000000000 { mstore(0, 0xBB) return(0, 32) }
|
||||
default { mstore(0, 0xCC) return(0, 32) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity >=0.8.0;
|
||||
|
||||
/// Regression reproducer for a newyork SSA-validation ICE that surfaced only with
|
||||
/// the solc optimizer disabled (`--disable-solc-optimizer`).
|
||||
///
|
||||
/// `try ... catch { return <constant>; }` lowers (with the solc optimizer off) to a
|
||||
/// switch whose default region is `let r := if (true) { ...; leave } else { ... }`
|
||||
/// followed by `yield r`. The inliner's leave-elimination produces the
|
||||
/// constant-condition `if` with an output; the simplifier folds it, appending the
|
||||
/// output binding after the branch's `leave`. The dead-code pass then truncated
|
||||
/// everything after that terminator — including the binding the `yield` still
|
||||
/// referenced — leaving `yield r` dangling ("value used before definition").
|
||||
///
|
||||
/// Both switch arms `leave`, so the yielded fall-through value is provably never
|
||||
/// observed; the fix zero-binds the rescued value before the terminator so SSA
|
||||
/// stays valid. Instantiated with the solc optimizer disabled to hit the path.
|
||||
contract TryCatchCatchReturn {
|
||||
function boom() external pure returns (uint256) {
|
||||
revert("boom");
|
||||
}
|
||||
|
||||
function run() public returns (uint256) {
|
||||
try this.boom() returns (uint256 value) {
|
||||
return value;
|
||||
} catch {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
object "TstoreProbe" {
|
||||
code { datacopy(0, dataoffset("TstoreProbe_deployed"), datasize("TstoreProbe_deployed")) return(0, datasize("TstoreProbe_deployed")) }
|
||||
object "TstoreProbe_deployed" {
|
||||
code {
|
||||
let a := calldataload(0)
|
||||
let b := calldataload(32)
|
||||
let r := 0
|
||||
tstore(a, b)
|
||||
tstore(add(a, 1), not(b))
|
||||
r := xor(r, tload(a))
|
||||
r := xor(r, tload(add(a, 1)))
|
||||
r := xor(r, tload(add(a, 2)))
|
||||
tstore(a, and(b, 0xFF))
|
||||
r := add(r, tload(a))
|
||||
mstore(0, r)
|
||||
return(0, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8;
|
||||
|
||||
// Regression test: arithmetic with uint128 types.
|
||||
// Bug: newyork pipeline produces wrong results for uint128 arithmetic with
|
||||
// values that trigger overflow checking in Solidity 0.8+.
|
||||
|
||||
/* runner.json
|
||||
{
|
||||
"differential": true,
|
||||
"actions": [
|
||||
{
|
||||
"Instantiate": {
|
||||
"code": { "Solidity": { "contract": "Uint128Arithmetic", "solc_optimizer": false } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "ba517410000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000100000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"dest": { "Instantiated": 0 },
|
||||
"data": "ba51741000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001f"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
contract Uint128Arithmetic {
|
||||
uint128 constant EPS = 1E10;
|
||||
uint128 constant PRECISION = 100;
|
||||
uint128 constant MAX_U128_SQRT = 18446744073709551615;
|
||||
|
||||
function entry(uint128 a, uint128 mb, uint128 c) public pure returns (bool, uint128, uint128) {
|
||||
(bool p, uint128 x1, uint128 x2) = main(a, mb, c);
|
||||
x1 *= PRECISION;
|
||||
x1 /= EPS;
|
||||
x2 *= PRECISION;
|
||||
x2 /= EPS;
|
||||
return (p, x1, x2);
|
||||
}
|
||||
|
||||
function sqrt(uint128 n) private pure returns (uint128) {
|
||||
uint128 l = 0;
|
||||
uint128 r = MAX_U128_SQRT;
|
||||
while (l < r) {
|
||||
uint128 m = (l + r + 1) / 2;
|
||||
if (m * m <= n) {
|
||||
l = m;
|
||||
} else {
|
||||
r = m - 1;
|
||||
}
|
||||
}
|
||||
if (n - l * l < (l + 1) * (l + 1) - n) {
|
||||
return l;
|
||||
} else {
|
||||
return l + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function main(uint128 a, uint128 mb, uint128 c) private pure returns (bool, uint128, uint128) {
|
||||
if (mb * mb < 4 * a * c) {
|
||||
return (false, 0, 0);
|
||||
}
|
||||
uint128 d = (mb * mb - 4 * a * c) * EPS * EPS;
|
||||
uint128 sd = sqrt(d);
|
||||
uint128 x1 = (mb * EPS + sd) / 2 / a;
|
||||
uint128 x2 = (mb * EPS - sd) / 2 / a;
|
||||
return (true, x1, x2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `mem_opt::optimize_statements` MStore8 handler only
|
||||
/// invalidates `memory_state[word_offset(offset)]`. An unaligned
|
||||
/// `mstore(0x70, AA…)` leaves a tracked entry at word 0x60 with
|
||||
/// `tracked.offset = 0x70`, whose 32-byte write range covers byte
|
||||
/// 0x80. A later `mstore8(0x80, 0xCC)` overwrites that byte but only
|
||||
/// removes `memory_state[0x80]`, not the overlapping entry at
|
||||
/// `memory_state[0x60]`. A subsequent `mload(0x70)` matches the
|
||||
/// stale tracked entry by exact-offset comparison and is forwarded
|
||||
/// to the pre-overwrite value, dropping the single-byte overwrite.
|
||||
/// EVM observes byte 0x80 = `0xCC` in the actual memory read.
|
||||
contract UnalignedMStore8Bug {
|
||||
function bug() external pure returns (bytes32 r) {
|
||||
assembly {
|
||||
mstore(0x70, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
|
||||
mstore8(0x80, 0xcc)
|
||||
r := mload(0x70)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8;
|
||||
|
||||
/// Soundness PoC: `mem_opt.rs` only updates `memory_state[word_offset]` when
|
||||
/// recording an mstore, where `word_offset = static_offset / 32 * 32`. An
|
||||
/// *unaligned* `mstore(0x70, v)` writes 32 bytes from byte 0x70 to 0x90, so
|
||||
/// it overwrites the lower half of the word starting at 0x80. mem_opt only
|
||||
/// touches `memory_state[0x60]` and leaves `memory_state[0x80]` intact.
|
||||
///
|
||||
/// A subsequent `mload(0x80)` finds the still-cached tracked entry at 0x80
|
||||
/// (offset == 0x80, no aliasing check across overlap with 0x70) and is
|
||||
/// forwarded to the *pre-overwrite* stored value. EVM reads the actual
|
||||
/// memory which contains the lower half of the unaligned store plus the
|
||||
/// untouched upper half of the original.
|
||||
contract UnalignedMStoreBug {
|
||||
function bug() external pure returns (bytes32 r) {
|
||||
assembly {
|
||||
// 32 byte sentinel at the aligned word 0x80.
|
||||
mstore(0x80, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
|
||||
// Unaligned write: 16 bytes of 0xbb at 0x70..0x80 and 16 bytes of
|
||||
// 0xcc at 0x80..0x90 (overwriting the lower half of the sentinel).
|
||||
mstore(0x70, 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc)
|
||||
// mem_opt forwards the stale sentinel, missing the partial overwrite.
|
||||
r := mload(0x80)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,12 +179,222 @@ sol!(
|
||||
function mod(uint n, uint d) public pure returns (uint r);
|
||||
|
||||
function smod(int n, int d) public pure returns (int r);
|
||||
|
||||
function divSelf(uint256 x) external pure returns (uint256 r);
|
||||
function sdivSelf(int256 x) external pure returns (int256 r);
|
||||
function modSelf(uint256 x) external pure returns (uint256 r);
|
||||
}
|
||||
);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, divCall, division_arithmetics_div, n: U256, d: U256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, sdivCall, division_arithmetics_sdiv, n: I256, d: I256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, modCall, division_arithmetics_mod, n: U256, d: U256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, smodCall, division_arithmetics_smod, n: I256, d: I256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, divSelfCall, division_arithmetics_div_self, x: U256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, sdivSelfCall, division_arithmetics_sdiv_self, x: I256);
|
||||
case!("DivisionArithmetics.sol", DivisionArithmetics, modSelfCall, division_arithmetics_mod_self, x: U256);
|
||||
|
||||
sol!(
|
||||
contract SDivNarrowBug {
|
||||
function sdiv_masked(uint256 a, uint256 b) external pure returns (int256 q);
|
||||
}
|
||||
);
|
||||
case!("SDivNarrowBug.sol", SDivNarrowBug, sdiv_maskedCall, sdiv_narrow_bug_masked, a: U256, b: U256);
|
||||
|
||||
sol!(
|
||||
contract KeccakFuseBug {
|
||||
function probe(uint256[8] calldata seeds) external view returns (uint256 r, bytes32 sink_out);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"KeccakFuseBug.sol",
|
||||
KeccakFuseBug,
|
||||
probeCall,
|
||||
keccak_fuse_bug_probe,
|
||||
seeds: [U256; 8]
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract ParamMload {
|
||||
function tryFetch(uint256 x) external pure returns (uint256);
|
||||
}
|
||||
);
|
||||
case!("ParamMload.sol", ParamMload, tryFetchCall, param_mload_try_fetch, x: U256);
|
||||
|
||||
// `PanicCodeBug` has only a `fallback()` — invoked by sending raw
|
||||
// calldata that doesn't match any function selector. Empty calldata
|
||||
// suffices.
|
||||
case!(
|
||||
"PanicCodeBug.sol",
|
||||
"PanicCodeBug",
|
||||
vec![],
|
||||
panic_code_bug_trigger
|
||||
);
|
||||
|
||||
// `FmpDynStoreBug` is invoked via empty fallback, passing 0x40 as the
|
||||
// only calldata word so the inline-asm `calldataload(0)` resolves to
|
||||
// 0x40 at runtime but stays opaque to the simplifier.
|
||||
case!(
|
||||
"FmpDynStoreBug.sol",
|
||||
"FmpDynStoreBug",
|
||||
{
|
||||
let mut bytes = vec![0u8; 32];
|
||||
bytes[31] = 0x40;
|
||||
bytes
|
||||
},
|
||||
fmp_dyn_store_bug
|
||||
);
|
||||
|
||||
// `FmpRangeProofBug` reads the 32-byte calldata as the value to put
|
||||
// into the FMP slot. Use `0x100000007 = 2^32 + 7` (well beyond the
|
||||
// 17-bit heap-size range) to make the range-proof truncation visible.
|
||||
case!(
|
||||
"FmpRangeProofBug.sol",
|
||||
"FmpRangeProofBug",
|
||||
{
|
||||
let mut bytes = vec![0u8; 32];
|
||||
bytes[31] = 0x07;
|
||||
bytes[27] = 0x01;
|
||||
bytes
|
||||
},
|
||||
fmp_range_proof_bug
|
||||
);
|
||||
|
||||
// `FmpNativeStoreBug` calldata: first word = FMP value `0x100000007`,
|
||||
// second word = non-zero condition to force the if-branch.
|
||||
case!(
|
||||
"FmpNativeStoreBug.sol",
|
||||
"FmpNativeStoreBug",
|
||||
{
|
||||
let mut bytes = vec![0u8; 64];
|
||||
bytes[31] = 0x07;
|
||||
bytes[27] = 0x01;
|
||||
bytes[63] = 0x01;
|
||||
bytes
|
||||
},
|
||||
fmp_native_store_bug
|
||||
);
|
||||
|
||||
// `FmpCrossObjectBug` calldata: first word = FMP value `0x100000007`,
|
||||
// second word = recursion depth (0 to hit the inner inline-asm branch).
|
||||
case!(
|
||||
"FmpCrossObjectBug.sol",
|
||||
"FmpCrossObjectBug",
|
||||
{
|
||||
let mut bytes = vec![0u8; 64];
|
||||
bytes[31] = 0x07;
|
||||
bytes[27] = 0x01;
|
||||
bytes
|
||||
},
|
||||
fmp_cross_object_bug
|
||||
);
|
||||
|
||||
// `FmpRevertBug` is invoked via empty calldata into its `fallback()`.
|
||||
case!("FmpRevertBug.sol", "FmpRevertBug", vec![], fmp_revert_bug);
|
||||
|
||||
// `FmpDynRevertBug` reads `offset` and `length` from calldata: pass
|
||||
// 64 bytes encoding `offset=0`, `length=96` so the dynamic revert
|
||||
// covers the FMP slot.
|
||||
case!(
|
||||
"FmpDynRevertBug.sol",
|
||||
"FmpDynRevertBug",
|
||||
{
|
||||
let mut bytes = vec![0u8; 64];
|
||||
bytes[63] = 96;
|
||||
bytes
|
||||
},
|
||||
fmp_dyn_revert_bug
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract UnalignedMStoreBug {
|
||||
function bug() external pure returns (bytes32);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"UnalignedMStoreBug.sol",
|
||||
UnalignedMStoreBug,
|
||||
bugCall,
|
||||
unaligned_mstore_bug,
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract ConstReturnOverflowBug {
|
||||
function bug() external pure returns (uint256);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"ConstReturnOverflowBug.sol",
|
||||
ConstReturnOverflowBug,
|
||||
bugCall,
|
||||
const_return_overflow_bug,
|
||||
);
|
||||
|
||||
// `PanicInterveneBug` has only a `fallback()` — invoked by sending
|
||||
// raw calldata that doesn't match any selector. Empty calldata works.
|
||||
case!(
|
||||
"PanicInterveneBug.sol",
|
||||
"PanicInterveneBug",
|
||||
vec![],
|
||||
panic_intervene_bug
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract UnalignedMStore8Bug {
|
||||
function bug() external pure returns (bytes32);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"UnalignedMStore8Bug.sol",
|
||||
UnalignedMStore8Bug,
|
||||
bugCall,
|
||||
unaligned_mstore8_bug,
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract CopyOverlapBug {
|
||||
function bug(uint256 length) external pure returns (bytes32);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"CopyOverlapBug.sol",
|
||||
CopyOverlapBug,
|
||||
bugCall,
|
||||
copy_overlap_bug,
|
||||
length: U256
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract CallerOriginAliasing {
|
||||
function caller_then_origin() external view returns (address, address);
|
||||
function origin_then_caller() external view returns (address, address);
|
||||
function caller_address_origin() external view returns (address, address, address);
|
||||
function repeated_caller() external view returns (address, address, address);
|
||||
}
|
||||
);
|
||||
case!(
|
||||
"CallerOriginAliasing.sol",
|
||||
CallerOriginAliasing,
|
||||
caller_then_originCall,
|
||||
caller_origin_aliasing_caller_then_origin,
|
||||
);
|
||||
case!(
|
||||
"CallerOriginAliasing.sol",
|
||||
CallerOriginAliasing,
|
||||
origin_then_callerCall,
|
||||
caller_origin_aliasing_origin_then_caller,
|
||||
);
|
||||
case!(
|
||||
"CallerOriginAliasing.sol",
|
||||
CallerOriginAliasing,
|
||||
caller_address_originCall,
|
||||
caller_origin_aliasing_caller_address_origin,
|
||||
);
|
||||
case!(
|
||||
"CallerOriginAliasing.sol",
|
||||
CallerOriginAliasing,
|
||||
repeated_callerCall,
|
||||
caller_origin_aliasing_repeated_caller,
|
||||
);
|
||||
|
||||
sol!(
|
||||
contract DivConst {
|
||||
@@ -293,6 +503,8 @@ sol!(
|
||||
contract ExtCode {
|
||||
function ExtCodeSize(address who) public view returns (uint ret);
|
||||
|
||||
function ExtCodeSizeSum(address a, address b) public view returns (uint ret);
|
||||
|
||||
function CodeSize() public pure returns (uint ret);
|
||||
|
||||
function ExtCodeHash(address who) public view returns (bytes32 ret);
|
||||
@@ -301,6 +513,7 @@ sol!(
|
||||
}
|
||||
);
|
||||
case!("ExtCode.sol", ExtCode, ExtCodeSizeCall, ext_code_size, address: Address);
|
||||
case!("ExtCode.sol", ExtCode, ExtCodeSizeSumCall, ext_code_size_sum, a: Address, b: Address);
|
||||
case!("ExtCode.sol", ExtCode, CodeSizeCall, code_size,);
|
||||
case!("ExtCode.sol", ExtCode, ExtCodeHashCall, ext_code_hash, address: Address);
|
||||
case!("ExtCode.sol", ExtCode, CodeHashCall, code_hash,);
|
||||
@@ -388,7 +601,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn codesize() {
|
||||
let path = "codesize.json";
|
||||
let path = if cfg!(feature = "newyork") {
|
||||
"codesize_newyork.json"
|
||||
} else {
|
||||
"codesize.json"
|
||||
};
|
||||
|
||||
let existing = File::open(path)
|
||||
.map(|file| {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,8 @@
|
||||
//! The debug IR type.
|
||||
|
||||
use revive_common::{
|
||||
EXTENSION_LLVM_SOURCE, EXTENSION_OBJECT, EXTENSION_POLKAVM_ASSEMBLY, EXTENSION_YUL,
|
||||
EXTENSION_LLVM_SOURCE, EXTENSION_NEWYORK, EXTENSION_OBJECT, EXTENSION_POLKAVM_ASSEMBLY,
|
||||
EXTENSION_YUL,
|
||||
};
|
||||
|
||||
/// The debug IR type.
|
||||
@@ -16,6 +17,8 @@ pub enum IRType {
|
||||
Assembly,
|
||||
/// Whether to dump the ELF shared object
|
||||
Object,
|
||||
/// Whether to dump the newyork IR.
|
||||
NewYork,
|
||||
}
|
||||
|
||||
impl IRType {
|
||||
@@ -26,6 +29,7 @@ impl IRType {
|
||||
Self::LLVM => EXTENSION_LLVM_SOURCE,
|
||||
Self::Assembly => EXTENSION_POLKAVM_ASSEMBLY,
|
||||
Self::Object => EXTENSION_OBJECT,
|
||||
Self::NewYork => EXTENSION_NEWYORK,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,29 @@ impl DebugConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dumps a newyork IR debug artifact for the current contract.
|
||||
///
|
||||
/// The file name is derived from the contract path (set via [`Self::set_contract_path`]) so it
|
||||
/// matches the other dumps and maps back to the source. `suffix` distinguishes artifacts
|
||||
/// (`"snapshot"`, `"heap"`, `"mem"`); `None` is the final optimized IR. A no-op when no output
|
||||
/// directory or contract path is configured.
|
||||
pub fn dump_newyork(&self, suffix: Option<&str>, text: &str) -> anyhow::Result<()> {
|
||||
if let Some(contract_path) = self.contract_path.as_ref() {
|
||||
let stem = contract_path
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or_default();
|
||||
let extension = IRType::NewYork.file_extension();
|
||||
let file_name = match suffix {
|
||||
Some(suffix) => format!("{stem}.{suffix}.{extension}"),
|
||||
None => format!("{stem}.{extension}"),
|
||||
};
|
||||
std::fs::write(contract_path.with_file_name(file_name), text)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a full file name, given the contract full path, suffix, and extension.
|
||||
fn full_file_name(contract_path: &str, suffix: Option<&str>, ir_type: IRType) -> String {
|
||||
let mut full_file_name = contract_path.replace('/', "_").replace(':', ".");
|
||||
|
||||
@@ -14,6 +14,7 @@ pub use self::polkavm::build as polkavm_build;
|
||||
pub use self::polkavm::context::address_space::AddressSpace as PolkaVMAddressSpace;
|
||||
pub use self::polkavm::context::argument::Argument as PolkaVMArgument;
|
||||
pub use self::polkavm::context::attribute::Attribute as PolkaVMAttribute;
|
||||
pub use self::polkavm::context::attribute::MemoryEffect as PolkaVMMemoryEffect;
|
||||
pub use self::polkavm::context::build::Build as PolkaVMBuild;
|
||||
pub use self::polkavm::context::code_type::CodeType as PolkaVMCodeType;
|
||||
pub use self::polkavm::context::debug_info::DebugInfo;
|
||||
@@ -27,7 +28,14 @@ pub use self::polkavm::context::function::runtime::arithmetics::SignedDivision a
|
||||
pub use self::polkavm::context::function::runtime::arithmetics::SignedRemainder as PolkaVMSignedRemainderFunction;
|
||||
pub use self::polkavm::context::function::runtime::deploy_code::DeployCode as PolkaVMDeployCodeFunction;
|
||||
pub use self::polkavm::context::function::runtime::entry::Entry as PolkaVMEntryFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::CallDataLoad as PolkaVMCallDataLoadFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::CallValue as PolkaVMCallValueFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::CallValueNonzero as PolkaVMCallValueNonzeroFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::Caller as PolkaVMCallerFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::Exit as PolkaVMExitFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::Revert as PolkaVMRevertFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::RevertEmpty as PolkaVMRevertEmptyFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::RevertPanic as PolkaVMRevertPanicFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::WordToPointer as PolkaVMWordToPointerFunction;
|
||||
pub use self::polkavm::context::function::runtime::runtime_code::RuntimeCode as PolkaVMRuntimeCodeFunction;
|
||||
pub use self::polkavm::context::function::runtime::sbrk::Sbrk as PolkaVMSbrkFunction;
|
||||
@@ -37,8 +45,12 @@ pub use self::polkavm::context::function::runtime::FUNCTION_RUNTIME_CODE as Polk
|
||||
pub use self::polkavm::context::function::yul_data::YulData as PolkaVMFunctionYulData;
|
||||
pub use self::polkavm::context::function::Function as PolkaVMFunction;
|
||||
pub use self::polkavm::context::global::Global as PolkaVMGlobal;
|
||||
pub use self::polkavm::context::pointer::heap::Keccak256OneWord as PolkaVMKeccak256OneWordFunction;
|
||||
pub use self::polkavm::context::pointer::heap::Keccak256TwoWords as PolkaVMKeccak256TwoWordsFunction;
|
||||
pub use self::polkavm::context::pointer::heap::LoadWord as PolkaVMLoadHeapWordFunction;
|
||||
pub use self::polkavm::context::pointer::heap::LoadWordNative as PolkaVMLoadHeapWordNativeFunction;
|
||||
pub use self::polkavm::context::pointer::heap::StoreWord as PolkaVMStoreHeapWordFunction;
|
||||
pub use self::polkavm::context::pointer::heap::StoreWordNative as PolkaVMStoreHeapWordNativeFunction;
|
||||
pub use self::polkavm::context::pointer::storage::LoadTransientWord as PolkaVMLoadTransientStorageWordFunction;
|
||||
pub use self::polkavm::context::pointer::storage::LoadWord as PolkaVMLoadStorageWordFunction;
|
||||
pub use self::polkavm::context::pointer::storage::StoreTransientWord as PolkaVMStoreTransientStorageWordFunction;
|
||||
@@ -84,19 +96,62 @@ pub(crate) mod target_machine;
|
||||
|
||||
static DID_INITIALIZE: OnceLock<()> = OnceLock::new();
|
||||
|
||||
/// The global LLVM options applied at [`OptimizerSettingsSizeLevel::Z`] **on the newyork path**.
|
||||
///
|
||||
/// These trade execution speed for code size: the machine outliner replaces repeated
|
||||
/// instruction sequences with calls, disabling LICM avoids i256 register pressure on the
|
||||
/// PVM target (rv64e) where hoisting causes excessive stack spills, and `ext-tsp-for-size`
|
||||
/// optimizes block layout for size instead of speed. At any other optimization level the
|
||||
/// upstream LLVM defaults apply: paying execution gas for smaller code is only acceptable
|
||||
/// when the user explicitly asked for `-Oz`.
|
||||
///
|
||||
/// They are applied only for modules produced by the newyork IR generator. Like the
|
||||
/// aggressive code-size pass pipeline in [`crate::optimizer::Optimizer::run`] and the
|
||||
/// `+unaligned-scalar-mem` target feature, these knobs are tuned and validated against
|
||||
/// newyork's outlined code; the stock Yul path keeps the upstream LLVM defaults it was
|
||||
/// validated against (matching `main`).
|
||||
const SIZE_LEVEL_Z_ARGUMENTS: &[&str] = &[
|
||||
"--disable-licm-promotion",
|
||||
"--disable-machine-licm",
|
||||
"--enable-machine-outliner",
|
||||
"--machine-outliner-reruns=2",
|
||||
"--disable-early-taildup=true",
|
||||
"--apply-ext-tsp-for-size=true",
|
||||
"--attributor-allow-deep-wrappers=true",
|
||||
"--hoist-common-insts=true",
|
||||
];
|
||||
|
||||
/// Initializes the LLVM compiler backend.
|
||||
///
|
||||
/// This is a no-op if called subsequentially.
|
||||
///
|
||||
/// `llvm_arguments` are passed as-is to the LLVM CL options parser.
|
||||
pub fn initialize_llvm(target: PolkaVMTarget, name: &str, llvm_arguments: &[String]) {
|
||||
///
|
||||
/// When `use_newyork` is set and the size level is [`OptimizerSettingsSizeLevel::Z`], the
|
||||
/// `SIZE_LEVEL_Z_ARGUMENTS` are prepended. They are gated on the newyork path because they
|
||||
/// are validated against its codegen; the stock Yul path must keep the upstream defaults.
|
||||
/// LLVM parses command line options once per process, so the first caller wins; production
|
||||
/// builds are unaffected because every contract is compiled in its own recursive process
|
||||
/// with its own optimizer settings and IR generator.
|
||||
pub fn initialize_llvm(
|
||||
target: PolkaVMTarget,
|
||||
name: &str,
|
||||
size_level: OptimizerSettingsSizeLevel,
|
||||
use_newyork: bool,
|
||||
llvm_arguments: &[String],
|
||||
) {
|
||||
let Ok(_) = DID_INITIALIZE.set(()) else {
|
||||
return; // Tests don't go through a recursive process
|
||||
};
|
||||
|
||||
let argv = [name.to_string()]
|
||||
.iter()
|
||||
.chain(llvm_arguments)
|
||||
let size_arguments = if use_newyork && size_level == OptimizerSettingsSizeLevel::Z {
|
||||
SIZE_LEVEL_Z_ARGUMENTS
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let argv = std::iter::once(name)
|
||||
.chain(size_arguments.iter().copied())
|
||||
.chain(llvm_arguments.iter().map(String::as_str))
|
||||
.map(|arg| CString::new(arg.as_bytes()).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let argv: Vec<*const libc::c_char> = argv.iter().map(|arg| arg.as_ptr()).collect();
|
||||
|
||||
@@ -14,24 +14,73 @@ pub mod settings;
|
||||
pub struct Optimizer {
|
||||
/// The optimizer settings.
|
||||
settings: Settings,
|
||||
/// Whether the module being optimized was produced by the newyork IR
|
||||
/// generator. The aggressive code-size pipeline is specific to newyork's
|
||||
/// outlined code; the stock Yul path must not run it (see [`Optimizer::run`]).
|
||||
#[serde(skip)]
|
||||
newyork: bool,
|
||||
}
|
||||
|
||||
impl Optimizer {
|
||||
/// A shortcut constructor.
|
||||
pub fn new(settings: Settings) -> Self {
|
||||
Self { settings }
|
||||
Self {
|
||||
settings,
|
||||
newyork: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks this optimizer as operating on a module produced by the newyork IR
|
||||
/// generator, enabling the aggressive code-size pipeline in [`Optimizer::run`].
|
||||
pub fn enable_newyork_pipeline(&mut self) {
|
||||
self.newyork = true;
|
||||
}
|
||||
|
||||
/// Whether the module being optimized was produced by the newyork IR generator.
|
||||
/// Gates the two pieces of newyork-specific codegen that the stock Yul path must not use:
|
||||
/// the aggressive code-size pipeline in [`Optimizer::run`] and the `+unaligned-scalar-mem`
|
||||
/// target feature in [`crate::target_machine::TargetMachine`].
|
||||
pub fn is_newyork(&self) -> bool {
|
||||
self.newyork
|
||||
}
|
||||
|
||||
/// Runs the new pass manager.
|
||||
///
|
||||
/// For modules produced by the **newyork** IR generator a code-size pipeline runs:
|
||||
///
|
||||
/// 1. `default<O{level}>`: initial optimization at the configured size level.
|
||||
/// 2. `ipsccp,deadargelim`: propagate inter-function constants through outlined helpers and
|
||||
/// remove now-constant arguments.
|
||||
/// 3. `attributor`: infer function attributes the next stage can exploit.
|
||||
/// 4. `default<O1>`: re-optimize with newly discovered constants and attributes.
|
||||
///
|
||||
/// `mergefunc` runs before stage 1, after stage 1, and after stage 4 to deduplicate functions
|
||||
/// that become identical as the surrounding passes canonicalize them.
|
||||
/// The second `default` pass is hard-coded to `O1` rather than the configured size level: an
|
||||
/// `Oz` second pass reproducibly changes the post-link PVM blob for some contracts in a way
|
||||
/// that fails pallet-revive deployment ("Code upload failed: Timeout"). The exact pass has not
|
||||
/// been isolated, so `O1` is kept as the empirically safe choice.
|
||||
///
|
||||
/// This pipeline is specific to newyork's outlined code. The stock Yul path uses a plain
|
||||
/// `default<O{level}>`, matching upstream: its IR does not benefit from the extra inter-
|
||||
/// procedural passes, and `attributor`'s inference is unsound on it (the `attributor`+`O1`
|
||||
/// combination miscompiles, e.g. `complex/array_one_element` returns the callee selector
|
||||
/// instead of the external-call result). Gating on the IR generator keeps each path on the
|
||||
/// pipeline it was validated against.
|
||||
pub fn run(
|
||||
&self,
|
||||
target_machine: &TargetMachine,
|
||||
module: &inkwell::module::Module,
|
||||
) -> Result<(), inkwell::support::LLVMString> {
|
||||
target_machine.run_optimization_passes(
|
||||
module,
|
||||
format!("default<O{}>", self.settings.middle_end_as_string()).as_str(),
|
||||
)
|
||||
let optimization_level = self.settings.middle_end_as_string();
|
||||
let pass_pipeline = if self.newyork {
|
||||
format!(
|
||||
"mergefunc,default<O{optimization_level}>,mergefunc,ipsccp,deadargelim,attributor,default<O1>,mergefunc"
|
||||
)
|
||||
} else {
|
||||
format!("default<O{optimization_level}>")
|
||||
};
|
||||
target_machine.run_optimization_passes(module, &pass_pipeline)
|
||||
}
|
||||
|
||||
/// Returns the optimizer settings reference.
|
||||
|
||||
@@ -53,6 +53,11 @@ impl<'ctx> Argument<'ctx> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns true if the argument holds an LLVM register value (not a pointer).
|
||||
pub fn is_register(&self) -> bool {
|
||||
matches!(&self.value, Value::Register(_))
|
||||
}
|
||||
|
||||
/// Returns the inner LLVM value.
|
||||
///
|
||||
/// Panics if `self` is a pointer argument.
|
||||
|
||||
@@ -121,6 +121,147 @@ pub enum Attribute {
|
||||
//LastConstantRangeListAttr = 101,
|
||||
}
|
||||
|
||||
/// Rust mirror of LLVM's `llvm::ModRefInfo`.
|
||||
///
|
||||
/// Discriminants are the C++ enum values verbatim from
|
||||
/// `llvm/include/llvm/Support/ModRef.h` lines 28-38 of the LLVM 21.1.8
|
||||
/// source tree (`LLVM_SYS_211_PREFIX`):
|
||||
///
|
||||
/// ```text
|
||||
/// enum class ModRefInfo : uint8_t {
|
||||
/// NoModRef = 0,
|
||||
/// Ref = 1,
|
||||
/// Mod = 2,
|
||||
/// ModRef = Ref | Mod,
|
||||
/// };
|
||||
/// ```
|
||||
#[allow(dead_code)] // ModRef mirror — every variant kept for completeness.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
enum ModRefInfo {
|
||||
NoModRef = 0,
|
||||
Ref = 1,
|
||||
Mod = 2,
|
||||
ModRef = 3,
|
||||
}
|
||||
|
||||
/// Rust mirror of LLVM's `llvm::IRMemLocation`.
|
||||
///
|
||||
/// Discriminants are the C++ enum values verbatim from
|
||||
/// `llvm/include/llvm/Support/ModRef.h` lines 60-73 of the LLVM 21.1.8
|
||||
/// source tree:
|
||||
///
|
||||
/// ```text
|
||||
/// enum class IRMemLocation {
|
||||
/// ArgMem = 0,
|
||||
/// InaccessibleMem = 1,
|
||||
/// ErrnoMem = 2,
|
||||
/// Other = 3,
|
||||
/// ...
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
/// The discriminant also serves as the location's index into the packed
|
||||
/// `memory(...)` payload — see the `memory_location_pos` helper.
|
||||
#[allow(dead_code)] // LLVM location mirror — every variant kept for completeness.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
enum MemoryLocation {
|
||||
ArgMem = 0,
|
||||
InaccessibleMem = 1,
|
||||
ErrnoMem = 2,
|
||||
Other = 3,
|
||||
}
|
||||
|
||||
/// Mirror of `llvm::MemoryEffectsBase::BitsPerLoc` — see
|
||||
/// `llvm/include/llvm/Support/ModRef.h:82` of LLVM 21.1.8:
|
||||
///
|
||||
/// ```text
|
||||
/// static constexpr uint32_t BitsPerLoc = 2;
|
||||
/// ```
|
||||
const MEMORY_EFFECT_BITS_PER_LOCATION: u64 = 2;
|
||||
|
||||
/// Mirror of `llvm::MemoryEffectsBase::getLocationPos` — see
|
||||
/// `llvm/include/llvm/Support/ModRef.h:85-87` of LLVM 21.1.8:
|
||||
///
|
||||
/// ```text
|
||||
/// static uint32_t getLocationPos(Location Loc) {
|
||||
/// return (uint32_t)Loc * BitsPerLoc;
|
||||
/// }
|
||||
/// ```
|
||||
const fn memory_location_pos(location: MemoryLocation) -> u64 {
|
||||
(location as u64) * MEMORY_EFFECT_BITS_PER_LOCATION
|
||||
}
|
||||
|
||||
/// Mirror of `llvm::MemoryEffectsBase::setModRef` for a single location on
|
||||
/// an initially-zero payload — see `llvm/include/llvm/Support/ModRef.h:91-94`
|
||||
/// of LLVM 21.1.8:
|
||||
///
|
||||
/// ```text
|
||||
/// void setModRef(Location Loc, ModRefInfo MR) {
|
||||
/// Data &= ~(LocMask << getLocationPos(Loc));
|
||||
/// Data |= static_cast<uint32_t>(MR) << getLocationPos(Loc);
|
||||
/// }
|
||||
/// ```
|
||||
const fn pack_memory_effect(location: MemoryLocation, mod_ref: ModRefInfo) -> u64 {
|
||||
(mod_ref as u64) << memory_location_pos(location)
|
||||
}
|
||||
|
||||
/// Per-location memory effect packed into the integer payload of
|
||||
/// [`Attribute::Memory`]. Every variant's encoding is derived through the
|
||||
/// crate-private `pack_memory_effect` helper from `MemoryLocation` /
|
||||
/// `ModRefInfo`, which are Rust mirrors of LLVM 21 enums with discriminants
|
||||
/// matching C++. No raw integer literals appear in [`MemoryEffect::encoding`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MemoryEffect {
|
||||
/// Do not attach a `memory(...)` attribute to the function.
|
||||
Unrestricted,
|
||||
/// `memory(none)` — function has no observable memory effect.
|
||||
None,
|
||||
/// `memory(inaccessiblemem: read)` — the function only reads pallet-revive
|
||||
/// runtime state (storage, call context, etc.) reached via host syscalls.
|
||||
/// LLVM sees no pointer effects from the caller's perspective.
|
||||
ReadInaccessible,
|
||||
/// `memory(argmem: read, inaccessiblemem: read)` — the function reads from
|
||||
/// a pointer argument **and** from pallet-revive runtime state. Used by
|
||||
/// helpers that dereference a key buffer and forward into a host syscall.
|
||||
ReadArgAndInaccessible,
|
||||
/// `memory(inaccessiblemem: write)` — the only externally visible effect
|
||||
/// is a write into pallet-revive runtime state. Heap loads remain CSE-able
|
||||
/// across calls to such a helper.
|
||||
WriteInaccessible,
|
||||
/// `memory(other: read)` — function reads from regular heap memory only.
|
||||
/// Pointer arguments, runtime state and errno are all untouched, so the
|
||||
/// helper is invalidated by any heap store rather than by sstore wrappers.
|
||||
ReadOther,
|
||||
}
|
||||
|
||||
impl MemoryEffect {
|
||||
/// LLVM's packed integer encoding of this effect, or `None` when no
|
||||
/// `memory(...)` attribute should be applied. See the crate-private
|
||||
/// `pack_memory_effect` helper above for the LLVM 21 source the
|
||||
/// encoding mirrors.
|
||||
pub const fn encoding(self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Unrestricted => None,
|
||||
Self::None => Some(0),
|
||||
Self::ReadInaccessible => Some(pack_memory_effect(
|
||||
MemoryLocation::InaccessibleMem,
|
||||
ModRefInfo::Ref,
|
||||
)),
|
||||
Self::ReadArgAndInaccessible => Some(
|
||||
pack_memory_effect(MemoryLocation::ArgMem, ModRefInfo::Ref)
|
||||
| pack_memory_effect(MemoryLocation::InaccessibleMem, ModRefInfo::Ref),
|
||||
),
|
||||
Self::WriteInaccessible => Some(pack_memory_effect(
|
||||
MemoryLocation::InaccessibleMem,
|
||||
ModRefInfo::Mod,
|
||||
)),
|
||||
Self::ReadOther => Some(pack_memory_effect(MemoryLocation::Other, ModRefInfo::Ref)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Attribute {
|
||||
type Error = String;
|
||||
|
||||
@@ -139,3 +280,27 @@ impl TryFrom<&str> for Attribute {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod memory_effect_tests {
|
||||
use super::*;
|
||||
|
||||
/// Locks the encoding against LLVM 21.1.8's `MemoryEffectsBase` layout.
|
||||
/// Expected integers were cross-checked by compiling probe contracts and
|
||||
/// reading the `memory(...)` strings off the emitted LLVM IR — every
|
||||
/// helper using one of these variants printed the matching attribute
|
||||
/// (e.g. `memory(inaccessiblemem: read)` for `ReadInaccessible`,
|
||||
/// `memory(read, argmem: none, inaccessiblemem: none, errnomem: none)` —
|
||||
/// LLVM 21's verbose form of `memory(other: read)` — for `ReadOther`).
|
||||
/// If LLVM ever reorders `IRMemLocation` (as happened between LLVM 18 and
|
||||
/// 21 when `ErrnoMem` was added), this test fires before codegen does.
|
||||
#[test]
|
||||
fn encoding_matches_llvm_21_layout() {
|
||||
assert_eq!(MemoryEffect::Unrestricted.encoding(), None);
|
||||
assert_eq!(MemoryEffect::None.encoding(), Some(0));
|
||||
assert_eq!(MemoryEffect::ReadInaccessible.encoding(), Some(4));
|
||||
assert_eq!(MemoryEffect::ReadArgAndInaccessible.encoding(), Some(5));
|
||||
assert_eq!(MemoryEffect::WriteInaccessible.encoding(), Some(8));
|
||||
assert_eq!(MemoryEffect::ReadOther.encoding(), Some(64));
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,12 @@ impl<'ctx> Function<'ctx> {
|
||||
}
|
||||
|
||||
/// Sets the memory writer function attributes.
|
||||
///
|
||||
/// `Attribute::Memory` is **not** valid here — it carries a payload that
|
||||
/// this enum-only API cannot express. Use the `MEMORY_EFFECT` associated
|
||||
/// constant on the `RuntimeFunction` trait (or
|
||||
/// `inkwell::attributes::Context::create_enum_attribute` directly when
|
||||
/// not declaring through the trait) instead.
|
||||
pub fn set_attributes(
|
||||
llvm: &'ctx inkwell::context::Context,
|
||||
declaration: Declaration<'ctx>,
|
||||
@@ -104,8 +110,13 @@ impl<'ctx> Function<'ctx> {
|
||||
force: bool,
|
||||
) {
|
||||
for attribute_kind in attributes {
|
||||
assert_ne!(
|
||||
*attribute_kind,
|
||||
Attribute::Memory,
|
||||
"Attribute::Memory cannot be set through set_attributes; \
|
||||
use RuntimeFunction::MEMORY_EFFECT to express the encoding",
|
||||
);
|
||||
match attribute_kind {
|
||||
Attribute::Memory => unimplemented!("`memory` attributes are not implemented"),
|
||||
attribute_kind @ Attribute::AlwaysInline if force => {
|
||||
declaration.value.remove_enum_attribute(
|
||||
inkwell::attributes::AttributeLoc::Function,
|
||||
@@ -172,16 +183,19 @@ impl<'ctx> Function<'ctx> {
|
||||
);
|
||||
}
|
||||
|
||||
if !optimizer.settings().is_middle_end_enabled() {
|
||||
Self::set_attributes(
|
||||
llvm,
|
||||
declaration,
|
||||
&[Attribute::NoInline, Attribute::OptimizeNone],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
Self::set_attributes(llvm, declaration, &[Attribute::NoFree], false);
|
||||
// `NoFree` and `NoUnwind` are facts about the PVM target, not optimization heuristics:
|
||||
// * PVM has no `free` (the heap is bump-allocated via `sbrk`), so no Solidity-emitted
|
||||
// function can ever release memory.
|
||||
// * PVM has no stack unwinding: Solidity errors leave a frame via `revert`
|
||||
// (`seal_return(1, ..)` + `unreachable`), never via an unwind edge. Marking every
|
||||
// function `nounwind` lets LLVM elide CFI directives, lower `invoke` to `call`, and
|
||||
// skip exception frame setup.
|
||||
Self::set_attributes(
|
||||
llvm,
|
||||
declaration,
|
||||
&[Attribute::NoFree, Attribute::NoUnwind],
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets the front-end runtime attributes.
|
||||
|
||||
@@ -3,16 +3,33 @@
|
||||
use inkwell::values::BasicValue;
|
||||
use revive_common::BIT_LENGTH_WORD;
|
||||
|
||||
use crate::polkavm::context::attribute::Attribute;
|
||||
use crate::polkavm::context::attribute::MemoryEffect;
|
||||
use crate::polkavm::context::runtime::RuntimeFunction;
|
||||
use crate::polkavm::context::Context;
|
||||
use crate::polkavm::WriteLLVM;
|
||||
|
||||
/// Standard attribute set for the pure arithmetic helpers. Each helper pairs
|
||||
/// this list with [`PURE_MEMORY_EFFECT`] so that LLVM GVN/CSE can merge
|
||||
/// repeated calls and hoist invariant divisions out of loops.
|
||||
const PURE_ATTRIBUTES: &[Attribute] = &[
|
||||
Attribute::NoFree,
|
||||
Attribute::NoRecurse,
|
||||
Attribute::WillReturn,
|
||||
];
|
||||
|
||||
/// The arithmetic helpers are pure (only depend on their integer arguments).
|
||||
const PURE_MEMORY_EFFECT: MemoryEffect = MemoryEffect::None;
|
||||
|
||||
/// Implements the division operator according to the EVM specification.
|
||||
pub struct Division;
|
||||
|
||||
impl RuntimeFunction for Division {
|
||||
const NAME: &'static str = "__revive_division";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = PURE_ATTRIBUTES;
|
||||
const MEMORY_EFFECT: MemoryEffect = PURE_MEMORY_EFFECT;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(
|
||||
&[context.word_type().into(), context.word_type().into()],
|
||||
@@ -52,6 +69,9 @@ pub struct SignedDivision;
|
||||
impl RuntimeFunction for SignedDivision {
|
||||
const NAME: &'static str = "__revive_signed_division";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = PURE_ATTRIBUTES;
|
||||
const MEMORY_EFFECT: MemoryEffect = PURE_MEMORY_EFFECT;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(
|
||||
&[context.word_type().into(), context.word_type().into()],
|
||||
@@ -126,6 +146,9 @@ pub struct Remainder;
|
||||
impl RuntimeFunction for Remainder {
|
||||
const NAME: &'static str = "__revive_remainder";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = PURE_ATTRIBUTES;
|
||||
const MEMORY_EFFECT: MemoryEffect = PURE_MEMORY_EFFECT;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(
|
||||
&[context.word_type().into(), context.word_type().into()],
|
||||
@@ -174,6 +197,9 @@ pub struct SignedRemainder;
|
||||
impl RuntimeFunction for SignedRemainder {
|
||||
const NAME: &'static str = "__revive_signed_remainder";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = PURE_ATTRIBUTES;
|
||||
const MEMORY_EFFECT: MemoryEffect = PURE_MEMORY_EFFECT;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(
|
||||
&[context.word_type().into(), context.word_type().into()],
|
||||
|
||||
@@ -49,7 +49,7 @@ where
|
||||
context.set_basic_block(context.current_function().borrow().entry_block());
|
||||
|
||||
self.inner.into_llvm(context)?;
|
||||
context.set_debug_location(0, 0, None)?;
|
||||
context.set_debug_location_to_function_scope()?;
|
||||
|
||||
match context
|
||||
.basic_block()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use inkwell::values::BasicValue;
|
||||
|
||||
use crate::polkavm::context::attribute::MemoryEffect;
|
||||
use crate::polkavm::context::function::Attribute;
|
||||
use crate::polkavm::context::runtime::RuntimeFunction;
|
||||
use crate::polkavm::context::Context;
|
||||
@@ -132,3 +133,386 @@ impl WriteLLVM for Exit {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `callvalue()` function that reads the value transferred with the call.
|
||||
///
|
||||
/// The `value_transferred` runtime API writes a 256-bit value to a buffer.
|
||||
/// Each inline call site generates: alloca i256 + store 0 + ptrtoint + call + load.
|
||||
/// By outlining into a shared function, all call sites reduce to a single `call`.
|
||||
/// Contracts with many non-payable checks (OpenZeppelin ERC20: 37 sites) save
|
||||
/// significant code size through this deduplication.
|
||||
pub struct CallValue;
|
||||
|
||||
impl RuntimeFunction for CallValue {
|
||||
const NAME: &'static str = "__revive_callvalue";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute::NoFree,
|
||||
Attribute::NoRecurse,
|
||||
Attribute::WillReturn,
|
||||
];
|
||||
|
||||
/// The call value is constant for the duration of a contract invocation,
|
||||
/// so this helper is effectively pure: LLVM can DCE redundant calls and
|
||||
/// CSE duplicate calls.
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::None;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(&[], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let output_pointer =
|
||||
context.build_alloca_at_entry(context.value_type(), "value_transferred");
|
||||
context.build_store(output_pointer, context.word_const(0))?;
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::VALUE_TRANSFERRED,
|
||||
&[output_pointer.to_int(context).into()],
|
||||
);
|
||||
let value = context.build_load(output_pointer, "value_transferred")?;
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for CallValue {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `callvalue() != 0` function that returns a boolean flag.
|
||||
///
|
||||
/// In ERC20 and similar contracts, callvalue is ONLY used for non-payable
|
||||
/// checks: `if (callvalue()) { revert(0, 0) }`. Each such check compares
|
||||
/// a 256-bit value against zero, generating 4 limb comparisons in PVM.
|
||||
/// By outlining the check into a function returning `i1`, each call site
|
||||
/// saves ~15 bytes (4 comparisons + OR vs. a single branch on i1).
|
||||
/// OpenZeppelin ERC20 has ~20 such checks, saving ~300 bytes.
|
||||
pub struct CallValueNonzero;
|
||||
|
||||
impl RuntimeFunction for CallValueNonzero {
|
||||
const NAME: &'static str = "__revive_callvalue_nonzero";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute::NoFree,
|
||||
Attribute::NoRecurse,
|
||||
Attribute::WillReturn,
|
||||
];
|
||||
|
||||
/// Same `memory(none)` rationale as [`CallValue`].
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::None;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.llvm().bool_type().fn_type(&[], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let output_pointer =
|
||||
context.build_alloca_at_entry(context.value_type(), "value_transferred");
|
||||
context.build_store(output_pointer, context.word_const(0))?;
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::VALUE_TRANSFERRED,
|
||||
&[output_pointer.to_int(context).into()],
|
||||
);
|
||||
let value = context
|
||||
.build_load(output_pointer, "value_transferred")?
|
||||
.into_int_value();
|
||||
let zero = context.word_type().const_zero();
|
||||
let is_nonzero = context.builder().build_int_compare(
|
||||
inkwell::IntPredicate::NE,
|
||||
value,
|
||||
zero,
|
||||
"callvalue_nonzero",
|
||||
)?;
|
||||
Ok(Some(is_nonzero.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for CallValueNonzero {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `calldataload(offset)` function that reads 32 bytes from call data.
|
||||
///
|
||||
/// The `call_data_load` runtime API writes a 256-bit value to a buffer.
|
||||
/// Each inline call site generates: alloca i256 + ptrtoint + call + load.
|
||||
/// By outlining into a shared function, all call sites reduce to `clip_to_xlen + call`.
|
||||
/// Contracts with many ABI parameters (OpenZeppelin ERC20: 51 sites) save
|
||||
/// significant code size through this deduplication.
|
||||
pub struct CallDataLoad;
|
||||
|
||||
impl RuntimeFunction for CallDataLoad {
|
||||
const NAME: &'static str = "__revive_calldataload";
|
||||
|
||||
/// Calldata is immutable for the duration of execution and only
|
||||
/// reachable through the host syscall. The helper's alloca write is
|
||||
/// invisible to callers, so the externally observable effect is a read
|
||||
/// of pallet-revive runtime state — letting LLVM GVN fold repeated
|
||||
/// `__revive_calldataload(offset)` calls even across heap mstores.
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::ReadInaccessible;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context
|
||||
.word_type()
|
||||
.fn_type(&[context.xlen_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let offset = Self::paramater(context, 0).into_int_value();
|
||||
let output_pointer = context.build_alloca_at_entry(context.word_type(), "call_data_output");
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::CALL_DATA_LOAD,
|
||||
&[output_pointer.to_int(context).into(), offset.into()],
|
||||
);
|
||||
let value = context.build_load(output_pointer, "call_data_load_value")?;
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for CallDataLoad {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `caller()` function that reads the calling account address.
|
||||
///
|
||||
/// The `caller` runtime API writes a 20-byte address to a buffer.
|
||||
/// Each inline call site generates: get_global + ptrtoint + call + load + bswap + zext.
|
||||
/// By outlining into a shared function, all call sites reduce to a single `call`.
|
||||
/// Contracts with many msg.sender checks (OpenZeppelin ERC20: 10+ sites) save
|
||||
/// significant code size through this deduplication.
|
||||
pub struct Caller;
|
||||
|
||||
impl RuntimeFunction for Caller {
|
||||
const NAME: &'static str = "__revive_caller";
|
||||
|
||||
/// The caller is immutable within an execution and the syscall reads it
|
||||
/// from pallet-revive runtime state. The body writes the result into a
|
||||
/// function-local alloca (not the shared `@GLOBAL_ADDRESS_SPILL_BUFFER`,
|
||||
/// which `origin()` and `build_address` also touch — see the
|
||||
/// `caller_origin_aliasing` regression test), so from the caller's view
|
||||
/// only pallet-revive runtime state is read and `ReadInaccessible` is
|
||||
/// sound. This lets GVN deduplicate repeated `__revive_caller()` calls
|
||||
/// across heap mstores.
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::ReadInaccessible;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(&[], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let address_type = context.integer_type(revive_common::BIT_LENGTH_ETH_ADDRESS);
|
||||
let output_pointer = context.build_alloca_at_entry(address_type, "caller_output");
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::CALLER,
|
||||
&[output_pointer.to_int(context).into()],
|
||||
);
|
||||
let value = context.build_load_address(output_pointer)?;
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for Caller {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `revert(0, 0)` function (empty revert / panic).
|
||||
///
|
||||
/// This is the most common revert pattern in Solidity contracts (20+ sites in ERC20).
|
||||
/// Each inlined site generates sbrk(0, 0) + ptrtoint + seal_return (~8 PVM instructions).
|
||||
/// By outlining into a zero-argument noreturn function, each call site becomes a single
|
||||
/// `call @__revive_revert_0` (~2 PVM instructions).
|
||||
pub struct RevertEmpty;
|
||||
|
||||
impl RuntimeFunction for RevertEmpty {
|
||||
const NAME: &'static str = "__revive_revert_0";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = &[Attribute::NoReturn, Attribute::NoFree];
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.void_type().fn_type(&[], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let zero = context.xlen_type().const_zero();
|
||||
// Use unchecked GEP: revert data at offset 0 is always within the
|
||||
// static heap. The sbrk overhead is unnecessary since the data was
|
||||
// already written to memory before this function is called.
|
||||
let heap_pointer = context.build_heap_gep_unchecked(zero)?;
|
||||
let offset_pointer = context.builder().build_ptr_to_int(
|
||||
heap_pointer.value,
|
||||
context.xlen_type(),
|
||||
"return_data_ptr_to_int",
|
||||
)?;
|
||||
let flags = context.integer_const(crate::polkavm::XLEN, 1);
|
||||
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::RETURN,
|
||||
&[flags.into(), offset_pointer.into(), zero.into()],
|
||||
);
|
||||
context.build_unreachable();
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for RevertEmpty {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `revert(0, length)` function for non-zero constant lengths.
|
||||
///
|
||||
/// After error data has been stored to heap memory at offset 0, this function
|
||||
/// calls seal_return(1, heap_base, length). Used for revert(0, 4), revert(0, 36),
|
||||
/// revert(0, 68), revert(0, 100) patterns. ERC20 has 34+ such sites.
|
||||
pub struct Revert;
|
||||
|
||||
impl RuntimeFunction for Revert {
|
||||
const NAME: &'static str = "__revive_revert";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = &[Attribute::NoReturn, Attribute::NoFree];
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context
|
||||
.void_type()
|
||||
.fn_type(&[context.xlen_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let length = Self::paramater(context, 0).into_int_value();
|
||||
|
||||
let zero = context.xlen_type().const_zero();
|
||||
let heap_pointer = context.build_heap_gep(zero, length)?;
|
||||
let offset_pointer = context.builder().build_ptr_to_int(
|
||||
heap_pointer.value,
|
||||
context.xlen_type(),
|
||||
"return_data_ptr_to_int",
|
||||
)?;
|
||||
let flags = context.integer_const(crate::polkavm::XLEN, 1);
|
||||
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::RETURN,
|
||||
&[flags.into(), offset_pointer.into(), length.into()],
|
||||
);
|
||||
context.build_unreachable();
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for Revert {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outlined `Panic(uint256)` revert helper: writes the Panic ABI encoding to
|
||||
/// scratch memory (selector at offset 0, code at offset 4) and reverts with
|
||||
/// length 36. The code is passed as an `i256` argument so a single helper
|
||||
/// covers every panic code (0x01, 0x11, 0x12, 0x21, 0x22, 0x31, 0x32, 0x41).
|
||||
/// Replaces the inline two-mstore + revert pattern at every panic site, which
|
||||
/// otherwise duplicates ~30+ instructions across every function that traps.
|
||||
pub struct RevertPanic;
|
||||
|
||||
impl RuntimeFunction for RevertPanic {
|
||||
const NAME: &'static str = "__revive_panic";
|
||||
|
||||
const ATTRIBUTES: &'static [Attribute] = &[Attribute::NoReturn, Attribute::NoFree];
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context
|
||||
.void_type()
|
||||
.fn_type(&[context.word_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let code = Self::paramater(context, 0).into_int_value();
|
||||
|
||||
let zero_xlen = context.xlen_type().const_zero();
|
||||
let four_xlen = context.xlen_type().const_int(4, false);
|
||||
let panic_selector =
|
||||
context.word_const_str_hex(revive_common::PANIC_UINT256_SELECTOR_WORD_HEX);
|
||||
|
||||
crate::polkavm::evm::memory::store_bswap_unchecked(context, zero_xlen, panic_selector)?;
|
||||
crate::polkavm::evm::memory::store_bswap_unchecked(context, four_xlen, code)?;
|
||||
|
||||
let length = context.xlen_type().const_int(0x24, false);
|
||||
let heap_pointer = context.build_heap_gep_unchecked(zero_xlen)?;
|
||||
let offset_pointer = context.builder().build_ptr_to_int(
|
||||
heap_pointer.value,
|
||||
context.xlen_type(),
|
||||
"panic_data_ptr_to_int",
|
||||
)?;
|
||||
let flags = context.integer_const(crate::polkavm::XLEN, 1);
|
||||
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::RETURN,
|
||||
&[flags.into(), offset_pointer.into(), length.into()],
|
||||
);
|
||||
context.build_unreachable();
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for RevertPanic {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ where
|
||||
context.set_basic_block(context.current_function().borrow().entry_block());
|
||||
|
||||
self.inner.into_llvm(context)?;
|
||||
context.set_debug_location(0, 0, None)?;
|
||||
context.set_debug_location_to_function_scope()?;
|
||||
|
||||
match context
|
||||
.basic_block()
|
||||
|
||||
@@ -10,6 +10,7 @@ use inkwell::debug_info::AsDIScope;
|
||||
use inkwell::debug_info::DIScope;
|
||||
use inkwell::types::BasicType;
|
||||
use inkwell::values::BasicValue;
|
||||
use inkwell::values::InstructionOpcode;
|
||||
use revive_solc_json_interface::PolkaVMDefaultHeapMemorySize;
|
||||
use revive_solc_json_interface::PolkaVMDefaultStackMemorySize;
|
||||
use revive_solc_json_interface::SolcStandardJsonInputSettingsPolkaVMMemory;
|
||||
@@ -20,11 +21,14 @@ use crate::polkavm::DebugConfig;
|
||||
use crate::target_machine::target::Target;
|
||||
use crate::target_machine::TargetMachine;
|
||||
use crate::PolkaVMLoadHeapWordFunction;
|
||||
use crate::PolkaVMLoadHeapWordNativeFunction;
|
||||
use crate::PolkaVMSbrkFunction;
|
||||
use crate::PolkaVMStoreHeapWordFunction;
|
||||
use crate::PolkaVMStoreHeapWordNativeFunction;
|
||||
|
||||
use self::address_space::AddressSpace;
|
||||
use self::attribute::Attribute;
|
||||
use self::attribute::MemoryEffect;
|
||||
use self::build::Build;
|
||||
use self::code_type::CodeType;
|
||||
use self::debug_info::DebugInfo;
|
||||
@@ -59,6 +63,29 @@ pub mod yul_data;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Imported host syscalls whose only externally visible effect is a read of
|
||||
/// pallet-revive runtime state. Each takes no arguments and returns a scalar —
|
||||
/// no input/output buffer and no heap traffic from the caller's view — so we
|
||||
/// can attach [`MemoryEffect::ReadInaccessible`] and let LLVM GVN dedupe
|
||||
/// repeated calls across heap mstores.
|
||||
///
|
||||
/// `code_size` is deliberately **not** in this list. Unlike the entries above,
|
||||
/// `extcodesize` passes the queried address to the syscall through the
|
||||
/// [`crate::polkavm::GLOBAL_ADDRESS_SPILL_BUFFER`] global (see
|
||||
/// [`Context::build_address_argument_store`]), so the call reads caller-accessible
|
||||
/// memory. That access lives in LLVM's "other" location, not "inaccessiblemem"
|
||||
/// (the address reaches the call as an integer, so it is not "argmem" either).
|
||||
/// Tagging it `inaccessiblemem: read` would hide the buffer read, letting GVN
|
||||
/// merge two `extcodesize` calls for different addresses — and DSE drop the
|
||||
/// first address store — so that `extcodesize(a) + extcodesize(b)` miscompiles
|
||||
/// to `2 * extcodesize(a)`. With no attribute the import keeps the inline-asm
|
||||
/// `~{memory}` barrier, matching its pre-newyork behaviour.
|
||||
const MEMORY_READ_IMPORTS: &[&str] = &[
|
||||
revive_runtime_api::polkavm_imports::CALL_DATA_SIZE,
|
||||
revive_runtime_api::polkavm_imports::GAS_LIMIT,
|
||||
revive_runtime_api::polkavm_imports::RETURNDATASIZE,
|
||||
];
|
||||
|
||||
/// The LLVM IR generator context.
|
||||
/// It is a not-so-big god-like object glueing all the compilers' complexity and act as an adapter
|
||||
/// and a superstructure over the inner `inkwell` LLVM context.
|
||||
@@ -123,8 +150,10 @@ impl<'ctx> Context<'ctx> {
|
||||
.expect("the stdlib module should be linkable");
|
||||
}
|
||||
|
||||
/// Link in the PolkaVM imports module, containing imported functions,
|
||||
/// and marking them as external (they need to be relocatable as too).
|
||||
/// Link in the PolkaVM imports module, mark host syscall imports as
|
||||
/// `Internal` linkage (they need to be relocatable too), and attach a
|
||||
/// [`MemoryEffect::ReadInaccessible`] attribute to every entry of
|
||||
/// [`MEMORY_READ_IMPORTS`].
|
||||
fn link_polkavm_imports(
|
||||
llvm: &'ctx inkwell::context::Context,
|
||||
module: &inkwell::module::Module<'ctx>,
|
||||
@@ -141,6 +170,20 @@ impl<'ctx> Context<'ctx> {
|
||||
.unwrap_or_else(|| panic!("{import} import should be declared"))
|
||||
.set_linkage(inkwell::module::Linkage::Internal);
|
||||
}
|
||||
|
||||
let read_inaccessible_encoding = MemoryEffect::ReadInaccessible
|
||||
.encoding()
|
||||
.expect("ICE: ReadInaccessible always encodes to an integer");
|
||||
let memory_read_attr =
|
||||
llvm.create_enum_attribute(Attribute::Memory as u32, read_inaccessible_encoding);
|
||||
for import in MEMORY_READ_IMPORTS {
|
||||
if let Some(function) = module.get_function(import) {
|
||||
function.add_attribute(
|
||||
inkwell::attributes::AttributeLoc::Function,
|
||||
memory_read_attr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn link_polkavm_exports(&self, contract_path: &str) -> anyhow::Result<()> {
|
||||
@@ -287,7 +330,11 @@ impl<'ctx> Context<'ctx> {
|
||||
) -> anyhow::Result<Build> {
|
||||
self.link_polkavm_exports(contract_path)?;
|
||||
self.link_immutable_data(contract_path)?;
|
||||
let target_machine = TargetMachine::new(Target::PVM, self.optimizer.settings())?;
|
||||
let target_machine = TargetMachine::new(
|
||||
Target::PVM,
|
||||
self.optimizer.settings(),
|
||||
self.optimizer.is_newyork(),
|
||||
)?;
|
||||
self.module().set_triple(&target_machine.get_triple());
|
||||
|
||||
self.debug_config
|
||||
@@ -311,6 +358,12 @@ impl<'ctx> Context<'ctx> {
|
||||
)
|
||||
})?;
|
||||
|
||||
// Narrow large integer div/rem where operands provably fit in a smaller
|
||||
// type. LLVM's i256 div/rem backend expansion produces enormous basic
|
||||
// blocks that pallet-revive rejects as `BasicBlockTooLarge`; narrowing
|
||||
// here keeps the expansion compact.
|
||||
self.narrow_divrem_instructions();
|
||||
|
||||
self.debug_config
|
||||
.dump_llvm_ir_optimized(contract_path, self.module())?;
|
||||
|
||||
@@ -441,12 +494,35 @@ impl<'ctx> Context<'ctx> {
|
||||
&self.llvm_runtime
|
||||
}
|
||||
|
||||
/// Returns the topmost frontend function-name → mangled-name scope.
|
||||
///
|
||||
/// Panics if no scope has been pushed; callers must invoke this from inside a region
|
||||
/// that has already set up its function scope.
|
||||
pub fn get_current_scope(&mut self) -> &mut BTreeMap<String, String> {
|
||||
self.function_scope
|
||||
.last_mut()
|
||||
.expect("ICE: function scope must be pushed before declaring frontend functions")
|
||||
}
|
||||
|
||||
/// Returns the mangled symbol name for a frontend (Solidity-emitted) function.
|
||||
///
|
||||
/// Frontend names collide across compilation units: separate Yul objects can both define
|
||||
/// `fun_main` for example. The mangling appends a code-section tag (deploy / runtime) and a
|
||||
/// per-context counter so each definition gets a globally unique LLVM symbol, while the
|
||||
/// original name remains the lookup key in `function_scope`.
|
||||
fn mangle_frontend_function_name(&mut self, name: &str) -> String {
|
||||
assert!(
|
||||
!self.get_current_scope().contains_key(name),
|
||||
"ICE: function '{name}' declared subsequently in the same scope"
|
||||
);
|
||||
let counter = self.function_counter;
|
||||
self.function_counter += 1;
|
||||
let mangled = format!("{name}_{}__{counter}", self.code_type().unwrap());
|
||||
self.get_current_scope()
|
||||
.insert(name.to_string(), mangled.clone());
|
||||
mangled
|
||||
}
|
||||
|
||||
/// Appends a function to the current module.
|
||||
pub fn add_function(
|
||||
&mut self,
|
||||
@@ -458,16 +534,7 @@ impl<'ctx> Context<'ctx> {
|
||||
is_frontend: bool,
|
||||
) -> anyhow::Result<Rc<RefCell<Function<'ctx>>>> {
|
||||
let name = if is_frontend {
|
||||
assert!(
|
||||
!self.get_current_scope().contains_key(name),
|
||||
"ICE: function '{name}' declared subsequently in the same scope"
|
||||
);
|
||||
let counter = self.function_counter;
|
||||
self.function_counter += 1;
|
||||
let mangled = format!("{name}_{}__{counter}", self.code_type().unwrap());
|
||||
self.get_current_scope()
|
||||
.insert(name.to_string(), mangled.clone());
|
||||
mangled
|
||||
self.mangle_frontend_function_name(name)
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
@@ -492,21 +559,28 @@ impl<'ctx> Context<'ctx> {
|
||||
let entry_block = self.llvm.append_basic_block(value, "entry");
|
||||
let return_block = self.llvm.append_basic_block(value, "return");
|
||||
|
||||
// Allocate return slots using the actual return type from the function signature.
|
||||
// Narrowed return types (e.g. i64 instead of i256) flow through unchanged, reducing
|
||||
// register pressure and spills.
|
||||
let r#return = match return_values_length {
|
||||
0 => FunctionReturn::none(),
|
||||
1 => {
|
||||
self.set_basic_block(entry_block);
|
||||
let pointer = self.build_alloca(self.word_type(), "return_pointer");
|
||||
let alloca_type = r#type
|
||||
.get_return_type()
|
||||
.unwrap_or_else(|| self.word_type().as_basic_type_enum());
|
||||
let pointer = self.build_alloca(alloca_type, "return_pointer");
|
||||
FunctionReturn::primitive(pointer)
|
||||
}
|
||||
size => {
|
||||
self.set_basic_block(entry_block);
|
||||
let pointer = self.build_alloca(
|
||||
let alloca_type = r#type.get_return_type().unwrap_or_else(|| {
|
||||
self.structure_type(
|
||||
vec![self.word_type().as_basic_type_enum(); size].as_slice(),
|
||||
),
|
||||
"return_pointer",
|
||||
);
|
||||
)
|
||||
.as_basic_type_enum()
|
||||
});
|
||||
let pointer = self.build_alloca(alloca_type, "return_pointer");
|
||||
FunctionReturn::compound(pointer, size)
|
||||
}
|
||||
};
|
||||
@@ -633,6 +707,20 @@ impl<'ctx> Context<'ctx> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Anchors the current debug location to the active function's own subprogram.
|
||||
///
|
||||
/// Synthetic trailing instructions (the implicit branch to the return block
|
||||
/// and the terminator) are emitted after the function body has been lowered,
|
||||
/// at which point the debug-info scope stack may still point at a nested or
|
||||
/// helper function generated along the way. Resolving `None` to the top scope
|
||||
/// there would attach a `!dbg` from an unrelated subprogram, which the LLVM
|
||||
/// verifier rejects. Using the function's own scope keeps the attachment valid.
|
||||
/// No-op when debug info is disabled.
|
||||
pub fn set_debug_location_to_function_scope(&self) -> anyhow::Result<()> {
|
||||
let scope = self.current_function().borrow().get_debug_scope();
|
||||
self.set_debug_location(0, 0, scope)
|
||||
}
|
||||
|
||||
/// Pushes a debug-info scope to the stack.
|
||||
pub fn push_debug_scope(&self, scope: DIScope<'ctx>) {
|
||||
if let Some(debug_info) = self.debug_info() {
|
||||
@@ -709,28 +797,35 @@ impl<'ctx> Context<'ctx> {
|
||||
}
|
||||
|
||||
/// Builds an aligned stack allocation at the function entry.
|
||||
///
|
||||
/// Allocas placed at the entry block coalesce into a single stack-frame allocation,
|
||||
/// eliminating per-alloca dynamic stack adjustment. LLVM's stack coloring pass handles
|
||||
/// lifetime-based slot reuse regardless of alloca placement.
|
||||
pub fn build_alloca_at_entry<T: BasicType<'ctx> + Clone + Copy>(
|
||||
&self,
|
||||
r#type: T,
|
||||
name: &str,
|
||||
) -> Pointer<'ctx> {
|
||||
// TODO: Revisit. While at entry should be preferred in theory:
|
||||
// - It has negligible code size impact on real word contracts.
|
||||
// - Sometimes has negative impact on code size.
|
||||
// - Messes up debug information used to analyze code size issues.
|
||||
self.build_alloca(r#type, name)
|
||||
let current_block = self.builder.get_insert_block().unwrap();
|
||||
let function = current_block.get_parent().unwrap();
|
||||
let entry_block = function.get_first_basic_block().unwrap();
|
||||
|
||||
// let current_block = self.basic_block();
|
||||
// let entry_block = self.current_function().borrow().entry_block();
|
||||
if let Some(terminator) = entry_block.get_terminator() {
|
||||
self.builder.position_before(&terminator);
|
||||
} else {
|
||||
self.builder.position_at_end(entry_block);
|
||||
}
|
||||
|
||||
// match entry_block.get_first_instruction() {
|
||||
// Some(instruction) => self.builder().position_before(&instruction),
|
||||
// None => self.builder().position_at_end(entry_block),
|
||||
// }
|
||||
let pointer = self.builder.build_alloca(r#type, name).unwrap();
|
||||
pointer
|
||||
.as_instruction()
|
||||
.unwrap()
|
||||
.set_alignment(revive_common::BYTE_LENGTH_STACK_ALIGN as u32)
|
||||
.expect("ICE: alignment is valid");
|
||||
|
||||
// let pointer = self.build_alloca(r#type, name);
|
||||
// self.set_basic_block(current_block);
|
||||
// pointer
|
||||
self.builder.position_at_end(current_block);
|
||||
|
||||
Pointer::new(r#type, AddressSpace::Stack, pointer)
|
||||
}
|
||||
|
||||
/// Builds an aligned stack allocation at the current position.
|
||||
@@ -848,6 +943,34 @@ impl<'ctx> Context<'ctx> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a heap load instruction without byte-swapping.
|
||||
/// Used for internal memory operations that don't escape to external code.
|
||||
pub fn build_load_native(
|
||||
&self,
|
||||
offset: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<inkwell::values::BasicValueEnum<'ctx>> {
|
||||
let name = <PolkaVMLoadHeapWordNativeFunction as RuntimeFunction>::NAME;
|
||||
let declaration = <PolkaVMLoadHeapWordNativeFunction as RuntimeFunction>::declaration(self);
|
||||
let arguments = [offset.as_basic_value_enum()];
|
||||
Ok(self
|
||||
.build_call(declaration, &arguments, "heap_load_native")
|
||||
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value")))
|
||||
}
|
||||
|
||||
/// Builds a heap store instruction without byte-swapping.
|
||||
/// Used for internal memory operations that don't escape to external code.
|
||||
pub fn build_store_native(
|
||||
&self,
|
||||
offset: inkwell::values::IntValue<'ctx>,
|
||||
value: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<()> {
|
||||
let declaration =
|
||||
<PolkaVMStoreHeapWordNativeFunction as RuntimeFunction>::declaration(self);
|
||||
let arguments = [offset.as_basic_value_enum(), value.as_basic_value_enum()];
|
||||
self.build_call(declaration, &arguments, "heap_store_native");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Swap the endianness of an intvalue
|
||||
pub fn build_byte_swap(
|
||||
&self,
|
||||
@@ -1053,45 +1176,111 @@ impl<'ctx> Context<'ctx> {
|
||||
}
|
||||
|
||||
/// Truncate a memory offset to register size, trapping if it doesn't fit.
|
||||
///
|
||||
/// Handles all integer widths: at xlen the value is returned unchanged, narrower types are
|
||||
/// zero-extended (no overflow possible), word-typed values delegate to the
|
||||
/// `WordToPointer` runtime function (which returns the truncated value or jumps to invalid),
|
||||
/// and intermediate widths inline a truncate-extend-compare at the source width because that
|
||||
/// is much cheaper on 32-bit PVM than extending to i256 first (i64 compare is 2 register
|
||||
/// pairs vs i256's 8 words — roughly 10 fewer instructions per check).
|
||||
pub fn safe_truncate_int_to_xlen(
|
||||
&self,
|
||||
value: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<inkwell::values::IntValue<'ctx>> {
|
||||
if value.get_type() == self.xlen_type() {
|
||||
let value_width = value.get_type().get_bit_width();
|
||||
let xlen_width = self.xlen_type().get_bit_width();
|
||||
let word_width = self.word_type().get_bit_width();
|
||||
|
||||
if value_width == xlen_width {
|
||||
return Ok(value);
|
||||
}
|
||||
assert_eq!(
|
||||
value.get_type(),
|
||||
self.word_type(),
|
||||
"expected XLEN or WORD sized int type for memory offset",
|
||||
);
|
||||
|
||||
Ok(self
|
||||
.build_call(
|
||||
<WordToPointer as RuntimeFunction>::declaration(self),
|
||||
&[value.into()],
|
||||
"word_to_pointer",
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"revive runtime function {} should return a value",
|
||||
<WordToPointer as RuntimeFunction>::NAME,
|
||||
if value_width < xlen_width {
|
||||
return Ok(self.builder().build_int_z_extend(
|
||||
value,
|
||||
self.xlen_type(),
|
||||
"narrow_to_xlen",
|
||||
)?);
|
||||
}
|
||||
|
||||
if value_width == word_width {
|
||||
return Ok(self
|
||||
.build_call(
|
||||
<WordToPointer as RuntimeFunction>::declaration(self),
|
||||
&[value.into()],
|
||||
"word_to_pointer",
|
||||
)
|
||||
})
|
||||
.into_int_value())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"ICE: revive runtime function {} should return a value",
|
||||
<WordToPointer as RuntimeFunction>::NAME,
|
||||
)
|
||||
})
|
||||
.into_int_value());
|
||||
}
|
||||
|
||||
let truncated =
|
||||
self.builder()
|
||||
.build_int_truncate(value, self.xlen_type(), "offset_truncated")?;
|
||||
let extended =
|
||||
self.builder()
|
||||
.build_int_z_extend(truncated, value.get_type(), "offset_extended")?;
|
||||
let is_overflow = self.builder().build_int_compare(
|
||||
inkwell::IntPredicate::NE,
|
||||
value,
|
||||
extended,
|
||||
"compare_truncated_extended",
|
||||
)?;
|
||||
|
||||
let block_continue = self.append_basic_block("offset_pointer_ok");
|
||||
let block_invalid = self.append_basic_block("offset_pointer_overflow");
|
||||
self.build_conditional_branch(is_overflow, block_invalid, block_continue)?;
|
||||
|
||||
self.set_basic_block(block_invalid);
|
||||
self.build_runtime_call(revive_runtime_api::polkavm_imports::INVALID, &[]);
|
||||
self.build_unreachable();
|
||||
|
||||
self.set_basic_block(block_continue);
|
||||
Ok(truncated)
|
||||
}
|
||||
|
||||
/// Clip a memory offset to the maximum value that fits into a register.
|
||||
///
|
||||
/// Behaves like [`Self::safe_truncate_int_to_xlen`] up to xlen-and-narrower widths, but for
|
||||
/// wider types it saturates to `xlen::MAX` rather than trapping. The wider-type branch
|
||||
/// keeps the original-width comparison so the codegen stays compact (word-type values
|
||||
/// remain at word_type; intermediate widths use the value's own type).
|
||||
pub fn clip_to_xlen(
|
||||
&self,
|
||||
value: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<inkwell::values::IntValue<'ctx>> {
|
||||
let value_width = value.get_type().get_bit_width();
|
||||
let xlen_width = self.xlen_type().get_bit_width();
|
||||
let word_width = self.word_type().get_bit_width();
|
||||
|
||||
if value_width == xlen_width {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
if value_width < xlen_width {
|
||||
return Ok(self.builder().build_int_z_extend(
|
||||
value,
|
||||
self.xlen_type(),
|
||||
"narrow_to_xlen",
|
||||
)?);
|
||||
}
|
||||
|
||||
let clipped = self.xlen_type().const_all_ones();
|
||||
let comparison_type = if value_width == word_width {
|
||||
self.word_type()
|
||||
} else {
|
||||
value.get_type()
|
||||
};
|
||||
let is_overflow = self.builder().build_int_compare(
|
||||
inkwell::IntPredicate::UGT,
|
||||
value,
|
||||
self.builder()
|
||||
.build_int_z_extend(clipped, self.word_type(), "value_clipped")?,
|
||||
.build_int_z_extend(clipped, comparison_type, "value_clipped")?,
|
||||
"is_value_overflow",
|
||||
)?;
|
||||
let truncated =
|
||||
@@ -1158,6 +1347,64 @@ impl<'ctx> Context<'ctx> {
|
||||
Ok(Pointer::new(self.byte_type(), AddressSpace::Stack, pointer))
|
||||
}
|
||||
|
||||
/// Returns a pointer to `offset` into the heap WITHOUT calling sbrk.
|
||||
///
|
||||
/// The heap global is a fixed-size byte array (the static heap) that is always live for the
|
||||
/// full duration of contract execution. sbrk only updates the *high-water mark* tracked in
|
||||
/// `GLOBAL_HEAP_SIZE`; the underlying memory backing offsets up to `HEAP_SIZE` is always
|
||||
/// addressable. Skipping sbrk is therefore safe whenever the *byte-order semantics* of the
|
||||
/// access do not depend on a previously published high-water mark — i.e., for offsets known
|
||||
/// to live inside the statically reserved scratch area (0x00-0x7f, including the free
|
||||
/// memory pointer slot at 0x40), and for compiler-internal native-mode I/O whose offset
|
||||
/// bound is proven by the type system.
|
||||
///
|
||||
/// # Panics
|
||||
/// Assumes `offset` to be a register sized value.
|
||||
pub fn build_heap_gep_unchecked(
|
||||
&self,
|
||||
offset: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<Pointer<'ctx>> {
|
||||
assert_eq!(offset.get_type(), self.xlen_type());
|
||||
let heap_global: Pointer<'ctx> =
|
||||
self.get_global(crate::polkavm::GLOBAL_HEAP_MEMORY)?.into();
|
||||
let pointer = self.build_gep(
|
||||
heap_global,
|
||||
&[self.xlen_type().const_zero(), offset],
|
||||
self.byte_type(),
|
||||
"heap_unchecked_ptr",
|
||||
);
|
||||
Ok(Pointer::new(
|
||||
self.byte_type(),
|
||||
AddressSpace::Stack,
|
||||
pointer.value,
|
||||
))
|
||||
}
|
||||
|
||||
/// Ensures the heap size (msize) is at least `min_size`.
|
||||
/// This emits a branchless max(current_msize, min_size) update.
|
||||
/// Used after native stores that bypass sbrk to keep msize consistent.
|
||||
pub fn ensure_heap_size(
|
||||
&self,
|
||||
min_size: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<()> {
|
||||
let current = self
|
||||
.get_global_value(crate::polkavm::GLOBAL_HEAP_SIZE)?
|
||||
.into_int_value();
|
||||
let needs_update = self.builder().build_int_compare(
|
||||
inkwell::IntPredicate::UGT,
|
||||
min_size,
|
||||
current,
|
||||
"msize_needs_update",
|
||||
)?;
|
||||
let new_size = self
|
||||
.builder()
|
||||
.build_select(needs_update, min_size, current, "msize_new")?
|
||||
.into_int_value();
|
||||
let heap_size_global = self.get_global(crate::polkavm::GLOBAL_HEAP_SIZE)?;
|
||||
self.build_store(heap_size_global.into(), new_size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a boolean type constant.
|
||||
pub fn bool_const(&self, value: bool) -> inkwell::values::IntValue<'ctx> {
|
||||
self.bool_type().const_int(u64::from(value), false)
|
||||
@@ -1292,6 +1539,8 @@ impl<'ctx> Context<'ctx> {
|
||||
}
|
||||
|
||||
/// Returns a Yul function type with the specified arguments and number of return values.
|
||||
/// All return values use word_type (i256). For narrowed return types, use
|
||||
/// `function_type_with_returns`.
|
||||
pub fn function_type<T>(
|
||||
&self,
|
||||
argument_types: Vec<T>,
|
||||
@@ -1318,6 +1567,39 @@ impl<'ctx> Context<'ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a function type with explicit return types instead of word_type.
|
||||
/// Used by the newyork codegen for functions with narrowed return types.
|
||||
pub fn function_type_with_returns<T>(
|
||||
&self,
|
||||
argument_types: Vec<T>,
|
||||
return_types: &[inkwell::types::IntType<'ctx>],
|
||||
) -> inkwell::types::FunctionType<'ctx>
|
||||
where
|
||||
T: BasicType<'ctx>,
|
||||
{
|
||||
let argument_types: Vec<inkwell::types::BasicMetadataTypeEnum> = argument_types
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(T::as_basic_type_enum)
|
||||
.map(inkwell::types::BasicMetadataTypeEnum::from)
|
||||
.collect();
|
||||
match return_types.len() {
|
||||
0 => self
|
||||
.llvm
|
||||
.void_type()
|
||||
.fn_type(argument_types.as_slice(), false),
|
||||
1 => return_types[0].fn_type(argument_types.as_slice(), false),
|
||||
_ => {
|
||||
let field_types: Vec<inkwell::types::BasicTypeEnum> = return_types
|
||||
.iter()
|
||||
.map(|t| t.as_basic_type_enum())
|
||||
.collect();
|
||||
self.structure_type(&field_types)
|
||||
.fn_type(argument_types.as_slice(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifies the call site value, setting the default attributes.
|
||||
/// The attributes only affect the LLVM optimizations.
|
||||
pub fn modify_call_site_value(
|
||||
@@ -1468,6 +1750,12 @@ impl<'ctx> Context<'ctx> {
|
||||
self.optimizer.settings()
|
||||
}
|
||||
|
||||
/// Whether this module is being compiled through the newyork IR generator.
|
||||
/// Gates newyork-specific codegen that is unsound on the stock Yul path.
|
||||
pub fn is_newyork(&self) -> bool {
|
||||
self.optimizer.is_newyork()
|
||||
}
|
||||
|
||||
pub fn heap_size(&self) -> inkwell::values::IntValue<'ctx> {
|
||||
self.xlen_type().const_int(
|
||||
self.memory_config
|
||||
@@ -1476,4 +1764,214 @@ impl<'ctx> Context<'ctx> {
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Narrows large integer div/rem instructions whose operands provably fit
|
||||
/// in a smaller type.
|
||||
///
|
||||
/// LLVM's i256 div/rem backend expansion produces enormous basic blocks
|
||||
/// that pallet-revive rejects as `BasicBlockTooLarge`. LLVM's own
|
||||
/// `DivRemNarrowing` pass also sometimes fails to narrow div/rem in large
|
||||
/// functions after inlining. This runs as a safety net after the full
|
||||
/// LLVM optimization pipeline so we operate on the final form where the
|
||||
/// proof sources we accept (LLVM constants, `and` masks, `zext`) are
|
||||
/// observable across function boundaries after IPSCCP and inlining.
|
||||
fn narrow_divrem_instructions(&self) {
|
||||
let builder = self.llvm.create_builder();
|
||||
|
||||
for function in self.module().get_functions() {
|
||||
let mut to_narrow = Vec::new();
|
||||
|
||||
for basic_block in function.get_basic_blocks() {
|
||||
for instruction in basic_block.get_instructions() {
|
||||
let is_divrem = matches!(
|
||||
instruction.get_opcode(),
|
||||
InstructionOpcode::UDiv
|
||||
| InstructionOpcode::SDiv
|
||||
| InstructionOpcode::URem
|
||||
| InstructionOpcode::SRem
|
||||
);
|
||||
if !is_divrem {
|
||||
continue;
|
||||
}
|
||||
if instruction.get_type().into_int_type().get_bit_width() < 256 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lhs = instruction.get_operand(0).and_then(|op| op.value());
|
||||
let rhs = instruction.get_operand(1).and_then(|op| op.value());
|
||||
|
||||
if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
|
||||
let lhs_width = Self::provable_bit_width(lhs);
|
||||
let rhs_width = Self::provable_bit_width(rhs);
|
||||
|
||||
if let (Some(lw), Some(rw)) = (lhs_width, rhs_width) {
|
||||
let max_operand_width = lw.max(rw);
|
||||
let narrow_width = Self::round_up_bit_width(max_operand_width);
|
||||
// Signed div/rem narrowing is sound only when bit (narrow_width - 1) of every operand is provably zero — otherwise the
|
||||
// narrow signed division treats a non-negative i256 operand as negative and the sign-extended result diverges from the
|
||||
// i256 sdiv. The proof sources accepted here (constants, AND-masks, ZExt) all pin the upper bits to zero strictly above
|
||||
// the operand width, so this holds iff the operand fits with at least one bit of headroom in the narrow type.
|
||||
let is_signed = matches!(
|
||||
instruction.get_opcode(),
|
||||
InstructionOpcode::SDiv | InstructionOpcode::SRem
|
||||
);
|
||||
let signed_sign_bit_safe =
|
||||
!is_signed || narrow_width > max_operand_width;
|
||||
if narrow_width < 256 && signed_sign_bit_safe {
|
||||
to_narrow.push((instruction, narrow_width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (instruction, narrow_width) in to_narrow {
|
||||
let lhs = instruction
|
||||
.get_operand(0)
|
||||
.unwrap()
|
||||
.value()
|
||||
.unwrap()
|
||||
.into_int_value();
|
||||
let rhs = instruction
|
||||
.get_operand(1)
|
||||
.unwrap()
|
||||
.value()
|
||||
.unwrap()
|
||||
.into_int_value();
|
||||
let wide_type = instruction.get_type().into_int_type();
|
||||
let narrow_type = self
|
||||
.llvm
|
||||
.custom_width_int_type(
|
||||
std::num::NonZeroU32::new(narrow_width).expect("narrow width is non-zero"),
|
||||
)
|
||||
.expect("valid integer width");
|
||||
|
||||
builder.position_before(&instruction);
|
||||
|
||||
let lhs_truncated = builder.build_int_truncate(lhs, narrow_type, "").unwrap();
|
||||
let rhs_truncated = builder.build_int_truncate(rhs, narrow_type, "").unwrap();
|
||||
|
||||
let narrow_result = match instruction.get_opcode() {
|
||||
InstructionOpcode::UDiv => builder
|
||||
.build_int_unsigned_div(lhs_truncated, rhs_truncated, "")
|
||||
.unwrap(),
|
||||
InstructionOpcode::SDiv => builder
|
||||
.build_int_signed_div(lhs_truncated, rhs_truncated, "")
|
||||
.unwrap(),
|
||||
InstructionOpcode::URem => builder
|
||||
.build_int_unsigned_rem(lhs_truncated, rhs_truncated, "")
|
||||
.unwrap(),
|
||||
InstructionOpcode::SRem => builder
|
||||
.build_int_signed_rem(lhs_truncated, rhs_truncated, "")
|
||||
.unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let wide_result = if matches!(
|
||||
instruction.get_opcode(),
|
||||
InstructionOpcode::SDiv | InstructionOpcode::SRem
|
||||
) {
|
||||
builder
|
||||
.build_int_s_extend(narrow_result, wide_type, "")
|
||||
.unwrap()
|
||||
} else {
|
||||
builder
|
||||
.build_int_z_extend(narrow_result, wide_type, "")
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let wide_instruction = wide_result.as_instruction().unwrap();
|
||||
instruction.replace_all_uses_with(&wide_instruction);
|
||||
instruction.erase_from_basic_block();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the provable bit width of a value, if it can be determined.
|
||||
///
|
||||
/// Checks for:
|
||||
/// - Constants: bit width needed to represent the value.
|
||||
/// - `and %x, mask`: bit width of the mask.
|
||||
/// - `zext from smaller_type`: bit width of the source type.
|
||||
/// - `trunc to smaller_type`: bit width of the result type.
|
||||
fn provable_bit_width(value: inkwell::values::BasicValueEnum) -> Option<u32> {
|
||||
let integer_value = value.into_int_value();
|
||||
if integer_value.is_const() {
|
||||
return Self::constant_bit_width(integer_value);
|
||||
}
|
||||
|
||||
let instruction = integer_value.as_instruction()?;
|
||||
match instruction.get_opcode() {
|
||||
InstructionOpcode::And => {
|
||||
let operand_0 = instruction.get_operand(0)?.value()?.into_int_value();
|
||||
let operand_1 = instruction.get_operand(1)?.value()?.into_int_value();
|
||||
if operand_1.is_const() {
|
||||
Self::constant_bit_width(operand_1)
|
||||
} else if operand_0.is_const() {
|
||||
Self::constant_bit_width(operand_0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
InstructionOpcode::ZExt => {
|
||||
let source = instruction.get_operand(0)?.value()?.into_int_value();
|
||||
Some(source.get_type().get_bit_width())
|
||||
}
|
||||
InstructionOpcode::Trunc => {
|
||||
Some(instruction.get_type().into_int_type().get_bit_width())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum number of bits needed to represent a constant integer.
|
||||
///
|
||||
/// For types up to 64 bits the direct LLVM API suffices. For wider types (e.g. i256) we
|
||||
/// truncate to i64 and verify the round-trip: if the reconstructed wide constant equals the
|
||||
/// original (LLVM constants are interned, so pointer equality is correct), the value fits
|
||||
/// in u64 and we report `64 - leading_zeros`.
|
||||
fn constant_bit_width(integer_value: inkwell::values::IntValue) -> Option<u32> {
|
||||
if let Some(value) = integer_value.get_zero_extended_constant() {
|
||||
return Some(if value == 0 {
|
||||
1
|
||||
} else {
|
||||
64 - value.leading_zeros()
|
||||
});
|
||||
}
|
||||
|
||||
let wide_type = integer_value.get_type();
|
||||
if wide_type.get_bit_width() > 64 {
|
||||
let i64_type = wide_type.get_context().i64_type();
|
||||
let truncated = integer_value.const_truncate(i64_type);
|
||||
if let Some(value) = truncated.get_zero_extended_constant() {
|
||||
let reconstructed = wide_type.const_int(value, false);
|
||||
if reconstructed == integer_value {
|
||||
return Some(if value == 0 {
|
||||
1
|
||||
} else {
|
||||
64 - value.leading_zeros()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Rounds a bit width up to the next standard integer type width.
|
||||
fn round_up_bit_width(bits: u32) -> u32 {
|
||||
if bits <= 8 {
|
||||
8
|
||||
} else if bits <= 16 {
|
||||
16
|
||||
} else if bits <= 32 {
|
||||
32
|
||||
} else if bits <= 64 {
|
||||
64
|
||||
} else if bits <= 128 {
|
||||
128
|
||||
} else {
|
||||
256
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
//! The revive simulated EVM linear memory pointer functions.
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use inkwell::values::BasicValueEnum;
|
||||
|
||||
use revive_common::BYTE_LENGTH_BYTE;
|
||||
use revive_common::BYTE_LENGTH_WORD;
|
||||
use revive_common::BYTE_LENGTH_X64;
|
||||
|
||||
use crate::polkavm::context::attribute::MemoryEffect;
|
||||
use crate::polkavm::context::runtime::RuntimeFunction;
|
||||
use crate::polkavm::context::Context;
|
||||
use crate::polkavm::WriteLLVM;
|
||||
|
||||
/// Load a word size value from a heap pointer.
|
||||
/// Uses 4x 64-bit loads with bswap for efficient byte-order conversion.
|
||||
pub struct LoadWord;
|
||||
|
||||
impl RuntimeFunction for LoadWord {
|
||||
@@ -21,6 +26,282 @@ impl RuntimeFunction for LoadWord {
|
||||
.fn_type(&[context.xlen_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<BasicValueEnum<'ctx>>> {
|
||||
let offset = Self::paramater(context, 0).into_int_value();
|
||||
let length = context
|
||||
.xlen_type()
|
||||
.const_int(BYTE_LENGTH_WORD as u64, false);
|
||||
let pointer = context.build_heap_gep(offset, length)?;
|
||||
let result = build_efficient_load_swap(context, pointer.value)?;
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for LoadWord {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a word size value through a heap pointer.
|
||||
/// Uses 4x 64-bit stores with bswap for efficient byte-order conversion.
|
||||
pub struct StoreWord;
|
||||
|
||||
impl RuntimeFunction for StoreWord {
|
||||
const NAME: &'static str = "__revive_store_heap_word";
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.void_type().fn_type(
|
||||
&[context.xlen_type().into(), context.word_type().into()],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<BasicValueEnum<'ctx>>> {
|
||||
let offset = Self::paramater(context, 0).into_int_value();
|
||||
let length = context
|
||||
.xlen_type()
|
||||
.const_int(BYTE_LENGTH_WORD as u64, false);
|
||||
let pointer = context.build_heap_gep(offset, length)?;
|
||||
let value = Self::paramater(context, 1).into_int_value();
|
||||
build_efficient_store_swap(context, pointer.value, value)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an efficient 256-bit load with byte-swap using 4x 64-bit operations.
|
||||
///
|
||||
/// This is much more efficient than LLVM's default lowering of llvm.bswap.i256
|
||||
/// which generates byte-by-byte loads. We instead:
|
||||
/// 1. Load 4x 64-bit values
|
||||
/// 2. Byte-swap each 64-bit value using llvm.bswap.i64
|
||||
/// 3. Combine them in reversed word order to form the 256-bit result
|
||||
///
|
||||
/// The unrolled form only pays off with the `+unaligned-scalar-mem` target feature,
|
||||
/// which is enabled for newyork modules exclusively. Without it the backend splits
|
||||
/// each 64-bit access into byte operations, producing larger and slower code than
|
||||
/// the single word load it replaces, so the stock Yul path keeps the legacy
|
||||
/// `llvm.bswap.i256` lowering it was validated against.
|
||||
pub(crate) fn build_efficient_load_swap<'ctx>(
|
||||
context: &Context<'ctx>,
|
||||
pointer: inkwell::values::PointerValue<'ctx>,
|
||||
) -> anyhow::Result<BasicValueEnum<'ctx>> {
|
||||
if !context.is_newyork() {
|
||||
let value = context
|
||||
.builder()
|
||||
.build_load(context.word_type(), pointer, "value")?;
|
||||
context
|
||||
.basic_block()
|
||||
.get_last_instruction()
|
||||
.expect("ICE: load instruction always exists")
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("ICE: alignment is valid");
|
||||
return context.build_byte_swap(value);
|
||||
}
|
||||
let i64_type = context
|
||||
.llvm()
|
||||
.custom_width_int_type(NonZeroU32::new(64).expect("const is non-zero"))
|
||||
.expect("valid integer width");
|
||||
let i8_type = context
|
||||
.llvm()
|
||||
.custom_width_int_type(NonZeroU32::new(8).expect("const is non-zero"))
|
||||
.expect("valid integer width");
|
||||
let word_type = context.word_type();
|
||||
|
||||
let bswap64 = inkwell::intrinsics::Intrinsic::find("llvm.bswap.i64")
|
||||
.expect("ICE: llvm.bswap.i64 intrinsic exists");
|
||||
let bswap64_function = bswap64
|
||||
.get_declaration(context.module(), &[i64_type.into()])
|
||||
.expect("ICE: bswap.i64 declaration");
|
||||
|
||||
let mut swapped_x64_values = Vec::with_capacity(4);
|
||||
for index in 0..4 {
|
||||
let gep_offset = context
|
||||
.xlen_type()
|
||||
.const_int(index * BYTE_LENGTH_X64 as u64, false);
|
||||
let byte_pointer = unsafe {
|
||||
context.builder().build_gep(
|
||||
i8_type,
|
||||
pointer,
|
||||
&[gep_offset],
|
||||
&format!("byte_pointer_{index}"),
|
||||
)?
|
||||
};
|
||||
let x64_value = context
|
||||
.builder()
|
||||
.build_load(i64_type, byte_pointer, &format!("x64_value_{index}"))?
|
||||
.into_int_value();
|
||||
context
|
||||
.basic_block()
|
||||
.get_last_instruction()
|
||||
.expect("ICE: load instruction always exists")
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("ICE: alignment is valid");
|
||||
|
||||
let swapped_x64_value = context
|
||||
.builder()
|
||||
.build_call(
|
||||
bswap64_function,
|
||||
&[x64_value.into()],
|
||||
&format!("swapped_x64_value_{index}"),
|
||||
)?
|
||||
.try_as_basic_value()
|
||||
.unwrap_basic()
|
||||
.into_int_value();
|
||||
swapped_x64_values.push(swapped_x64_value);
|
||||
}
|
||||
|
||||
// Combine in reversed register order: chunks 3, 2, 1, 0 from most to
|
||||
// least significant. This achieves the full big-endian to little-endian
|
||||
// conversion of the 256-bit word.
|
||||
let mut result =
|
||||
context
|
||||
.builder()
|
||||
.build_int_z_extend(swapped_x64_values[3], word_type, "ext_0")?;
|
||||
for (index, &x64_value) in swapped_x64_values.iter().rev().skip(1).enumerate() {
|
||||
let extended = context.builder().build_int_z_extend(
|
||||
x64_value,
|
||||
word_type,
|
||||
&format!("ext_{}", index + 1),
|
||||
)?;
|
||||
let shift_amount = word_type.const_int(64 * (index + 1) as u64, false);
|
||||
let shifted = context.builder().build_left_shift(
|
||||
extended,
|
||||
shift_amount,
|
||||
&format!("shift_{}", index + 1),
|
||||
)?;
|
||||
result = context
|
||||
.builder()
|
||||
.build_or(result, shifted, &format!("or_{}", index + 1))?;
|
||||
}
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
/// Builds an efficient 256-bit store with byte-swap using 4x 64-bit operations.
|
||||
///
|
||||
/// This is much more efficient than LLVM's default lowering of llvm.bswap.i256
|
||||
/// which generates byte-by-byte stores. We instead:
|
||||
/// 1. Extract 4x 64-bit values from the 256-bit word
|
||||
/// 2. Byte-swap each 64-bit value using llvm.bswap.i64
|
||||
/// 3. Store them in reversed word order
|
||||
///
|
||||
/// As with [`build_efficient_load_swap`], the unrolled form requires the
|
||||
/// `+unaligned-scalar-mem` target feature and is therefore reserved for newyork
|
||||
/// modules; the stock Yul path keeps the legacy `llvm.bswap.i256` lowering.
|
||||
pub(crate) fn build_efficient_store_swap<'ctx>(
|
||||
context: &Context<'ctx>,
|
||||
pointer: inkwell::values::PointerValue<'ctx>,
|
||||
value: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !context.is_newyork() {
|
||||
let swapped_value = context.build_byte_swap(value.into())?;
|
||||
context
|
||||
.builder()
|
||||
.build_store(pointer, swapped_value)?
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("ICE: alignment is valid");
|
||||
return Ok(());
|
||||
}
|
||||
let i64_type = context
|
||||
.llvm()
|
||||
.custom_width_int_type(NonZeroU32::new(64).expect("const is non-zero"))
|
||||
.expect("valid integer width");
|
||||
let i8_type = context
|
||||
.llvm()
|
||||
.custom_width_int_type(NonZeroU32::new(8).expect("const is non-zero"))
|
||||
.expect("valid integer width");
|
||||
let word_type = context.word_type();
|
||||
|
||||
let bswap64 = inkwell::intrinsics::Intrinsic::find("llvm.bswap.i64")
|
||||
.expect("ICE: llvm.bswap.i64 intrinsic exists");
|
||||
let bswap64_function = bswap64
|
||||
.get_declaration(context.module(), &[i64_type.into()])
|
||||
.expect("ICE: bswap.i64 declaration");
|
||||
|
||||
for index in 0..4 {
|
||||
let shift_amount = word_type.const_int(64 * index as u64, false);
|
||||
let shifted = context.builder().build_right_shift(
|
||||
value,
|
||||
shift_amount,
|
||||
false,
|
||||
&format!("shift_{index}"),
|
||||
)?;
|
||||
let x64_value =
|
||||
context
|
||||
.builder()
|
||||
.build_int_truncate(shifted, i64_type, &format!("trunc_{index}"))?;
|
||||
|
||||
let swapped_x64_value = context
|
||||
.builder()
|
||||
.build_call(
|
||||
bswap64_function,
|
||||
&[x64_value.into()],
|
||||
&format!("swap_x64_value_{index}"),
|
||||
)?
|
||||
.try_as_basic_value()
|
||||
.unwrap_basic()
|
||||
.into_int_value();
|
||||
|
||||
// Reverse the position so the least-significant chunk lands at the
|
||||
// highest byte offset; this performs the big-endian byte order on
|
||||
// the 256-bit word as a whole.
|
||||
let store_byte_offset = (3 - index) * BYTE_LENGTH_X64;
|
||||
let gep_offset = context
|
||||
.xlen_type()
|
||||
.const_int(store_byte_offset as u64, false);
|
||||
let byte_pointer = unsafe {
|
||||
context.builder().build_gep(
|
||||
i8_type,
|
||||
pointer,
|
||||
&[gep_offset],
|
||||
&format!("store_pointer_{index}"),
|
||||
)?
|
||||
};
|
||||
let store_instruction = context
|
||||
.builder()
|
||||
.build_store(byte_pointer, swapped_x64_value)?;
|
||||
store_instruction
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("ICE: alignment is valid");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl WriteLLVM for StoreWord {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a word size value from a heap pointer without byte-swapping.
|
||||
/// Used for internal memory operations that don't escape to external code.
|
||||
pub struct LoadWordNative;
|
||||
|
||||
impl RuntimeFunction for LoadWordNative {
|
||||
const NAME: &'static str = "__revive_load_heap_word_native";
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context
|
||||
.word_type()
|
||||
.fn_type(&[context.xlen_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
@@ -36,16 +317,14 @@ impl RuntimeFunction for LoadWord {
|
||||
context
|
||||
.basic_block()
|
||||
.get_last_instruction()
|
||||
.expect("Always exists")
|
||||
.expect("ICE: load instruction always exists")
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("Alignment is valid");
|
||||
|
||||
let swapped_value = context.build_byte_swap(value)?;
|
||||
Ok(Some(swapped_value))
|
||||
.expect("ICE: alignment is valid");
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for LoadWord {
|
||||
impl WriteLLVM for LoadWordNative {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
@@ -55,11 +334,12 @@ impl WriteLLVM for LoadWord {
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a word size value through a heap pointer.
|
||||
pub struct StoreWord;
|
||||
/// Store a word size value through a heap pointer without byte-swapping.
|
||||
/// Used for internal memory operations that don't escape to external code.
|
||||
pub struct StoreWordNative;
|
||||
|
||||
impl RuntimeFunction for StoreWord {
|
||||
const NAME: &'static str = "__revive_store_heap_word";
|
||||
impl RuntimeFunction for StoreWordNative {
|
||||
const NAME: &'static str = "__revive_store_heap_word_native";
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.void_type().fn_type(
|
||||
@@ -77,19 +357,178 @@ impl RuntimeFunction for StoreWord {
|
||||
.xlen_type()
|
||||
.const_int(BYTE_LENGTH_WORD as u64, false);
|
||||
let pointer = context.build_heap_gep(offset, length)?;
|
||||
|
||||
let value = context.build_byte_swap(Self::paramater(context, 1))?;
|
||||
let value = Self::paramater(context, 1);
|
||||
|
||||
context
|
||||
.builder()
|
||||
.build_store(pointer.value, value)?
|
||||
.set_alignment(BYTE_LENGTH_BYTE as u32)
|
||||
.expect("Alignment is valid");
|
||||
.expect("ICE: alignment is valid");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for StoreWord {
|
||||
impl WriteLLVM for StoreWordNative {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Keccak256 hash of one 256-bit word from scratch memory.
|
||||
/// Equivalent to: mstore(0, word0); sha3(0, 32)
|
||||
/// Deduplicated into a single function to reduce code size for storage slot lookups.
|
||||
pub struct Keccak256OneWord;
|
||||
|
||||
impl Keccak256OneWord {
|
||||
/// The function name.
|
||||
pub const NAME: &'static str = "__revive_keccak256_one_word";
|
||||
}
|
||||
|
||||
impl RuntimeFunction for Keccak256OneWord {
|
||||
const NAME: &'static str = "__revive_keccak256_one_word";
|
||||
|
||||
const ATTRIBUTES: &'static [crate::polkavm::context::Attribute] = &[
|
||||
crate::polkavm::context::Attribute::NoFree,
|
||||
crate::polkavm::context::Attribute::NoRecurse,
|
||||
crate::polkavm::context::Attribute::WillReturn,
|
||||
crate::polkavm::context::Attribute::NoInline,
|
||||
];
|
||||
|
||||
/// Writes the input back to heap[0..32] (matching EVM's
|
||||
/// `mstore(0, word0); keccak256(0, 32)`), then hashes from there.
|
||||
/// This lets `mem_opt`'s keccak fusion safely dead-eliminate the
|
||||
/// preceding `mstore(0, k)` — a later `mload(0)` whose mem_opt
|
||||
/// forwarding has been invalidated (e.g., by an intervening
|
||||
/// external call) reads the helper's heap write and gets the
|
||||
/// same value EVM would. The helper is no longer
|
||||
/// `MemoryEffect::None`; `Unrestricted` gives LLVM accurate
|
||||
/// information about its memory effects.
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::Unrestricted;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context
|
||||
.word_type()
|
||||
.fn_type(&[context.word_type().into()], false)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<BasicValueEnum<'ctx>>> {
|
||||
let word0 = Self::paramater(context, 0).into_int_value();
|
||||
|
||||
// Mirror EVM's `mstore(0, word0)`: byte-swap and store to heap[0..32].
|
||||
let zero_xlen = context.xlen_type().const_zero();
|
||||
crate::polkavm::evm::memory::store_bswap_unchecked(context, zero_xlen, word0)?;
|
||||
|
||||
let length = context
|
||||
.xlen_type()
|
||||
.const_int(BYTE_LENGTH_WORD as u64, false);
|
||||
|
||||
let output_pointer = context.build_alloca_at_entry(context.word_type(), "output_pointer");
|
||||
let input_pointer = context.build_heap_gep_unchecked(zero_xlen)?;
|
||||
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::HASH_KECCAK_256,
|
||||
&[
|
||||
input_pointer.to_int(context).into(),
|
||||
length.into(),
|
||||
output_pointer.to_int(context).into(),
|
||||
],
|
||||
);
|
||||
|
||||
let result = context.build_byte_swap(context.build_load(output_pointer, "sha3_output")?)?;
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for Keccak256OneWord {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Keccak256 hash of two 256-bit words from scratch memory.
|
||||
/// Equivalent to: mstore(0, word0); mstore(32, word1); sha3(0, 64)
|
||||
/// Deduplicated into a single function to reduce code size for mapping lookups.
|
||||
pub struct Keccak256TwoWords;
|
||||
|
||||
impl Keccak256TwoWords {
|
||||
/// The function name.
|
||||
pub const NAME: &'static str = "__revive_keccak256_two_words";
|
||||
}
|
||||
|
||||
impl RuntimeFunction for Keccak256TwoWords {
|
||||
const NAME: &'static str = "__revive_keccak256_two_words";
|
||||
|
||||
const ATTRIBUTES: &'static [crate::polkavm::context::Attribute] = &[
|
||||
crate::polkavm::context::Attribute::NoFree,
|
||||
crate::polkavm::context::Attribute::NoRecurse,
|
||||
crate::polkavm::context::Attribute::WillReturn,
|
||||
crate::polkavm::context::Attribute::NoInline,
|
||||
];
|
||||
|
||||
/// Writes the inputs back to heap[0..64] (matching EVM's
|
||||
/// `mstore(0, word0); mstore(32, word1); keccak256(0, 64)`), then
|
||||
/// hashes from there. This lets `mem_opt`'s keccak fusion safely
|
||||
/// dead-eliminate the preceding mstores — a later `mload` at offset
|
||||
/// 0 or 32 whose mem_opt forwarding has been invalidated reads the
|
||||
/// helper's heap writes and gets the same value EVM would.
|
||||
const MEMORY_EFFECT: MemoryEffect = MemoryEffect::Unrestricted;
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.word_type().fn_type(
|
||||
&[context.word_type().into(), context.word_type().into()],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx>,
|
||||
) -> anyhow::Result<Option<BasicValueEnum<'ctx>>> {
|
||||
let word0 = Self::paramater(context, 0).into_int_value();
|
||||
let word1 = Self::paramater(context, 1).into_int_value();
|
||||
|
||||
// Mirror EVM's `mstore(0, word0); mstore(32, word1)`: byte-swap
|
||||
// and store both words to heap[0..64].
|
||||
let zero_xlen = context.xlen_type().const_zero();
|
||||
let word_xlen = context
|
||||
.xlen_type()
|
||||
.const_int(BYTE_LENGTH_WORD as u64, false);
|
||||
crate::polkavm::evm::memory::store_bswap_unchecked(context, zero_xlen, word0)?;
|
||||
crate::polkavm::evm::memory::store_bswap_unchecked(context, word_xlen, word1)?;
|
||||
|
||||
let length = context
|
||||
.xlen_type()
|
||||
.const_int(2 * BYTE_LENGTH_WORD as u64, false);
|
||||
|
||||
let output_pointer = context.build_alloca_at_entry(context.word_type(), "output_pointer");
|
||||
let input_pointer = context.build_heap_gep_unchecked(zero_xlen)?;
|
||||
|
||||
context.build_runtime_call(
|
||||
revive_runtime_api::polkavm_imports::HASH_KECCAK_256,
|
||||
&[
|
||||
input_pointer.to_int(context).into(),
|
||||
length.into(),
|
||||
output_pointer.to_int(context).into(),
|
||||
],
|
||||
);
|
||||
|
||||
let result = context.build_byte_swap(context.build_load(output_pointer, "sha3_output")?)?;
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteLLVM for Keccak256TwoWords {
|
||||
fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction>::declare(self, context)
|
||||
}
|
||||
|
||||
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user