Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

Supported `polkadot-sdk` rev: `2604.2.0`

### Added
- Contracts exceeding the target runtime's deployment limits are now detected at link time instead of failing on deployment. The budget is configurable via `--memory-limit` and the check can be downgraded to a warning with `--ignore-memory-limit`. [#599](https://github.com/paritytech/revive/pull/599)

### Fixed
- `--newyork`: A bug in dead call value analysis. [#589](https://github.com/paritytech/revive/pull/589)

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/linker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ description = "revive compiler linker utils"
[dependencies]
anyhow = { workspace = true }
libc = { workspace = true }
polkavm-common = { workspace = true }
polkavm-linker = { workspace = true }
tempfile = { workspace = true }

Expand Down
1 change: 1 addition & 0 deletions crates/linker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! The revive ELF object to PVM blob linker library.

pub mod elf;
pub mod limits;
pub mod pvm;
239 changes: 239 additions & 0 deletions crates/linker/src/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//! Deployment limit checks for linked PVM blobs.
//!
//! `pallet_revive` rejects a contract at deployment time if its blob is too large, contains a
//! basic block that is too long, or needs more interpreter memory than the runtime is willing to
//! hold. Those checks live in `pallet_revive::limits::code::enforce`. Failing them means the
//! contract compiles fine but can never be deployed, which is a poor thing to find out after the
//! fact, so this module reproduces them at link time.
//!
//! The estimates are not approximated: the pallet derives its memory numbers from
//! [`ProgramBlob::estimate_interpreter_memory_usage`], and so do we, from the same crate and with
//! the same arguments. What the compiler cannot know is which runtime the blob is destined for,
//! hence [`PalletLimits`] is configurable and merely defaults to Asset Hub.

use polkavm_common::program::{
EstimateInterpreterMemoryUsageArgs, ISA_ReviveV1, InstructionSetKind, ProgramBlob,
};

/// The deployment limits of a target runtime.
///
/// Mirrors the constants in `pallet_revive::limits`. Different chains, or the same chain at a
/// different runtime version, can carry different values.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PalletLimits {
/// `limits::code::BLOB_BYTES`: the maximum length of a code blob.
pub blob_bytes: u32,
/// `limits::code::BASIC_BLOCK_SIZE`: the maximum basic block length in instructions.
pub basic_block_size: u32,
/// `limits::code::INTERPRETER_CACHE_BYTES`: the interpreter compilation artifact budget.
pub interpreter_cache_bytes: u32,
/// `limits::code::PURGABLE_MEMORY_LIMIT`: memory that is dropped when calling out.
pub purgable_memory_limit: u32,
/// `limits::code::BASELINE_MEMORY_LIMIT`: memory held for the contract's whole life time.
pub baseline_memory_limit: u32,
/// `limits::PAGE_SIZE`.
pub page_size: u32,
}

impl PalletLimits {
/// The limits of the Asset Hub runtime.
pub const ASSET_HUB: Self = Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of duplicating code here, we should re-use this from pallet-revive directly. It's already in the dependency tree.

blob_bytes: 1024 * 1024,
basic_block_size: 1000,
interpreter_cache_bytes: 1024 * 1024,
purgable_memory_limit: 1024 * 1024 + 2 * 1024 * 1024,
baseline_memory_limit: 1024 * 1024 + 512 * 1024,
page_size: 4 * 1024,
};
}

impl Default for PalletLimits {
fn default() -> Self {
Self::ASSET_HUB
}
}

/// How the compiler reacts to a blob that would be rejected at deployment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Enforcement {
/// Fail the compilation. A contract that cannot be deployed is not a useful artifact.
#[default]
Deny,
/// Report the violation but emit the contract anyway.
Warn,
}

/// The deployment limit configuration a build is checked against.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DeploymentLimits {
/// The limits of the target runtime.
pub limits: PalletLimits,
/// What to do when they are exceeded.
pub enforcement: Enforcement,
}

impl DeploymentLimits {
/// A shorthand constructor.
pub fn new(baseline_memory_limit: u32, enforcement: Enforcement) -> Self {
Self {
limits: PalletLimits {
baseline_memory_limit,
..PalletLimits::ASSET_HUB
},
enforcement,
}
}
}

/// A reason the linked blob would be rejected at deployment.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Violation {
/// The blob is longer than the runtime accepts.
BlobTooLarge { size: u32, limit: u32 },
/// A basic block exceeds the maximum instruction count.
BasicBlockTooLarge { size: u32, limit: u32 },
/// The interpreter would need too much purgeable memory.
PurgeableMemoryTooLarge { size: u32, limit: u32 },
/// The interpreter would need too much memory for the contract's whole life time.
BaselineMemoryTooLarge { size: u32, limit: u32 },
}

impl core::fmt::Display for Violation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::BlobTooLarge { size, limit } => write!(
f,
"the contract code is {size} bytes but the runtime accepts at most {limit} bytes"
),
Self::BasicBlockTooLarge { size, limit } => write!(
f,
"the contract contains a basic block of {size} instructions but the runtime \
accepts at most {limit}"
),
Self::PurgeableMemoryTooLarge { size, limit } => write!(
f,
"the contract needs {size} bytes of purgeable interpreter memory but the runtime \
provides at most {limit} bytes"
),
Self::BaselineMemoryTooLarge { size, limit } => write!(
f,
"the contract needs {size} bytes of baseline interpreter memory but the runtime \
provides at most {limit} bytes; the static heap and stack buffers are part of \
this budget, so lowering `--heap-size` or `--stack-size` frees room for code"
),
}
}
}

/// What the runtime would observe about a linked blob.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlobFootprint {
/// The blob length in bytes.
pub size: u32,
/// The longest basic block, in instructions.
pub max_basic_block_size: u32,
/// Interpreter memory that is dropped when the contract calls out.
pub purgeable_ram_consumption: u32,
/// Interpreter memory held for the contract's whole life time.
pub baseline_ram_consumption: u32,
}

impl BlobFootprint {
/// Measures `blob` the way `pallet_revive::limits::code::enforce` does.
///
/// `interpreter_cache_bytes` and `page_size` come from the target runtime, because the
/// interpreter memory estimate depends on them.
pub fn measure(
blob: &[u8],
interpreter_cache_bytes: u32,
page_size: u32,
) -> anyhow::Result<Self> {
let size = u32::try_from(blob.len())
.map_err(|_| anyhow::anyhow!("the linked blob does not fit into a u32"))?;

let program = ProgramBlob::parse(blob.into())
.map_err(|error| anyhow::anyhow!("failed to parse the linked blob: {error}"))?;

anyhow::ensure!(
program.isa() == InstructionSetKind::ReviveV1,
"the linked blob targets the '{}' instruction set instead of '{}'",
program.isa().name(),
InstructionSetKind::ReviveV1.name(),
);

// The same single pass the pallet does: the interpreter memory estimate needs the
// instruction and basic block counts, and the longest basic block is a limit of its own.
let mut max_basic_block_size = 0u32;
let mut basic_block_size = 0u32;
let mut basic_block_count = 0u32;
let mut instruction_count = 0u32;
for instruction in program.instructions_with_isa(ISA_ReviveV1) {
basic_block_size += 1;
instruction_count += 1;
if instruction.kind.opcode().starts_new_basic_block() {
max_basic_block_size = max_basic_block_size.max(basic_block_size);
basic_block_size = 0;
basic_block_count += 1;
}
}
max_basic_block_size = max_basic_block_size.max(basic_block_size);

let usage = program
.estimate_interpreter_memory_usage(EstimateInterpreterMemoryUsageArgs::BoundedCache {
max_cache_size_bytes: interpreter_cache_bytes,
instruction_count,
max_block_size: max_basic_block_size,
basic_block_count,
page_size,
})
.map_err(|error| {
anyhow::anyhow!("failed to estimate the interpreter memory usage: {error}")
})?;

Ok(Self {
size,
max_basic_block_size,
purgeable_ram_consumption: usage.purgeable_ram_consumption,
baseline_ram_consumption: usage.baseline_ram_consumption,
})
}

/// Returns every limit this footprint exceeds, in the order the pallet checks them.
pub fn violations(&self, limits: &PalletLimits) -> Vec<Violation> {
let mut violations = Vec::new();

if self.size > limits.blob_bytes {
violations.push(Violation::BlobTooLarge {
size: self.size,
limit: limits.blob_bytes,
});
}
if self.max_basic_block_size > limits.basic_block_size {
violations.push(Violation::BasicBlockTooLarge {
size: self.max_basic_block_size,
limit: limits.basic_block_size,
});
}
if self.purgeable_ram_consumption > limits.purgable_memory_limit {
violations.push(Violation::PurgeableMemoryTooLarge {
size: self.purgeable_ram_consumption,
limit: limits.purgable_memory_limit,
});
}
if self.baseline_ram_consumption > limits.baseline_memory_limit {
violations.push(Violation::BaselineMemoryTooLarge {
size: self.baseline_ram_consumption,
limit: limits.baseline_memory_limit,
});
}

violations
}
}

/// Measures `blob` and returns the limits it exceeds for the given runtime.
pub fn check(blob: &[u8], limits: &PalletLimits) -> anyhow::Result<Vec<Violation>> {
let footprint = BlobFootprint::measure(blob, limits.interpreter_cache_bytes, limits.page_size)?;

Ok(footprint.violations(limits))
}
1 change: 1 addition & 0 deletions crates/resolc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ which = { workspace = true }
normpath = { workspace = true }

revive-common = { workspace = true }
revive-linker = { workspace = true }
revive-llvm-context = { workspace = true }
revive-newyork = { workspace = true }
revive-solc-json-interface = { workspace = true, features = ["resolc"] }
Expand Down
51 changes: 50 additions & 1 deletion crates/resolc/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use normpath::PathExt;

use revive_common::ObjectFormat;
use revive_common::BYTE_LENGTH_ETH_ADDRESS;
use revive_linker::limits::{check, DeploymentLimits, Enforcement};
use revive_llvm_context::polkavm_disassemble;
use revive_llvm_context::polkavm_hash;
use revive_llvm_context::polkavm_link;
Expand Down Expand Up @@ -53,6 +54,7 @@ impl Build {
mut self,
linker_symbols: BTreeMap<String, [u8; BYTE_LENGTH_ETH_ADDRESS]>,
debug_config: &DebugConfig,
deployment_limits: &DeploymentLimits,
) -> Self {
let mut contracts: BTreeMap<String, Contract> = self
.results
Expand Down Expand Up @@ -86,6 +88,12 @@ impl Build {
!debug_config.emit_debug_info,
) {
Ok((memory_buffer_linked, ObjectFormat::PVM)) => {
self.messages.extend(check_deployment_limits(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build::write_to_standard_json considers errors, so does this doesn't work in std json mode? Would be nice to have a test for that too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — it didn't. write_to_standard_json only forwarded errors from results and dropped messages, which is where the deployment-limit diagnostics (and other link-time issues) live.

They now land in the standard JSON errors array. Added coverage for --standard-json: the error, --ignore-memory-limit as a warning, and a larger --memory-limit.

Addressed in 98e6232.

path,
&memory_buffer_linked,
deployment_limits,
));

let bytecode_hash = polkavm_hash(&memory_buffer_linked);
let assembly_text =
polkavm_disassemble(path, &memory_buffer_linked, debug_config)
Expand Down Expand Up @@ -263,7 +271,10 @@ impl Build {
standard_json: &mut SolcStandardJsonOutput,
solc_version: &SolcVersion,
) -> anyhow::Result<()> {
let mut errors = Vec::with_capacity(self.results.len());
// Link-time diagnostics (deployment limits, unresolved symbols) live in
// `messages`, not `results`.
let mut errors = self.messages;
errors.reserve(self.results.len());
for result in self.results.into_values() {
let build = match result {
Ok(build) => build,
Expand Down Expand Up @@ -350,3 +361,41 @@ impl SolcStandardJsonOutputErrorHandler for Build {
warnings
}
}

/// Reports the deployment limits `bytecode` exceeds, if any.
///
/// A contract over the limits compiles but can never be deployed, so by default this is an error.
/// The check needs the linked blob, hence it runs here rather than in code generation.
fn check_deployment_limits(
path: &str,
bytecode: &[u8],
deployment_limits: &DeploymentLimits,
) -> Vec<SolcStandardJsonOutputError> {
let violations = match check(bytecode, &deployment_limits.limits) {
Ok(violations) => violations,
Err(error) => {
// Measuring is best effort: a blob we cannot parse is a separate problem, and the
// linker or the pallet will report it in its own terms.
return vec![SolcStandardJsonOutputError::new_warning(
format!("{path}: unable to check the deployment limits: {error}"),
None,
None,
)];
}
};

violations
.into_iter()
.map(|violation| {
let message = format!(
"{path} cannot be deployed to the target runtime: {violation}. Pass \
`--ignore-memory-limit` to compile anyway, or `--memory-limit` if the target \
runtime has a different budget."
);
match deployment_limits.enforcement {
Enforcement::Deny => SolcStandardJsonOutputError::new_error(message, None, None),
Enforcement::Warn => SolcStandardJsonOutputError::new_warning(message, None, None),
}
})
.collect()
}
4 changes: 4 additions & 0 deletions crates/resolc/src/cli_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ pub const STANDARD_JSON_YUL_PVM_CODEGEN_PATH: &str =
/// PVM bytecode generation and only validate the Yul.
pub const STANDARD_JSON_YUL_NO_PVM_CODEGEN_PATH: &str =
"src/tests/data/standard_json/yul_no_pvm_codegen.json";
/// A standard JSON fixture whose heap buffer exceeds the default Asset Hub
/// baseline interpreter memory budget.
pub const STANDARD_JSON_OVERSIZED_HEAP_PATH: &str =
"src/tests/data/standard_json/oversized_heap.json";

/// The `resolc` YUL mode flag.
pub const RESOLC_YUL_FLAG: &str = "--yul";
Expand Down
Loading