Skip to content

feat: restore snapshots with a different scratch size - #1947

Open
danbugs wants to merge 2 commits into
mainfrom
scratch-resize
Open

danbugs wants to merge 2 commits into
mainfrom
scratch-resize

Conversation

@danbugs

@danbugs danbugs commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Sandbox::snapshot_with_options takes a snapshot with SnapshotOptions, and SnapshotOptions::scratch_size makes it restore with a different scratch size. A guest boots with the scratch its initialisation needs and runs guest function calls from a snapshot with only what they need, so each restore resets less memory, or with more scratch than it booted with. Follows the discussion on #1901.

  • The transport arena sits at a fixed address below the reserved scratch-top pages, with one guard page between them that the guest page tables leave unmapped. On amd64 an exception stack overrun faults there. aarch64 needs an SP check in its exception vectors for the same, left as a follow-up. Page tables start at the scratch base. Snapshot ABI 7, goldens v7.0.
  • SnapshotOptions leaves room for more snapshot options. Sandbox::snapshot is the default-options case.
  • snapshot_with_options rejects a size above the maximum memory size, too small for the transport, or not whole host pages, before the guest is entered. The snapshot loader refuses a scratch size the host cannot map.
  • A larger scratch window covers guest-virtual addresses below the current one, so building the snapshot fails if the guest has a mapping there.
  • hyperlight-unikraft, python restore plus guest function call, 256 MiB → 8 MiB: MSHV 32.6 → 1.07 ms, WHP 11.6 → 3.80 ms, KVM 2.30 → 1.84 ms.

Addresses #1767

Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:32
@danbugs danbugs added kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. regen-goldens Regenerate snapshot golden fixtures labels Oct 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Memory-layout and ABI changes need cross-platform human validation, and a host-page-dependent test failure remains unresolved.

1 open issue
What changed in this PR

Enables snapshots with smaller scratch regions to reduce restore costs while preserving transport addresses.

Changes:

  • Adds scratch-size selection and validation for snapshots.
  • Fixes transport placement and bounds guest allocation below it.
  • Updates snapshot ABI, tests, and documentation.
File Description
src/​hyperlight_host/​tests/​snapshot_goldens/​goldens_version.rs Selects v7.0 goldens.
src/​hyperlight_host/​src/​sandbox/​snapshot/​tripwires.rs Pins ABI 7.
src/​hyperlight_host/​src/​sandbox/​snapshot/​mod.rs Builds snapshots with target layouts.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file/​mod.rs Validates host-page alignment.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file/​media_types.rs Sets ABI 7.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file_tests.rs Tests alignment and ABI rejection.
src/​hyperlight_host/​src/​sandbox/​initialized.rs Adds resizing API and runtime tests.
src/​hyperlight_host/​src/​mem/​virtq/​tests.rs Updates transport address tests.
src/​hyperlight_host/​src/​mem/​virtq/​mod.rs Supports resized transport snapshots.
src/​hyperlight_host/​src/​mem/​mgr.rs Passes target layouts and mailbox offsets.
src/​hyperlight_host/​src/​mem/​layout.rs Defines fixed transport placement and sizing.
src/​hyperlight_guest/​src/​transport/​mem.rs Bounds memory access to transport ranges.
src/​hyperlight_guest/​src/​transport/​context.rs Uses scratch-independent transport bounds.
src/​hyperlight_guest/​src/​layout.rs Defines the allocator’s transport boundary.
src/​hyperlight_guest/​src/​arch/​amd64/​prim_alloc.rs Applies the allocator boundary.
src/​hyperlight_guest/​src/​arch/​aarch64/​prim_alloc.rs Applies the allocator boundary.
src/​hyperlight_common/​src/​layout.rs Defines transport placement helpers.
docs/​virtio-host-guest-communication.md Documents transport placement.
docs/​snapshot-versioning.md Documents ABI 7 compatibility.
docs/​paging-development-notes.md Explains resizing and allocation limits.
CHANGELOG.md Records the API and breaking changes.

Review effort: Balanced

Comment thread src/hyperlight_host/src/sandbox/snapshot/file/mod.rs
@hyperlight-gh-bot

