Skip to content

Remove the legacy heap region from snapshots - #1968

Merged
ludfjig merged 7 commits into
mainfrom
ll/4-unbounded-heap
Nov 4, 2026
Merged

ludfjig merged 7 commits into
mainfrom
ll/4-unbounded-heap

Conversation

@ludfjig

@ludfjig ludfjig commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Remove the fixed-size guest heap region from sandbox memory. It held only zeros, and the first write to each page copied it into scratch anyway. The guest now maps each heap page from scratch on first touch, as it does for stack pages. Snapshots no longer carry the zero-filled region, and the heap is bounded only by scratch.

Each first write to a heap page no longer copies a zero page out of the snapshot, which speeds up all guest workloads that allocate.

Please review commit by commit.

Closes #1697

details

  • At init the guest claims a fixed 512 GiB virtual range with talc. The page fault handler maps a fresh scratch page on each heap page's first touch, so untouched pages cost nothing.
  • This replaces a copy-on-write fault that also copied a 4 KiB zero page, made the host populate it, and flushed the TLB entry.
  • Heap pages written since the last snapshot use scratch, shared with the stack, page tables and transport. Allocations can exceed scratch. A write that needs a page when scratch is full aborts the guest with MallocFailed.
  • executable_heap stays a host feature. The guest sets the heap's execute permission in its own page tables, as the host did before. The hypervisor never enforced it, so a malicious guest can circumvent it. We could consider removing the feature, since it adds little security. See Consider removing executable_heap host cargo feature #2003.

Alternatives considered

  • ZeroInit page-table entries for the heap. Rejected for performance, and because it needs a fixed heap size again. The marker exists only on 4 KiB leaf entries, so all of the heap's page tables exist up front, 1/512 of the heap size. Every snapshot captures them and every restore copies them into scratch. First touch is also slower, because the fault handler walks the page tables to find the marker instead of checking a range.

Breaking changes

  • SandboxConfiguration::set_heap_size, SandboxConfiguration::DEFAULT_HEAP_SIZE, SandboxBuilder::heap_size and SandboxBuilder::get_heap_size are removed. Size scratch for the guest's heap use.
  • Running out of memory aborts the guest with MallocFailed on the write that needs a page. Allocations no longer return null, and Rust guests no longer panic with memory allocation of N bytes failed.
  • Snapshots use ABI 9 and config schema v5. Existing snapshots must be regenerated. Goldens move to v9.0.
  • HyperlightPEB::guest_heap is removed.

Benchmarks

Compared to the PR below. MSHV runs Linux 6.6.137.mshv2-5 and 6.6.100.mshv1-2. KVM runs Linux 6.17.

Each size keeps the PR below's scratch and drops its heap of 128 KiB, 8, 64 or 256 MiB. This PR then redefines the sizes as 8, 64 and 256 MiB of scratch. heap_first_touch is new. It writes half of 8, 64 or 256 MiB of scratch, and the PR below gets a heap as large as scratch.

Benchmark MSHV 6.6.137 MSHV 6.6.100 KVM
guest_calls/heap_first_touch/medium 15.6 ms → 6.43 ms (−59%) 8.92 ms → 6.11 ms (−31%) 137 ms → 67.5 ms (−51%)
guest_calls/heap_first_touch/large 101 ms → 28 ms (−72%) 37 ms → 25.2 ms (−32%) 538 ms → 271 ms (−50%)
guest_functions_with_large_parameters/guest_call_with_large_parameters 179 ms → 92.7 ms (−48%) 83.4 ms → 66 ms (−21%) 870 ms → 560 ms (−36%)
sandboxes/create_initialized/large 8.14 ms → 1.71 ms (−79%) 30 ms → 1.76 ms (−94%) 3.42 ms → 2.52 ms (−26%)
sandboxes/sandbox_from_snapshot/large 6.21 ms → 709 µs (−89%) 97.3 ms → 1.14 ms (−99%) 1.01 ms → 798 µs (−21%)
snapshots/create/large 1.65 ms → 329 µs (−80%) 1.71 ms → 257 µs (−85%) 1.59 ms → 220 µs (−86%)
snapshot_files/save_snapshot/large 264 ms → 869 µs (−99.7%) 267 ms → 861 µs (−99.7%) 2.41 s → 39.8 ms (−98%)
snapshot_files/cold_start_via_snapshot/large 203 ms → 2.04 ms (−99%) 291 ms → 2.09 ms (−99%) 191 ms → 2.54 ms (−99%)

