Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bb1ecb7
feat(virtq): add stateful chain byte streams
andreiltd Jul 21, 2026
1422f41
refactor(virtq): remove reset api and harden allocation rollback
andreiltd Jul 22, 2026
33ab860
feat(virtq): add tiered fixed slot allocation
andreiltd Jul 22, 2026
09b3bec
feat(virtq): add canonical packed ring images
andreiltd Jul 23, 2026
a019bd5
refactor(layout): model scratch-top metadata
andreiltd Jul 24, 2026
bbbf798
fix(guest): correct scratch allocator boundary
andreiltd Jul 24, 2026
49ba024
feat(virtq): define virtq transport metadata
andreiltd Jul 27, 2026
ec82827
feat(virtq): implement host side memory access
andreiltd Jul 28, 2026
2822781
feat(virtq): configure transport geometry
andreiltd Jul 29, 2026
cac2511
feat(virtq): initialize runtime transport
andreiltd Jul 30, 2026
4936bd6
feat(snapshot): preserve canonical virtq state
andreiltd Jul 31, 2026
d08b197
feat(virtq): optimize inflight bookkeeping memory
andreiltd Aug 10, 2026
b409cae
feat(virtq): use grouped allocation for virtq chains
andreiltd Aug 13, 2026
67d084b
refactor(virtq) adjust virtio terminology
andreiltd Aug 11, 2026
9c73f11
fix: size foundation memory tests
andreiltd Sep 2, 2026
8b68f79
fix: add missing license headers
andreiltd Sep 5, 2026
c3c1580
feat(virtq): remove run pool
andreiltd Sep 8, 2026
6e7a475
fix: inline allocation_exceeds_limit calls
andreiltd Sep 8, 2026
8aa7ca0
fix: align up default scratch size to 16K pages
andreiltd Sep 8, 2026
fb9079e
fix: validate queue types at construction
andreiltd Sep 8, 2026
3c597c4
refactor: cleanup queue layout types
andreiltd Sep 9, 2026
23c3fab
fix: typos
andreiltd Sep 9, 2026
efb6474
fix: adjust scratch size for fuzzing
andreiltd Sep 9, 2026
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Prerelease] - Unreleased

### Added
* Add per-direction virtqueue configuration and account its allocations in
scratch sizing.
* Shared virtqueue framing with a 12-byte `MsgHeader` and external byte values.
* Producer batch completion without notification and segmented payload
extraction without flattening.

### Changed
* `Snapshot::save` now writes the guest memory blob sparsely, skipping all-zero
Expand All @@ -17,10 +22,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
* Expose C guest `ByteChunks` values as pointer and length arrays.
* Return typed `hl_ReturnValue` objects from C guest functions through
`hl_result_from_*` constructors.
* Place virtqueue rings and pools in host-owned scratch before page tables.
Snapshot ABI 3 rejects snapshots created with earlier layouts.
* Virtqueue producers use concrete `SlotPool` allocation and `BufferLease`
ownership. `BufferMap` supplies complete owners exposing initialized bytes.
* `ChainBuilder::build()` allocates readable and writable requests.
`writable_avail()` reserves available upper-tier slots within the descriptor
budget. It may add zero slots to a nonempty chain.

### Removed
* `RunPool` and the run-specific `AllocError::InvalidAlign` variant.

### Fixed
* Use a 16 KiB-aligned default scratch size for Apple Silicon compatibility.

## [v0.17.0] - 2026-08-27

