Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions vm-executor-wasmer/src/wasmer_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,11 @@ pub(crate) fn is_control_flow_operator(operator: &Operator) -> bool {
| Operator::Return
)
}

pub (crate) fn is_supported_bulk_memory_operator(operator: &Operator) -> bool {
matches!(
operator,
Operator::MemoryCopy { src: _, dst: _ }
| Operator::MemoryFill { mem: _ }
)
}
62 changes: 59 additions & 3 deletions vm-executor-wasmer/src/wasmer_metering.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::executor_interface::OpcodeCost;
use crate::wasmer_breakpoints::{Breakpoints, BREAKPOINT_VALUE_OUT_OF_GAS};
use crate::wasmer_helpers::{
create_global_index, get_global_value_u64, is_control_flow_operator, set_global_value_u64,
MiddlewareWithProtectedGlobals,
create_global_index, get_global_value_u64, is_control_flow_operator, is_supported_bulk_memory_operator, set_global_value_u64, MiddlewareWithProtectedGlobals
};
use crate::{get_local_cost, get_opcode_cost};
use loupe::{MemoryUsage, MemoryUsageTracker};
Expand All @@ -17,12 +16,14 @@ use wasmer_types::{GlobalIndex, ModuleInfo};

const METERING_POINTS_LIMIT: &str = "metering_points_limit";
const METERING_POINTS_USED: &str = "metering_points_used";
const METERING_BULK_MEMORY_SIZE_OPERAND_BACKUP: &str = "metering_bulk_memory_size_operand_backup";
const MAX_LOCAL_COUNT: u32 = 4000;

#[derive(Clone, Debug, MemoryUsage)]
struct MeteringGlobalIndexes {
points_limit_global_index: GlobalIndex,
points_used_global_index: GlobalIndex,
bulk_memory_size_operand_backup_global_index: GlobalIndex,
}

#[derive(Debug)]
Expand Down Expand Up @@ -67,6 +68,15 @@ impl Metering {
.unwrap()
.points_used_global_index
}

fn get_bulk_memory_size_operand_backup_global_index(&self) -> GlobalIndex {
self.global_indexes
.lock()
.unwrap()
.as_ref()
.unwrap()
.bulk_memory_size_operand_backup_global_index
}
}

unsafe impl Send for Metering {}
Expand Down Expand Up @@ -105,6 +115,11 @@ impl ModuleMiddleware for Metering {
points_limit,
),
points_used_global_index: create_global_index(module_info, METERING_POINTS_USED, 0),
bulk_memory_size_operand_backup_global_index: create_global_index(
module_info,
METERING_BULK_MEMORY_SIZE_OPERAND_BACKUP,
0,
),
});
}
}
Expand All @@ -114,6 +129,7 @@ impl MiddlewareWithProtectedGlobals for Metering {
vec![
self.get_points_limit_global_index().as_u32(),
self.get_points_used_global_index().as_u32(),
self.get_bulk_memory_size_operand_backup_global_index().as_u32(),
]
}
}
Expand Down Expand Up @@ -156,6 +172,39 @@ impl FunctionMetering {
self.breakpoints_middleware
.inject_breakpoint_condition(state, BREAKPOINT_VALUE_OUT_OF_GAS);
}

