Skip to content

Add Sandbox::recreate to recover unrecoverable sandboxes - #1751

Open
ludfjig wants to merge 1 commit into
hyperlight-dev:mainfrom
ludfjig:recover_unrecoverable
Open

ludfjig wants to merge 1 commit into
hyperlight-dev:mainfrom
ludfjig:recover_unrecoverable

Conversation

@ludfjig

@ludfjig ludfjig commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

When a restore fails to remap a sandbox's base memory, the sandbox becomes Unrecoverable and rejects every operation, including restore(). The only way out is to drop it and build a new sandbox, which loses its configuration, host functions, and interrupt handles.

Add Sandbox::recreate(&mut self, snapshot), which replaces the sandbox's VM and memory with new ones built from snapshot.

  • The sandbox keeps its configuration, host functions, page table root finder, guest log level, and interrupt handles. Existing interrupt handles control the new VM.
  • The new VM is built before the old one is dropped. A failed recreate leaves the sandbox unchanged, so it can be retried.
  • Recreation accepts any status. restore() stays the cheaper choice for ready and poisoned sandboxes.
  • Like restore(), recreation removes regions added with map_region() or map_file_cow().

On Windows the new VM needs its own surrogate process while the old VM still holds one. Recreation fails with HYPERLIGHT_MAX_SURROGATES=0. When all surrogates are in use it waits, as creating any sandbox does. With HYPERLIGHT_MAX_SURROGATES=1 that wait never ends. The rustdoc documents this.

Closes #1747

@ludfjig

ludfjig commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@syntactically first draft at #1747

@ludfjig ludfjig added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Aug 21, 2026
@ludfjig ludfjig changed the title Add method for recreate unrecoverable sandbox Add method for recreating unrecoverable sandbox Aug 21, 2026
@ludfjig
ludfjig marked this pull request as ready for review August 21, 2026 17:06
Copilot AI lite review requested due to automatic review settings August 21, 2026 17:07

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.

Pull request overview

Adds a public MultiUseSandbox::recreate() API in hyperlight-host to rebuild an unrecoverable sandbox from scratch using its existing configuration and the snapshot that triggered the unrecoverable restore failure (addressing #1747’s “replace this sandbox with a new one with the same configuration” request).

Changes:

  • Track additional lifecycle state needed for recreation (store the failed-restore snapshot and sandbox config when transitioning to Unrecoverable).
  • Add MultiUseSandbox::recreate(self) -> Result<Self> plus tests validating what state/config is preserved vs not preserved.
  • Update errors, changelog, and test recipes to cover the new recreation flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/hyperlight_host/src/sandbox/uninitialized_evolve.rs Thread sandbox configuration into MultiUseSandbox::from_uninit so it can be reused for recreation.
src/hyperlight_host/src/sandbox/initialized_multi_use.rs Introduce SandboxLifecycle, store recreation snapshot/config, and implement MultiUseSandbox::recreate() with accompanying tests.
src/hyperlight_host/src/sandbox/host_funcs.rs Add an internal constructor to rebuild HostFunctions from a FunctionRegistry during recreation.
src/hyperlight_host/src/error.rs Add recreation-specific error variants and adjust unrecoverable messaging.
Justfile Ensure the new unrecoverable-mapping test is exercised in crashdump/coverage recipes.
CHANGELOG.md Document the new MultiUseSandbox::recreate() API.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/hyperlight_host/src/error.rs Resolved
@ludfjig
ludfjig force-pushed the recover_unrecoverable branch from 9e79d10 to f806823 Compare August 21, 2026 20:21

@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 left a few comments inline.

I'm still trying to work out what the best API here is. It feels to me like it is a smaller/less breaking change to move this into restore() and add a new way to opt-out of the implicit recreate, but the possibility of recreation itself failing (which is probably pretty likely if we've recently failed to restore, since the relevant mapping failures seem likely to be resource exhaustion) is a little difficult to handle ergonomically. It seems inescapable to me that client code, if it is going to handle this locally, will need to do something like

