Skip to content

Commit bfd6fa8

Browse files
feat: RUN-1036: Validate initial wasm memory size for Wasm64 (#1534)
In this PR we add an additional validation step for the initial size for Wasm64 memories. The size of initial wasm heap memory should not be more than the protocol allows.
1 parent 285a5db commit bfd6fa8

File tree

4 files changed

+117
-10
lines changed

4 files changed

+117
-10
lines changed

rs/embedders/src/wasm_utils/validation.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use crate::{
2727
};
2828
use wasmparser::{CompositeInnerType, ExternalKind, FuncType, Operator, TypeRef, ValType};
2929

30+
const WASM_PAGE_SIZE: u32 = wasmtime_environ::Memory::DEFAULT_PAGE_SIZE;
31+
3032
/// Symbols that are reserved and cannot be exported by canisters.
3133
#[doc(hidden)] // pub for usage in tests
3234
pub const RESERVED_SYMBOLS: [&str; 6] = [
@@ -1048,6 +1050,29 @@ fn validate_function_section(
10481050
Ok(())
10491051
}
10501052

1053+
// Checks that the initial size of the wasm (heap) memory is not larger than
1054+
// the allowed maximum size. This is only needed for Wasm64, because in Wasm32 this
1055+
// is checked by Wasmtime.
1056+
fn validate_initial_wasm_memory_size(
1057+
module: &Module,
1058+
max_wasm_memory_size_in_bytes: NumBytes,
1059+
) -> Result<(), WasmValidationError> {
1060+
for memory in &module.memories {
1061+
if memory.memory64 {
1062+
let declared_size_in_wasm_pages = memory.initial;
1063+
let allowed_size_in_wasm_pages =
1064+
max_wasm_memory_size_in_bytes.get() / WASM_PAGE_SIZE as u64;
1065+
if declared_size_in_wasm_pages > allowed_size_in_wasm_pages {
1066+
return Err(WasmValidationError::InitialWasm64MemoryTooLarge {
1067+
declared_size: declared_size_in_wasm_pages,
1068+
allowed_size: allowed_size_in_wasm_pages,
1069+
});
1070+
}
1071+
}
1072+
}
1073+
Ok(())
1074+
}
1075+
10511076
// Extracts the name of the custom section.
10521077
// Possible options:
10531078
// * icp:public <name>
@@ -1511,6 +1536,7 @@ pub(super) fn validate_wasm_binary<'a>(
15111536
validate_data_section(&module)?;
15121537
validate_global_section(&module, config.max_globals)?;
15131538
validate_function_section(&module, config.max_functions)?;
1539+
validate_initial_wasm_memory_size(&module, config.max_wasm_memory_size)?;
15141540
let (largest_function_instruction_count, max_complexity) = validate_code_section(&module)?;
15151541
let wasm_metadata = validate_custom_section(&module, config)?;
15161542
Ok((

rs/embedders/tests/validation.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,3 +1132,36 @@ fn wasm_with_multiple_code_sections_is_invalid() {
11321132
))
11331133
)
11341134
}
1135+
1136+
#[test]
1137+
fn test_wasm64_initial_wasm_memory_size_validation() {
1138+
use crate::WasmValidationError::InitialWasm64MemoryTooLarge;
1139+
use ic_config::embedders::FeatureFlags;
1140+
use ic_config::flag_status::FlagStatus;
1141+
1142+
let embedders_config = EmbeddersConfig {
1143+
feature_flags: FeatureFlags {
1144+
wasm64: FlagStatus::Enabled,
1145+
..Default::default()
1146+
},
1147+
..Default::default()
1148+
};
1149+
let allowed_wasm_memory_size_in_pages =
1150+
embedders_config.max_wasm_memory_size.get() / WASM_PAGE_SIZE as u64;
1151+
let declared_wasm_memory_size_in_pages = allowed_wasm_memory_size_in_pages + 10;
1152+
let wasm = wat2wasm(&format!(
1153+
r#"(module
1154+
(memory i64 {} {})
1155+
)"#,
1156+
declared_wasm_memory_size_in_pages, declared_wasm_memory_size_in_pages
1157+
))
1158+
.unwrap();
1159+
1160+
assert_eq!(
1161+
validate_wasm_binary(&wasm, &embedders_config),
1162+
Err(InitialWasm64MemoryTooLarge {
1163+
declared_size: declared_wasm_memory_size_in_pages,
1164+
allowed_size: allowed_wasm_memory_size_in_pages
1165+
})
1166+
);
1167+
}

rs/embedders/tests/wasmtime_embedder.rs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use ic_types::{
2121
Cycles, NumBytes, NumInstructions,
2222
};
2323