fn inject_bulk_memory_cost(&self, state: &mut MiddlewareReaderState, cost_per_byte: u32) {
// backup the bulk memory size
state.extend(&[Operator::GlobalSet {
global_index: self.global_indexes.bulk_memory_size_operand_backup_global_index.as_u32(),
}]);

// inject bulk memory cost
state.extend(&[
// memory size * price
Operator::GlobalGet {
global_index: self.global_indexes.bulk_memory_size_operand_backup_global_index.as_u32(),
},
Operator::I64Const {
value: cost_per_byte as i64,
},
Operator::I64Mul,
Comment on lines +196 to +199
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The bulk memory cost calculation multiplies the memory size (i64) by the cost_per_byte (u32 cast to i64) without overflow checking. For large memory operations, this could potentially overflow an i64. While WASM memory is typically limited, it would be safer to either validate the size before multiplication or use checked arithmetic to prevent potential overflow issues.

Copilot uses AI. Check for mistakes.

// points user += memory size * price
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The inject_bulk_memory_cost function assumes the size operand is on top of the stack. However, for MemoryCopy, the WASM stack order is [dst, src, size], and for MemoryFill it's [dst, val, size]. After backing up the size with GlobalSet, the function attempts to restore it with GlobalGet at the end. This is correct, but the comment on line 200 is misleading: it says 'points user +=' but the actual operation is 'points_used += memory_size * price', which should be reflected more clearly in the comment.

Copilot uses AI. Check for mistakes.
Operator::GlobalGet {
global_index: self.global_indexes.points_used_global_index.as_u32(),
},
Operator::I64Add,
Operator::GlobalSet {
global_index: self.global_indexes.points_used_global_index.as_u32(),
},
]);

// bring back the bulk memory size
state.extend(&[Operator::GlobalGet {
global_index: self.global_indexes.bulk_memory_size_operand_backup_global_index.as_u32(),
}]);
}
}

impl FunctionMiddleware for FunctionMetering {
Expand All @@ -169,7 +218,14 @@ impl FunctionMiddleware for FunctionMetering {
// corner cases.
let option = get_opcode_cost(&operator, &self.opcode_cost.lock().unwrap());
match option {
Some(cost) => self.accumulated_cost += cost as u64,
Some(cost) if is_supported_bulk_memory_operator(&operator) => {
self.inject_bulk_memory_cost(state, cost);
// immediatly insert out of gas check as this operation might be expensive
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

Typo in the comment: 'immediatly' should be 'immediately'.

Suggested change
// immediatly insert out of gas check as this operation might be expensive
// immediately insert out of gas check as this operation might be expensive

Copilot uses AI. Check for mistakes.
self.inject_out_of_gas_check(state);
},
Some(cost) => {
self.accumulated_cost += cost as u64
},
None => {
return Err(MiddlewareError::new(
"metering_middleware",
Expand Down
2 changes: 2 additions & 0 deletions vm-executor-wasmer/src/wasmer_metering_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ pub fn get_opcode_cost(op: &Operator, opcode_cost: &OpcodeCost) -> Option<u32> {
Operator::Loop { .. } => Some(opcode_cost.opcode_loop),
Operator::MemoryGrow { .. } => Some(opcode_cost.opcode_memorygrow),
Operator::MemorySize { .. } => Some(opcode_cost.opcode_memorysize),
Operator::MemoryCopy { .. } => Some(opcode_cost.opcode_memorycopy),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need proper opcode check versioning.
I will be adding it soon.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I do not think we need. Like we might add some opcodes once per year, there is no need for that. SpaceVM can treat activations, there is no need to complicated the executor.

Operator::MemoryFill { .. } => Some(opcode_cost.opcode_memoryfill),
Operator::Nop { .. } => Some(opcode_cost.opcode_nop),
Operator::RefFunc { .. } => Some(opcode_cost.opcode_reffunc),
Operator::RefIsNull { .. } => Some(opcode_cost.opcode_refisnull),
Expand Down
4 changes: 4 additions & 0 deletions vm-executor/src/opcode_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ pub struct OpcodeCost {
pub opcode_memorygrow: u32,
#[serde(rename = "MemorySize", default)]
pub opcode_memorysize: u32,
#[serde(rename = "MemoryCopy", default)]
pub opcode_memorycopy: u32,
#[serde(rename = "MemoryFill", default)]
pub opcode_memoryfill: u32,
#[serde(rename = "Nop", default)]
pub opcode_nop: u32,
#[serde(rename = "RefFunc", default)]
Expand Down
Loading