Skip to content

Use page tracking for snapshot and restore #683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
71d0c00
Update dependencies
simongdavies Jul 2, 2025
0629843
Add benchmarks for guest calls with large parameters and sandbox heap…
simongdavies Jul 2, 2025
52f79a5
Add Linux and Windows implementations for dirty page tracking
simongdavies Jul 2, 2025
1e6c62f
Enhance signal handling documentation to include SIGSEGV and debuggin…
simongdavies Jul 2, 2025
0f721f5
Implement dirty page tracking for Hyperv and KVM drivers
simongdavies Jul 2, 2025
ae3077e
Improve error handling in interrupt_same_thread test and add sandbox …
simongdavies Jul 2, 2025
e7cb144
Add bitmap module with helper functions for page management
simongdavies Jul 2, 2025
59c5aeb
Add PageSnapshot struct for efficient memory snapshot management
simongdavies Jul 2, 2025
280195d
Integrate DirtyPageTracker into UninitializedSandbox starting host pa…
simongdavies Jul 2, 2025
cf78725
Implement a new module to create/manage snapshots and restore shared …
simongdavies Jul 2, 2025
3f496f3
Add constant for the number of pages in a block to track dirty pages
simongdavies Jul 2, 2025
36f0a1e
Add method to retrieve and clear dirty pages as a bitmap
simongdavies Jul 2, 2025
ad09ebb
Make stack pointer size constant and update output/input data pointer…
simongdavies Jul 2, 2025
66a4287
Refactor SandboxMemoryManager to use SharedMemorySnapshotManager for …
simongdavies Jul 2, 2025
c392443
Refactor memory module to remove shared_mem_snapshot and replace with…
simongdavies Jul 2, 2025
4f1adf4
Remove SharedMemorySnapshot implementation to streamline memory manag…
simongdavies Jul 2, 2025
aefd16d
Enhance shared memory management by adding dirty page tracking and re…
simongdavies Jul 2, 2025
e8a5953
Update MultiUseSandbox to use dirty page tracking in restore and devo…
simongdavies Jul 2, 2025
56fd983
Fix test setup to unpack manager from load_guest_binary_into_memory m…
simongdavies Jul 2, 2025
411a668
Remove dirty_pages from HostMapping struct, remove some unnecessary O…
ludfjig Jul 7, 2025
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
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 18 additions & 6 deletions docs/signal-handlers-development-notes.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Signal Handling in Hyperlight

Hyperlight registers custom signal handlers to intercept and manage specific signals, primarily `SIGSYS` and `SIGRTMIN`. Here's an overview of the registration process:
- **Preserving Old Handlers**: When registering a new signal handler, Hyperlight first retrieves and stores the existing handler using `OnceCell`. This allows Hyperlight to delegate signals to the original handler if necessary.
- **Custom Handlers**:
- **`SIGSYS` Handler**: Captures disallowed syscalls enforced by seccomp. If the signal originates from a hyperlight thread, Hyperlight logs the syscall details. Otherwise, it delegates the signal to the previously registered handler.
- **`SIGRTMIN` Handler**: Utilized for inter-thread signaling, such as execution cancellation. Similar to SIGSYS, it distinguishes between application and non-hyperlight threads to determine how to handle the signal.
- **Thread Differentiation**: Hyperlight uses thread-local storage (IS_HYPERLIGHT_THREAD) to identify whether the current thread is a hyperlight thread. This distinction ensures that signals are handled appropriately based on the thread's role.
Hyperlight registers custom signal handlers to intercept and manage specific signals, primarily `SIGSYS` , `SIGRTMIN` and `SIGSEGV` Here's an overview of the registration process:

- **Preserving Old Handlers**: When registering a new signal handler, Hyperlight first retrieves and stores the existing handler using either `OnceCell` or a `static AtomicPtr` This allows Hyperlight to delegate signals to the original handler if necessary.
- **Custom Handlers**:
- **`SIGSYS` Handler**: Captures disallowed syscalls enforced by seccomp. If the signal originates from a hyperlight thread, Hyperlight logs the syscall details. Otherwise, it delegates the signal to the previously registered handler.
- **`SIGRTMIN` Handler**: Utilized for inter-thread signaling, such as execution cancellation. Similar to SIGSYS, it distinguishes between application and non-hyperlight threads to determine how to handle the signal.
- **`SIGSEGV` Handler**: Handles segmentation faults for dirty page tracking of host memory mapped into a VM. If the signal applies to an address that is mapped to a VM, it is processed by Hyperlight; otherwise, it is passed to the original handler.

