Skip to content

Commit 03118ad

Browse files
committed
Implement user fault handling with userfaultfd on Linux.
This commit implements the `uffd` feature which turns on support for utilizing the `userfaultfd` system call on Linux for the pooling instance allocator. By handling page faults in userland, we are able to detect guard page accesses without having to constantly change memory page protections. This should help reduce the number of syscalls as well as kernel lock contentions when many threads are allocating and deallocating instances. Additionally, the user fault handler can lazy initialize table and linear memories of an instance (implementation to come).
1 parent 03d9ea7 commit 03118ad

7 files changed

Lines changed: 707 additions & 9 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ jobs:
122122
- run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features jitdump
123123
- run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features cache
124124
- run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features async
125+
- run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features uffd
125126

126127
# Check some feature combinations of the `wasmtime-c-api` crate
127128
- run: cargo check --manifest-path crates/c-api/Cargo.toml --no-default-features

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ jitdump = ["wasmtime/jitdump"]
8989
vtune = ["wasmtime/vtune"]
9090
wasi-crypto = ["wasmtime-wasi-crypto"]
9191
wasi-nn = ["wasmtime-wasi-nn"]
92+
uffd = ["wasmtime/uffd"]
9293

9394
# Try the experimental, work-in-progress new x86_64 backend. This is not stable
9495
# as of June 2020.

crates/runtime/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ cc = "1.0"
3737

3838
[badges]
3939
maintenance = { status = "actively-developed" }
40+
41+
[features]
42+
default = []
43+
44+
# Enables support for userfaultfd in the pooling allocator when building on Linux
45+
uffd = ["userfaultfd"]

