Skip to content

Commit 9c97c2b

Browse files
authored
fix(memory_instance): zero out heap memory when reallocating after reset (#941)
* fix(memory_instance): zero out heap memory when reallocating * fix: pin base64ct * fix: tests and saturating_sub * fix: more deps * fix: test * fix: naming, fmt
1 parent d8fa4ae commit 9c97c2b

File tree

6 files changed

+84
-1
lines changed

6 files changed

+84
-1
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ jobs:
221221
targets: "wasm32-unknown-unknown"
222222

223223
- name: Installing required crates
224-
run: cargo install wasm-bindgen-cli wasm-opt
224+
run: cargo install wasm-bindgen-cli wasm-opt --locked
225225

226226
- name: Setup PNPM
227227
uses: pnpm/action-setup@v4

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- [941](https://github.com/FuelLabs/fuel-vm/pull/941): Fix heap memory reallocation after reset.
13+
1014
## [Version 0.59.2]
1115

1216
### Fixed

fuel-crypto/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ repository = { workspace = true }
1111
description = "Fuel cryptographic primitives."
1212

1313
[dependencies]
14+
base64ct = { version = "=1.6.0" }
1415
coins-bip32 = { version = "0.8", default-features = false, optional = true }
1516
coins-bip39 = { version = "0.8", default-features = false, features = ["english"], optional = true }
1617
ecdsa = { version = "0.16", default-features = false }
1718
ed25519-dalek = { version = "2.0.0", default-features = false }
1819
fuel-types = { workspace = true, default-features = false }
20+
half = { version = "=2.4.1", default-features = false }
1921
k256 = { version = "0.13", default-features = false, features = ["digest", "ecdsa"] }
2022
lazy_static = { version = "1.4", optional = true }
2123
p256 = { version = "0.13", default-features = false, features = ["digest", "ecdsa"] }

fuel-crypto/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
#[cfg(test)]
2020
use fuel_crypto as _;
2121

22+
use base64ct as _;
23+
use half as _;
24+
2225
/// Required export for using mnemonic keygen on [`SecretKey::new_from_mnemonic`]
2326
#[cfg(feature = "std")]
2427
#[doc(no_inline)]

fuel-vm/src/interpreter/memory.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod tests;
5151
#[cfg(test)]
5252
mod impl_tests;
5353

54+
#[allow(non_snake_case)]
5455
#[cfg(test)]
5556
mod allocation_tests;
5657

@@ -187,6 +188,26 @@ impl MemoryInstance {
187188
self.heap[start..end].fill(0);
188189
} else {
189190
// Reallocation is needed.
191+
192+
// Need to clear dirty memory before expanding it. An example:
193+
// Heap vector: [dirty, dirty, dirty, 0, 0, 0]
194+
// /|\
195+
// |
196+
// HP
197+
//
198+
// If we copy from [0, old_len), it means we copy the dirty memory as well.
199+
// Ending up with:
200+
// Heap vector: [0, 0, dirty, dirty, dirty, 0, 0, 0]
201+
// /|\
202+
// |
203+
// HP
204+
//
205+
// So, either we need to clear the memory before copying,
206+
// or after we copied dirty parts.
207+
// Clearing before looks like more readable solution.
208+
let end = self.hp.saturating_sub(self.heap_offset());
209+
self.heap[..end].fill(0);
210+
190211
// To reduce frequent reallocations, allocate at least 256 bytes at once.
191212
// After that, double the allocation every time.
192213
let cap = new_len.next_power_of_two().clamp(256, MEM_SIZE);

fuel-vm/src/interpreter/memory/allocation_tests.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,56 @@ fn test_store_word(has_ownership: bool, a: Word, b: Word, c: Imm12) -> SimpleRes
284284

285285
Ok(())
286286
}
287+
288+
macro_rules! generate_test__memory_instance__grow_heap_by_after_reset__does_not_retain_dirty_memory_size {
289+
($size:expr) => {
290+
paste::paste! {
291+
#[test]
292+
fn [<memory_instance__grow_heap_by_after_reset__does_not_retain_dirty_memory_size_ $size>]() -> SimpleResult<()> {
293+
// given: a pre-initialized memory instance with $size bytes of heap memory
294+
let mut memory_instance = MemoryInstance::new();
295+
let sp = Reg::new(&10);
296+
let mut hp = VM_MAX_RAM;
297+
const SIZE: usize = $size;
298+
299+
memory_instance.grow_heap_by(sp, RegMut::new(&mut hp), SIZE.try_into().unwrap())?;
300+
memory_instance.write_bytes_noownerchecks(
301+
memory_instance.hp,
302+
[1u8; SIZE],
303+
)?;
304+
305+
// when: we reset and grow the heap again, triggering the reallocation
306+
// after we grow the heap again, it should not have any memory left over from before
307+
// the reset
308+
memory_instance.reset();
309+
let mut hp = VM_MAX_RAM;
310+
const NEW_SIZE: usize = SIZE * 3;
311+
memory_instance.grow_heap_by(
312+
sp,
313+
RegMut::new(&mut hp),
314+
NEW_SIZE.try_into().unwrap(),
315+
)?;
316+
let heap = memory_instance.heap_raw();
317+
let heap_len = heap.len();
318+
319+
// then: we check that the heap is all zeroed out
320+
assert_eq!(heap, vec![0u8; heap_len]);
321+
322+
Ok(())
323+
}
324+
}
325+
};
326+
}
327+
328+
// Generate tests with different sizes
329+
macro_rules! generate_tests__memory_instance__grow_heap_by_after_reset__does_not_retain_dirty_memory_size {
330+
($($size:expr),*) => {
331+
$(
332+
generate_test__memory_instance__grow_heap_by_after_reset__does_not_retain_dirty_memory_size!($size);
333+
)*
334+
};
335+
}
336+
337+
generate_tests__memory_instance__grow_heap_by_after_reset__does_not_retain_dirty_memory_size!(
338+
0, 1, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072
339+
);

0 commit comments

Comments
 (0)