Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ jobs:
run: make build-release
# Builds node binary AND runtime WASM (no need to rebuild separately)

- name: Check the PolkaVM guest ABI
run: |
# `frame/evm-polkavm/uapi` compiles its host-function ABI under
# `cfg(target_arch = "riscv64")`. Nothing else in the workspace builds for that
# target, so this half went unchecked from the commit that added it until
# someone tried by hand — and it did not compile. The target is installed here
# rather than in rust-toolchain.toml so a normal `cargo build` does not pay for
# a ~100 MB std nobody else needs.
rustup target add riscv64imac-unknown-none-elf
cargo check -p pallet-evm-polkavm-uapi \
--target riscv64imac-unknown-none-elf

- name: Upload node binary
uses: actions/upload-artifact@v4
with:
Expand Down
56 changes: 9 additions & 47 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frame/evm-polkavm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pallet-evm = { workspace = true }
# PolkaVM executor
pallet-evm-polkavm-proc-macro = { workspace = true }
pallet-evm-polkavm-uapi = { workspace = true, features = ["scale"] }
polkavm = { version = "0.29.1", default-features = false }
polkavm = { version = "0.33.1", default-features = false }

[features]
default = ["std"]
Expand Down
20 changes: 17 additions & 3 deletions frame/evm-polkavm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl<Inner: PrecompileSet, T: Config> PrecompileSet for PolkaVmSet<Inner, T> {
) -> Option<Result<PrecompileOutput, PrecompileFailure>> {
let code_address = handle.code_address();
let code = pallet_evm::AccountCodes::<T>::get(code_address);
if code[0..8] == vm::PREFIX {
if code.get(0..8) == Some(&vm::PREFIX[..]) {
let mut run = || {
let prepared_call: vm::PreparedCall<'_, T, _> = vm::PreparedCall::load(handle)?;
prepared_call.call()
Expand Down Expand Up @@ -90,7 +90,7 @@ impl<Inner: PrecompileSet, T: Config> PrecompileSet for PolkaVmSet<Inner, T> {

fn is_precompile(&self, address: H160, remaining_gas: u64) -> IsPrecompileResult {
let code = pallet_evm::AccountCodes::<T>::get(address);
if code[0..8] == vm::PREFIX {
if code.get(0..8) == Some(&vm::PREFIX[..]) {
IsPrecompileResult::Answer {
is_precompile: true,
extra_cost: 0,
Expand Down Expand Up @@ -134,6 +134,14 @@ pub mod pallet {
NotPolkaVmContract,
/// Contract already exist in state.
AlreadyExist,
/// The code is not a parseable PolkaVM program blob.
InvalidProgramBlob,
/// The blob declares an instruction set this chain does not accept.
///
/// Only `ReviveV1` and `JamV1` are allowed. The `Latest32`/`Latest64` sets include
/// the `sbrk` opcode, which lets a contract grow its heap at run time — memory
/// growth changes gas consumption, and this is consensus code.
UnsupportedInstructionSet,
}

#[pallet::call]
Expand All @@ -150,10 +158,16 @@ pub mod pallet {
return Err(Error::<T>::MaxCodeSizeExceeded.into());
}

if code[0..8] != crate::vm::PREFIX {
if code.get(0..8) != Some(&crate::vm::PREFIX[..]) {
return Err(Error::<T>::NotPolkaVmContract.into());
}

let blob = polkavm::ProgramBlob::parse(code[8..].to_vec().into())
.map_err(|_| Error::<T>::InvalidProgramBlob)?;
if !crate::vm::is_accepted_isa(blob.isa()) {
return Err(Error::<T>::UnsupportedInstructionSet.into());
}

let caller = ensure_signed(origin)?;
let address =
<T as Config>::CreateAddressScheme::create_address_scheme(caller, &code[..], salt);
Expand Down
111 changes: 110 additions & 1 deletion frame/evm-polkavm/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ pub use self::runtime::{ExecResult, Runtime, RuntimeCosts, SupervisorError};
pub const PREFIX: [u8; 8] = [0xef, 0x70, 0x6F, 0x6C, 0x6B, 0x61, 0x76, 0x6D];
pub const CALL_IDENTIFIER: &str = "call";
pub const PAGE_SIZE: u32 = 4 * 1024;

/// Whether this chain accepts contracts compiled for `isa`.
///
/// Until polkavm 0.30 the host refused `sbrk` with `ModuleConfig::set_allow_sbrk(false)`.
/// That knob is gone: the blob's own instruction set now decides whether the opcode
/// decodes, and the linker takes the ISA as an explicit argument, so the choice belongs
/// to whoever built the contract. `Latest32`/`Latest64` include `sbrk` (opcode 101);
/// `ReviveV1` and `JamV1` do not.
///
/// A growable heap changes gas consumption, and this runs in consensus, so the answer is
/// a whitelist rather than a blocklist: a future ISA is rejected until someone checks it.
pub fn is_accepted_isa(isa: polkavm::program::InstructionSetKind) -> bool {
use polkavm::program::InstructionSetKind as Isa;
match isa {
Isa::ReviveV1 | Isa::JamV1 => true,
Isa::Latest32 | Isa::Latest64 => false,
}
}

pub const SENTINEL: u32 = u32::MAX;
pub const LOG_TARGET: &str = "runtime::evm::polkavm";

Expand Down Expand Up @@ -68,7 +87,6 @@ impl<'a, T: Config, H: PrecompileHandle> PreparedCall<'a, T, H> {
let mut module_config = polkavm::ModuleConfig::new();
module_config.set_page_size(PAGE_SIZE);
module_config.set_gas_metering(Some(polkavm::GasMeteringKind::Sync));
module_config.set_allow_sbrk(false);
let module =
polkavm::Module::new(&engine, &module_config, polkavm_code.into()).map_err(|err| {
log::debug!(target: LOG_TARGET, "failed to create polkavm module: {err:?}");
Expand Down Expand Up @@ -116,3 +134,94 @@ impl<'a, T: Config, H: PrecompileHandle> PreparedCall<'a, T, H> {
exec_result
}
}

#[cfg(test)]
mod tests {
use super::*;
use polkavm::program::InstructionSetKind as Isa;

/// Smallest blob `ProgramBlob::parse` accepts, with the ISA we want to declare.
///
/// Hand-built rather than linked from a guest program: the ISA lives in one header
/// byte, so a real riscv64 contract would add a toolchain dependency and prove
/// nothing extra about the check under test.
fn blob_with_isa(isa: Isa) -> Vec<u8> {
let version: u8 = match isa {
Isa::ReviveV1 => 0,
Isa::Latest32 => 1,
Isa::Latest64 => 2,
Isa::JamV1 => 3,
};

// jump_table_entry_count, jump_table_entry_size, code_length, code, bitmask.
// The bitmask is ceil(code_len / 8) bytes and marks instruction boundaries.
let code_section: Vec<u8> = vec![0, 0, 1, 0, 0b0000_0001];
let mut body = vec![6u8]; // SECTION_CODE_AND_JUMP_TABLE
body.push(code_section.len() as u8);
body.extend_from_slice(&code_section);
body.push(0u8); // SECTION_END_OF_FILE

// magic + version + u64 length-of-whole-blob + body
let total = (4 + 1 + 8 + body.len()) as u64;
let mut blob = vec![b'P', b'V', b'M', 0u8, version];
blob.extend_from_slice(&total.to_le_bytes());
blob.extend_from_slice(&body);
blob
}

/// The blobs the test feeds the check must actually declare the ISA asked for,
/// otherwise the cases below would pass by accident.
#[test]
fn fixture_declares_the_requested_isa() {
for isa in [Isa::ReviveV1, Isa::JamV1, Isa::Latest32, Isa::Latest64] {
let parsed = polkavm::ProgramBlob::parse(blob_with_isa(isa).into())
.expect("hand-built blob should parse");
assert_eq!(parsed.isa(), isa);
}
}

/// This is the guarantee `ModuleConfig::set_allow_sbrk(false)` used to provide: no
/// contract may grow its heap. `Latest32`/`Latest64` carry the `sbrk` opcode, so
/// accepting them would silently restore what the old knob forbade.
#[test]
fn only_sbrk_free_instruction_sets_are_accepted() {
assert!(is_accepted_isa(Isa::ReviveV1));
assert!(is_accepted_isa(Isa::JamV1));
assert!(!is_accepted_isa(Isa::Latest32));
assert!(!is_accepted_isa(Isa::Latest64));
}

/// The prefix test must tolerate short and empty code.
///
/// `AccountCodes::get` returns an empty `Vec` for every address without contract
/// code, and the precompile set consults it on each call, so `code[0..8]` panics on
/// the most ordinary path there is: a transfer to a plain account.
#[test]
fn prefix_check_tolerates_code_shorter_than_the_prefix() {
let matches = |code: &[u8]| code.get(0..8) == Some(&PREFIX[..]);

assert!(!matches(&[]));
assert!(!matches(&PREFIX[..7]));
assert!(matches(&PREFIX));

let mut with_body = PREFIX.to_vec();
with_body.extend_from_slice(b"blob");
assert!(matches(&with_body));

let mut wrong = PREFIX;
wrong[0] = 0x00;
assert!(!matches(&wrong));
}

/// A blob that does not parse must be rejected before the ISA is consulted — the
/// deploy path calls `parse` first and maps the failure to its own error.
#[test]
fn malformed_blobs_do_not_parse() {
assert!(polkavm::ProgramBlob::parse(vec![].into()).is_err());
assert!(polkavm::ProgramBlob::parse(b"PVM\0".to_vec().into()).is_err());
// Right shape, unknown ISA version byte.
let mut bad = blob_with_isa(Isa::ReviveV1);
bad[4] = 99;
assert!(polkavm::ProgramBlob::parse(bad.into()).is_err());
}
}
Loading
Loading