while should_keep_trying() {
  match sb.<restore/recreate/whatever>(snapshot) {
    Ok(()) => break,
    Err(HyperlightError::UnrecoverableSandbox) => {
      // maybe try to free up resources somehow, try a different snapshot, etc
      continue;
    },
    Err(e) => return e,
}

which is just kind of nasty, whether you have one function for restore/recreate or multiple.

(The other option for clients I guess would be to just back out all the way to some top level retry when they get an unrecoverable error. That probably doesn't benefit at all from having recreate functionality at all, though...)

self.0
}

#[cfg(not(gdb))]

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.

Why is so much stuff not(gdb) guarded?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

/// again on the rebuilt sandbox.
///
/// Existing interrupt handles do not apply to the rebuilt sandbox. Call
/// [`interrupt_handle()`](Self::interrupt_handle) on the rebuilt sandbox to

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.

This seems like a nasty footgun. Is there really no way to get around this? I guess we should just need to update the partition handle here on the WHP sandboxes? I would expect the linux/hvf ones might just work, since the tid update happens just before run? If the latter is true, maybe we can just check/update the partition id on the whp sandboxes before run so that the lifecycle is the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed! I didn't realize it would be possible

if let Err(error) = self.restore_memory_and_mappings(&snapshot) {
self.status = SandboxStatus::Unrecoverable;
self.lifecycle = SandboxLifecycle::Unrecoverable {
recreation_snapshot: snapshot,

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.

Why do we want to save the snapshot here rather than in self.snapshot?

@ludfjig ludfjig Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

changed so recreate now takes snapshot as parameter

Poisoned,
Unrecoverable {
#[cfg_attr(gdb, allow(dead_code))]
recreation_snapshot: Arc<Snapshot>,

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 with the current lifecycle of MultiUseSandbox::snapshot, this means we can only ever transition into the unrecoverable state from restore(), since we are not guaranteed to have a valid snapshot around otherwise? That doesn't seem right to me---surely we should be able to end up in unrecoverable from e.g. map_region as well, even if we don't have a current valid snapshot? Like with poisoned, I would expect that the new snapshot should be supplied on the recreate operation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed, recreate now takes snapshot as param, just like restore

@ludfjig
ludfjig force-pushed the recover_unrecoverable branch from f806823 to d37d029 Compare October 19, 2026 20:54
@ludfjig ludfjig changed the title Add method for recreating unrecoverable sandbox Add Sandbox::recreate to recover unrecoverable sandboxes Oct 19, 2026
@hyperlight-gh-bot

This comment has been minimized.

@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the recover_unrecoverable branch from a3f0ebe to 6d37d09 Compare October 21, 2026 23:18
@hyperlight-gh-bot

This comment has been minimized.

/// holds two VMs and their scratch memory. On Windows the new VM needs a
/// surrogate process, as when creating any sandbox, and this sandbox's
/// surrogate is not released until recreation finishes. Recreation fails when
/// `HYPERLIGHT_MAX_SURROGATES=0` and blocks while all surrogates are in use.

@jsturtevant jsturtevant Oct 29, 2026 •

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.

Will it work with windows when HYPERLIGHT_MAX_SURROGATES=1? Will this work? I think we use 1 in unifraft but the usage pattern might not fit here anyways. @danbugs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it won't, just like creating a second vm won't work. Added clearer docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

and I think unikraft uses 0 if I am not mistaken

jsturtevant
jsturtevant previously approved these changes Oct 29, 2026
recreate() replaces a sandbox's VM and memory with new ones built
from a snapshot. It recovers unrecoverable sandboxes. A failed
recreate leaves the sandbox unchanged, so it can be retried.

The sandbox keeps its configuration, host functions, page table root
finder, guest log level, and interrupt handles. Existing interrupt
handles control the new VM.

Sandbox stores its configuration, which replaces the separate guest
log level field.

Closes hyperlight-dev#1747

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@ludfjig
ludfjig force-pushed the recover_unrecoverable branch from 11f17c2 to bae38dc Compare November 2, 2026 19:51
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: bae38dcce8b7
Baseline commit: fa13116cd998

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
380.17 µs (➖ 1.01x slower)
byte_chunks 769.48 ns (➖ 1.02x slower)
vec_bytes 584.75 ns (➖ 1.00x faster)

payload_allocation

slot_pool_segmented
65536 145.63 ns (➖ 1.03x slower)
262144 533.06 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 13.26 ms (➖ 1.05x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
7.69 ns (➖ 1.00x faster) 7.70 ns (➖ 1.00x faster) 7.69 ns (➖ 1.11x faster)

snapshot_files

load_snapshot_unverified
small 100.41 µs (➖ 1.02x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 1.93 µs (➖ 1.09x faster) 2.04 µs (➖ 1.07x slower)
262144 7.01 µs (➖ 1.21x faster) 7.31 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.03 µs (➖ 1.01x faster) 1.04 µs (➖ 1.01x faster)
65536 6.17 µs (➖ 1.02x faster) 6.15 µs (➖ 1.04x faster)
262144 28.43 µs (➖ 1.05x slower)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
663.83 µs (➖ 1.00x faster)
byte_chunks 667.71 ns (➖ 1.02x faster)
vec_bytes 519.87 ns (➖ 1.03x slower)

payload_allocation

slot_pool_segmented
65536 169.84 ns (➖ 1.00x faster)
262144 493.55 ns (➖ 1.04x faster)

sandboxes

create_initialized_and_drop
medium 14.44 ms (➖ 1.09x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
6.76 ns (➖ 1.00x faster) 6.83 ns (➖ 1.01x slower) 6.87 ns (➖ 1.02x slower)

snapshot_files

load_snapshot_unverified
small 48.30 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.12 µs (➖ 1.00x slower) 2.12 µs (➖ 1.00x faster)
262144 7.57 µs (➖ 1.01x slower) 7.57 µs (➖ 1.00x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 697.58 ns (➖ 1.00x faster) 719.05 ns (➖ 1.00x slower)
65536 7.01 µs (➖ 1.01x faster) 7.03 µs (➖ 1.01x faster)
262144 29.30 µs (➖ 1.03x faster)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
267.31 µs (➖ 1.01x slower)
byte_chunks 940.60 ns (➖ 1.03x slower)
vec_bytes 696.14 ns (➖ 1.06x faster)

payload_allocation

slot_pool_segmented
65536 192.22 ns (➖ 1.33x faster)
262144 728.62 ns (➖ 1.07x faster)

sandboxes

create_initialized_and_drop
medium 62.32 ms (➖ 1.00x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.51 ns (➖ 1.01x faster) 9.54 ns (➖ 1.00x faster) 9.62 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 104.39 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.22 µs (➖ 1.00x slower) 2.42 µs (➖ 1.01x faster)
262144 9.77 µs (➖ 1.00x slower) 10.48 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.23 µs (➖ 1.01x slower) 1.23 µs (➖ 1.04x faster)
65536 8.10 µs (➖ 1.24x faster) 8.31 µs (➖ 1.16x faster)
262144 35.50 µs (➖ 1.05x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
694.50 µs (➖ 1.04x faster)
byte_chunks 913.27 ns (➖ 1.00x faster)
vec_bytes 652.96 ns (➖ 1.01x faster)

payload_allocation

slot_pool_segmented
65536 258.78 ns (➖ 1.08x slower)
262144 670.25 ns (➖ 1.09x faster)

sandboxes

create_initialized_and_drop
medium 97.92 ms (➖ 1.04x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
8.70 ns (➖ 1.00x faster) 8.80 ns (➖ 1.01x slower) 8.76 ns (➖ 1.07x faster)

snapshot_files

load_snapshot_unverified
small 62.28 µs (➖ 1.04x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.36 µs (➖ 1.05x slower) 2.27 µs (➖ 1.00x faster)
262144 7.95 µs (➖ 1.01x slower) 7.99 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 983.83 ns (➖ 1.06x slower) 903.10 ns (➖ 1.02x faster)
65536 7.47 µs (➖ 1.00x faster) 7.54 µs (➖ 1.01x slower)
262144 38.58 µs (➖ 1.02x faster)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
2.17 ms (➖ 1.02x slower)
byte_chunks 1.20 µs (➖ 1.06x slower)
vec_bytes 762.24 ns (➖ 1.09x faster)

payload_allocation

slot_pool_segmented
65536 232.48 ns (➖ 1.01x slower)
262144 789.78 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 11.53 ms (➖ 1.05x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.63 ns (➖ 1.00x faster) 10.62 ns (➖ 1.01x faster) 12.24 ns (➖ 1.15x slower)

snapshot_files

load_snapshot_unverified
small 967.15 µs (➖ 1.27x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.51 µs (➖ 1.11x faster) 2.52 µs (➖ 1.01x faster)
262144 8.55 µs (➖ 1.20x faster) 8.62 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.35 µs (➖ 1.07x faster) 1.37 µs (➖ 1.00x faster)
65536 9.07 µs (➖ 1.04x faster) 9.06 µs (➖ 1.01x slower)
262144 39.20 µs (➖ 1.40x faster)
hyperv-ws2025 / intel (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
3.18 ms (➖ 1.04x faster)
byte_chunks 1.21 µs (➖ 1.01x slower)
vec_bytes 795.28 ns (➖ 1.06x slower)

payload_allocation

slot_pool_segmented
65536 218.92 ns (➖ 1.04x slower)
262144 755.80 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 15.77 ms (➖ 1.10x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.24 ns (➖ 1.01x slower) 9.84 ns (➖ 1.01x slower) 9.80 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 594.71 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.45 µs (➖ 1.02x slower) 2.32 µs (➖ 1.01x slower)
262144 7.52 µs (➖ 1.02x faster) 7.78 µs (➖ 1.03x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.10 µs (➖ 1.02x slower) 1.20 µs (➖ 1.12x slower)
65536 8.41 µs (➖ 1.14x faster) 8.28 µs (➖ 1.00x faster)
262144 41.44 µs (➖ 1.25x faster)

Reported by cargo ci bench-report --candidate run:41513850601 --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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add API to "replace this sandbox with a new one with the same configuration"

4 participants