This comment has been minimized.

@hyperlight-gh-bot

This comment has been minimized.

@andreiltd

andreiltd commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Hey! Numbers look great. Could you explain why transport needs to be moved? Would it be possible to only move pools out of arena and reserve the top region for g2h/h2g pools only? This way we could remove all the knowledge about the pool placement from the host which would simplify the code quite a bit.

Currently the pools need to be pinned in scratch memory between restores. This is a bit annoying but so far it has been the only way to avoid an extra round trip on restore. This is because host request can already be queued on restore in h2g queue. IIUC, this patch doesn't change this behavior?

@danbugs

danbugs commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Hey! Numbers look great. Could you explain why transport needs to be moved? Would it be possible to only move pools out of arena and reserve the top region for g2h/h2g pools only? This way we could remove all the knowledge about the pool placement from the host which would simplify the code quite a bit.

Currently the pools need to be pinned in scratch memory between restores. This is a bit annoying but so far it has been the only way to avoid an extra round trip on restore. This is because host request can already be queued on restore in h2g queue. IIUC, this patch doesn't change this behavior?

Thanks! Currently, the transport arena location is calculated once at boot and cached. With scratch resizing, those values end up wrong (since the base depends on the scratch size) and would have to be recalculated. If we move the transport arena just beneath the reserved pages, the remembered addresses stay valid. We could move only the pools, which would also let the host drop the pool placement logic, but then the guest would have to re-learn the ring and mailbox addresses after a resize. I figured moving the whole arena avoids that extra work...

And right, this doesn't change the pool pinning: pools and rings keep their addresses across restores, including at a different scratch size, so host requests can still be queued in the H2G queue before the guest resumes.

syntactically
syntactically previously approved these changes Oct 26, 2026

@syntactically syntactically left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I generally agree with @andreiltd that the degree of knowledge of the layout in the host here is a little bit unpleasant. It would be nicer in the medium term to change things so that there is less agreement needed here---either the host does not need to know so much, or the host changes to fully understand arbitrary layouts set up by the guest. However, from my side I don't think I see doing something about that as a blocker that prevents merging this PR. (Unless you disagree @andreiltd ?)

Comment thread docs/paging-development-notes.md Outdated
Comment thread src/hyperlight_host/src/mem/virtq/mod.rs Outdated
Comment thread src/hyperlight_host/src/mem/mgr.rs Outdated
@hyperlight-gh-bot

This comment has been minimized.

@danbugs danbugs changed the title feat: restore snapshots with less scratch feat: restore snapshots with a different scratch size Oct 27, 2026
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig ludfjig left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Main open point is the API shape. Rather than one snapshot_with_* method per setting, I'd suggest snapshot_with_options(&SnapshotOptions), with snapshot() calling it with defaults:

#[derive(Clone, Debug, Default)]
pub struct SnapshotOptions {
    scratch_size: Option<usize>, // None keeps the sandbox's size
}

let snap = sbox.snapshot_with_options(&SnapshotOptions::default().scratch_size(8 << 20))?;

This will let us add more settings without having many different methods. Thoughts?

Comment thread src/hyperlight_common/src/layout.rs
Comment thread src/hyperlight_guest/src/transport/context.rs
@danbugs
danbugs force-pushed the scratch-resize branch 2 times, most recently from 043378c to 98ac01d Compare October 27, 2026 19:26
@hyperlight-gh-bot

This comment has been minimized.

@danbugs

danbugs commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Looks good! Main open point is the API shape. Rather than one snapshot_with_* method per setting, I'd suggest snapshot_with_options(&SnapshotOptions), with snapshot() calling it with defaults:

#[derive(Clone, Debug, Default)]
pub struct SnapshotOptions {
    scratch_size: Option<usize>, // None keeps the sandbox's size
}

let snap = sbox.snapshot_with_options(&SnapshotOptions::default().scratch_size(8 << 20))?;

This will let us add more settings without having many different methods. Thoughts?

I liked the idea and had a go at it in 98ac01d. snapshot_with_options(&SnapshotOptions), with snapshot() as the default-options case.

