Skip to content

newyork: Terminate object code with the implicit EVM return - #598

Open
dimartiro wants to merge 2 commits into
paritytech:mainfrom
dimartiro:fix/newyork-implicit-code-return
Open

newyork: Terminate object code with the implicit EVM return#598
dimartiro wants to merge 2 commits into
paritytech:mainfrom
dimartiro:fix/newyork-implicit-code-return

Conversation

@dimartiro

Copy link
Copy Markdown

Description

--newyork crashes in the linker on a Yul object whose _deployed runtime code block is empty:

object "T" {
    code { stop() }
    object "T_deployed" {
        code { }
    }
}
thread 'main' panicked at polkavm-linker-0.35.0/src/program_from_elf.rs:10396:
internal error: inconsistent reachability after optimization; this is a bug, please report it!

The default pipeline compiles the same input fine.

Cause

The panic is fallout. The actual problem is a silent miscompile one step earlier.

Code::into_llvm in the legacy pipeline terminates every code block with an implicit stop
("The EVM lets the code return implicitly"). LlvmCodegen::generate_object in newyork does not,
so a block that falls off the end reaches the

define private void @__runtime() {
entry:
  br label %return
return:
  ret void        ; <-- __runtime returns
}

__entry is marked NoReturn and puts unreatwo calls. A __runtimethat returns therefore makes theis_deploy ==d LLVM removes the
dispatch entirely:

define void @__entry(i1 %0) {
entry:
  tail call fastcc void @__deploy()
  unreachable
}

Calling the deployed contract would run the constructor. The linker then sees exports whose
reachability no longer agrees and asserts.

Fix

Append the implicit EVM return in generate_obcy pipeline does. The runtime function then ends in the exit syscall and keeps its noreturn` shape, so the dispatch
edge stays live:

define private void @__runtime() {
entry:
  call void @__revive_exit(i32 0, i256 0, i256 0)
  br label %return
return:
  ret void        ; now unreachable, cleaned up by LLVM
}
define void @__entry(i1 %0) {
entry:
  br i1 %0, label %deploy_code_call_block, label %runtime_code_call_block
  ...
}

Verification

input default --newyork before --newyork after
_deployed with an empty block 💥 linker assert
_deployed with stop()

Across crates/integration/contracts/*.sol:

  • default pipeline: 83/83 byte-identical, as expected since this only touches newyork.

  • --newyork: 82/83 byte-identical. The ostruct.sol, and it changes in the right direction. selfdestructlowers to aterminatecall that neither terminates the basic block nor is markednoreturn, so itsfall through and return into __entry's unreachable`. It now ends peline already ends it:

      call void @terminate(...)
      call void @seal_return(i32 0, i32 ptrtoint2), i32 0)
      unreachable

    So the diff is newyork converging onto the which is the point of the
    change.

Test added: compiles_newyork_empty_runtime_obpty_runtime_object.yul
fixture through --yul --newyork --bin, mirroring the existing
compiles_newyork_folded_guard_inlined_loop r

cargo test -p revive-newyork 92 passed, cargo test -p resolc --lib 69 passed, clippy clean.

Note

Found while working on #490, but independent of it: #490 is an object with no _deployed sub-object at all, which fails in both pipelines with an LLVM verifier error. This one is an object with a _deployed sub-object whoseaffects --newyork. Neither fix depends on the other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant