mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 06:40:53 +00:00
Limit vm table size
This commit is contained in:
@@ -97,6 +97,8 @@ mod ffi {
|
||||
EntryPoint,
|
||||
/// The module asks for more linear memory than the engine grants.
|
||||
Memory,
|
||||
/// The module asks for a larger table than the engine grants.
|
||||
Table,
|
||||
/// The engine panicked. A defect in this crate or the one below it, and
|
||||
/// not a fault in the module — which is why it is a status of its own
|
||||
/// rather than one more way a contract can be malformed.
|
||||
@@ -1036,6 +1038,7 @@ impl From<&CheckError> for ffi::CheckStatus {
|
||||
CheckError::Import(_) => ffi::CheckStatus::Import,
|
||||
CheckError::EntryPoint(_) => ffi::CheckStatus::EntryPoint,
|
||||
CheckError::Memory(_) => ffi::CheckStatus::Memory,
|
||||
CheckError::Table(_) => ffi::CheckStatus::Table,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,6 +1245,7 @@ mod tests {
|
||||
CheckError::Import(String::new()),
|
||||
CheckError::EntryPoint(String::new()),
|
||||
CheckError::Memory(String::new()),
|
||||
CheckError::Table(String::new()),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ mod vm;
|
||||
|
||||
pub use preflight::{CheckError, check};
|
||||
pub use vm::{
|
||||
MAX_FIELD_BYTES, MAX_MEMORY_BYTES, MAX_MEMORY_PAGES, RunError, RunFailure, RunOutcome,
|
||||
TRANSFER_LIMIT_BYTES, run,
|
||||
MAX_FIELD_BYTES, MAX_MEMORY_BYTES, MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError, RunFailure,
|
||||
RunOutcome, TRANSFER_LIMIT_BYTES, run,
|
||||
};
|
||||
|
||||
@@ -13,15 +13,17 @@
|
||||
//! passes: it is guest code, and executing it is the one thing a check must not do
|
||||
//! — a trap in one is charged to the contract like any other trap.
|
||||
//!
|
||||
//! One thing it screens that a run can only discover: an exported memory larger
|
||||
//! than the engine grants. See [`check_memory`] for what stays invisible.
|
||||
//! Two things it screens that a run can only discover: an exported memory, or an
|
||||
//! exported table, larger than the engine grants. Both read the same export list, so
|
||||
//! [`check_exported_resources`] is one pass — see it for what stays invisible, and
|
||||
//! why the table case leaves much more of it there.
|
||||
|
||||
use std::fmt;
|
||||
use wasmi::{ExternType, FuncType, Module, ValType};
|
||||
use xrpl_host_functions::HostFunctionSpec;
|
||||
|
||||
use crate::register::HOST_MODULE;
|
||||
use crate::vm::{MAX_MEMORY_PAGES, compile};
|
||||
use crate::vm::{MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, compile};
|
||||
|
||||
/// Why a module cannot be run. One variant per stage, since the caller maps the
|
||||
/// stages separately.
|
||||
@@ -37,6 +39,8 @@ pub enum CheckError {
|
||||
EntryPoint(String),
|
||||
/// The module asks for more linear memory than the engine grants.
|
||||
Memory(String),
|
||||
/// The module asks for a larger table than the engine grants.
|
||||
Table(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CheckError {
|
||||
@@ -48,22 +52,24 @@ impl fmt::Display for CheckError {
|
||||
// "no entry point" would be wrong for an export of the wrong type.
|
||||
CheckError::EntryPoint(detail) => write!(f, "{detail}"),
|
||||
CheckError::Memory(detail) => write!(f, "memory: {detail}"),
|
||||
CheckError::Table(detail) => write!(f, "table: {detail}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen `wasm`: it must compile, import only what the engine serves, export
|
||||
/// `function_name` as `() -> i32`, and ask for no more memory than it may have.
|
||||
/// `function_name` as `() -> i32`, and ask for no more memory or table than it may
|
||||
/// have.
|
||||
///
|
||||
/// The stages are ordered by how much of the module each explains. An import fault
|
||||
/// is reported before a missing entry point because the imports are what the rest of
|
||||
/// the module is built on; memory comes last, being a resource request rather than a
|
||||
/// the module is built on; the resource caps come last, being a request rather than a
|
||||
/// mistake about the ABI.
|
||||
pub fn check(wasm: &[u8], function_name: &str) -> Result<(), CheckError> {
|
||||
let module = compile(wasm).map_err(CheckError::Compile)?;
|
||||
check_imports(&module)?;
|
||||
check_entry_point(&module, function_name)?;
|
||||
check_memory(&module)
|
||||
check_exported_resources(&module)
|
||||
}
|
||||
|
||||
/// Every import must be one the linker defines. The first that is not ends the
|
||||
@@ -116,18 +122,23 @@ fn is_entry_point(ty: &FuncType) -> bool {
|
||||
ty.params().is_empty() && matches!(ty.results(), [ValType::I32])
|
||||
}
|
||||
|
||||
/// A module may not declare more linear memory than the engine grants.
|
||||
/// A module may declare no more linear memory, and no larger a table, than the
|
||||
/// engine grants. One pass over the exports, since both rules read the same list and
|
||||
/// the export table is the only place either is visible.
|
||||
///
|
||||
/// Only what it *exports* is visible here. A memory a module keeps to itself is not
|
||||
/// in its exports, and the store's limiter is what refuses that one — at
|
||||
/// instantiation, where the run is charged nothing and the caller cannot tell it
|
||||
/// from any other resource failure. Screening the exported case covers every
|
||||
/// contract built against the guest SDK, since a contract needs an exported memory
|
||||
/// to make a host call at all.
|
||||
fn check_memory(module: &Module) -> Result<(), CheckError> {
|
||||
/// A module faulting on both is reported by whichever it declares first. Neither
|
||||
/// fault explains the other, so there is no precedence to preserve — only the need
|
||||
/// for every node to reach the same verdict, which export order already gives.
|
||||
fn check_exported_resources(module: &Module) -> Result<(), CheckError> {
|
||||
for export in module.exports() {
|
||||
if let ExternType::Memory(ty) = export.ty() {
|
||||
check_initial_pages(ty.minimum()).map_err(CheckError::Memory)?;
|
||||
match export.ty() {
|
||||
ExternType::Memory(ty) => {
|
||||
check_initial_pages(ty.minimum()).map_err(CheckError::Memory)?;
|
||||
}
|
||||
ExternType::Table(ty) => {
|
||||
check_initial_elements(ty.minimum()).map_err(CheckError::Table)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -148,6 +159,21 @@ fn check_initial_pages(pages: u64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the engine will grant a table of this declared initial size.
|
||||
///
|
||||
/// The *minimum* is the whole question: `table.grow` belongs to the reference-types
|
||||
/// proposal, which [`crate::vm`]'s engine turns off, so a table never becomes larger
|
||||
/// than it was declared and a declared maximum past the cap is simply unreachable.
|
||||
fn check_initial_elements(elements: u64) -> Result<(), String> {
|
||||
let cap = u64::try_from(MAX_TABLE_ELEMENTS).expect("the cap is a small constant");
|
||||
if elements > cap {
|
||||
return Err(format!(
|
||||
"initial table of {elements} elements is past the {MAX_TABLE_ELEMENTS}-element cap"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How an entry-point lookup failed, in the words both stages use: a check and a
|
||||
/// run describe the same module the same way, and "no entry point" would send a
|
||||
/// contract author looking for a function they already have.
|
||||
@@ -310,6 +336,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The cap itself is granted; one element past it is not. The boundary is the
|
||||
/// whole rule, and it is the same boundary the store's limiter applies at
|
||||
/// instantiation.
|
||||
#[test]
|
||||
fn the_initial_table_may_reach_the_cap_but_not_pass_it() {
|
||||
let cap = u64::try_from(MAX_TABLE_ELEMENTS).expect("fits");
|
||||
assert_eq!(check_initial_elements(0), Ok(()));
|
||||
assert_eq!(check_initial_elements(cap), Ok(()));
|
||||
|
||||
let past = cap + 1;
|
||||
let refusal = check_initial_elements(past).expect_err("one element past the cap");
|
||||
assert_eq!(
|
||||
refusal,
|
||||
format!(
|
||||
"initial table of {past} elements is past the {MAX_TABLE_ELEMENTS}-element cap"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// The bridge logs this string and the C++ tests match on it, so the stage's
|
||||
/// prefix is part of the interface rather than a debugging aid.
|
||||
#[test]
|
||||
@@ -322,6 +367,10 @@ mod tests {
|
||||
CheckError::Memory("initial memory of 129 pages".to_string()).to_string(),
|
||||
"memory: initial memory of 129 pages"
|
||||
);
|
||||
assert_eq!(
|
||||
CheckError::Table("initial table of 1025 elements".to_string()).to_string(),
|
||||
"table: initial table of 1025 elements"
|
||||
);
|
||||
assert_eq!(
|
||||
CheckError::Import("no host function 'x'".to_string()).to_string(),
|
||||
"import: no host function 'x'"
|
||||
|
||||
@@ -20,6 +20,14 @@ pub const MAX_MEMORY_PAGES: u32 = 128;
|
||||
/// [`MAX_MEMORY_PAGES`] in bytes: 8 MiB.
|
||||
pub const MAX_MEMORY_BYTES: usize = (MAX_MEMORY_PAGES * WASM_PAGE_BYTES) as usize;
|
||||
|
||||
/// Cap on a table's element count.
|
||||
///
|
||||
/// A table entry is 8 bytes and wasmi materializes every one of them inside
|
||||
/// `instantiate_and_start` — before the guest's first instruction, so no gas charge
|
||||
/// can reach the cost. Without this cap the ceiling is the validator's, `u32::MAX`
|
||||
/// entries, which a module asks for in five bytes of LEB128 and pays for in ~34 GiB.
|
||||
pub const MAX_TABLE_ELEMENTS: usize = 1024;
|
||||
|
||||
/// Total bytes that may cross the host/guest boundary in one [`run`], separate
|
||||
/// from gas.
|
||||
pub const TRANSFER_LIMIT_BYTES: u64 = 1 << 20;
|
||||
@@ -33,8 +41,8 @@ pub const MAX_FIELD_BYTES: usize = 1024;
|
||||
/// State threaded through every host call, stored in the wasmi [`Store`].
|
||||
pub(crate) struct VmState<'h> {
|
||||
pub(crate) host: &'h dyn HostFunctions,
|
||||
/// Enforces [`MAX_MEMORY_BYTES`] via `Store::limiter`, which needs a `&mut`
|
||||
/// into it from `&mut VmState` — hence a field rather than a local.
|
||||
/// Enforces [`store_limits`] via `Store::limiter`, which needs a `&mut` into it
|
||||
/// from `&mut VmState` — hence a field rather than a local.
|
||||
pub(crate) mem_limits: StoreLimits,
|
||||
/// Remaining transfer budget for this run ([`TRANSFER_LIMIT_BYTES`]).
|
||||
///
|
||||
@@ -254,6 +262,30 @@ fn build_wasm_engine() -> Engine {
|
||||
Engine::new(&config)
|
||||
}
|
||||
|
||||
/// Every resource ceiling a run is given, in one place.
|
||||
///
|
||||
/// The two *size* caps are what a contract can reach today. The three *count* caps
|
||||
/// are set to 1 although [`build_wasm_engine`] already forces each: turning
|
||||
/// `wasm_reference_types` on would let a module declare up to
|
||||
/// `wasmparser::MAX_WASM_TABLES` tables, `wasm_multi_memory` likewise for memories,
|
||||
/// and both size caps are **per table and per memory, not aggregate** — so a feature
|
||||
/// flag flipped in isolation would multiply the ceiling by a hundred rather than
|
||||
/// leave it be. The counts are what keeps those two decisions independent.
|
||||
///
|
||||
/// wasmi enforces the counts by asking the limiter before it allocates
|
||||
/// (`can_create_more_instances`/`_memories`/`_tables`); they default to 10000, so
|
||||
/// leaving them unset is not the same as their being unreachable.
|
||||
fn store_limits() -> StoreLimits {
|
||||
StoreLimitsBuilder::new()
|
||||
.memory_size(MAX_MEMORY_BYTES)
|
||||
.table_elements(MAX_TABLE_ELEMENTS)
|
||||
.instances(1)
|
||||
.tables(1)
|
||||
.memories(1)
|
||||
.trap_on_grow_failure(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Compile `wasm` for this engine.
|
||||
///
|
||||
/// The one path to a [`Module`]: the configuration is what decides whether a
|
||||
@@ -275,15 +307,11 @@ pub fn run<'h>(
|
||||
let module =
|
||||
compile(wasm).map_err(|detail| RunFailure::owing_nothing(RunError::Compile(detail)))?;
|
||||
|
||||
let mem_limits = StoreLimitsBuilder::new()
|
||||
.memory_size(MAX_MEMORY_BYTES)
|
||||
.trap_on_grow_failure(true)
|
||||
.build();
|
||||
let mut store = Store::new(
|
||||
engine,
|
||||
VmState {
|
||||
host,
|
||||
mem_limits,
|
||||
mem_limits: store_limits(),
|
||||
transfer_budget: Cell::new(TRANSFER_LIMIT_BYTES),
|
||||
memory: None,
|
||||
out_buffer: [0u8; MAX_FIELD_BYTES],
|
||||
@@ -341,12 +369,30 @@ mod tests {
|
||||
assert!(Engine::same(wasm_engine(), wasm_engine()));
|
||||
}
|
||||
|
||||
/// One instance, one table, one memory — asserted here rather than through a
|
||||
/// module, because no module can reach these. `wasm_reference_types(false)` and
|
||||
/// `wasm_multi_memory(false)` make a module declaring a second table or memory
|
||||
/// fail *validation*, so a run never gets far enough to consult the limiter.
|
||||
/// That is exactly why the counts are worth pinning: they are the ceiling that
|
||||
/// survives one of those flags being turned on, and nothing else would fail if
|
||||
/// they were silently dropped.
|
||||
#[test]
|
||||
fn the_store_grants_one_of_each_thing_a_module_can_own() {
|
||||
use wasmi::ResourceLimiter;
|
||||
|
||||
let limits = store_limits();
|
||||
assert_eq!(limits.instances(), 1);
|
||||
assert_eq!(limits.tables(), 1);
|
||||
assert_eq!(limits.memories(), 1);
|
||||
}
|
||||
|
||||
/// The only place these numbers appear as literals; every other test derives
|
||||
/// them from the constants.
|
||||
#[test]
|
||||
fn the_limits_are_the_protocol_limits() {
|
||||
assert_eq!(MAX_MEMORY_PAGES, 128, "linear-memory page cap");
|
||||
assert_eq!(MAX_MEMORY_BYTES, 8 * 1024 * 1024, "page cap in bytes");
|
||||
assert_eq!(MAX_TABLE_ELEMENTS, 1024, "table-element cap");
|
||||
assert_eq!(MAX_FIELD_BYTES, 1024, "kMaxWasmDataLength");
|
||||
assert_eq!(TRANSFER_LIMIT_BYTES, 1 << 20, "kWasmTransferLimit");
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ mod support;
|
||||
|
||||
use support::{ENTRY, FakeHost, ONE_PAGE, PLENTY_OF_GAS, assemble, import, module};
|
||||
use xrpl_host_functions::HostFunctionSpec;
|
||||
use xrpl_wasm_vm::{CheckError, MAX_MEMORY_PAGES, RunError};
|
||||
use xrpl_wasm_vm::{CheckError, MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError};
|
||||
|
||||
/// Assert which stage screening refused a module at, because the caller maps the
|
||||
/// stages separately. The error comes back out for the tests that also read its
|
||||
@@ -464,30 +464,104 @@ fn a_declared_maximum_past_the_cap_still_passes() {
|
||||
));
|
||||
}
|
||||
|
||||
/// The gap, listed rather than described, and now one entry long. A memory a module
|
||||
/// keeps to itself is not in its exports, so this is the one module that passes
|
||||
/// screening and then fails to *instantiate* — which is why a run's refusal at that
|
||||
/// stage cannot be read as the node's fault.
|
||||
/// A module asking for more table than the engine grants is refused for the same
|
||||
/// reason a memory is. The cap itself passes.
|
||||
#[test]
|
||||
fn an_exported_table_past_the_cap_does_not_pass() {
|
||||
let wat = module(
|
||||
&[&format!(
|
||||
r#"(table (export "t") {} funcref)"#,
|
||||
MAX_TABLE_ELEMENTS + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
let refusal = assert_stage!(refusal(&wat), CheckError::Table(_)).to_string();
|
||||
assert!(refusal.contains("past the 1024-element cap"), "{refusal}");
|
||||
|
||||
passes(&module(
|
||||
&[&format!(r#"(table (export "t") {MAX_TABLE_ELEMENTS} funcref)"#)],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
}
|
||||
|
||||
/// Both caps are applied in one pass over the exports, so neither may end the walk
|
||||
/// early: a passing memory must not hide a failing table declared after it, and a
|
||||
/// passing table must not hide a failing memory.
|
||||
#[test]
|
||||
fn one_pass_screens_both_resources() {
|
||||
let after_a_passing_memory = refusal(&module(
|
||||
&[
|
||||
ONE_PAGE,
|
||||
&format!(r#"(table (export "t") {} funcref)"#, MAX_TABLE_ELEMENTS + 1),
|
||||
],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
assert_stage!(after_a_passing_memory, CheckError::Table(_));
|
||||
|
||||
let after_a_passing_table = refusal(&module(
|
||||
&[
|
||||
r#"(table (export "t") 1 funcref)"#,
|
||||
&format!(r#"(memory (export "memory") {})"#, MAX_MEMORY_PAGES + 1),
|
||||
],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
assert_stage!(after_a_passing_table, CheckError::Memory(_));
|
||||
}
|
||||
|
||||
/// As with memory, a declared *maximum* past the cap is unreachable rather than
|
||||
/// wrong: `vm_limits` runs this very module to completion.
|
||||
#[test]
|
||||
fn a_declared_table_maximum_past_the_cap_still_passes() {
|
||||
passes(&module(
|
||||
&[&format!(
|
||||
r#"(table (export "t") 1 {} funcref)"#,
|
||||
MAX_TABLE_ELEMENTS + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
));
|
||||
}
|
||||
|
||||
/// The gap, listed rather than described. A memory or a table a module keeps to
|
||||
/// itself is not in its exports, so these are the modules that pass screening and
|
||||
/// then fail to *instantiate* — which is why a run's refusal at that stage cannot be
|
||||
/// read as the node's fault.
|
||||
///
|
||||
/// A contract needs an exported memory to make any host call, so a module of this
|
||||
/// shape can do nothing but compute; the SDK does not produce one.
|
||||
/// The two entries are not equally remote. A contract needs an exported memory to
|
||||
/// make any host call, so the memory row can do nothing but compute and the SDK does
|
||||
/// not produce one. A table, though, is *normally* unexported — Rust exports
|
||||
/// `__indirect_function_table` only under `--export-table` — so the table row is the
|
||||
/// shape a hostile module actually takes, and the store's limiter is the only thing
|
||||
/// standing in front of it.
|
||||
#[test]
|
||||
fn what_static_screening_cannot_see() {
|
||||
let host = FakeHost::new();
|
||||
let wat = format!(
|
||||
r#"(module (memory {})
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#,
|
||||
MAX_MEMORY_PAGES + 1
|
||||
);
|
||||
|
||||
passes(&wat);
|
||||
for (label, declaration) in [
|
||||
("memory", format!("(memory {})", MAX_MEMORY_PAGES + 1)),
|
||||
(
|
||||
"table",
|
||||
format!("(table {} funcref)", MAX_TABLE_ELEMENTS + 1),
|
||||
),
|
||||
] {
|
||||
let wat = format!(
|
||||
r#"(module {declaration}
|
||||
(func (export "finish") (result i32) (i32.const 0)))"#
|
||||
);
|
||||
|
||||
let failure = xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY)
|
||||
.expect_err("the store's limiter must refuse the memory");
|
||||
assert!(
|
||||
matches!(failure.error, RunError::Instantiate(_)),
|
||||
"{failure}"
|
||||
);
|
||||
passes(&wat);
|
||||
|
||||
let failure = match xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY) {
|
||||
Err(failure) => failure,
|
||||
Ok(outcome) => panic!(
|
||||
"the store's limiter must refuse the {label}, but the module returned {}",
|
||||
outcome.result
|
||||
),
|
||||
};
|
||||
assert!(
|
||||
matches!(failure.error, RunError::Instantiate(_)),
|
||||
"{label}: {failure}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A start section is guest code, so screening cannot see whether it traps — but it
|
||||
|
||||
@@ -9,7 +9,7 @@ mod support;
|
||||
use support::{
|
||||
FakeHost, ONE_PAGE, PLENTY_OF_GAS, failure, import, module, run, run_entry, run_with_gas,
|
||||
};
|
||||
use xrpl_wasm_vm::{MAX_MEMORY_PAGES, RunError};
|
||||
use xrpl_wasm_vm::{MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError};
|
||||
|
||||
/// Assert which stage a run failed at, because the caller maps the stages to
|
||||
/// different outcomes. A stage is one `RunError` variant, so the expectation is a
|
||||
@@ -99,6 +99,68 @@ fn a_declared_maximum_past_the_cap_is_allowed_but_unreachable() {
|
||||
assert_stage!(failure(&wat, &host), RunError::Trap(_));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A table's whole cost is paid at instantiation: wasmi writes all 8 bytes of every
|
||||
/// element before the guest's first instruction, so a module declaring more than the
|
||||
/// cap must be refused there rather than charged for it.
|
||||
#[test]
|
||||
fn an_initial_table_past_the_cap_is_refused() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!("(table {} funcref)", MAX_TABLE_ELEMENTS + 1)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
|
||||
/// The cap itself is allowed.
|
||||
#[test]
|
||||
fn an_initial_table_at_the_cap_is_allowed() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!("(table {MAX_TABLE_ELEMENTS} funcref)")],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
}
|
||||
|
||||
/// The cap binds a table the module keeps to itself, which is the case that matters:
|
||||
/// a contract has no reason to export its table, so screening never sees the one a
|
||||
/// hostile module declares.
|
||||
#[test]
|
||||
fn the_table_cap_binds_an_unexported_table() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!("(table {} funcref)", u32::from(u16::MAX) * 100)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
|
||||
}
|
||||
|
||||
/// A declared *maximum* past the cap is legal and simply unreachable, mirroring what
|
||||
/// linear memory allows. Nothing can reach it: `table.grow` is a reference-types
|
||||
/// instruction and the engine turns that feature off, so a table's declared minimum
|
||||
/// is also its final size.
|
||||
#[test]
|
||||
fn a_declared_table_maximum_past_the_cap_is_allowed_but_unreachable() {
|
||||
let host = FakeHost::new();
|
||||
|
||||
let wat = module(
|
||||
&[&format!(
|
||||
"(table 1 {} funcref)",
|
||||
u64::try_from(MAX_TABLE_ELEMENTS).expect("fits") + 1
|
||||
)],
|
||||
"(i32.const 0)",
|
||||
);
|
||||
assert_eq!(run(&wat, &host).expect("should run").result, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Engine configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -96,12 +96,13 @@ verdict(CheckStatus status)
|
||||
return tesSUCCESS;
|
||||
|
||||
// The module will not compile, imports what no engine of this ABI serves, does
|
||||
// not export the entry point as `() -> i32`, or asks for more linear memory than
|
||||
// it may have.
|
||||
// not export the entry point as `() -> i32`, or asks for more linear memory or
|
||||
// table than it may have.
|
||||
case CheckStatus::Compile:
|
||||
case CheckStatus::Import:
|
||||
case CheckStatus::EntryPoint:
|
||||
case CheckStatus::Memory:
|
||||
case CheckStatus::Table:
|
||||
return temBAD_WASM;
|
||||
|
||||
// The engine panicked: a defect in the engine, reported rather than fatal to
|
||||
|
||||
@@ -127,6 +127,31 @@ TEST_F(PreflightTest, MemoryPastTheCapIsRefused)
|
||||
EXPECT_EQ(preflight(atTheCap), tesSUCCESS);
|
||||
}
|
||||
|
||||
// A table is allocated in full at instantiation, before any gas is charged, so an oversized
|
||||
// one is refused before it can be escrowed. Screening sees only an *exported* table; the
|
||||
// store's limiter is what refuses the table a contract keeps to itself.
|
||||
TEST_F(PreflightTest, TablePastTheCapIsRefused)
|
||||
{
|
||||
constexpr std::string_view tooMuch = R"wat(
|
||||
(module
|
||||
(memory (export "memory") 1)
|
||||
(table (export "t") 1025 funcref)
|
||||
(func (export "escrow_finish") (result i32) (i32.const 0)))
|
||||
)wat";
|
||||
|
||||
EXPECT_EQ(preflight(tooMuch), temBAD_WASM);
|
||||
EXPECT_THAT(logged(), testing::HasSubstr("table: initial table of 1025 elements"));
|
||||
|
||||
constexpr std::string_view atTheCap = R"wat(
|
||||
(module
|
||||
(memory (export "memory") 1)
|
||||
(table (export "t") 1024 funcref)
|
||||
(func (export "escrow_finish") (result i32) (i32.const 0)))
|
||||
)wat";
|
||||
|
||||
EXPECT_EQ(preflight(atTheCap), tesSUCCESS);
|
||||
}
|
||||
|
||||
TEST_F(PreflightTest, MissingEntryPointIsRefused)
|
||||
{
|
||||
constexpr std::string_view wat = R"wat(
|
||||
|
||||
Reference in New Issue
Block a user