6.6.137 populates the old heap region on first touch and 6.6.100 at sandbox creation, so first touch gains more on 6.6.137 and creation gains more on 6.6.100.

Regressions (not really when compared to main before this stack!)

Benchmark MSHV 6.6.137 MSHV 6.6.100 KVM Cause
snapshots/create/default 230 µs → 252 µs (+10%) 231 µs → 251 µs (+9%) 252 µs → 267 µs (+6%) Likely the heap's own page tables. Init writes both ends of the 512 GiB heap range, which needs 5 page-table pages that capture walks and rebuilds
snapshot_files/cold_start_via_snapshot_unverified/default 1.37 ms → 1.67 ms (+22%) 1.44 ms → 1.36 ms (−6%) 2.13 ms → 2.19 ms (+3%) Likely the same page tables. Restore copies them into fresh scratch, and each fresh page faults on KVM and MSHV 6.6.137

WHP and aarch64 were not benchmarked.

@ludfjig ludfjig added A-perf Addresses performance C-refactor For PRs that restructure or remove code without adding new functionality. A-api Related to the API or public interface S-needs-goldens Regenerate snapshot golden fixtures labels Oct 21, 2026
@ludfjig ludfjig changed the title Map guest heap pages on first touch Remove the legacy heap region from snapshots Oct 21, 2026
@ludfjig
ludfjig added this pull request to stack #1929 October 21, 2026 18:14
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch from 1642d27 to f27c1ef Compare October 21, 2026 19:27
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch from f27c1ef to 8b0f611 Compare October 21, 2026 21:02
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig marked this pull request as ready for review October 23, 2026 21:25
Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:25

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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
What changed in this PR

Removes the legacy fixed-size guest heap region from snapshot memory and switches the guest to mapping heap pages from scratch on first touch, reducing snapshot size and improving performance.

Changes:

  • Removes heap-size configuration and updates tests/fixtures/benchmarks to size by scratch instead.
  • Bumps snapshot ABI to 8 and config schema media type to v5, updating goldens and versioning docs.
  • Updates guest fault handling to map heap pages from scratch on demand and propagates heap executability via a PEB flag.