## Potential Issues and Considerations

Expand All @@ -15,3 +16,14 @@ Hyperlight registers custom signal handlers to intercept and manage specific sig
- **Invalidation of `old_handler`**: The stored old_handler reference may no longer point to a valid handler, causing undefined behavior when Hyperlight attempts to delegate signals.
- **Loss of Custom Handling**: Hyperlight's custom handler might not be invoked as expected, disrupting its ability to enforce syscall restrictions or manage inter-thread signals.

### Debugging and Signal Handling

By default when debugging a host application/test/example with GDB or LLDB the debugger will handle the `SIGSEGV` signal by breaking when it is raised, to prevent this and let hyperlight handle the signal enter the following in the debug console:

#### LLDB

```process handle SIGSEGV -n true -p true -s false```

#### GDB

```handle SIGSEGV nostop noprint pass```
2 changes: 2 additions & 0 deletions src/hyperlight_common/src/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
pub const PAGE_SHIFT: u64 = 12;
pub const PAGE_SIZE: u64 = 1 << 12;
pub const PAGE_SIZE_USIZE: usize = 1 << 12;
// The number of pages in 1 "block". A single u64 can be used as bitmap to keep track of all dirty pages in a block.
pub const PAGES_IN_BLOCK: usize = 64;

/// A memory region in the guest address space
#[derive(Debug, Clone, Copy)]
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ anyhow = "1.0"
metrics = "0.24.2"
serde_json = "1.0"
elfcore = "2.0"
lockfree ="0.5"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
Expand Down
210 changes: 186 additions & 24 deletions src/hyperlight_host/benches/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,37 +79,65 @@ fn guest_call_benchmark(c: &mut Criterion) {
group.finish();
}

fn guest_call_benchmark_large_param(c: &mut Criterion) {
fn guest_call_benchmark_large_params(c: &mut Criterion) {
let mut group = c.benchmark_group("guest_functions_with_large_parameters");
#[cfg(target_os = "windows")]
group.sample_size(10); // This benchmark is very slow on Windows, so we reduce the sample size to avoid long test runs.

// This benchmark includes time to first clone a vector and string, so it is not a "pure' benchmark of the guest call, but it's still useful
group.bench_function("guest_call_with_large_parameters", |b| {
const SIZE: usize = 50 * 1024 * 1024; // 50 MB
let large_vec = vec![0u8; SIZE];
let large_string = unsafe { String::from_utf8_unchecked(large_vec.clone()) }; // Safety: indeed above vec is valid utf8
// Helper function to create a benchmark for a specific size
let create_benchmark = |group: &mut criterion::BenchmarkGroup<_>, size_mb: usize| {
let benchmark_name = format!("guest_call_with_2_large_parameters_{}mb each", size_mb);
group.bench_function(&benchmark_name, |b| {
let size = size_mb * 1024 * 1024; // Convert MB to bytes
let large_vec = vec![0u8; size];
let large_string = unsafe { String::from_utf8_unchecked(large_vec.clone()) }; // Safety: indeed above vec is valid utf8

let mut config = SandboxConfiguration::default();
config.set_input_data_size(2 * SIZE + (1024 * 1024)); // 2 * SIZE + 1 MB, to allow 1MB for the rest of the serialized function call
config.set_heap_size(SIZE as u64 * 15);
let mut config = SandboxConfiguration::default();
config.set_input_data_size(2 * size + (1024 * 1024));

let sandbox = UninitializedSandbox::new(
GuestBinary::FilePath(simple_guest_as_string().unwrap()),
Some(config),
)
.unwrap();
let mut sandbox = sandbox.evolve(Noop::default()).unwrap();
if size < 50 * 1024 * 1024 {
config.set_heap_size(size as u64 * 16);
} else {
config.set_heap_size(size as u64 * 11); // Set to 1GB for larger sizes
}

b.iter(|| {
sandbox
.call_guest_function_by_name::<()>(
"LargeParameters",
(large_vec.clone(), large_string.clone()),
)
.unwrap()
let sandbox = UninitializedSandbox::new(
GuestBinary::FilePath(simple_guest_as_string().unwrap()),
Some(config),
)
.unwrap();
let mut sandbox = sandbox.evolve(Noop::default()).unwrap();

b.iter_custom(|iters| {
let mut total_duration = std::time::Duration::new(0, 0);

for _ in 0..iters {
// Clone the data (not measured)
let vec_clone = large_vec.clone();
let string_clone = large_string.clone();

// Measure only the guest function call
let start = std::time::Instant::now();
sandbox
.call_guest_function_by_name::<()>(
"LargeParameters",
(vec_clone, string_clone),
)
.unwrap();
total_duration += start.elapsed();
}

total_duration
});
});
});
};

// Create benchmarks for different sizes
create_benchmark(&mut group, 5); // 5MB
create_benchmark(&mut group, 10); // 10MB
create_benchmark(&mut group, 20); // 20MB
create_benchmark(&mut group, 40); // 40MB
create_benchmark(&mut group, 60); // 60MB

group.finish();
}
Expand Down Expand Up @@ -153,9 +181,143 @@ fn sandbox_benchmark(c: &mut Criterion) {
group.finish();
}

fn sandbox_heap_size_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("sandbox_heap_sizes");

// Helper function to create sandbox with specific heap size
let create_sandbox_with_heap_size = |heap_size_mb: Option<u64>| {
let path = simple_guest_as_string().unwrap();
let config = if let Some(size_mb) = heap_size_mb {
let mut config = SandboxConfiguration::default();
config.set_heap_size(size_mb * 1024 * 1024); // Convert MB to bytes
Some(config)
} else {
None
};

let uninit_sandbox =
UninitializedSandbox::new(GuestBinary::FilePath(path), config).unwrap();
uninit_sandbox.evolve(Noop::default()).unwrap()
};

// Benchmark sandbox creation with default heap size
group.bench_function("create_sandbox_default_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(None));
});

// Benchmark sandbox creation with 50MB heap
group.bench_function("create_sandbox_50mb_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(Some(50)));
});

// Benchmark sandbox creation with 100MB heap
group.bench_function("create_sandbox_100mb_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(Some(100)));
});