crates/runtime/src/instance/allocator/pooling.rs

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ cfg_if::cfg_if! {
3131
if #[cfg(windows)] {
3232
mod windows;
3333
use windows as imp;
34+
} else if #[cfg(all(feature = "uffd", target_os = "linux"))] {
35+
mod uffd;
36+
use uffd as imp;
37+
use imp::{PageFaultHandler, reset_guard_page};
38+
use std::sync::atomic::{AtomicBool, Ordering};
3439
} else if #[cfg(target_os = "linux")] {
3540
mod linux;
3641
use linux as imp;
@@ -335,6 +340,9 @@ impl Iterator for BasePointerIterator {
335340
/// structure depending on the limits used to create the pool.
336341
///
337342
/// The pool maintains a free list for fast instance allocation.
343+
///
344+
/// The userfault handler relies on how instances are stored in the mapping,
345+
/// so make sure the uffd implementation is kept up-to-date.
338346
#[derive(Debug)]
339347
struct InstancePool {
340348
mapping: Mmap,
@@ -472,6 +480,10 @@ impl Drop for InstancePool {
472480
///
473481
/// Each index into the pool returns an iterator over the base addresses
474482
/// of the instance's linear memories.
483+
///
484+
///
485+
/// The userfault handler relies on how memories are stored in the mapping,
486+
/// so make sure the uffd implementation is kept up-to-date.
475487
#[derive(Debug)]
476488
struct MemoryPool {
477489
mapping: Mmap,
@@ -524,6 +536,9 @@ impl MemoryPool {
524536
///
525537
/// Each index into the pool returns an iterator over the base addresses
526538
/// of the instance's tables.
539+
///
540+
/// The userfault handler relies on how tables are stored in the mapping,
541+
/// so make sure the uffd implementation is kept up-to-date.
527542
#[derive(Debug)]
528543
struct TablePool {
529544
mapping: Mmap,
@@ -588,13 +603,18 @@ impl TablePool {
588603
///
589604
/// The top of the stack (starting stack pointer) is returned when a stack is allocated
590605
/// from the pool.
606+
///
607+
/// The userfault handler relies on how stacks are stored in the mapping,
608+
/// so make sure the uffd implementation is kept up-to-date.
591609
#[derive(Debug)]
592610
struct StackPool {
593611
mapping: Mmap,
594612
stack_size: usize,
595613
max_instances: usize,
596614
page_size: usize,
597615
free_list: Mutex<Vec<usize>>,
616+
#[cfg(all(feature = "uffd", target_os = "linux"))]
617+
faulted_guard_pages: Arc<[AtomicBool]>,
598618
}
599619

600620
impl StackPool {
@@ -623,6 +643,11 @@ impl StackPool {
623643
max_instances,
624644
page_size,
625645
free_list: Mutex::new((0..max_instances).collect()),
646+
#[cfg(all(feature = "uffd", target_os = "linux"))]
647+
faulted_guard_pages: std::iter::repeat_with(|| false.into())
648+
.take(max_instances)
649+
.collect::<Vec<_>>()
650+
.into(),
626651
})
627652
}
628653

@@ -647,11 +672,25 @@ impl StackPool {
647672
.as_mut_ptr()
648673
.add((index * self.stack_size) + self.page_size);
649674

650-
// Make the stack accessible (excluding the guard page)
651-
if !make_accessible(bottom_of_stack, size_without_guard) {
652-
return Err(FiberStackError::Resource(
653-
"failed to make instance memory accessible".into(),
654-
));
675+
cfg_if::cfg_if! {
676+
if #[cfg(all(feature = "uffd", target_os = "linux"))] {
677+
// Check to see if a guard page needs to be reset
678+
if self.faulted_guard_pages[index].swap(false, Ordering::SeqCst) {
679+
if !reset_guard_page(bottom_of_stack.sub(self.page_size), self.page_size) {
680+
return Err(FiberStackError::Resource(
681+
"failed to reset stack guard page".into(),
682+
));
683+
}
684+
}
685+
686+
} else {
687+
// Make the stack accessible (excluding the guard page)
688+
if !make_accessible(bottom_of_stack, size_without_guard) {
689+
return Err(FiberStackError::Resource(
690+
"failed to make instance memory accessible".into(),
691+
));
692+
}
693+
}
655694
}
656695

657696
// The top of the stack should be returned
@@ -697,6 +736,8 @@ pub struct PoolingInstanceAllocator {
697736
memories: mem::ManuallyDrop<MemoryPool>,
698737
tables: mem::ManuallyDrop<TablePool>,
699738
stacks: mem::ManuallyDrop<StackPool>,
739+
#[cfg(all(feature = "uffd", target_os = "linux"))]
740+
_fault_handler: PageFaultHandler,
700741
}
701742

702743
impl PoolingInstanceAllocator {
@@ -744,6 +785,9 @@ impl PoolingInstanceAllocator {
744785
let tables = TablePool::new(&module_limits, &instance_limits)?;
745786
let stacks = StackPool::new(&instance_limits, stack_size)?;
746787

788+
#[cfg(all(feature = "uffd", target_os = "linux"))]
789+
let _fault_handler = PageFaultHandler::new(&instances, &memories, &tables, &stacks)?;
790+
747791
Ok(Self {
748792
strategy,
749793
module_limits,
@@ -752,6 +796,8 @@ impl PoolingInstanceAllocator {
752796
memories: mem::ManuallyDrop::new(memories),
753797
tables: mem::ManuallyDrop::new(tables),
754798
stacks: mem::ManuallyDrop::new(stacks),
799+
#[cfg(all(feature = "uffd", target_os = "linux"))]
800+
_fault_handler,
755801
})
756802
}
757803

@@ -800,14 +846,28 @@ impl PoolingInstanceAllocator {
800846
) -> Result<(), InstantiationError> {
801847
let module = instance.module.as_ref();
802848

849+
// Reset all guard pages before clearing the previous memories
850+
#[cfg(all(feature = "uffd", target_os = "linux"))]
851+
for (_, m) in instance.memories.iter() {
852+
m.reset_guard_pages()
853+
.map_err(InstantiationError::Resource)?;
854+
}
855+
803856
instance.memories.clear();
804857

805858
for plan in
806859
(&module.memory_plans.values().as_slice()[module.num_imported_memories..]).iter()
807860
{
808861
instance.memories.push(
809-
Memory::new_static(plan, memories.next().unwrap(), max_pages, make_accessible)
810-
.map_err(InstantiationError::Resource)?,
862+
Memory::new_static(
863+
plan,
864+
memories.next().unwrap(),
865+
max_pages,
866+
make_accessible,
867+
#[cfg(all(feature = "uffd", target_os = "linux"))]
868+
reset_guard_page,
869+
)
870+
.map_err(InstantiationError::Resource)?,
811871
);
812872
}
813873

@@ -826,7 +886,6 @@ impl PoolingInstanceAllocator {
826886
let module = instance.module.as_ref();
827887

828888
instance.tables.clear();
829-
830889
for plan in (&module.table_plans.values().as_slice()[module.num_imported_tables..]).iter() {
831890
let base = tables.next().unwrap();
832891

@@ -852,7 +911,8 @@ impl PoolingInstanceAllocator {
852911

853912
impl Drop for PoolingInstanceAllocator {
854913
fn drop(&mut self) {
855-
// There are manually dropped for the future uffd implementation
914+
// Manually drop the pools before the fault handler (if uffd is enabled)
915+
// This ensures that any fault handler thread monitoring the pool memory terminates
856916
unsafe {
857917
mem::ManuallyDrop::drop(&mut self.instances);
858918
mem::ManuallyDrop::drop(&mut self.memories);

0 commit comments

Comments
 (0)