File Description
src/​tests/​rust_guests/​simpleguest/​src/​main.rs Updates guest OOM/exhaustion behavior to reflect scratch-backed heap semantics.
src/​hyperlight_testing/​src/​lib.rs Renames benchmark/test size constants from heap sizes to scratch sizes and adjusts types.
src/​hyperlight_host/​tests/​snapshot_goldens/​goldens_version.rs Bumps goldens version to v8.0 for the new snapshot ABI/schema.
src/​hyperlight_host/​tests/​snapshot_goldens/​fixtures.rs Removes heap sizing from golden snapshot config fixture.
src/​hyperlight_host/​tests/​sandbox_host_tests.rs Updates leak-related tests to constrain scratch rather than heap.
src/​hyperlight_host/​tests/​integration_test.rs Updates integration tests to assert scratch exhaustion aborts with MallocFailed.
src/​hyperlight_host/​src/​sandbox/​uninitialized.rs Updates sandbox/snapshot tests to remove heap sizing and validate scratch-only layout behavior.
src/​hyperlight_host/​src/​sandbox/​snapshot/​tripwires.rs Bumps expected ABI/schema and updates invariant commentary around layout offsets.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file_tests.rs Updates snapshot file tests for schema v5 and ABI 8; removes heap layout assertions.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file/​mod.rs Updates loader to accept config v5 only and removes heap from persisted layout reconstruction.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file/​media_types.rs Adds config v5 media type and bumps snapshot ABI version to 8.
src/​hyperlight_host/​src/​sandbox/​snapshot/​file/​config.rs Removes heap_size from config schema/layout and updates schema pins/validation.
src/​hyperlight_host/​src/​sandbox/​initialized.rs Removes heap override propagation from snapshots and updates tests to size by scratch.
src/​hyperlight_host/​src/​sandbox/​config.rs Removes heap size configuration API; clarifies scratch semantics and updates tests.
src/​hyperlight_host/​src/​sandbox/​builder.rs Removes heap_size builder APIs; updates docs to show scratch sizing.
src/​hyperlight_host/​src/​mem/​mgr.rs Updates memory manager tests to use scratch sizing constants (no heap sizing).
src/​hyperlight_host/​src/​mem/​layout.rs Removes heap region from snapshot memory layout; updates offsets/PEB contents accordingly.
src/​hyperlight_host/​src/​hypervisor/​mod.rs Adjusts initialise test parameters now that heap region assumptions are gone.
src/​hyperlight_host/​examples/​map-file-cow-test/​main.rs Removes heap sizing from example; relies on scratch sizing only.
src/​hyperlight_host/​benches/​benchmarks.rs Re-keys benchmark configurations to scratch sizes and adds heap_first_touch benchmark.
src/​hyperlight_guest_bin/​src/​lib.rs Claims a fixed virtual heap range and reads heap executability from the PEB.
src/​hyperlight_guest_bin/​src/​arch/​amd64/​layout.rs Adds a static assert to ensure heap range doesn’t overlap processor control.
src/​hyperlight_guest_bin/​src/​arch/​amd64/​exception/​handle.rs Maps heap pages from scratch on first touch; supports executable heap via PEB flag.
src/​hyperlight_guest_bin/​src/​arch/​aarch64/​exception/​handle.rs Same as amd64: scratch-backed heap faults with executable flag support.
src/​hyperlight_guest/​src/​layout.rs Exposes heap virtual range constants and asserts no overlap with transport/stack.
src/​hyperlight_guest/​src/​arch/​amd64/​layout.rs Defines 512GiB-aligned heap virtual range for amd64.
src/​hyperlight_guest/​src/​arch/​aarch64/​layout.rs Defines 512GiB-aligned heap virtual range for aarch64.
src/​hyperlight_common/​src/​mem.rs Replaces PEB heap region field with a heap executability flag.
fuzz/​fuzz_targets/​host_call.rs Removes heap sizing from fuzz target configuration.
docs/​snapshot-versioning.md Documents ABI 8/config v5 and notes heap region removal from snapshot memory.
docs/​snapshot-oci-format.md Updates config media type reference to v5.
docs/​hyperlight-execution-details.md Updates lifecycle docs to describe scratch-backed stack+heap mapping.
CHANGELOG.md Records breaking changes: ABI/schema bump and removal of heap sizing APIs.

Review effort: Lite

Comment thread src/hyperlight_guest_bin/src/lib.rs Resolved
Comment thread src/hyperlight_guest_bin/src/lib.rs Resolved
Comment thread src/hyperlight_host/src/mem/layout.rs Resolved
@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch 2 times, most recently from 53bbd23 to 360b772 Compare October 30, 2026 19:14
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch from 360b772 to 40f3bec Compare October 30, 2026 22:13
@hyperlight-gh-bot

This comment has been minimized.

Base automatically changed from ll/3-talc-allocator to main October 30, 2026 23:16
dummyguest halts only when it receives its expected PEB address and
seed. Otherwise it reads 0x8000 to force an exit. The test passed a
dummy PEB address and seed, so it relied on 0x8000 being mapped, which
held only because that address fell inside the guest heap region.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
A not-present fault in the stack range always maps a fresh scratch
page, so the page table walk in virt_to_phys is wasted work. Check the
range first and walk only for faults outside it.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch from 40f3bec to a7dc630 Compare October 30, 2026 23:16
The guest heap is a fixed 512 GiB virtual range. Guest init claims the
whole range with talc, and the page fault handler maps each heap page
from scratch on first touch, the same way it maps stack pages. Heap
memory is bounded by scratch rather than a fixed heap size, and
untouched heap pages cost nothing.

The host publishes the executable heap policy in the scratch-top
metadata, and the PEB drops the heap address and size.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
The guest maps heap pages from scratch on first touch, so the host no
longer reserves a zero-filled heap region in snapshot memory. This
removes SandboxConfiguration::set_heap_size, DEFAULT_HEAP_SIZE, and
SandboxBuilder::heap_size and get_heap_size. Scratch is the only guest
memory setting.

