1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
use crate::{
CallStack, GlobalIdx, GlobalMemory, ImportLinker, InstructionPtr, Pages, RwasmModule,
SignatureIdx, StoreTr, SyscallHandler, TableEntity, TableIdx, TrapCode, UntypedValue,
ValueStack, N_DEFAULT_MAX_MEMORY_PAGES,
};
use alloc::{sync::Arc, vec::Vec};
use bitvec::{order::Lsb0, vec::BitVec};
use hashbrown::HashMap;
/// Host-side store that holds memory, tables, globals, and host context for a rwasm instance.
/// It also tracks fuel for metering and provides access to imported functions and syscalls.
/// The store is passed to host callbacks and persists across invocations of the same module.
pub struct RwasmStore<T: 'static> {
/// Total amount of fuel consumed by the currently running instance.
pub(crate) consumed_fuel: u64,
/// The linear memory shared by the running module and the host.
pub(crate) global_memory: GlobalMemory,
/// User-defined context available to host functions and syscalls.
pub(crate) data: T,
/// The last used signature index used for validating indirect calls.
pub(crate) last_signature: Option<SignatureIdx>,
/// Runtime-managed tables (may differ from compile-time layout due to mutations).
pub(crate) tables: HashMap<TableIdx, TableEntity>,
/// Runtime values of mutable and immutable globals.
pub(crate) global_variables: HashMap<GlobalIdx, UntypedValue>,
/// Bitset tracking which data segments have been consumed/emptied.
pub(crate) empty_data_segments: BitVec,
/// Bitset tracking which element segments have been consumed/emptied.
pub(crate) empty_elem_segments: BitVec,
/// Dispatcher for system calls made by the guest.
pub(crate) syscall_handler: SyscallHandler<T>,
/// Linker that resolves imports to host functions/globals.
pub(crate) import_linker: Arc<ImportLinker>,
/// If set, contains the instruction/value-stack pointers to resume after a suspension.
pub(crate) resumable_context: Option<ReusableContext>,
/// A fuel config (None stands for no limit).
pub(crate) fuel_limit: Option<u64>,
/// Execution tracer used when the `tracing` feature is enabled.
#[cfg(feature = "tracing")]
pub tracer: crate::Tracer,
}
pub struct ReusableContext {
pub module: RwasmModule,
pub call_stack: CallStack,
pub ip: InstructionPtr,
pub value_stack: ValueStack,
}
impl<T: 'static + Default> Default for RwasmStore<T> {
fn default() -> Self {
Self::new(
Arc::new(ImportLinker::default()),
T::default(),
crate::always_failing_syscall_handler,
None,
None,
)
}
}
impl<T: 'static> StoreTr<T> for RwasmStore<T> {
fn memory_read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), TrapCode> {
self.global_memory.read(offset, buffer)
}
fn memory_read_into_vec(&mut self, offset: usize, length: usize) -> Result<Vec<u8>, TrapCode> {
self.global_memory.read_into_vec(offset, length)
}
fn memory_write(&mut self, offset: usize, buffer: &[u8]) -> Result<(), TrapCode> {
self.global_memory.write(offset, buffer)?;
#[cfg(feature = "tracing")]
self.tracer
.memory_change(offset as u32, buffer.len() as u32, buffer);
Ok(())
}
fn data_mut(&mut self) -> &mut T {
&mut self.data
}
fn data(&self) -> &T {
&self.data
}
fn try_consume_fuel(&mut self, delta: u64) -> Result<(), TrapCode> {
let consumed_fuel = self.consumed_fuel.saturating_add(delta);
if let Some(fuel_limit) = self.fuel_limit {
if consumed_fuel > fuel_limit {
return Err(TrapCode::OutOfFuel);
}
}
self.consumed_fuel = consumed_fuel;
Ok(())
}
fn remaining_fuel(&self) -> Option<u64> {
Some(self.fuel_limit? - self.consumed_fuel)
}
fn reset_fuel(&mut self, new_fuel_limit: u64) {
// If new fuel limit is presented then change it
self.fuel_limit = Some(new_fuel_limit);
// Reset consumed fuel to 0 (indicating we have the entire fuel limit unspent)
self.consumed_fuel = 0;
}
}
impl<T: 'static> RwasmStore<T> {
pub fn new(
import_linker: Arc<ImportLinker>,
context: T,
syscall_handler: SyscallHandler<T>,
fuel_limit: Option<u64>,
max_allowed_memory_pages: Option<u32>,
) -> Self {
let global_memory = GlobalMemory::new(
Pages::new_unchecked(0),
Pages::new_unchecked(max_allowed_memory_pages.unwrap_or(N_DEFAULT_MAX_MEMORY_PAGES)),
);
Self {
consumed_fuel: 0,
global_memory,
data: context,
#[cfg(feature = "tracing")]
tracer: crate::Tracer::default(),
global_variables: Default::default(),
tables: Default::default(),
last_signature: None,
syscall_handler,
empty_data_segments: BitVec::EMPTY,
empty_elem_segments: BitVec::EMPTY,
import_linker,
resumable_context: None,
fuel_limit,
}
}
/// Resets the state of the current execution context.
pub fn reset(&mut self, keep_flags: bool) {
// reset consumed fuel to 0
self.consumed_fuel = 0;
// we might want to keep data/elem flags between calls, it's required for e2e tests
if !keep_flags {
// we don't do any assumptions regarding how data segments are used,
// maybe there is a way to optimize reuse of bitset.
if self.empty_data_segments.len() <= size_of::<usize>() {
self.empty_data_segments.fill(false);
} else {
self.empty_data_segments = BitVec::<usize, Lsb0>::EMPTY;
}
// we don't do any assumptions regarding how tables are used inside the applications,
// so keep it always empty, probably there is an optimization here.
if self.empty_elem_segments.len() <= size_of::<usize>() {
self.empty_elem_segments.fill(false);
} else {
self.empty_elem_segments = BitVec::<usize, Lsb0>::EMPTY;
}
}
// in case of a trap, we might have this flag remains active
self.last_signature = None;
}
pub fn fuel_consumed(&self) -> u64 {
self.consumed_fuel
}
/// Returns the current linear memory size in bytes.
///
/// Note: rwasm currently supports only the default (index 0) linear memory.
pub fn memory_size_bytes(&self) -> usize {
self.global_memory.data().len()
}
/// Returns a snapshot of the first `max_bytes` of linear memory.
///
/// This is intended for differential testing/fuzzing where we want to compare post-call
/// side effects without copying potentially huge memories.
pub fn memory_snapshot_prefix(&self, max_bytes: usize) -> Vec<u8> {
let mem = self.global_memory.data();
let n = core::cmp::min(mem.len(), max_bytes);
mem[..n].to_vec()
}
/// Returns a full snapshot of linear memory.
///
/// This is primarily intended for differential fuzzing using Wasmtime's oracle strategy,
/// which compares full exported memories.
pub fn memory_snapshot(&self) -> Vec<u8> {
self.global_memory.data().to_vec()
}
/// Returns per-table snapshots as `(table_index, size, non_null_prefix)` tuples.
///
/// `non_null_prefix` contains `0/1` bytes describing whether each element is null (0) or non-null (1)
/// for the first `max_elems` elements of the table.
pub fn table_snapshots_nullness_prefix(&self, max_elems: usize) -> Vec<(u32, u32, Vec<u8>)> {
let mut out: Vec<(u32, u32, Vec<u8>)> = Vec::new();
// Ensure deterministic ordering for differential comparisons.
let mut table_indices: Vec<TableIdx> = self.tables.keys().copied().collect();
table_indices.sort_unstable();
for idx in table_indices {
// TableIdx is a new type wrapper around u32.
let table = &self.tables[&idx];
let size = table.size();
let n = core::cmp::min(size as usize, max_elems);
let mut prefix = Vec::with_capacity(n);
for &bits in table.elements.iter().take(n) {
// In rwasm, a null funcref is represented as 0.
prefix.push(if bits == 0 { 0 } else { 1 });
}
out.push((idx as u32, size, prefix));
}
out
}
/// Returns the raw 32-bit "global word" at the given internal index.
///
/// rwasm stores globals as 32-bit words. `i64`/`f64` globals occupy **two** words:
/// - low word at `global_index * 2`
/// - high word at `global_index * 2 + 1`
///
/// This accessor is intended for differential fuzzing/oracles.
pub fn global_word_bits(&self, global_word_index: u32) -> u32 {
self.global_variables
.get(&global_word_index)
.copied()
.unwrap_or_default()
.to_bits()
}
}