Expand Down
8 changes: 4 additions & 4 deletions docs/snapshot-oci-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ Three blob kinds per tag:
* **manifest** (`application/vnd.oci.image.manifest.v1+json`). Tiny JSON
pointer record selected via `index.json`. References one config and
one layer by digest.
* **config** (`application/vnd.hyperlight.snapshot.config.v1+json`). The
* **config** (`application/vnd.hyperlight.snapshot.config.v2+json`). The
snapshot descriptor: arch, hypervisor, CPU vendor, ABI version,
resume address and captured registers, memory layout, registered
host functions, snapshot generation counter. Loaded eagerly and
fully parsed.
resume address and captured registers, memory and transport layout,
registered host functions, snapshot generation counter. Loaded
eagerly and fully parsed.
* **layer / memory** (`application/vnd.hyperlight.snapshot.memory.v1`).
The raw guest memory image, exactly `memory_size` bytes. mmap'd on
restore.
Expand Down
13 changes: 6 additions & 7 deletions docs/snapshot-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ A snapshot carries three independently evolvable version markers:
[src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs](../src/hyperlight_host/src/sandbox/snapshot/file/media_types.rs)).
This is what the host reads back from a snapshot: the `OutBAction`
and `VmAction` port numbers, the input and output buffer stack
format, the offset and size of each memory region (including the
`HyperlightPEB` size), and the calling convention for guest function
entry. A change to any of these breaks older snapshots unless the
loader adds a compat path.
format, the virtqueue transport layout, the offset and size of each
memory region (including the `HyperlightPEB` size), and the calling
convention for guest function entry. A change to any of these breaks
older snapshots unless the loader adds a compat path.
* **Snapshot blob encoding**, `MT_SNAPSHOT_V1`
(`application/vnd.hyperlight.snapshot.memory.v1`), aliased as
`MT_SNAPSHOT_CURRENT`. This is the on-wire format of the snapshot
blob: framing, section ordering, alignment, dirty/zero-page elision,
anything about how the bytes are packed inside the OCI layer.
* **Config schema**, `MT_CONFIG_V1`
(`application/vnd.hyperlight.snapshot.config.v1+json`), aliased as
* **Config schema**, `MT_CONFIG_V2`
(`application/vnd.hyperlight.snapshot.config.v2+json`), aliased as
`MT_CONFIG_CURRENT`. This is the JSON shape of the config blob:
field names, types, required vs optional, the descriptors the loader
needs in order to reconstruct the sandbox (memory sizes, buffer
Expand Down Expand Up @@ -367,4 +367,3 @@ major:
* The loader accepts the old `abi_version` (Option 2 step 4), so the old
golden loads.
* Register the host functions the old golden's checks call.

2 changes: 1 addition & 1 deletion fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ which evaluates to the following command `cargo +nightly fuzz run fuzz_host_prin

As per Microsoft's Offensive Research & Security Engineering (MORSE) team, all host exposed functions that receive or interact with guest data must be continuously fuzzed for, at least, 500 million fuzz test cases without any crashes. Because `cargo-fuzz` doesn't support setting a maximum number of iterations; instead, we use the `--max_total_time` flag to set a maximum time to run the fuzzer. We have a GitHub action (acting like a CRON job) that runs the fuzzers for 24 hours every week.

Currently, we fuzz the parameters and return type to a hardcoded `PrintOutput` guest function, the `HostPrint` host function, and the packed virtqueue ring parser. We plan to add more fuzzers in the future.
Currently, we fuzz the parameters and return type to a hardcoded `PrintOutput` guest function, the `HostPrint` host function, the packed virtqueue ring parser, and canonical ring image validation. We plan to add more fuzzers in the future.

## On Failure

Expand Down
2 changes: 0 additions & 2 deletions fuzz/fuzz_targets/guest_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ impl<'a> Arbitrary<'a> for FuzzInput {
// Any unexpected errors from the guest should be reported.
fuzz_target!(
init: {
// In local tests, 256 KiB seemed sufficient for deep recursion
let mu_sbox = SandboxBuilder::from_file(simple_guest_for_fuzzing_as_pathbuf())
.scratch_size(256 * 1024)
.build()
.unwrap();

Expand Down
81 changes: 66 additions & 15 deletions fuzz/fuzz_targets/virtq_packed_ring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use std::num::NonZeroU16;
use std::ops::Range;
use std::rc::Rc;

use hyperlight_common::virtq::{Descriptor, Layout, MemOps, RingConsumer};
use hyperlight_common::virtq::canonical::validate_canon_image;
use hyperlight_common::virtq::{Descriptor, Layout, MemOps, RingConsumer, RingError};
use libfuzzer_sys::{Corpus, fuzz_target};

const DEFAULT_QUEUE_SIZE: usize = 16;
Expand All @@ -30,6 +31,7 @@ struct FuzzDesc {
#[derive(Clone, Debug)]
struct FuzzCase {
queue_size: usize,
avail_descs: usize,
driver_event_off_wrap: u16,
driver_event_flags: u16,
written_len: u32,
Expand Down Expand Up @@ -112,9 +114,9 @@ unsafe impl MemOps for FuzzMem {
}
}

fn write_driver_event(mem: &FuzzMem, layout: Layout, off_wrap: u16, flags: u16) -> Result<(), ()> {
fn write_event(mem: &FuzzMem, addr: u64, off_wrap: u16, flags: u16) -> Result<(), ()> {
mem.write(
layout.drv_evt_addr(),
addr,
&[
(off_wrap & 0xff) as u8,
(off_wrap >> 8) as u8,
Expand Down Expand Up @@ -150,7 +152,8 @@ fn parse_case(data: &[u8]) -> Option<FuzzCase> {

let raw_queue_size = read_u16(0);
let queue_size = normalize_queue_size(raw_queue_size);
let desc_count = usize::from(read_u16(2)).min(MAX_DESCS).min(queue_size);
let avail_descs = usize::from(read_u16(2));
let desc_count = avail_descs.min(MAX_DESCS).min(queue_size);

let driver_event_off_wrap = read_u16(4);
let driver_event_flags = read_u16(6);
Expand All @@ -177,6 +180,7 @@ fn parse_case(data: &[u8]) -> Option<FuzzCase> {

Some(FuzzCase {
queue_size,
avail_descs,
driver_event_off_wrap,
driver_event_flags,
written_len,
Expand All @@ -194,6 +198,60 @@ fn normalize_queue_size(raw: u16) -> usize {
raw.min(MAX_QUEUE_SIZE)
}

fn fuzz_canon_image(
mem: &FuzzMem,
layout: Layout,
case: &FuzzCase,
payload_base: u64,
) -> Result<(), ()> {
write_event(
mem,
layout.drv_evt_addr(),
case.driver_event_off_wrap,
case.driver_event_flags,
)?;
let _ = validate_canon_image(mem, layout, case.avail_descs, |_, _| true);

write_event(mem, layout.drv_evt_addr(), 0, 0)?;
let canon = validate_canon_image(mem, layout, case.avail_descs, |_, _| true);

let payload_end = payload_base + PAYLOAD_SIZE as u64;
let _ = validate_canon_image(mem, layout, case.avail_descs, |_, elem| {
elem.addr >= payload_base
&& elem
.addr
.checked_add(u64::from(elem.len))
.is_some_and(|end| end <= payload_end)
});

if let Ok(chains) = canon {
let mut consumer = RingConsumer::new(layout, mem.clone());
for expected in chains {
let Ok((id, actual)) = consumer.poll_available() else {
panic!("canonical image was rejected by the ring consumer");
};
assert_eq!(id, expected.id());
assert_eq!(actual.elems().len(), expected.buffers().elems().len());
for (actual, expected) in actual.elems().iter().zip(expected.buffers().elems()) {
assert_eq!(actual.addr, expected.addr);
assert_eq!(actual.len, expected.len);
assert_eq!(actual.writable, expected.writable);
}
}
assert!(matches!(
consumer.poll_available(),
Err(RingError::WouldBlock)
));
}

write_event(
mem,
layout.drv_evt_addr(),
case.driver_event_off_wrap,
case.driver_event_flags,
)
}

fn run_case(case: FuzzCase) -> Corpus {
let Some(num_descs) = NonZeroU16::new(case.queue_size as u16) else {
return Corpus::Reject;
Expand All @@ -206,17 +264,6 @@ fn run_case(case: FuzzCase) -> Corpus {
Err(_) => return Corpus::Reject,
};

if write_driver_event(
&mem,
layout,
case.driver_event_off_wrap,
case.driver_event_flags,
)
.is_err()
{
return Corpus::Reject;
}

let payload_base = BASE_ADDR + ring_size as u64;
for (idx, fuzz_desc) in case.descs.iter().enumerate() {
let payload_offset = fuzz_desc.addr_offset as usize % PAYLOAD_SIZE;
Expand All @@ -232,6 +279,10 @@ fn run_case(case: FuzzCase) -> Corpus {
}
}

if fuzz_canon_image(&mem, layout, &case, payload_base).is_err() {
return Corpus::Reject;
}

let mut consumer = RingConsumer::new(layout, mem);
for _ in 0..case.poll_count {
let Ok((id, _chain)) = consumer.poll_available() else {
Expand Down
Loading
Loading