Snapshots no longer carry up to hundreds of MiB of zero pages, which
speeds up sandbox creation and snapshot capture, save, and load.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Snapshot memory no longer contains a heap region, and the PEB drops the
heap address and size. The scratch-top metadata carries the executable
heap policy. The config schema drops the heap size. This is a hard
break. Loaders reject config v4 and older, and the goldens move to v9.0.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Tests that sized the heap now size scratch. Allocation succeeds beyond
scratch, and the guest aborts with MallocFailed when a write needs a
page and scratch is full, so heap exhaustion tests expect that abort.
FillHeapAndCauseException is removed because the heap can no longer
run out before scratch does. The shared sandbox sizes in
hyperlight-testing become scratch sizes.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Benchmark sandbox sizes set scratch, which is the only guest memory
setting. The small, medium, and large variants use 8, 64, and 256 MiB
of scratch.

heap_first_touch measures writes to heap the guest has not touched
since the last restore. Each variant writes half its scratch. The
default size is skipped because fixed reservations take most of its
scratch.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@ludfjig
ludfjig force-pushed the ll/4-unbounded-heap branch from a7dc630 to 94f50e6 Compare October 30, 2026 23:33
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: 94f50e6a8394
Baseline commit: 767aac222fc2

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
373.17 µs (➖ 1.00x faster)
byte_chunks 745.41 ns (➖ 1.03x faster)
vec_bytes 585.33 ns (➖ 1.03x faster)

payload_allocation