24+
const WASM_PAGE_SIZE: u32 = wasmtime_environ::Memory::DEFAULT_PAGE_SIZE;
25+
2426
#[cfg(target_os = "linux")]
2527
use ic_types::PrincipalId;
2628

@@ -2790,23 +2792,52 @@ fn wasm64_cycles_burn128() {
27902792

27912793
#[test]
27922794
fn large_wasm64_memory_allocation_test() {
2793-
let wat = r#"
2794-
(module
2795-
(func $test (export "canister_update test"))
2796-
(memory i64 0 16777216)
2797-
)"#;
2795+
// This test checks if maximum memory size
2796+
// is capped to the maximum allowed memory size in 64 bit mode.
27982797

27992798
let mut config = ic_config::embedders::Config::default();
28002799
config.feature_flags.wasm64 = FlagStatus::Enabled;
2800+
let max_heap_size_in_pages = config.max_wasm_memory_size.get() / WASM_PAGE_SIZE as u64;
2801+
let wat = format!(
2802+
r#"
2803+
(module
2804+
(import "ic0" "msg_reply" (func $msg_reply))
2805+
(import "ic0" "msg_reply_data_append" (func $msg_reply_data_append (param $src i64) (param $size i64)))
2806+
(func $test (export "canister_update test")
2807+
;; store the result of memory.grow at heap address 0
2808+
(i64.store (i64.const 0) (memory.grow (i64.const 1)))
2809+
;; return the result of memory.grow
2810+
(call $msg_reply_data_append (i64.const 0) (i64.const 1))
2811+
(call $msg_reply)
2812+
)
2813+
;; declare a memory with initial size max_heap and another max large value
2814+
(memory i64 {} {})
2815+
)"#,
2816+
max_heap_size_in_pages,
2817+
max_heap_size_in_pages * 100
2818+
);
2819+
28012820
let mut instance = WasmtimeInstanceBuilder::new()
28022821
.with_config(config)
2803-
.with_wat(wat)
2822+
.with_api_type(ic_system_api::ApiType::update(
2823+
UNIX_EPOCH,
2824+
vec![],
2825+
Cycles::zero(),
2826+
user_test_id(24).get(),
2827+
call_context_test_id(13),
2828+
))
2829+
.with_wat(&wat)
28042830
.build();
28052831

2806-
match instance.run(FuncRef::Method(WasmMethod::Update("test".to_string()))) {
2807-
Ok(_) => {}
2808-
Err(e) => panic!("Unexpected error: {:?}", e),
2809-
}
2832+
let result = instance.run(FuncRef::Method(WasmMethod::Update("test".to_string())));
2833+
let wasm_res = instance
2834+
.store_data_mut()
2835+
.system_api_mut()
2836+
.unwrap()
2837+
.take_execution_result(result.as_ref().err());
2838+
2839+
// The reply is actually the encoding of -1 (the memory grow failed).
2840+
assert_eq!(wasm_res, Ok(Some(WasmResult::Reply(vec![255]))));
28102841
}
28112842

28122843
#[test]

rs/types/wasm_types/src/errors.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ pub enum WasmValidationError {
9999
CodeSectionTooLarge { size: u32, allowed: u32 },
100100
/// The total module size is too large.
101101
ModuleTooLarge { size: u64, allowed: u64 },
102+
// The initial Wasm64 heap memory is too large.
103+
InitialWasm64MemoryTooLarge {
104+
declared_size: u64,
105+
allowed_size: u64,
106+
},
102107
}
103108

104109
impl std::fmt::Display for WasmValidationError {
@@ -204,6 +209,14 @@ impl std::fmt::Display for WasmValidationError {
204209
"Wasm module size of {size} exceeds the maximum \
205210
allowed size of {allowed}.",
206211
),
212+
Self::InitialWasm64MemoryTooLarge {
213+
declared_size,
214+
allowed_size,
215+
} => write!(
216+
f,
217+
"Wasm module declares an initial Wasm64 heap memory size of {declared_size} pages \
218+
which exceeds the maximum allowed size of {allowed_size} pages.",
219+
),
207220
}
208221
}
209222
}
@@ -270,6 +283,10 @@ impl AsErrorHelp for WasmValidationError {
270283
.to_string(),
271284
doc_link: doc_ref("wasm-module-too-large"),
272285
},
286+
WasmValidationError::InitialWasm64MemoryTooLarge { .. } => ErrorHelp::UserError {
287+
suggestion: "Try reducing the initial Wasm64 heap memory size.".to_string(),
288+
doc_link: doc_ref("wasm-module-initial-wasm64-memory-too-large"),
289+
},
273290
}
274291
}
275292
}

0 commit comments

Comments
 (0)