Skip to content

Commit 24dccad

Browse files
committed
Validate guest-writable fields in try_pop_buffer_into before allocation
The back-pointer and flatbuffer size prefix in the shared output buffer are written by the guest and were used without validation, allowing a malicious guest to trigger a ~4 GB host-side allocation. Add bounds checks on both fields before any heap allocation occurs and return descriptive errors on violation. Add unit and integration tests exercising corrupt size prefixes and back-pointers. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 620339a commit 24dccad

3 files changed

Lines changed: 223 additions & 1 deletion

File tree

src/hyperlight_host/src/mem/shared_mem.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,9 +1121,21 @@ impl HostSharedMemory {
11211121
let last_element_offset_rel: usize =
11221122
self.read::<u64>(last_element_offset_abs - 8)? as usize;
11231123

1124+
// Validate element offset (guest-writable): must be in [8, stack_pointer_rel).
1125+
if last_element_offset_rel >= stack_pointer_rel || last_element_offset_rel < 8 {
1126+
return Err(new_error!(
1127+
"Corrupt buffer back-pointer: element offset {} is outside valid range [8, {}).",
1128+
last_element_offset_rel,
1129+
stack_pointer_rel,
1130+
));
1131+
}
1132+
11241133
// make it absolute
11251134
let last_element_offset_abs = last_element_offset_rel + buffer_start_offset;
11261135

1136+
// Max bytes the element can span (excluding the 8-byte back-pointer).
1137+
let max_element_size = stack_pointer_rel - last_element_offset_rel - 8;
1138+
11271139
// Get the size of the flatbuffer buffer from memory
11281140
let fb_buffer_size = {
11291141
let size_i32 = self.read::<u32>(last_element_offset_abs)? + 4;
@@ -1133,6 +1145,14 @@ impl HostSharedMemory {
11331145
usize::try_from(size_i32)
11341146
}?;
11351147

1148+
if fb_buffer_size > max_element_size {
1149+
return Err(new_error!(
1150+
"Corrupt buffer size prefix: flatbuffer claims {} bytes but the element slot is only {} bytes.",
1151+
fb_buffer_size,
1152+
max_element_size
1153+
));
1154+
}
1155+
11361156
let mut result_buffer = vec![0; fb_buffer_size];
11371157

11381158
self.copy_to_slice(&mut result_buffer, last_element_offset_abs)?;
@@ -1631,6 +1651,139 @@ mod tests {
16311651
}
16321652
}
16331653

1654+
/// Bounds checking for `try_pop_buffer_into` against corrupt guest data.
1655+
mod try_pop_buffer_bounds {
1656+
use super::*;
1657+
1658+
#[derive(Debug, PartialEq)]
1659+
struct RawBytes(Vec<u8>);
1660+
1661+
impl TryFrom<&[u8]> for RawBytes {
1662+
type Error = String;
1663+
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
1664+
Ok(RawBytes(value.to_vec()))
1665+
}
1666+
}
1667+
1668+
/// Create a buffer with stack pointer initialized to 8 (empty).
1669+
fn make_buffer(mem_size: usize) -> super::super::HostSharedMemory {
1670+
let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
1671+
let (hshm, _) = eshm.build();
1672+
hshm.write::<u64>(0, 8u64).unwrap();
1673+
hshm
1674+
}
1675+
1676+
#[test]
1677+
fn normal_push_pop_roundtrip() {
1678+
let mem_size = 4096;
1679+
let mut hshm = make_buffer(mem_size);
1680+
1681+
// Size-prefixed flatbuffer-like payload: [size: u32 LE][payload]
1682+
let payload = b"hello";
1683+
let mut data = Vec::new();
1684+
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1685+
data.extend_from_slice(payload);
1686+
1687+
hshm.push_buffer(0, mem_size, &data).unwrap();
1688+
let result: RawBytes = hshm.try_pop_buffer_into(0, mem_size).unwrap();
1689+
assert_eq!(result.0, data);
1690+
}
1691+
1692+
#[test]
1693+
fn malicious_flatbuffer_size_prefix() {
1694+
let mem_size = 4096;
1695+
let mut hshm = make_buffer(mem_size);
1696+
1697+
let payload = b"small";
1698+
let mut data = Vec::new();
1699+
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1700+
data.extend_from_slice(payload);
1701+
hshm.push_buffer(0, mem_size, &data).unwrap();
1702+
1703+
// Corrupt size prefix at element start (offset 8) to near u32::MAX.
1704+
hshm.write::<u32>(8, 0xFFFF_FFFBu32).unwrap(); // +4 = 0xFFFF_FFFF
1705+
1706+
let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
1707+
let err_msg = format!("{}", result.unwrap_err());
1708+
assert!(
1709+
err_msg.contains("Corrupt buffer size prefix: flatbuffer claims 4294967295 bytes but the element slot is only 9 bytes"),
1710+
"Unexpected error message: {}",
1711+
err_msg
1712+
);
1713+
}
1714+
1715+
#[test]
1716+
fn malicious_element_offset_too_small() {
1717+
let mem_size = 4096;
1718+
let mut hshm = make_buffer(mem_size);
1719+
1720+
let payload = b"test";
1721+
let mut data = Vec::new();
1722+
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1723+
data.extend_from_slice(payload);
1724+
hshm.push_buffer(0, mem_size, &data).unwrap();
1725+
1726+
// Corrupt back-pointer (offset 16) to 0 (before valid range).
1727+
hshm.write::<u64>(16, 0u64).unwrap();
1728+
1729+
let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
1730+
let err_msg = format!("{}", result.unwrap_err());
1731+
assert!(
1732+
err_msg.contains(
1733+
"Corrupt buffer back-pointer: element offset 0 is outside valid range [8, 24)"
1734+
),
1735+
"Unexpected error message: {}",
1736+
err_msg
1737+
);
1738+
}
1739+
1740+
#[test]
1741+
fn malicious_element_offset_past_stack_pointer() {
1742+
let mem_size = 4096;
1743+
let mut hshm = make_buffer(mem_size);
1744+
1745+
let payload = b"test";
1746+
let mut data = Vec::new();
1747+
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1748+
data.extend_from_slice(payload);
1749+
hshm.push_buffer(0, mem_size, &data).unwrap();
1750+
1751+
// Corrupt back-pointer (offset 16) to 9999 (past stack pointer 24).
1752+
hshm.write::<u64>(16, 9999u64).unwrap();
1753+
1754+
let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
1755+
let err_msg = format!("{}", result.unwrap_err());
1756+
assert!(
1757+
err_msg.contains("Corrupt buffer back-pointer: element offset 9999 is outside valid range [8, 24)"),
1758+
"Unexpected error message: {}",
1759+
err_msg
1760+
);
1761+
}
1762+
1763+
#[test]
1764+
fn malicious_flatbuffer_size_off_by_one() {
1765+
let mem_size = 4096;
1766+
let mut hshm = make_buffer(mem_size);
1767+
1768+
let payload = b"abcd";
1769+
let mut data = Vec::new();
1770+
data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1771+
data.extend_from_slice(payload);
1772+
hshm.push_buffer(0, mem_size, &data).unwrap();
1773+
1774+
// Corrupt size prefix: claim 5 bytes (total 9), exceeding the 8-byte slot.
1775+
hshm.write::<u32>(8, 5u32).unwrap(); // fb_buffer_size = 5 + 4 = 9
1776+
1777+
let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
1778+
let err_msg = format!("{}", result.unwrap_err());
1779+
assert!(
1780+
err_msg.contains("Corrupt buffer size prefix: flatbuffer claims 9 bytes but the element slot is only 8 bytes"),
1781+
"Unexpected error message: {}",
1782+
err_msg
1783+
);
1784+
}
1785+
}
1786+
16341787
#[cfg(target_os = "linux")]
16351788
mod guard_page_crash_test {
16361789
use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};