// Benchmark sandbox creation with 250MB heap
group.bench_function("create_sandbox_250mb_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(Some(250)));
});

// Benchmark sandbox creation with 500MB heap
group.bench_function("create_sandbox_500mb_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(Some(500)));
});

// Benchmark sandbox creation with 995MB heap (close to the limit of 1GB for a Sandbox )
group.bench_function("create_sandbox_995mb_heap", |b| {
b.iter_with_large_drop(|| create_sandbox_with_heap_size(Some(995)));
});

group.finish();
}

fn guest_call_heap_size_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("guest_call_heap_sizes");

// Helper function to create sandbox with specific heap size
let create_sandbox_with_heap_size = |heap_size_mb: Option<u64>| {
let path = simple_guest_as_string().unwrap();
let config = if let Some(size_mb) = heap_size_mb {
let mut config = SandboxConfiguration::default();
config.set_heap_size(size_mb * 1024 * 1024); // Convert MB to bytes
Some(config)
} else {
None
};

let uninit_sandbox =
UninitializedSandbox::new(GuestBinary::FilePath(path), config).unwrap();
uninit_sandbox.evolve(Noop::default()).unwrap()
};

// Benchmark guest function call with default heap size
group.bench_function("guest_call_default_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(None);
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

// Benchmark guest function call with 50MB heap
group.bench_function("guest_call_50mb_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(Some(50));
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

// Benchmark guest function call with 100MB heap
group.bench_function("guest_call_100mb_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(Some(100));
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

// Benchmark guest function call with 250MB heap
group.bench_function("guest_call_250mb_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(Some(250));
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

// Benchmark guest function call with 500MB heap
group.bench_function("guest_call_500mb_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(Some(500));
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

// Benchmark guest function call with 995MB heap
group.bench_function("guest_call_995mb_heap", |b| {
let mut sandbox = create_sandbox_with_heap_size(Some(995));
b.iter(|| {
sandbox
.call_guest_function_by_name::<String>("Echo", "hello\n".to_string())
.unwrap()
});
});

group.finish();
}

criterion_group! {
name = benches;
config = Criterion::default();
targets = guest_call_benchmark, sandbox_benchmark, guest_call_benchmark_large_param
targets = guest_call_benchmark, sandbox_benchmark, sandbox_heap_size_benchmark, guest_call_benchmark_large_params, guest_call_heap_size_benchmark
}
criterion_main!(benches);
Loading