Skip to content

Commit 80b3bac

Browse files
committed
Fix pooling allocator predicate to reset VM permissions
This commit fixes a mistake that was introduced in bytecodealliance#9583 where the logic to reset a linear memory slot in the pooling allocator used the wrong predicate. Specifically VM permissions must be reset if virtual memory can be relied on at all, and the preexisting predicate of `can_elide_bounds_check` was an inaccurate representation of this. The correct predicate to check is `can_use_virtual_memory`.
1 parent 6844a83 commit 80b3bac

3 files changed

Lines changed: 159 additions & 97 deletions

File tree

crates/wasmtime/src/runtime/vm/cow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ impl MemoryImageSlot {
460460
let host_page_size_log2 = u8::try_from(host_page_size().ilog2()).unwrap();
461461
if initial_size_bytes_page_aligned < self.accessible
462462
&& (tunables.memory_guard_size > 0
463-
|| ty.can_elide_bounds_check(tunables, host_page_size_log2))
463+
|| ty.can_use_virtual_memory(tunables, host_page_size_log2))
464464
{
465465
self.set_protection(initial_size_bytes_page_aligned..self.accessible, false)?;
466466
self.accessible = initial_size_bytes_page_aligned;

tests/all/pooling_allocator.rs

Lines changed: 142 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -670,103 +670,106 @@ fn instance_too_large() -> Result<()> {
670670
#[cfg_attr(miri, ignore)]
671671
fn dynamic_memory_pooling_allocator() -> Result<()> {
672672
for guard_size in [0, 1 << 16] {
673-
let max_size = 128 << 20;
674-
let mut pool = crate::small_pool_config();
675-
pool.max_memory_size(max_size as usize);
676-
let mut config = Config::new();
677-
config.memory_reservation(max_size);
678-
config.memory_guard_size(guard_size);
679-
config.allocation_strategy(pool);
680-
681-
let engine = Engine::new(&config)?;
682-
683-
let module = Module::new(
684-
&engine,
685-
r#"
686-
(module
687-
(memory (export "memory") 1)
688-
689-
(func (export "grow") (param i32) (result i32)
690-
local.get 0
691-
memory.grow)
692-
693-
(func (export "size") (result i32)
694-
memory.size)
695-
696-
(func (export "i32.load") (param i32) (result i32)
697-
local.get 0
698-
i32.load)
699-
700-
(func (export "i32.store") (param i32 i32)
701-
local.get 0
702-
local.get 1
703-
i32.store)
704-
705-
(data (i32.const 100) "x")
706-
)
707-
"#,
708-
)?;
709-
710-
let mut store = Store::new(&engine, ());
711-
let instance = Instance::new(&mut store, &module, &[])?;
673+
for signals_based_traps in [false, true] {
674+
let max_size = 128 << 20;
675+
let mut pool = crate::small_pool_config();
676+
pool.max_memory_size(max_size as usize);
677+
let mut config = Config::new();
678+
config.memory_reservation(max_size);
679+
config.memory_guard_size(guard_size);
680+
config.allocation_strategy(pool);
681+
config.signals_based_traps(signals_based_traps);
682+
683+
let engine = Engine::new(&config)?;
684+
685+
let module = Module::new(
686+
&engine,
687+
r#"
688+
(module
689+
(memory (export "memory") 1)
690+
691+
(func (export "grow") (param i32) (result i32)
692+
local.get 0
693+
memory.grow)
694+
695+
(func (export "size") (result i32)
696+
memory.size)
697+
698+
(func (export "i32.load") (param i32) (result i32)
699+
local.get 0
700+
i32.load)
701+
702+
(func (export "i32.store") (param i32 i32)
703+
local.get 0
704+
local.get 1
705+
i32.store)
706+
707+
(data (i32.const 100) "x")
708+
)
709+
"#,
710+
)?;
712711

713-
let grow = instance.get_typed_func::<u32, i32>(&mut store, "grow")?;
714-
let size = instance.get_typed_func::<(), u32>(&mut store, "size")?;
715-
let i32_load = instance.get_typed_func::<u32, i32>(&mut store, "i32.load")?;
716-
let i32_store = instance.get_typed_func::<(u32, i32), ()>(&mut store, "i32.store")?;
717-
let memory = instance.get_memory(&mut store, "memory").unwrap();
718-
719-
// basic length 1 tests
720-
// assert_eq!(memory.grow(&mut store, 1)?, 0);
721-
assert_eq!(memory.size(&store), 1);
722-
assert_eq!(size.call(&mut store, ())?, 1);
723-
assert_eq!(i32_load.call(&mut store, 0)?, 0);
724-
assert_eq!(i32_load.call(&mut store, 100)?, i32::from(b'x'));
725-
i32_store.call(&mut store, (0, 0))?;
726-
i32_store.call(&mut store, (100, i32::from(b'y')))?;
727-
assert_eq!(i32_load.call(&mut store, 100)?, i32::from(b'y'));
728-
729-
// basic length 2 tests
730-
let page = 64 * 1024;
731-
assert_eq!(grow.call(&mut store, 1)?, 1);
732-
assert_eq!(memory.size(&store), 2);
733-
assert_eq!(size.call(&mut store, ())?, 2);
734-
i32_store.call(&mut store, (page, 200))?;
735-
assert_eq!(i32_load.call(&mut store, page)?, 200);
736-
737-
// test writes are visible
738-
i32_store.call(&mut store, (2, 100))?;
739-
assert_eq!(i32_load.call(&mut store, 2)?, 100);
740-
741-
// test growth can't exceed maximum
742-
let too_many = max_size / (64 * 1024);
743-
assert_eq!(grow.call(&mut store, too_many as u32)?, -1);
744-
assert!(memory.grow(&mut store, too_many).is_err());
745-
746-
assert_eq!(memory.data(&store)[page as usize], 200);
747-
748-
// Re-instantiate in another store.
749-
store = Store::new(&engine, ());
750-
let instance = Instance::new(&mut store, &module, &[])?;
751-
let i32_load = instance.get_typed_func::<u32, i32>(&mut store, "i32.load")?;
752-
let memory = instance.get_memory(&mut store, "memory").unwrap();
753-
754-
// This is out of bounds...
755-
assert!(i32_load.call(&mut store, page).is_err());
756-
assert_eq!(memory.data_size(&store), page as usize);
757-
758-
// ... but implementation-wise it should still be mapped memory from
759-
// before if we don't have any guard pages.
760-
//
761-
// Note though that prior writes should all appear as zeros and we can't see
762-
// data from the prior instance.
763-
//
764-
// Note that this part is only implemented on Linux which has
765-
// `MADV_DONTNEED`.
766-
if cfg!(target_os = "linux") && guard_size == 0 {
767-
unsafe {
768-
let ptr = memory.data_ptr(&store);
769-
assert_eq!(*ptr.offset(page as isize), 0);
712+
let mut store = Store::new(&engine, ());
713+
let instance = Instance::new(&mut store, &module, &[])?;
714+
715+
let grow = instance.get_typed_func::<u32, i32>(&mut store, "grow")?;
716+
let size = instance.get_typed_func::<(), u32>(&mut store, "size")?;
717+
let i32_load = instance.get_typed_func::<u32, i32>(&mut store, "i32.load")?;
718+
let i32_store = instance.get_typed_func::<(u32, i32), ()>(&mut store, "i32.store")?;
719+
let memory = instance.get_memory(&mut store, "memory").unwrap();
720+
721+
// basic length 1 tests
722+
// assert_eq!(memory.grow(&mut store, 1)?, 0);
723+
assert_eq!(memory.size(&store), 1);
724+
assert_eq!(size.call(&mut store, ())?, 1);
725+
assert_eq!(i32_load.call(&mut store, 0)?, 0);
726+
assert_eq!(i32_load.call(&mut store, 100)?, i32::from(b'x'));
727+
i32_store.call(&mut store, (0, 0))?;
728+
i32_store.call(&mut store, (100, i32::from(b'y')))?;
729+
assert_eq!(i32_load.call(&mut store, 100)?, i32::from(b'y'));
730+
731+
// basic length 2 tests
732+
let page = 64 * 1024;
733+
assert_eq!(grow.call(&mut store, 1)?, 1);
734+
assert_eq!(memory.size(&store), 2);
735+
assert_eq!(size.call(&mut store, ())?, 2);
736+
i32_store.call(&mut store, (page, 200))?;
737+
assert_eq!(i32_load.call(&mut store, page)?, 200);
738+
739+
// test writes are visible
740+
i32_store.call(&mut store, (2, 100))?;
741+
assert_eq!(i32_load.call(&mut store, 2)?, 100);
742+
743+
// test growth can't exceed maximum
744+
let too_many = max_size / (64 * 1024);
745+
assert_eq!(grow.call(&mut store, too_many as u32)?, -1);
746+
assert!(memory.grow(&mut store, too_many).is_err());
747+
748+
assert_eq!(memory.data(&store)[page as usize], 200);
749+
750+
// Re-instantiate in another store.
751+
store = Store::new(&engine, ());
752+
let instance = Instance::new(&mut store, &module, &[])?;
753+
let i32_load = instance.get_typed_func::<u32, i32>(&mut store, "i32.load")?;
754+
let memory = instance.get_memory(&mut store, "memory").unwrap();
755+
756+
// This is out of bounds...
757+
assert!(i32_load.call(&mut store, page).is_err());
758+
assert_eq!(memory.data_size(&store), page as usize);
759+
760+
// ... but implementation-wise it should still be mapped memory from
761+
// before if we don't have any guard pages.
762+
//
763+
// Note though that prior writes should all appear as zeros and we can't see
764+
// data from the prior instance.
765+
//
766+
// Note that this part is only implemented on Linux which has
767+
// `MADV_DONTNEED`.
768+
if cfg!(target_os = "linux") && guard_size == 0 && !signals_based_traps {
769+
unsafe {
770+
let ptr = memory.data_ptr(&store);
771+
assert_eq!(*ptr.offset(page as isize), 0);
772+
}
770773
}
771774
}
772775
}
@@ -1354,6 +1357,49 @@ fn pagemap_scan_enabled_or_disabled() -> Result<()> {
13541357
Ok(())
13551358
}
13561359

1360+
#[test]
1361+
#[cfg_attr(miri, ignore)]
1362+
fn pooling_reuse_resets() -> Result<()> {
1363+
let mut config = Config::new();
1364+
let mut cfg = crate::small_pool_config();
1365+
cfg.total_memories(1);
1366+
cfg.max_memory_size(0x2000000);
1367+
config.allocation_strategy(InstanceAllocationStrategy::Pooling(cfg));
1368+
config.memory_guard_size(0);
1369+
config.memory_reservation(0x2000000);
1370+
let engine = Engine::new(&config)?;
1371+
1372+
let a = Module::new(&engine, r#"(module (memory (export "m") 10 100))"#)?;
1373+
let b = Module::new(
1374+
&engine,
1375+
r#"
1376+
(module $B
1377+
(memory 5 100)
1378+
(func (export "read_oob") (result i32)
1379+
(i32.load (i32.const 983040))
1380+
)
1381+
)
1382+
"#,
1383+
)?;
1384+
1385+
{
1386+
let mut store = Store::new(&engine, ());
1387+
let instance = Instance::new(&mut store, &a, &[])?;
1388+
let memory = instance.get_memory(&mut store, "m").unwrap();
1389+
memory.grow(&mut store, 10)?;
1390+
}
1391+
1392+
{
1393+
let mut store = Store::new(&engine, ());
1394+
let instance = Instance::new(&mut store, &b, &[])?;
1395+
let read_oob = instance.get_typed_func::<(), i32>(&mut store, "read_oob")?;
1396+
let trap: Trap = read_oob.call(&mut store, ()).unwrap_err().downcast()?;
1397+
assert_eq!(trap, Trap::MemoryOutOfBounds);
1398+
}
1399+
1400+
Ok(())
1401+
}
1402+
13571403
// This test instantiates a memory with an image into a slot in the pooling
13581404
// allocator in a way that maps the image into the allocator but fails
13591405
// instantiation. Failure here is injected with `ResourceLimiter`. Afterwards
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(module)
2+
3+
(thread $t1
4+
(module $A
5+
(memory 10 100)
6+
(func (export "grow")
7+
(drop (memory.grow (i32.const 10)))))
8+
(invoke "grow")
9+
)
10+
(wait $t1)
11+
12+
(module $B
13+
(memory 5 100)
14+
(func (export "read_oob") (result i32)
15+
(i32.load (i32.const 983040))))
16+
(assert_trap (invoke "read_oob") "out of bounds memory access")

0 commit comments

Comments
 (0)