slot_pool_segmented
65536 142.44 ns (➖ 1.00x slower)
262144 520.79 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 13.48 ms (➖ 1.01x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
7.68 ns (➖ 1.00x faster) 7.69 ns (➖ 1.00x faster) 7.79 ns (➖ 1.01x faster)

snapshot_files

load_snapshot_unverified
small 116.73 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 1.93 µs (➖ 1.08x faster) 2.07 µs (➖ 1.08x slower)
262144 6.99 µs (➖ 1.02x faster) 7.16 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.02 µs (➖ 1.00x faster) 1.01 µs (➖ 1.00x faster)
65536 6.16 µs (➖ 1.03x faster) 6.18 µs (➖ 1.03x faster)
262144 28.04 µs (➖ 1.04x slower)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
656.14 µs (➖ 1.00x faster)
byte_chunks 677.60 ns (➖ 1.03x slower)
vec_bytes 512.29 ns (➖ 1.01x faster)

payload_allocation

slot_pool_segmented
65536 171.04 ns (➖ 1.00x faster)
262144 495.56 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 14.78 ms (➖ 1.04x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
6.77 ns (➖ 1.01x faster) 6.76 ns (➖ 1.00x faster) 6.79 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 56.46 µs (➖ 1.02x faster)

virtq_readonly

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

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 720.80 ns (➖ 1.02x slower) 710.37 ns (➖ 1.02x faster)
65536 7.02 µs (➖ 1.01x faster) 7.02 µs (➖ 1.01x faster)
262144 28.97 µs (➖ 1.09x faster)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
376.87 µs (➖ 1.06x slower)
byte_chunks 941.49 ns (➖ 1.05x faster)
vec_bytes 724.68 ns (➖ 1.03x faster)

payload_allocation

slot_pool_segmented
65536 191.32 ns (➖ 1.01x faster)
262144 725.98 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 67.36 ms (➖ 1.01x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.79 ns (➖ 1.02x faster) 9.75 ns (➖ 1.01x slower) 10.06 ns (➖ 1.04x slower)

snapshot_files

load_snapshot_unverified
small 104.13 µs (➖ 1.03x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.22 µs (➖ 1.01x faster) 2.41 µs (➖ 1.02x faster)
262144 9.58 µs (➖ 1.02x faster) 10.96 µs (➖ 1.04x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.25 µs (➖ 1.06x slower) 1.31 µs (➖ 1.05x slower)
65536 8.24 µs (➖ 1.15x faster) 8.05 µs (➖ 1.20x faster)
262144 36.29 µs (➖ 1.06x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
640.16 µs (➖ 1.21x faster)
byte_chunks 928.72 ns (➖ 1.01x faster)
vec_bytes 649.71 ns (➖ 1.03x faster)

payload_allocation

slot_pool_segmented
65536 253.26 ns (➖ 1.02x faster)
262144 840.77 ns (➖ 1.27x slower)

sandboxes

create_initialized_and_drop
medium 76.16 ms (➖ 1.04x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
8.74 ns (➖ 1.00x slower) 8.69 ns (➖ 1.01x faster) 8.78 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 55.24 µs (➖ 1.07x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.31 µs (➖ 1.02x slower) 2.28 µs (➖ 1.01x slower)
262144 7.93 µs (➖ 1.00x slower) 7.93 µs (➖ 1.01x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 891.89 ns (➖ 1.03x faster) 915.18 ns (➖ 1.00x faster)
65536 7.51 µs (➖ 1.00x slower) 7.52 µs (➖ 1.05x faster)
262144 36.57 µs (➖ 1.05x faster)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
1.98 ms (➖ 1.03x slower)
byte_chunks 1.19 µs (➖ 1.01x faster)
vec_bytes 838.50 ns (➖ 1.07x slower)

payload_allocation

slot_pool_segmented
65536 232.82 ns (➖ 1.02x slower)
262144 793.58 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 11.68 ms (➖ 1.07x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
11.16 ns (➖ 1.12x slower) 10.97 ns (➖ 1.10x slower) 10.34 ns (➖ 1.05x slower)

snapshot_files

load_snapshot_unverified
small 760.18 µs (➖ 1.23x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.68 µs (➖ 1.06x slower) 2.37 µs (➖ 1.20x faster)
262144 9.38 µs (➖ 1.02x faster) 9.10 µs (➖ 1.06x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.28 µs (➖ 1.14x faster) 1.31 µs (➖ 1.03x slower)
65536 9.18 µs (➖ 1.03x faster) 8.63 µs (➖ 1.07x faster)
262144 38.72 µs (➖ 1.01x faster)
hyperv-ws2025 / intel (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
3.08 ms (➖ 1.01x faster)
byte_chunks 1.18 µs (➖ 1.01x faster)
vec_bytes 786.75 ns (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
65536 211.98 ns (➖ 1.01x slower)
262144 744.71 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 14.11 ms (➖ 1.07x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.02 ns (➖ 1.00x faster) 10.15 ns (➖ 1.04x slower) 9.63 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 692.20 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.46 µs (➖ 1.05x slower) 2.33 µs (➖ 1.00x slower)
262144 7.48 µs (➖ 1.02x faster) 7.68 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.09 µs (➖ 1.03x slower) 1.19 µs (➖ 1.12x slower)
65536 8.49 µs (➖ 1.11x faster) 8.28 µs (➖ 1.00x faster)
262144 42.32 µs (➖ 1.20x faster)

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

@ludfjig ludfjig added this to the v0.19.0 milestone Nov 2, 2026
Comment thread src/hyperlight_common/src/layout.rs Resolved

@andreiltd andreiltd 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.

Nice!

@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.

Very glad to see the old heap region go! Two very minor comments/questions.

Comment thread src/hyperlight_common/src/layout.rs Resolved
@ludfjig
ludfjig merged commit 94071f4 into main Nov 4, 2026
75 checks passed
@ludfjig
ludfjig deleted the ll/4-unbounded-heap branch November 4, 2026 18:51
danbugs added a commit that referenced this pull request Nov 4, 2026
#1968 removed SandboxBuilder::heap_size (and set_heap_size/get_heap_size),
but the scratch_allocated_bytes_counts_written_pages test added by #2019
still called it, so main stopped compiling its tests once both landed —
which is why clippy and miri fail on every PR.  Drop the .heap_size(8 << 20)
line; the test already sizes scratch with .scratch_size(16 << 20).

Signed-off-by: danbugs <danilochiarlone@gmail.com>
danbugs added a commit that referenced this pull request Nov 4, 2026
Prepares the v0.19.0 release per `docs/how-to-make-releases.md` (the
Cargo.toml version and CHANGELOG steps). The tag and GitHub release are
cut after this merges.

- Bump the workspace version `0.18.0` → `0.19.0`, and the internal
`hyperlight-*` dependency version requirements with it.
- Roll the CHANGELOG `[Prerelease]` section into `[v0.19.0]`, add a
fresh empty `[Prerelease]`, and update the compare-link footer
(`[v0.19.0]` added; `[Prerelease]` now compares from `v0.19.0`).
- Sync the root `Cargo.lock` and `src/tests/rust_guests/Cargo.lock` to
the new versions (workspace-only; no other dependencies touched).
- Drop a stray `.heap_size(8 << 20)` call in the
`scratch_allocated_bytes` test: #1968 removed
`SandboxBuilder::heap_size`, but #2019's test (which landed separately)
still called it, so main stopped compiling its tests once both merged.
Folding the one-line fix in here so the release compiles.

---------

Signed-off-by: danbugs <danilochiarlone@gmail.com>
danbugs added a commit to unikraft/unikraft that referenced this pull request Nov 5, 2026
Hyperlight 0.19 removes the guest heap region from sandbox memory
(hyperlight-dev/hyperlight#1968).  The PEB holds only init_data, and the
memory the guest allocates from is scratch.  This kernel read guest_heap
past the end of the PEB, found no heap, and crashed at boot: "Boot stack
alloc failed".

The heap region served only as the boot stack: with paging and
HEAP_BASE, the frame allocator and the kernel heap already come from
scratch, the heap as frames mapped at HEAP_BASE, which a snapshot
captures.  The boot stack, which stays the "init" thread's for good, is
needed before the heap is up, and memory taken straight from scratch
(hl_scratch_alloc_pages(), used through the scratch window) is not
captured by a snapshot and is zeroed by a restore.  So the 1 MiB boot
stack is a page-aligned array in .bss, which is snapshot memory as the
heap region was, and is pre-faulted for CoW as before.

A canary in the stack's lowest word crashes the guest if the stack is
run down to it.  It is checked at four points.  The halt that ends boot
checks it, so an overflow during boot fails the boot.  Each later entry
from the host (every guest function call, and the checkpoint before a
snapshot) checks it, catching later use of the stack by the init thread
(a native main() without LIBUKBOOT_MAINTHREAD that outlives the boot
halt) or a stray write, so such a guest fails a snapshot rather than
being captured.  The end of a call's entry, before its result, checks it
for the call's own work, so a call that ran the stack down fails rather
than returning.  A shutdown, usually run by the init thread, checks it
before reporting the exit, so a call in which the stack was run down and
the guest then shut down fails rather than ending cleanly.  A snapshot
of a guest with no call since its last snapshot or restore enters no
guest code: it captures memory the boot halt or a checkpoint has already
checked.

At the boot halt, in a shutdown and at the start and end of an ordinary
call's entry the report ("Boot stack canary overwritten (stack
overflow?)") goes the console's usual way, to the output the host
collects.  During a checkpoint the host refuses a host call, the
console's HostWriteErr and HostWrite included, and would end the run
before the report reached it, so for any entry but an ordinary call the
transport is marked lost first (hl_transport_abandon(), which
transport_lost() and a failed checkpoint now use too) and the report
goes out through DebugPrint.  Each check crashes through
hyperlight_boot_stack_crash(), which prints the report and aborts
directly: UK_CRASH, under LIBUKDEBUG_CRASH_ACTION_REBOOT, ends in
uk_pm_sysrestart(), the shutdown, whose check would fail again and crash
again, for good.

Drop guest_heap from struct hyperlight_peb to match HyperlightPEB, and
the FREE region and its bookkeeping with it.  Nothing reads the PEB any
more, so the pointer kept to it goes too; its address is still checked.
Without paging there is nothing left to allocate from, so
PLAT_HYPERLIGHT selects LIBUKPAGING, which also gives the kernel heap
its LIBUKBOOT_HEAP_BASE once a boot allocator is chosen; memory.c
refuses a build without one (LIBUKBOOT_INITALLOC).  pagetable64.S goes:
it only defined bpt_unmap_mrd, which plat/common reads only in the
ukplat_mem_init() it builds without paging.

Since it reads nothing from the PEB and registers no free region, this
kernel still boots on a host that maps the heap region (hyperlight main
before hyperlight-dev/hyperlight#1968), leaving that region unused.

Signed-off-by: danbugs <danilochiarlone@gmail.com>
NopeCharon pushed a commit to NopeCharon/hyperlight-unikraft that referenced this pull request Nov 5, 2026
Bump the kernel (plat-hyperlight-v2 9f414729 -> 3b7cba88), rebuild the
kernels and the native-kernel fixture, and stop sizing the heap:

- Hyperlight 0.19 gives the guest no heap region
  (hyperlight-dev/hyperlight#1968), and the kernel took its 1 MiB boot
  stack from it: on 0.19 the guest crashed at boot ("Boot stack alloc
  failed").  The boot stack is in the kernel's .bss now, which a
  snapshot keeps as it kept the heap, with a canary in its lowest word.
  The halt that ends boot checks it, so an overflow during boot fails
  the boot.  Each later entry from the host (every guest function call,
  and the checkpoint before a snapshot) checks it too, catching later
  use of the stack by the init thread (a native main() without
  LIBUKBOOT_MAINTHREAD that outlives the boot halt) or a stray write, so
  such a guest fails a snapshot rather than being captured; a snapshot
  with no call since the last snapshot or restore enters no guest code,
  and captures memory the boot halt or a checkpoint has already checked.
  The end of a call's entry, before its result, checks it for the call's
  own work, so a call that ran the stack down fails rather than
  returning.  A shutdown checks it before reporting the exit, so a call
  in which the stack was run down and the guest then shut down fails
  rather than ending cleanly.  The report goes to the output hluk
  collects, or through DebugPrint for an entry that may be a checkpoint,
  during which the host refuses a host call; the crash bypasses
  UK_CRASH, whose reboot action would re-enter the shutdown's check.
- The kernel's PEB matches Hyperlight 0.19's, init_data alone.
- hluk no longer sizes the heap: HEAP_SIZE and its two set_heap_size
  calls go, as nothing uses the region now, and the SNAPSHOT_CONTRACT
  doc in build.rs no longer lists a heap among what a snapshot depends
  on.  The hyperlight main this tree builds against still maps one, at
  its default size, unused.
- plat/hyperlight selects paging, the only way it has to allocate, so
  the elfloader and native-kernel defconfigs no longer set LIBUKPAGING
  and LIBUKFALLOC themselves; the resolved .config and the kernels built
  from it are unchanged by that.  It also refuses a build without a boot
  allocator, which its heap needs, and drops pagetable64.S, whose only
  symbol nothing reads with paging on.  The CHANGELOG notes these for
  native-kernel builders.

The kernel boots on hyperlight 0.19.0 and on the hyperlight main this
tree builds against (6c17f7cd).

Signed-off-by: danbugs <danilochiarlone@gmail.com>
NopeCharon pushed a commit to NopeCharon/hyperlight-unikraft that referenced this pull request Nov 5, 2026
hyperlight-host and hyperlight-common move to the 0.19.0 release, which
has scratch resize (hyperlight-dev/hyperlight#1947) and
scratch_allocated_bytes (hyperlight-dev/hyperlight#2019), so the
[patch.crates-io] git pin goes from the crate, the clickhouse
integration and the pptx-gen demo, and the crate can be published to
crates.io again.  The three lockfiles take 0.19.0 from crates.io; in
re-resolving them cargo also moves some crates' windows-sys dependency
to versions already in the locks (mostly 0.61.2).

0.19 drops the guest heap region (hyperlight-dev/hyperlight#1968), and
with it SandboxConfiguration::set_heap_size, which the previous commit
stopped calling.  The CHANGELOG's 0.19 entry says what the re-exported
hyperlight_host 0.19 changes, that a native kernel built against an
earlier plat/hyperlight must be rebuilt, and, folding in the previous
commit's note, what plat/hyperlight now requires and checks; the
transport arena entry's own rebuild note goes.

SNAPSHOT_CONTRACT stays 8: it was bumped for the transport arena after
v0.20.1 and has not been released.  Dev builds since then carry it, and
their snapshots are refused all the same: the kernel change (the
previous commit) rolls the key's other half, k<hash>, the embedded
kernel's SHA-256, and Hyperlight's own snapshot ABI 9 refuses them
besides.

Closes hyperlight-dev#233

Signed-off-by: danbugs <danilochiarlone@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-api Related to the API or public interface A-perf Addresses performance C-refactor For PRs that restructure or remove code without adding new functionality. S-needs-goldens Regenerate snapshot golden fixtures

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Remove legacy heap region from snapshot

4 participants