The guest builds its transport context once and keeps the ring
addresses. For scratch to change size across a snapshot, the transport
arena (virtqueue rings, pools and the checkpoint mailbox) needs an
address that does not depend on the scratch size.

The arena ends one guard page below the reserved top pages. The page
tables start at the scratch base. The guard page is left unmapped, so an
amd64 exception stack overrun faults there before it reaches the arena.
aarch64 exception vectors store below SP before any check, so nested
faults there can step past it. The guest's page allocator stops at the
arena GPA the host publishes, and aborts if that is not between the
scratch base and the end of the transport range. The guest's transport
memory access is bound to the arena, which must start above the scratch
base.

scratch_allocator_limit_gpa is renamed scratch_reserved_top_gpa, so a
guest that stops its allocator at the old limit fails to compile. The
minimum scratch size grows by the guard page.

This changes the snapshot layout: snapshot ABI 7, goldens v7.0. Existing
snapshots must be regenerated, and guests rebuilt.

Signed-off-by: danbugs <[email protected]>
Sandbox::snapshot_with_options takes a snapshot with SnapshotOptions,
which leaves room for more options later. Sandbox::snapshot is the
default-options case. SnapshotOptions::scratch_size gives the snapshot's
layout another scratch size. The snapshot's page tables are built for
that size, walking the live tables with the sandbox's own layout. Its
virtqueue rings carry over unchanged, since the transport arena's
address does not depend on the scratch size. Restore replaces scratch
when the size differs.

A size above the maximum memory size, too small for the transport, or
not a whole number of host pages is rejected before the guest is
entered. Room for the page tables is checked when they are built. A
larger scratch window covers guest-virtual addresses below the current
one, so building the snapshot fails if the guest has a mapping there.
The snapshot loader refuses a scratch size this host cannot map.

A guest can boot with the scratch its initialisation needs and run guest
function calls from a snapshot with only the scratch they need, or with
more scratch than it booted with. Less scratch makes restores that zero
or replace all of it cost less.