src/hyperlight_host/tests/integration_test.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,42 @@ fn guest_outb_with_invalid_port_poisons_sandbox() {
578578
});
579579
}
580580

581+
#[test]
582+
fn corrupt_output_size_prefix_rejected() {
583+
with_rust_sandbox(|mut sbox| {
584+
let res = sbox.call::<i32>("CorruptOutputSizePrefix", ());
585+
assert!(
586+
res.is_err(),
587+
"Expected error when guest corrupts size prefix, got: {:?}",
588+
res,
589+
);
590+
let err_msg = format!("{:?}", res.unwrap_err());
591+
assert!(
592+
err_msg.contains("Corrupt buffer size prefix: flatbuffer claims 4294967295 bytes but the element slot is only 8 bytes"),
593+
"Unexpected error message: {err_msg}"
594+
);
595+
});
596+
}
597+
598+
#[test]
599+
fn corrupt_output_back_pointer_rejected() {
600+
with_rust_sandbox(|mut sbox| {
601+
let res = sbox.call::<i32>("CorruptOutputBackPointer", ());
602+
assert!(
603+
res.is_err(),
604+
"Expected error when guest corrupts back-pointer, got: {:?}",
605+
res,
606+
);
607+
let err_msg = format!("{:?}", res.unwrap_err());
608+
assert!(
609+
err_msg.contains(
610+
"Corrupt buffer back-pointer: element offset 57005 is outside valid range [8, 24)"
611+
),
612+
"Unexpected error message: {err_msg}"
613+
);
614+
});
615+
}
616+
581617
#[test]
582618
fn guest_panic_no_alloc() {
583619
let heap_size = 0x4000;

src/tests/rust_guests/simpleguest/src/main.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use hyperlight_guest_bin::host_comm::{
5252
print_output_with_host_print, read_n_bytes_from_user_memory,
5353
};
5454
use hyperlight_guest_bin::memory::malloc;
55-
use hyperlight_guest_bin::{guest_function, guest_logger, host_function};
55+
use hyperlight_guest_bin::{GUEST_HANDLE, guest_function, guest_logger, host_function};
5656
use log::{LevelFilter, error};
5757
use tracing::{Span, instrument};
5858

@@ -804,6 +804,39 @@ fn fuzz_guest_trace(max_depth: u32, msg: String) -> u32 {
804804
fuzz_traced_function(0, max_depth, &msg)
805805
}
806806

807+
#[guest_function("CorruptOutputSizePrefix")]
808+
fn corrupt_output_size_prefix() -> i32 {
809+
unsafe {
810+
let peb_ptr = core::ptr::addr_of!(GUEST_HANDLE).read().peb().unwrap();
811+
let output_stack_ptr = (*peb_ptr).output_stack.ptr as *mut u8;
812+
813+
// Write a fake stack entry with a ~4 GB size prefix (0xFFFF_FFFB + 4).
814+
let buf = core::slice::from_raw_parts_mut(output_stack_ptr, 24);
815+
buf[0..8].copy_from_slice(&24_u64.to_le_bytes());
816+
buf[8..12].copy_from_slice(&0xFFFF_FFFBu32.to_le_bytes());
817+
buf[12..16].copy_from_slice(&[0u8; 4]);
818+
buf[16..24].copy_from_slice(&8_u64.to_le_bytes());
819+
820+
core::arch::asm!("hlt", options(noreturn));
821+
}
822+
}
823+
824+
#[guest_function("CorruptOutputBackPointer")]
825+
fn corrupt_output_back_pointer() -> i32 {
826+
unsafe {
827+
let peb_ptr = core::ptr::addr_of!(GUEST_HANDLE).read().peb().unwrap();
828+
let output_stack_ptr = (*peb_ptr).output_stack.ptr as *mut u8;
829+
830+
// Write a fake stack entry with back-pointer 0xDEAD (past stack pointer 24).
831+
let buf = core::slice::from_raw_parts_mut(output_stack_ptr, 24);
832+
buf[0..8].copy_from_slice(&24_u64.to_le_bytes());
833+
buf[8..16].copy_from_slice(&[0u8; 8]);
834+
buf[16..24].copy_from_slice(&0xDEAD_u64.to_le_bytes());
835+
836+
core::arch::asm!("hlt", options(noreturn));
837+
}
838+
}
839+
807840
// Interprets the given guest function call as a host function call and dispatches it to the host.
808841
fn fuzz_host_function(func: FunctionCall) -> Result<Vec<u8>> {
809842
let mut params = func.parameters.unwrap();

0 commit comments

Comments
 (0)