Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ 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.

### Changed
* 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.

### Removed

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
5 changes: 2 additions & 3 deletions docs/snapshot-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ A snapshot carries three independently evolvable version markers:
`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
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
52 changes: 28 additions & 24 deletions src/hyperlight_common/benches/buffer_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
use std::hint::black_box;

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use hyperlight_common::virtq::{BufferPool, BufferProvider, RecyclePool};
use hyperlight_common::virtq::{BufferProvider, RunPool, SlotLayout, SlotPool};

// Helper to create a pool for benchmarking
fn make_pool<const L: usize, const U: usize>(size: usize) -> BufferPool<L, U> {
fn make_run_pool<const L: usize, const U: usize>(size: usize) -> RunPool<L, U> {
let base = 0x10000;
BufferPool::<L, U>::new(base, size).unwrap()
RunPool::<L, U>::new(base, size).unwrap()
}

// Single allocation performance
Expand All @@ -19,7 +19,7 @@ fn bench_alloc_single(c: &mut Criterion) {
for size in [64, 128, 256, 512, 1024, 1500, 4096].iter() {
group.throughput(Throughput::Elements(1));
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
b.iter(|| {
let alloc = pool.alloc(black_box(size)).unwrap();
pool.dealloc(alloc.addr).unwrap();
Expand All @@ -36,7 +36,7 @@ fn bench_alloc_lifo(c: &mut Criterion) {
for size in [256, 1500, 4096].iter() {
group.throughput(Throughput::Elements(100));
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
b.iter(|| {
for _ in 0..100 {
let alloc = pool.alloc(black_box(size)).unwrap();
Expand All @@ -53,7 +53,7 @@ fn bench_alloc_fragmented(c: &mut Criterion) {
let mut group = c.benchmark_group("alloc_fragmented");

group.bench_function("fragmented_256", |b| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);

// Create fragmentation pattern: allocate many, free every other
let mut allocations = Vec::new();
Expand All @@ -79,7 +79,7 @@ fn bench_free(c: &mut Criterion) {

for size in [256, 1500, 4096].iter() {
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
b.iter(|| {
let alloc = pool.alloc(size).unwrap();
pool.dealloc(black_box(alloc.addr)).unwrap();
Expand All @@ -96,7 +96,7 @@ fn bench_free_list_reuse(c: &mut Criterion) {

// With cursor optimization (LIFO)
group.bench_function("lifo_pattern", |b| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
b.iter(|| {
let alloc = pool.alloc(256).unwrap();
pool.dealloc(alloc.addr).unwrap();
Expand All @@ -107,7 +107,7 @@ fn bench_free_list_reuse(c: &mut Criterion) {

// Without cursor benefit (FIFO-like)
group.bench_function("fifo_pattern", |b| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
let mut queue = Vec::new();

// Pre-fill queue
Expand Down Expand Up @@ -136,11 +136,11 @@ fn bench_segmented_payload(c: &mut Criterion) {
BenchmarkId::from_parameter(payload_size),
&payload_size,
|b, &payload_size| {
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
let pool = make_run_pool::<256, 4096>(4 * 1024 * 1024);
b.iter(|| {
let sgs = pool.alloc_sg(black_box(payload_size)).unwrap();
for sg in sgs {
pool.dealloc(sg.addr).unwrap();
let regions = pool.alloc_regions([black_box(payload_size)]).unwrap();
for alloc in regions.into_iter().flatten() {
pool.dealloc(alloc.addr).unwrap();
}
});
},
Expand All @@ -150,39 +150,43 @@ fn bench_segmented_payload(c: &mut Criterion) {
group.finish();
}

fn bench_recycle_pool(c: &mut Criterion) {
let mut group = c.benchmark_group("recycle_pool");
fn bench_slot_pool(c: &mut Criterion) {
let mut group = c.benchmark_group("slot_pool");

group.bench_function("alloc_dealloc_4096", |b| {
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
let layout = SlotLayout::new(0x80000, 4096, 1024);
let pool = SlotPool::new(layout).unwrap();
b.iter(|| {
let alloc = pool.alloc(black_box(4096)).unwrap();
pool.dealloc(alloc.addr).unwrap();
});
});

group.bench_function("alloc_dealloc_128", |b| {
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 256).unwrap();
let layout = SlotLayout::new(0x80000, 256, 16 * 1024);
let pool = SlotPool::new(layout).unwrap();
b.iter(|| {
let alloc = pool.alloc(black_box(128)).unwrap();
pool.dealloc(alloc.addr).unwrap();
});
});

group.bench_function("alloc_dealloc_1500", |b| {
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
let layout = SlotLayout::new(0x80000, 4096, 1024);
let pool = SlotPool::new(layout).unwrap();
b.iter(|| {
let alloc = pool.alloc(black_box(1500)).unwrap();
pool.dealloc(alloc.addr).unwrap();
});
});

group.bench_function("alloc_sg_64k", |b| {
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
group.bench_function("alloc_regions_64k", |b| {
let layout = SlotLayout::new(0x80000, 4096, 1024);
let pool = SlotPool::new(layout).unwrap();
b.iter(|| {
let sgs = pool.alloc_sg(black_box(64 * 1024)).unwrap();
for sg in sgs {
pool.dealloc(sg.addr).unwrap();
let regions = pool.alloc_regions([black_box(64 * 1024)]).unwrap();
for alloc in regions.into_iter().flatten() {
pool.dealloc(alloc.addr).unwrap();
}
});
});
Expand All @@ -198,7 +202,7 @@ criterion_group!(
bench_free,
bench_free_list_reuse,
bench_segmented_payload,
bench_recycle_pool,
bench_slot_pool,
);

criterion_main!(benches);
Loading