Signed-off-by: danbugs <[email protected]>
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: 991129c1405b
Baseline commit: 076ede448e34

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
372.48 µs (➖ 1.00x faster)
byte_chunks 759.13 ns (➖ 1.01x faster)
vec_bytes 585.25 ns (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
65536 141.50 ns (➖ 1.00x slower)
262144 527.78 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 80.10 ms (➖ 1.01x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
7.70 ns (➖ 1.00x slower) 7.69 ns (➖ 1.00x slower) 7.72 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 92.69 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 1.91 µs (➖ 1.09x faster) 2.03 µs (➖ 1.02x slower)
262144 8.79 µs (➖ 1.26x slower) 7.46 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.03 µs (➖ 1.01x slower) 1.01 µs (➖ 1.01x slower)
65536 6.20 µs (➖ 1.01x faster) 6.28 µs (➖ 1.03x faster)
262144 26.98 µs (➖ 1.00x faster)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
777.48 µs (➖ 1.00x faster)
byte_chunks 807.91 ns (➖ 1.02x slower)
vec_bytes 624.69 ns (➖ 1.04x slower)

payload_allocation

slot_pool_segmented
65536 205.84 ns (➖ 1.00x faster)
262144 594.03 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 82.41 ms (➖ 1.02x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
8.14 ns (➖ 1.00x faster) 8.17 ns (➖ 1.00x slower) 8.15 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 54.93 µs (➖ 1.04x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.58 µs (➖ 1.00x slower) 2.56 µs (➖ 1.01x faster)
262144 9.05 µs (➖ 1.00x slower) 8.99 µs (➖ 1.00x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 864.61 ns (➖ 1.01x slower) 872.48 ns (➖ 1.02x faster)
65536 8.44 µs (➖ 1.03x faster) 8.45 µs (➖ 1.00x faster)
262144 34.89 µs (➖ 1.03x faster)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
347.20 µs (➖ 1.10x faster)
byte_chunks 925.41 ns (➖ 1.00x faster)
vec_bytes 717.68 ns (➖ 1.02x faster)

payload_allocation

slot_pool_segmented
65536 192.44 ns (➖ 1.02x faster)
262144 723.71 ns (➖ 1.08x faster)

sandboxes

create_initialized_and_drop
medium 60.88 ms (➖ 1.10x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.49 ns (➖ 1.01x faster) 9.60 ns (➖ 1.01x faster) 10.03 ns (➖ 1.04x slower)

snapshot_files

load_snapshot_unverified
small 85.92 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.35 µs (➖ 1.04x slower) 2.38 µs (➖ 1.04x faster)
262144 9.66 µs (➖ 1.01x faster) 10.35 µs (➖ 1.04x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.28 µs (➖ 1.05x slower) 1.25 µs (➖ 1.03x faster)
65536 8.40 µs (➖ 1.15x faster) 8.14 µs (➖ 1.38x faster)
262144 35.93 µs (➖ 1.05x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
770.53 µs (➖ 1.21x slower)
byte_chunks 930.39 ns (➖ 1.03x slower)
vec_bytes 663.66 ns (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
65536 254.00 ns (➖ 1.03x faster)
262144 778.04 ns (➖ 1.18x slower)

sandboxes

create_initialized_and_drop
medium 64.84 ms (➖ 1.11x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
8.72 ns (➖ 1.00x slower) 8.68 ns (➖ 1.01x faster) 8.69 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 46.48 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.28 µs (➖ 1.01x faster) 2.30 µs (➖ 1.01x slower)
262144 7.95 µs (➖ 1.01x faster) 8.08 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 896.89 ns (➖ 1.02x faster) 906.76 ns (➖ 1.01x faster)
65536 7.67 µs (➖ 1.03x slower) 7.46 µs (➖ 1.01x faster)
262144 38.48 µs (➖ 1.08x faster)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
2.05 ms (➖ 1.01x slower)
byte_chunks 1.13 µs (➖ 1.01x slower)
vec_bytes 763.83 ns (➖ 1.00x faster)

payload_allocation

slot_pool_segmented
65536 237.04 ns (➖ 1.02x slower)
262144 833.62 ns (➖ 1.05x slower)

sandboxes

create_initialized_and_drop
medium 84.91 ms (➖ 1.03x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.69 ns (➖ 1.01x slower) 10.69 ns (➖ 1.01x slower) 10.80 ns (➖ 1.02x slower)

snapshot_files

load_snapshot_unverified
small 571.48 µs (➖ 1.35x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.92 µs (➖ 1.03x slower) 2.58 µs (➖ 1.03x slower)
262144 8.78 µs (➖ 1.06x faster) 8.70 µs (➖ 1.07x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.36 µs (➖ 1.04x faster) 1.35 µs (➖ 1.01x slower)
65536 9.03 µs (➖ 1.03x faster) 8.65 µs (➖ 1.09x faster)
262144 39.64 µs (➖ 1.05x slower)
hyperv-ws2025 / intel (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
3.17 ms (➖ 1.07x faster)
byte_chunks 1.20 µs (➖ 1.01x faster)
vec_bytes 763.93 ns (➖ 1.03x faster)

payload_allocation

slot_pool_segmented
65536 209.62 ns (➖ 1.01x faster)
262144 748.35 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 104.37 ms (➖ 1.07x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.27 ns (➖ 1.00x slower) 10.84 ns (➖ 1.07x slower) 9.63 ns (➖ 1.08x faster)

snapshot_files

load_snapshot_unverified
small 545.17 µs (➖ 1.19x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.32 µs (➖ 1.01x faster) 2.30 µs (➖ 1.01x faster)
262144 7.66 µs (➖ 1.00x faster) 7.53 µs (➖ 1.00x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.13 µs (➖ 1.06x slower) 1.07 µs (➖ 1.01x faster)
65536 8.28 µs (➖ 1.02x faster) 8.35 µs (➖ 1.03x faster)
262144 42.21 µs (➖ 1.01x slower)

Reported by cargo ci bench-report --candidate run:40710852708 --config-file bench_report.toml.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. regen-goldens Regenerate snapshot golden fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants