use crate::abi::AbiError;
pub const MAX_KERNEL_ARGS: usize = 128;
pub const MAX_KERNEL_ARGS_BYTES: usize = 4 * 1024;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgTag {
I32 = 0x01,
I64 = 0x02,
F32 = 0x03,
F64 = 0x04,
U32 = 0x05,
U64 = 0x06,
Ptr = 0x07,
}
impl ArgTag {
pub fn from_byte(b: u8) -> Result<Self, AbiError> {
Ok(match b {
0x01 => ArgTag::I32,
0x02 => ArgTag::I64,
0x03 => ArgTag::F32,
0x04 => ArgTag::F64,
0x05 => ArgTag::U32,
0x06 => ArgTag::U64,
0x07 => ArgTag::Ptr,
_ => return Err(AbiError::InvalidArgs),
})
}
pub const fn value_bytes(self) -> usize {
match self {
ArgTag::I32 | ArgTag::F32 | ArgTag::U32 => 4,
ArgTag::I64 | ArgTag::F64 | ArgTag::U64 => 8,
ArgTag::Ptr => 8, }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LoweredArg {
I32(i32),
I64(i64),
F32(f32),
F64(f64),
U32(u32),
U64(u64),
#[non_exhaustive]
Ptr {
host_ptr: *const u8,
len: u32,
guest_offset: u32,
},
}
unsafe impl Send for LoweredArg {}
#[derive(Debug, Clone, PartialEq)]
pub enum LoweredArgSnapshot {
I32(i32),
I64(i64),
F32(f32),
F64(f64),
U32(u32),
U64(u64),
Ptr {
len: u32,
guest_offset: u32,
},
}
impl From<&LoweredArg> for LoweredArgSnapshot {
fn from(arg: &LoweredArg) -> Self {
match arg {
LoweredArg::I32(v) => LoweredArgSnapshot::I32(*v),
LoweredArg::I64(v) => LoweredArgSnapshot::I64(*v),
LoweredArg::F32(v) => LoweredArgSnapshot::F32(*v),
LoweredArg::F64(v) => LoweredArgSnapshot::F64(*v),
LoweredArg::U32(v) => LoweredArgSnapshot::U32(*v),
LoweredArg::U64(v) => LoweredArgSnapshot::U64(*v),
LoweredArg::Ptr {
host_ptr: _,
len,
guest_offset,
} => LoweredArgSnapshot::Ptr {
len: *len,
guest_offset: *guest_offset,
},
}
}
}
impl LoweredArg {
pub fn ptr_for_encoding(guest_offset: u32, len: u32) -> Self {
LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len,
guest_offset,
}
}
}
pub fn parse_argv(buf: &[u8], mem: &[u8]) -> Result<Vec<LoweredArg>, AbiError> {
if buf.len() > MAX_KERNEL_ARGS_BYTES {
return Err(AbiError::KernelArgsUnsupported);
}
let mut out: Vec<LoweredArg> = Vec::new();
let mut i = 0usize;
while i < buf.len() {
if out.len() >= MAX_KERNEL_ARGS {
return Err(AbiError::KernelArgsUnsupported);
}
let tag = ArgTag::from_byte(buf[i])?;
i += 1;
let need = tag.value_bytes();
if i.checked_add(need).map_or(true, |end| end > buf.len()) {
return Err(AbiError::InvalidArgs);
}
let val = &buf[i..i + need];
i += need;
let arg = match tag {
ArgTag::I32 => {
let mut b = [0u8; 4];
b.copy_from_slice(val);
LoweredArg::I32(i32::from_le_bytes(b))
}
ArgTag::I64 => {
let mut b = [0u8; 8];
b.copy_from_slice(val);
LoweredArg::I64(i64::from_le_bytes(b))
}
ArgTag::F32 => {
let mut b = [0u8; 4];
b.copy_from_slice(val);
LoweredArg::F32(f32::from_le_bytes(b))
}
ArgTag::F64 => {
let mut b = [0u8; 8];
b.copy_from_slice(val);
LoweredArg::F64(f64::from_le_bytes(b))
}
ArgTag::U32 => {
let mut b = [0u8; 4];
b.copy_from_slice(val);
LoweredArg::U32(u32::from_le_bytes(b))
}
ArgTag::U64 => {
let mut b = [0u8; 8];
b.copy_from_slice(val);
LoweredArg::U64(u64::from_le_bytes(b))
}
ArgTag::Ptr => {
let mut p = [0u8; 4];
p.copy_from_slice(&val[0..4]);
let mut l = [0u8; 4];
l.copy_from_slice(&val[4..8]);
let guest_offset = u32::from_le_bytes(p);
let len = u32::from_le_bytes(l);
let start = guest_offset as usize;
let end = start
.checked_add(len as usize)
.ok_or(AbiError::InvalidPointer)?;
if end > mem.len() {
return Err(AbiError::InvalidPointer);
}
let host_ptr = unsafe { mem.as_ptr().add(start) };
LoweredArg::Ptr {
host_ptr,
len,
guest_offset,
}
}
};
out.push(arg);
}
Ok(out)
}
pub struct KernelParamStorage {
backing: Box<[u8]>,
slots: Vec<*mut std::ffi::c_void>,
}
unsafe impl Send for KernelParamStorage {}
impl KernelParamStorage {
pub fn as_ptr(&mut self) -> *mut *mut std::ffi::c_void {
self.slots.as_mut_ptr()
}
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
#[doc(hidden)]
pub fn backing(&self) -> &[u8] {
&self.backing
}
#[doc(hidden)]
pub fn slot_ptrs(&self) -> &[*mut std::ffi::c_void] {
&self.slots
}
}
pub fn build_kernel_param_storage(args: &[LoweredArg]) -> KernelParamStorage {
let estimated_cap: usize = args
.iter()
.map(|a| match a {
LoweredArg::I32(_) | LoweredArg::F32(_) | LoweredArg::U32(_) => {
std::mem::align_of::<u32>() - 1 + 4
}
LoweredArg::I64(_) | LoweredArg::F64(_) | LoweredArg::U64(_) => {
std::mem::align_of::<u64>() - 1 + 8
}
LoweredArg::Ptr { .. } => {
std::mem::align_of::<usize>() - 1 + std::mem::size_of::<usize>()
}
})
.sum();
let mut backing: Vec<u8> = Vec::with_capacity(estimated_cap);
let mut offsets: Vec<usize> = Vec::with_capacity(args.len());
fn push_slot(backing: &mut Vec<u8>, offsets: &mut Vec<usize>, align: usize, bytes: &[u8]) {
let unpadded = backing.len();
let padding = unpadded.wrapping_neg() & (align - 1);
backing.resize(unpadded + padding, 0);
let offset = backing.len();
backing.extend_from_slice(bytes);
offsets.push(offset);
}
for a in args {
match a {
LoweredArg::I32(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<i32>(),
&v.to_ne_bytes(),
),
LoweredArg::I64(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<i64>(),
&v.to_ne_bytes(),
),
LoweredArg::F32(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<f32>(),
&v.to_ne_bytes(),
),
LoweredArg::F64(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<f64>(),
&v.to_ne_bytes(),
),
LoweredArg::U32(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<u32>(),
&v.to_ne_bytes(),
),
LoweredArg::U64(v) => push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<u64>(),
&v.to_ne_bytes(),
),
LoweredArg::Ptr { host_ptr, .. } => {
let as_usize = *host_ptr as usize;
push_slot(
&mut backing,
&mut offsets,
std::mem::align_of::<usize>(),
&as_usize.to_ne_bytes(),
);
}
}
}
let backing: Box<[u8]> = backing.into_boxed_slice();
let base = backing.as_ptr() as *mut u8;
let slots: Vec<*mut std::ffi::c_void> = offsets
.iter()
.map(|&off| {
unsafe { base.add(off) as *mut std::ffi::c_void }
})
.collect();
KernelParamStorage { backing, slots }
}
pub fn encode_argv(args: &[LoweredArg]) -> Vec<u8> {
let mut out = Vec::new();
for a in args {
match a {
LoweredArg::I32(v) => {
out.push(ArgTag::I32 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::I64(v) => {
out.push(ArgTag::I64 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::F32(v) => {
out.push(ArgTag::F32 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::F64(v) => {
out.push(ArgTag::F64 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::U32(v) => {
out.push(ArgTag::U32 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::U64(v) => {
out.push(ArgTag::U64 as u8);
out.extend_from_slice(&v.to_le_bytes());
}
LoweredArg::Ptr {
guest_offset, len, ..
} => {
out.push(ArgTag::Ptr as u8);
out.extend_from_slice(&guest_offset.to_le_bytes());
out.extend_from_slice(&len.to_le_bytes());
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_mem() -> Vec<u8> {
vec![0u8; 4096]
}
#[test]
fn empty_buffer_parses_to_empty_vec() {
let mem = fake_mem();
let out = parse_argv(&[], &mem).unwrap();
assert!(out.is_empty());
}
#[test]
fn scalar_argv_round_trips() {
let mem = fake_mem();
let args = vec![
LoweredArg::I32(-7),
LoweredArg::U32(0xDEAD_BEEF),
LoweredArg::I64(-1_000_000_000_000),
LoweredArg::U64(0x00C0_FFEE_BABE_F00D),
LoweredArg::F32(core::f32::consts::PI),
LoweredArg::F64(core::f64::consts::E),
];
let buf = encode_argv(&args);
let parsed = parse_argv(&buf, &mem).unwrap();
assert_eq!(parsed.len(), args.len());
assert_eq!(parsed, args);
}
#[test]
fn pointer_argv_resolves_host_ptr_and_records_offset() {
let mem = fake_mem();
let args = vec![LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 128,
guest_offset: 64,
}];
let buf = encode_argv(&args);
let parsed = parse_argv(&buf, &mem).unwrap();
assert_eq!(parsed.len(), 1);
match &parsed[0] {
LoweredArg::Ptr {
host_ptr,
len,
guest_offset,
} => {
assert_eq!(*len, 128);
assert_eq!(*guest_offset, 64);
let expected = unsafe { mem.as_ptr().add(64) };
assert_eq!(*host_ptr, expected);
}
other => panic!("expected Ptr, got {other:?}"),
}
}
#[test]
fn mixed_argv_preserves_order() {
let mem = fake_mem();
let args = vec![
LoweredArg::I32(11),
LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 16,
guest_offset: 32,
},
LoweredArg::F64(1.5),
LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 8,
guest_offset: 1024,
},
LoweredArg::U64(42),
];
let buf = encode_argv(&args);
let parsed = parse_argv(&buf, &mem).unwrap();
assert_eq!(parsed.len(), 5);
assert!(matches!(parsed[0], LoweredArg::I32(11)));
assert!(matches!(parsed[2], LoweredArg::F64(v) if v == 1.5));
assert!(matches!(parsed[4], LoweredArg::U64(42)));
match &parsed[1] {
LoweredArg::Ptr {
guest_offset, len, ..
} => {
assert_eq!(*guest_offset, 32);
assert_eq!(*len, 16);
}
_ => panic!("idx 1 not Ptr"),
}
match &parsed[3] {
LoweredArg::Ptr {
guest_offset, len, ..
} => {
assert_eq!(*guest_offset, 1024);
assert_eq!(*len, 8);
}
_ => panic!("idx 3 not Ptr"),
}
}
#[test]
fn unknown_tag_byte_returns_invalid_args() {
let mem = fake_mem();
let err = parse_argv(&[0xFF, 0, 0, 0, 0], &mem).unwrap_err();
assert_eq!(err, AbiError::InvalidArgs);
}
#[test]
fn truncated_record_returns_invalid_args() {
let mem = fake_mem();
let buf = [ArgTag::I64 as u8, 1, 2, 3];
let err = parse_argv(&buf, &mem).unwrap_err();
assert_eq!(err, AbiError::InvalidArgs);
}
#[test]
fn oversized_buffer_returns_kernel_args_unsupported() {
let mem = fake_mem();
let buf = vec![0u8; MAX_KERNEL_ARGS_BYTES + 1];
let err = parse_argv(&buf, &mem).unwrap_err();
assert_eq!(err, AbiError::KernelArgsUnsupported);
}
#[test]
fn too_many_args_returns_kernel_args_unsupported() {
let mem = fake_mem();
let one = vec![ArgTag::I32 as u8, 0, 0, 0, 0];
let mut buf = Vec::new();
for _ in 0..(MAX_KERNEL_ARGS + 1) {
buf.extend_from_slice(&one);
}
assert!(buf.len() <= MAX_KERNEL_ARGS_BYTES);
let err = parse_argv(&buf, &mem).unwrap_err();
assert_eq!(err, AbiError::KernelArgsUnsupported);
}
#[test]
fn pointer_out_of_bounds_returns_invalid_pointer() {
let mem = vec![0u8; 32];
let args = vec![LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 16,
guest_offset: 24, }];
let buf = encode_argv(&args);
let err = parse_argv(&buf, &mem).unwrap_err();
assert_eq!(err, AbiError::InvalidPointer);
}
#[test]
fn pointer_offset_overflow_returns_invalid_pointer() {
let mem = vec![0u8; 32];
let args = vec![LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 1024,
guest_offset: u32::MAX,
}];
let buf = encode_argv(&args);
let err = parse_argv(&buf, &mem).unwrap_err();
assert_eq!(err, AbiError::InvalidPointer);
}
#[test]
fn zero_length_pointer_at_memory_end_is_allowed() {
let mem = vec![0u8; 32];
let args = vec![LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 0,
guest_offset: 32,
}];
let buf = encode_argv(&args);
let parsed = parse_argv(&buf, &mem).unwrap();
assert_eq!(parsed.len(), 1);
}
#[test]
fn build_kernel_param_storage_slot_count_matches_args() {
let args = vec![
LoweredArg::I32(1),
LoweredArg::F64(2.0),
LoweredArg::Ptr {
host_ptr: std::ptr::null(),
len: 8,
guest_offset: 0,
},
];
let storage = build_kernel_param_storage(&args);
assert_eq!(storage.len(), 3);
assert!(!storage.is_empty());
}
#[test]
fn build_kernel_param_storage_encodes_bytes_and_alignment() {
let i32_val: i32 = -0x0102_0304;
let f64_val: f64 = 1234.5;
let u64_val: u64 = 0x00C0_FFEE_BABE_F00D;
let args = vec![
LoweredArg::I32(i32_val),
LoweredArg::F64(f64_val),
LoweredArg::ptr_for_encoding( 64, 16),
LoweredArg::U64(u64_val),
];
let storage = build_kernel_param_storage(&args);
assert_eq!(storage.len(), 4);
let backing = storage.backing();
let base = backing.as_ptr() as usize;
let slots = storage.slot_ptrs();
assert_eq!(slots.len(), 4);
let offset_of = |i: usize| -> usize {
let p = slots[i] as usize;
assert!(
p >= base && p <= base + backing.len(),
"slot {i} pointer escapes backing buffer"
);
p - base
};
let read = |off: usize, n: usize| -> &[u8] { &backing[off..off + n] };
let off0 = offset_of(0);
assert_eq!(off0 % std::mem::align_of::<i32>(), 0, "i32 slot misaligned");
assert_eq!(read(off0, 4), &i32_val.to_ne_bytes());
let off1 = offset_of(1);
assert_eq!(off1 % std::mem::align_of::<f64>(), 0, "f64 slot misaligned");
assert_eq!(read(off1, 8), &f64_val.to_ne_bytes());
let off2 = offset_of(2);
assert_eq!(
off2 % std::mem::align_of::<usize>(),
0,
"ptr slot misaligned"
);
assert_eq!(
read(off2, std::mem::size_of::<usize>()),
&(0usize).to_ne_bytes()
);
let off3 = offset_of(3);
assert_eq!(off3 % std::mem::align_of::<u64>(), 0, "u64 slot misaligned");
assert_eq!(read(off3, 8), &u64_val.to_ne_bytes());
assert!(off1 >= off0 + 4, "f64 slot overlaps i32 slot");
assert!(off2 >= off1 + 8, "ptr slot overlaps f64 slot");
assert!(
off3 >= off2 + std::mem::size_of::<usize>(),
"u64 slot overlaps ptr slot"
);
}
#[test]
fn arg_tag_value_bytes_pinned() {
assert_eq!(ArgTag::I32.value_bytes(), 4);
assert_eq!(ArgTag::I64.value_bytes(), 8);
assert_eq!(ArgTag::F32.value_bytes(), 4);
assert_eq!(ArgTag::F64.value_bytes(), 8);
assert_eq!(ArgTag::U32.value_bytes(), 4);
assert_eq!(ArgTag::U64.value_bytes(), 8);
assert_eq!(ArgTag::Ptr.value_bytes(), 8);
}
#[test]
fn arg_tag_byte_values_pinned() {
assert_eq!(ArgTag::I32 as u8, 0x01);
assert_eq!(ArgTag::I64 as u8, 0x02);
assert_eq!(ArgTag::F32 as u8, 0x03);
assert_eq!(ArgTag::F64 as u8, 0x04);
assert_eq!(ArgTag::U32 as u8, 0x05);
assert_eq!(ArgTag::U64 as u8, 0x06);
assert_eq!(ArgTag::Ptr as u8, 0x07);
}
}