From eab17f1d85b354d822e8d982d095844d1264d8d1 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Fri, 17 Jul 2026 17:11:43 -0700 Subject: [PATCH 01/32] Add rest of opentmk invariant code --- Cargo.lock | 33 + Cargo.toml | 7 + opentmk/inv_decoder/Cargo.toml | 13 + opentmk/inv_decoder/src/atomicrefqueue.rs | 138 ++ opentmk/inv_decoder/src/lib.rs | 1334 +++++++++++++++++ opentmk/inv_decoder/src/safememory.rs | 300 ++++ opentmk/inv_decoder/src/wire.rs | 110 ++ opentmk/inv_packet/Cargo.toml | 7 + opentmk/inv_packet/src/lib.rs | 86 ++ opentmk/opentmk_invariant/Cargo.toml | 21 + opentmk/opentmk_invariant/src/comms/mod.rs | 185 +++ opentmk/opentmk_invariant/src/comms/test.rs | 154 ++ .../opentmk_invariant/src/deserializer/mod.rs | 16 + .../src/deserializer/syzlang/mod.rs | 178 +++ .../src/deserializer/syzlang/test.rs | 188 +++ opentmk/opentmk_invariant/src/executor/mod.rs | 166 ++ .../opentmk_invariant/src/executor/test.rs | 222 +++ .../src/functions/hvcall_meta.rs | 44 + .../opentmk_invariant/src/functions/hyperv.rs | 82 + .../src/functions/io_port/mod.rs | 135 ++ .../opentmk_invariant/src/functions/mod.rs | 10 + .../src/functions/registry.rs | 53 + .../src/functions/variable.rs | 57 + opentmk/opentmk_invariant/src/main.rs | 61 + opentmk/opentmk_invariant/src/prelude.rs | 16 + 25 files changed, 3616 insertions(+) create mode 100644 opentmk/inv_decoder/Cargo.toml create mode 100644 opentmk/inv_decoder/src/atomicrefqueue.rs create mode 100644 opentmk/inv_decoder/src/lib.rs create mode 100644 opentmk/inv_decoder/src/safememory.rs create mode 100644 opentmk/inv_decoder/src/wire.rs create mode 100644 opentmk/inv_packet/Cargo.toml create mode 100644 opentmk/inv_packet/src/lib.rs create mode 100644 opentmk/opentmk_invariant/Cargo.toml create mode 100644 opentmk/opentmk_invariant/src/comms/mod.rs create mode 100644 opentmk/opentmk_invariant/src/comms/test.rs create mode 100644 opentmk/opentmk_invariant/src/deserializer/mod.rs create mode 100644 opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs create mode 100644 opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs create mode 100644 opentmk/opentmk_invariant/src/executor/mod.rs create mode 100644 opentmk/opentmk_invariant/src/executor/test.rs create mode 100644 opentmk/opentmk_invariant/src/functions/hvcall_meta.rs create mode 100644 opentmk/opentmk_invariant/src/functions/hyperv.rs create mode 100644 opentmk/opentmk_invariant/src/functions/io_port/mod.rs create mode 100644 opentmk/opentmk_invariant/src/functions/mod.rs create mode 100644 opentmk/opentmk_invariant/src/functions/registry.rs create mode 100644 opentmk/opentmk_invariant/src/functions/variable.rs create mode 100644 opentmk/opentmk_invariant/src/main.rs create mode 100644 opentmk/opentmk_invariant/src/prelude.rs diff --git a/Cargo.lock b/Cargo.lock index 4b4a4e84e35..497eabe9594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4041,6 +4041,23 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "inv_decoder" +version = "0.3.0" +dependencies = [ + "anyhow", + "num-traits", + "spin", + "zerocopy", +] + +[[package]] +name = "inv_packet" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "io-uring" version = "0.7.11" @@ -8365,6 +8382,22 @@ dependencies = [ "x86defs", ] +[[package]] +name = "tmk_invariant" +version = "0.1.0" +dependencies = [ + "hvdef", + "inv_decoder", + "inv_packet", + "log", + "opentmk_core", + "postcard", + "serde", + "spin", + "uefi", + "zerocopy", +] + [[package]] name = "tmk_macros" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 8ea1f02a841..af4c1a65606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "vm/vmgs/vmgstool", # opentmk "opentmk", + "opentmk/opentmk_invariant", # support crates consumed by closed-source only "support/debug_output_tracing", ] @@ -82,6 +83,10 @@ opentmk_protocol = { path = "opentmk/opentmk_protocol" } opentmk_core = { path = "opentmk/opentmk_core" } opentmk_disk = { path = "opentmk/opentmk_disk" } +# opentmk invariant +inv_packet = { path = "opentmk/inv_packet" } +inv_decoder = { path = "opentmk/inv_decoder" } + flowey = { path = "flowey/flowey" } flowey_cli = { path = "flowey/flowey_cli" } flowey_core = { path = "flowey/flowey_core" } @@ -521,6 +526,7 @@ hex = "0.4" pbjson = "0.5" pbjson-build = "0.5" pbjson-types = "0.5" +postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } prost = "0.11" prost-build = "0.11" prost-types = "0.11" @@ -562,6 +568,7 @@ arrayvec = { version = "0.7", default-features = false } bitfield-struct = "0.11.0" bitvec = { version = "1.1", default-features = false } heapless = { version = "0.8", default-features = false } +num-traits = { version = "0.2.19", default-features = false } petgraph = "0.8.0" range_map_vec = "0.2.0" rustc-hash = "2.1.1" diff --git a/opentmk/inv_decoder/Cargo.toml b/opentmk/inv_decoder/Cargo.toml new file mode 100644 index 00000000000..4fa8146726c --- /dev/null +++ b/opentmk/inv_decoder/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "inv_decoder" +version = "0.3.0" +authors = ["VRTD "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +num-traits.workspace = true +spin.workspace = true +zerocopy.workspace = true diff --git a/opentmk/inv_decoder/src/atomicrefqueue.rs b/opentmk/inv_decoder/src/atomicrefqueue.rs new file mode 100644 index 00000000000..ca44b0093d2 --- /dev/null +++ b/opentmk/inv_decoder/src/atomicrefqueue.rs @@ -0,0 +1,138 @@ +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use alloc::vec::Vec; + +/// Read-only vector of entries `T` that can be accessed concurrently by multiple threads. +/// Each entry is associated with an atomic boolean flag that is used to mark if the entry +/// is referenced by a thread. If an entry is referenced, it is considered owned by that thread +/// and cannot be referenced again. +/// Note that referenced / taken entries are not cleared from the vector until the entire vector is dropped. +/// This is to enable the vector to be "popped" by multiple threads concurrently. +pub(crate) struct AtomicRefQueue { + data: Vec<(T, AtomicBool)>, + start_idx: AtomicUsize, +} + +// AtomicRefQueue is safe to send between threads, as long as the entries are also Send. +// Same goes for sync. +unsafe impl Send for AtomicRefQueue where T: Send {} +unsafe impl Sync for AtomicRefQueue where T: Sync {} + +impl AtomicRefQueue { + /// Create a new AtomicRefQueue from a list of entries. + pub(crate) fn new(list: Vec) -> Self { + // Convert the list of entries into a list of (entry, atomic flag) pairs + let list = list + .into_iter() + .map(|entry| (entry, AtomicBool::new(false))) + .collect(); + // Return the AtomicRefQueue + AtomicRefQueue { + data: list, + start_idx: AtomicUsize::new(0), + } + } + /// Searches for the next entry in the vector that hasn't already been taken / referenced + /// and that passes the conditional function check. + /// If found, marks the entry and returns a reference to the instruction to the callee. + /// If no unmarked entry is found, returns None. + /// Note: This is logically a conditional `pop` operation on a `Mutex>`, but with a unique implementation + /// due to the no_std requirement. + pub(crate) fn pop_ref_conditional(&self, conditional_func: F) -> Option<&T> + where + F: Fn(&T) -> bool, + { + // Track whether we should update the start idx, this gets set to false if we skip an entry + // (i.e. if we don't pop out a sequential entry) + let mut update_start_idx = true; + let start_idx = self.start_idx.load(Ordering::SeqCst); + for (idx, entry) in self.data[start_idx..].iter().enumerate() { + // Check if the entry is already marked without marking it first + // If it is not, then ensure it also passes the conditional function check + if !entry.1.load(Ordering::SeqCst) { + if conditional_func(&entry.0) { + // Passed initial checks, now try to mark the entry + if let Ok(false) = + entry + .1 + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + { + // Marked it, return a reference to the entry and optionally update the start idx + if update_start_idx && idx > start_idx { + // Just use the current idx as the start idx, even though the current is currently being + // returned and marked. Prevents the need for checking edge cases e.g. if the current idx is the + // last. + // + // Also, if multiple threads are concurrently popping entries, they may both be writing to the + // start_idx. This is fine, as the start_idx will be valid regardless which thread writes to it. + self.start_idx.store(idx, Ordering::SeqCst); + } + return Some(&entry.0); + } + } else { + // Failed the conditional function, we may not be processing entries sequentially. Ensure + // we do not update the start idx. + update_start_idx = false; + } + + // Failed to mark the entry, continue iterating + } + // Failed to mark the entry or the initial checks, continue iterating + } + None + } +} + +// Add tests for the AtomicRefQueue, ensuring that the vector is correctly populated and that +// the pop_ref method returns the correct entries, and stops when the vector is all marked. +#[cfg(test)] +#[allow(non_snake_case)] +mod tests { + + use super::*; + + #[test] + fn test_AtomicRefQueue_sequential_take() { + let vec = AtomicRefQueue::new(vec![1, 2, 3]); + fn always_true(_: &i32) -> bool { + true + } + // After pushing, all entries should be available in order. + assert_eq!(vec.pop_ref_conditional(always_true), Some(&1)); + assert_eq!(vec.pop_ref_conditional(always_true), Some(&2)); + assert_eq!(vec.pop_ref_conditional(always_true), Some(&3)); + // All entries are now marked, so further calls should return None. + assert_eq!(vec.pop_ref_conditional(always_true), None); + } + + #[test] + fn test_AtomicRefQueue_non_sequential_take() { + let vec = AtomicRefQueue::new(vec![1, 2, 3, 4]); + fn always_true(_: &i32) -> bool { + true + } + fn true_on_even(val: &i32) -> bool { + *val % 2 == 0 + } + fn always_false(_: &i32) -> bool { + false + } + // After pushing, all entries should be available in order. + assert_eq!(vec.pop_ref_conditional(true_on_even), Some(&2)); + assert_eq!(vec.pop_ref_conditional(always_true), Some(&1)); + assert_eq!(vec.pop_ref_conditional(always_true), Some(&3)); + assert_eq!(vec.pop_ref_conditional(always_false), None); + // All entries are now marked, so further calls should return None. + assert_eq!(vec.pop_ref_conditional(true_on_even), Some(&4)); + assert_eq!(vec.pop_ref_conditional(always_true), None); + } + + #[test] + fn test_AtomicRefQueue_empty() { + let vec: AtomicRefQueue = AtomicRefQueue::new(Vec::new()); + fn always_true(_: &i32) -> bool { + true + } + assert_eq!(vec.pop_ref_conditional(always_true), None); + } +} diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs new file mode 100644 index 00000000000..8742273b53c --- /dev/null +++ b/opentmk/inv_decoder/src/lib.rs @@ -0,0 +1,1334 @@ +#![no_std] + +#[macro_use] +extern crate alloc; + +mod atomicrefqueue; +mod safememory; +mod wire; + +pub use safememory::SafeMemoryMap; + +use spin::Mutex; + +use core::{ + marker::PhantomData, + ops, + sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, +}; + +use alloc::{string::String, sync::Arc, vec::Vec}; +use anyhow::{Context, bail}; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::atomicrefqueue::AtomicRefQueue; + +const K_MAX_COMMANDS: usize = 1000; + +pub const EXEC_INPUT_REQ_SIZE: usize = 0x1000000; + +// Supported (min) input size (kMaxInput in executor.cc). +pub const SUPPORTED_INPUT_SIZE: usize = 8 << 20; + +// Max supported args +pub const MAX_ARGS: usize = 30; + +// All syzkaller pointers are offsets from this presumed base address +pub const ADDR_SYZ_BEGIN: u64 = 0x20000000; + +// Call failed +const _SYZKALLER_CALL_END_FAILED: u64 = 3; + +pub type Result = anyhow::Result; + +pub type TestcaseResults = [ResT; K_MAX_COMMANDS]; + +pub type Executor<'f> = + dyn Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + 'f + Send + Sync; + +/// Instructions for a decoded syzkaller program alongside the required +/// components to execute the program (e.g. the exec function and the results array). +/// +/// 'm - lifetime of the memory map instance +/// 'f - lifetime of the executor function +pub struct DecodedProgram<'m, 'f> { + instr_vec: Arc>, + /// Memory-layout that holds the address layout of the syzkaller program + mem: Arc>, + /// The executor function + exec: Arc>, + /// Holds the results for each call. Due to syzkaller internals, this only + /// tracks results for calls that have direct return values (i.e. calls that are assigned + /// a copyout index). Calls that are not assigned a copyout index are tracked in the custom_results) + results: Arc, + /// Holds the results for calls that are not assigned a copyout index (i.e. calls that do not have + /// direct return values) + custom_results: Arc, +} + +/// Implementation for SafeMemoryMap. Note that because the memory map instance owned here +/// is behind a [`spin::Mutex`], so we can do memory operations without requiring +/// `&mut DecodedProgram` (we need this loosening of restrictions internally, i.e. so that +/// the reentrancy fuzz loop could perform read/write operations while holding only a +/// shared global reference to it). +impl SafeMemoryMap for &DecodedProgram<'_, '_> { + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + self.mem.lock().partial_write_mem(base, val) + } + + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + self.mem.lock().partial_read_mem(base, val) + } +} + +impl<'m, 'f> DecodedProgram<'m, 'f> { + /// Creates a new [`DecodedProgram`] that decodes the syzkaller program from the provided input + /// buffer and exec callback. + fn new< + M: SafeMemoryMap + 'm, + F: Fn(&DecodedProgram, InputCase) -> InputResult + Send + Sync + 'f, + >( + mem: M, + syz_input_buffer: &[u8], + exec: F, + ) -> Result { + let mut decoder = + Decoder::new(syz_input_buffer).context("failed to instantiate decoder")?; + let mut instr_vec_tmp: Vec = Vec::new(); + { + let mut call = None; + let mut instrout: Vec = Vec::new(); + while let Some(instr) = decoder.try_next().context("failed to decode instruction")? { + match instr { + Instr::CopyOut(i) => { + // CopyOuts always follow a call and are tied to calls, so we collect them + // and then attach them to their associated call later + instrout.push(i); + } + Instr::CopyIn(i) => { + let entry = InstrEntry::CopyIn(i); + instr_vec_tmp.push(entry); + } + Instr::Call(i) => { + // Process any cached call + copyouts if present + if let Some(call_entry) = call.take() { + let entry = InstrEntry::Call(call_entry, instrout.clone()); + instr_vec_tmp.push(entry); + // Clear the collected CopyOuts vector. + instrout.clear(); + } + // Now that we've flushed any previously cached call + copyouts, we can work with the + // current instruction. + + // Cache the call, it'll be processed after any following CopyOuts + call = Some(i); + } + } + } + + // If the last instruction was a Call it would be in our cached `call` variable unprocessed. + // If the last instruction was a CopyOut, we wouldn't have processed the last cached `call` yet either. + // We do this here. + if let Some(call_entry) = call.take() { + let entry = InstrEntry::Call(call_entry, instrout.clone()); + instr_vec_tmp.push(entry); + // Clear the collected CopyOuts vector. + instrout.clear(); + } + } + + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + let custom_results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + Ok(Self { + mem: Arc::new(Mutex::new(mem)), + instr_vec: Arc::new(AtomicRefQueue::new(instr_vec_tmp)), + exec: Arc::new(exec), + results: results.clone(), + custom_results: custom_results.clone(), + }) + } + + /// Checks if the instruction is a call, and if so whether it depends on the results of + /// another call that has not executed yet + fn is_instr_call_ready(&self, instr_entry: &InstrEntry) -> bool { + // Get a reference to the global results array we're working with + let results = self.results.as_ref(); + // Check if Instr is a call and if it depends on any unexecuted calls + if let InstrEntry::Call(i, _) = &instr_entry { + for arg in i.args.iter() { + if let Arg::Result(a) = arg { + let r = &results[a.idx as usize]; + if !r.executed.load(Ordering::SeqCst) { + // Found a dependent call that has not executed yet. + // Return false to indicate this call is not ready to be executed. + return false; + } + } + } + } + // Either Instr was not a call, or it was a call and all dependent calls have executed. + // Return true to indicate this Instr is ready to be executed. + true + } + + /// Check if the provided call was executed and returned success, based on the contents + /// of the provided results array. + fn was_call_successful(&self, call: &InstrCall) -> bool { + let (copyout_index, results) = + if call.wire.copyout_index != wire::COPYOUT_INDEX_INVALID as u64 { + (call.wire.copyout_index as usize, &self.results) + } else { + ( + call.custom_copyout_index + .expect("copyout index was invalid, but no custom copyout index was set"), + &self.custom_results, + ) + }; + + if copyout_index >= K_MAX_COMMANDS { + panic!( + "copyout_call_results: result idx {:#x} overflows/underflows K_MAX_COMMANDS", + copyout_index + ); + } + + // Get the result value from the results array. + let r = &results[copyout_index]; + + r.was_successful() + } + + /// Continues execution of the instructions contains in our own instruction vector + pub fn continue_execution(&self) -> Result<()> { + // SAFETY: This should always be sound because we cannot reach this point without having a valid + // execution function. + // We can _only_ take an immutable borrow here because this function is reentrant and we may have + // other references to the execution function in higher stack frames. + let instr_vec = self.instr_vec.clone(); + while let Some(entry) = instr_vec.pop_ref_conditional(|ent| self.is_instr_call_ready(ent)) { + // Execute all the instructions, regardless of their type + let instr_target = entry.get_inner_instr(); + self.exec_single(instr_target) + .context("failed to execute instruction")?; + + // If the instr was a Call, it may have copyouts to execute. + if let InstrEntry::Call(instr_call, copyouts) = entry { + // Only execute copyouts if the call itself was successful, otherwise the values being + // copied out may be invalid. + if self.was_call_successful(instr_call) { + for copyout_entry in copyouts.iter() { + self.exec_single(Instr::CopyOut(*copyout_entry)) + .context("failed to execute instruction")?; + } + } + } // No copyouts to process if the instruction was not a call + } + + Ok(()) + } + + /// Executes the instructions in the provided instruction vector. + fn exec_instrs(&self) -> Result<()> { + let instr_vec = self.instr_vec.clone(); + while let Some(entry) = instr_vec.pop_ref_conditional(|ent| self.is_instr_call_ready(ent)) { + // Execute all the instructions, regardless of their type + self.exec_single(entry.get_inner_instr()) + .context("failed to execute instruction")?; + + // If the instr was a Call, it may have copyouts to execute. + if let InstrEntry::Call(instr_call, copyouts) = entry { + // Only execute copyouts if the call itself was successful, otherwise the values being + // copied out may be invalid. + if self.was_call_successful(instr_call) { + for copyout_entry in copyouts.iter() { + self.exec_single(Instr::CopyOut(*copyout_entry)) + .context("failed to execute instruction")?; + } + } + } // No copyouts to process if the instruction was not a call + } + + Ok(()) + } + + /// Executes the provided instruction. + /// If the instruction is a call, the provided exec function is called with the provided input case + /// and the results are stored in the results array if the call has a valid copyout index. + fn exec_single(&self, instr: Instr) -> Result<()> { + /// The number of bytes to offset all data operations by. + const COPYIN_OFFSET: u64 = 0; + + let mut mem = self.mem.lock(); + match instr { + Instr::CopyIn(i) => match i.arg { + Arg::Const(a) => { + let size = a.meta & 0xff; + let bf = (a.meta >> 8) & 0xff; + let bf_off = (a.meta >> 16) & 0xff; + let bf_len = (a.meta >> 24) & 0xff; + let val = a.val + ((a.meta >> 32) * i.rpid); + + copyin( + &mut *mem, + i.wire.addr + COPYIN_OFFSET, + val, + size, + bf, + bf_off, + bf_len, + ); + } + Arg::Result(a) => { + let size = a.meta & 0xff; + let bf = (a.meta >> 8) & 0xff; + + let r = &self.results[a.idx as usize]; + let val = if r.was_successful() { + let mut v = r.val.load(Ordering::SeqCst); + v = v.checked_div(a.op_div).unwrap_or(v); + v + a.op_add + } else { + a.arg + }; + + copyin(&mut *mem, i.wire.addr + COPYIN_OFFSET, val, size, bf, 0, 0); + } + Arg::Data((a, d)) => { + mem.write_mem( + (i.wire.addr + COPYIN_OFFSET) as usize, + &d.as_bytes()[..a.size as usize], + ); + } + }, + + Instr::CopyOut(i) => { + let mut val = 0u64; + copyout(&mut *mem, i.wire.addr, i.wire.size, &mut val); + + let r = &self.results[i.wire.index as usize]; + // Its assumed if we're executing a CopyOut, that the associated call was + // successful. We should not have been passed a CopyOut instruction to execute if + // the associated call was not successful. + r.mark_executed(true, val); + } + + Instr::Call(i) => { + // Evaluate all input arguments. + let mut args = [0u64; MAX_ARGS]; + + for (n, arg) in i.args.iter().enumerate() { + match arg { + Arg::Const(a) => { + // Calculate the constant value and store it in the input case. + let val = a.val + ((a.meta >> 32) * i.rpid); + + args[n] = val; + } + Arg::Result(a) => { + let r = &self.results[a.idx as usize]; + let val = if !r.was_successful() { + // The dependent call that's expected to fill this result argument value has either + // not been executed or did not execute successfully, meaning the result value + // is invalid. We will skip this call + return Ok(()); + } else { + let mut v = r.val.load(Ordering::SeqCst); + v = v.checked_div(a.op_div).unwrap_or(v); + v + a.op_add + }; + + args[n] = val; + } + Arg::Data(_) => panic!("data argument for function call!?"), + } + } + + let input_struct = InputCase { + call_num: i.idx as u64, + args, + num_args: i.args.len() as u64, + _priv: PhantomData, + }; + + // Ensure that we don't keep a lock to the safe memory map + // when we pass execution to the executor. That way we can + // perform reentrancy as needed. + drop(mem); + + // Call the provided exec function now that we've parsed an input. + let exec_result = (self.exec)(self, input_struct); + + // If the call has a valid associated copyout index, we need to set the result in the results array. + let (copyout_index, results) = + if i.wire.copyout_index != wire::COPYOUT_INDEX_INVALID as u64 { + (i.wire.copyout_index as usize, &self.results) + } else { + ( + i.custom_copyout_index.expect( + "copyout index was invalid, but no custom copyout index was set", + ), + &self.custom_results, + ) + }; + + if copyout_index >= K_MAX_COMMANDS { + panic!( + "copyout_call_results: result idx {:#x} overflows/underflows K_MAX_COMMANDS", + copyout_index + ); + } + let r = &results[copyout_index]; + + r.mark_executed(exec_result.is_success, exec_result.code); + } + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct InstrCopyIn { + /// The on-the-wire instruction. + wire: wire::InstrCopyIn, + /// The PID of the program. + rpid: u64, + /// The data source. + arg: Arg, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +enum Arg { + Const(wire::ArgConst), + Result(wire::ArgResult), + Data((wire::ArgData, Vec)), + // CSum(), +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +struct InstrCopyOut { + /// The on-the-wire instruction. + wire: wire::InstrCopyOut, + /// The PID of the program. + rpid: u64, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct InstrCall { + /// The on-the-wire instruction. + wire: wire::InstrCall, + /// The PID of the program. + rpid: u64, + /// The index of the function to call. + idx: i64, + /// Custom copyout index, *only* set if the wire::InstrCall::copyout_index is COPYOUT_INDEX_INVALID. + /// This allows us to track success/failure of a call that has not been provided a copyout index from syzkaller. + /// In this case, these indexes index into our custom results array (not the default results array). + custom_copyout_index: Option, + /// The arguments. + args: Vec, +} + +/// A decoded syzkaller instruction. +enum InstrEntry { + /// A call instruction with a list of associated copyout instructions. + Call(InstrCall, Vec), + /// A copyin instruction. + CopyIn(InstrCopyIn), +} + +impl InstrEntry { + fn get_inner_instr(&self) -> Instr { + match self { + InstrEntry::Call(i, _) => Instr::Call(i.clone()), + InstrEntry::CopyIn(i) => Instr::CopyIn(i.clone()), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +enum Instr { + /// Copy data into memory. + CopyIn(InstrCopyIn), + /// Copy data out of memory into the results array. + /// These instructions usually follow a call instruction. + CopyOut(InstrCopyOut), + /// Call a system call. + Call(InstrCall), +} + +struct Decoder<'a> { + hdr: wire::ProgramHeader, + buf: &'a [u8], + custom_results_counter: AtomicUsize, +} + +impl<'a> Decoder<'a> { + fn new(mut buf: &'a [u8]) -> Result { + let hdr = read_struct::(&mut buf) + .context("failed to read program header")?; + + if hdr.magic != 0xbadc0ffeebadface { + bail!("bad execute request magic {:#?}", hdr.magic); + } else if hdr.prog_size == 0 { + bail!("prog size is 0"); + } else if hdr.prog_size + > ((SUPPORTED_INPUT_SIZE as u64) - (core::mem::size_of::() as u64)) + { + bail!("input size too large! InputSz:{:#?}", hdr.prog_size); + } + + if hdr.prog_size > buf.len() as u64 { + bail!("syzkaller program is larger than input buffer"); + } + + Ok(Self { + // Resize the buffer to the program size. + buf: &buf[..hdr.prog_size as usize], + hdr, + custom_results_counter: AtomicUsize::new(0), + }) + } + + /// Fetch and decode the next instruction. Returns `None` if we have reached EOF. + pub fn try_next(&mut self) -> Result> { + let n = read_inc_input(&mut self.buf).context("unexpected end of input")? as i64; + match n { + wire::INSTR_EOF => Ok(None), + + wire::INSTR_COPYIN => { + let insn = read_struct::(&mut self.buf) + .context("failed to read instruction")?; + + let arg = read_arg(&mut self.buf).context("failed to read argument")?; + + Ok(Some(Instr::CopyIn(InstrCopyIn { + rpid: self.hdr.pid, + wire: insn, + arg, + }))) + } + + wire::INSTR_COPYOUT => { + let insn = read_struct::(&mut self.buf) + .context("failed to read instruction")?; + + Ok(Some(Instr::CopyOut(InstrCopyOut { + rpid: self.hdr.pid, + wire: insn, + }))) + } + + c if c < 0 => { + bail!("unknown instruction {n}"); + } + + _ => { + let insn = read_struct::(&mut self.buf) + .context("failed to read instruction")?; + + let mut args = Vec::new(); + for _i in 0..insn.num_args { + args.push(read_arg(&mut self.buf).context("failed to read argument")?); + } + let custom_copyout_index = + if insn.copyout_index == wire::COPYOUT_INDEX_INVALID as u64 { + Some(self.custom_results_counter.fetch_add(1, Ordering::SeqCst)) + } else { + None + }; + Ok(Some(Instr::Call(InstrCall { + rpid: self.hdr.pid, + idx: n, + wire: insn, + args, + custom_copyout_index, + }))) + } + } + } +} + +/// Represents a parsed syzkaller input. +pub struct InputCase { + pub call_num: u64, + /// argument array + pub args: [u64; MAX_ARGS], + // Number of args set in args + pub num_args: u64, + /// Prevent construction by our callers so we can ensure maximum SemVer flexibility. + #[doc(hidden)] + _priv: PhantomData<()>, +} + +/// Result of an executed syzkaller input +#[derive(Default)] +pub struct InputResult { + pub code: u64, + pub name: String, + pub is_success: bool, +} + +pub struct ResT { + executed: AtomicBool, + val: AtomicU64, + is_success: AtomicBool, +} + +// Derive Clone for ResT by simply copying the atomic values. +impl Clone for ResT { + fn clone(&self) -> Self { + ResT { + executed: AtomicBool::new(self.executed.load(Ordering::SeqCst)), + val: AtomicU64::new(self.val.load(Ordering::SeqCst)), + is_success: AtomicBool::new(self.is_success.load(Ordering::SeqCst)), + } + } +} + +impl ResT { + const fn new() -> Self { + ResT { + executed: AtomicBool::new(false), + val: AtomicU64::new(0), + is_success: AtomicBool::new(false), + } + } + + /// Returns whether the result entry has been executed and was successful + pub fn was_successful(&self) -> bool { + self.executed.load(Ordering::SeqCst) && self.is_success.load(Ordering::SeqCst) + } + + /// Returns the contained value. The value is only returned if the result entry has been executed. + pub fn value(&self) -> Option { + if self.executed.load(Ordering::SeqCst) { + Some(self.val.load(Ordering::SeqCst)) + } else { + None + } + } + + // Mark the result as executed, whether it was successful, and sets the value + fn mark_executed(&self, is_success: bool, val: u64) { + // First, update the value. + // This *must* occur before we set is_success/executed to ensure other threads don't attempt + // to read the value until executed/is_success are also set. + self.val.store(val, Ordering::SeqCst); + + self.executed.store(true, Ordering::SeqCst); + self.is_success.store(is_success, Ordering::SeqCst); + } +} + +/// Read a 64-bit little-endian word from a slice and advance the slice's pointer. +/// +/// This will panic if the slice pointed to by `input_data` is smaller than 8 bytes. +fn read_inc_input(input_data: &mut &[u8]) -> Option { + let (s, t) = u64::read_from_prefix(input_data).ok()?; + *input_data = t; + Some(s) +} + +/// Read a struct out of `input_data` and advance the slice's pointer. +fn read_struct(input_data: &mut &[u8]) -> Option { + let (s, t) = T::read_from_prefix(input_data).ok()?; + + *input_data = t; + Some(s) +} + +/// Read out an argument from an input buffer. +fn read_arg(buf: &mut &[u8]) -> Result { + let typ = read_inc_input(buf).context("unexpected end of input")?; + + Ok(match typ { + wire::ARG_CONST => { + let arg = read_struct::(buf).context("failed to read argument")?; + + Arg::Const(arg) + } + wire::ARG_RESULT => { + let arg = read_struct::(buf).context("failed to read argument")?; + + Arg::Result(arg) + } + wire::ARG_DATA => { + let arg = read_struct::(buf).context("failed to read argument")?; + + // Read out each data word. + let cnt = arg.size.div_ceil(8); + if cnt == 0 { + panic!("data argument with size of zero!?"); + } + + let mut words = Vec::new(); + for _i in 0..cnt { + words.push(read_inc_input(buf).context("unexpected end of input")?); + } + + Arg::Data((arg, words)) + } + // arg_csum + 0x3 => todo!(), + // Catchall + _ => bail!("unsupported argument type: {typ}"), + }) +} + +fn store_by_bitmask(mem: &mut M, addr: u64, val: N, bf_off: u64, bf_len: u64) +where + M: SafeMemoryMap + ?Sized, + N: FromBytes + + IntoBytes + + Immutable + + Default + + Copy + + num_traits::Num + + ops::Shl + + ops::Not + + ops::BitOrAssign + + ops::BitAndAssign + + ops::BitAnd, +{ + if bf_off == 0 && bf_len == 0 { + mem.write_mem(addr as usize, val.as_bytes()) + } else { + let mut new_val = N::default(); + mem.read_mem(addr as usize, new_val.as_mut_bytes()); + + // unset bitmask + let mask = (N::one() << bf_len) - N::one(); + new_val &= !(mask << bf_off); + + // set val into bitmask + new_val |= (val & mask) << bf_off; + + mem.write_mem(addr as usize, new_val.as_bytes()) + } +} + +/// Copy a value into a buffer with the specified binary format (in `bf`). +fn copyin( + mem: &mut M, + addr: u64, + val: u64, + size: u64, + bf: u64, + bf_off: u64, + bf_len: u64, +) { + if bf != 0 && (bf_off != 0 || bf_len != 0) { + panic!("copyin: bitmask for string format invalid"); + } + + let tmp_str = match bf { + // Case: binary_format_native + 0 => { + match size { + 1 => store_by_bitmask(mem, addr, val as u8, bf_off, bf_len), + 2 => store_by_bitmask(mem, addr, val as u16, bf_off, bf_len), + 4 => store_by_bitmask(mem, addr, val as u32, bf_off, bf_len), + 8 => store_by_bitmask(mem, addr, val, bf_off, bf_len), + _ => { + panic!("copyin: bad argument size {}", size); + } + } + return; + } + + // Case: binary_format_bigendian + 1 => panic!("unhandled: bigendian binary format"), + + // Case: binary_format_strdec + 2 => { + // Converts 0xffffffffffffffff into 34343736`34343831 + // 35353930`37333730 00000000`35313631 + // TODO: verify endianness & correctness and re-assess implementation for perf + assert!(size == 20); + format!("{val:020}") + } + + // Case: binary_format_strhex + 3 => { + // Stores ascii of hex, e.g val 0xffffffffffffffff turns + // into 0x66666666`66667830 66666666`66666666 + // TODO: verify endianness & correctness and re-assess implementation for perf + assert!(size == 18); + format!("{val:#018x}") + } + + // Case: binary_format_stroct + 4 => { + // turns 0xffffffffffffffff into 37373737`37373130 + // 37373737`37373737 00373737`37373737 + // TODO: verify endianness & correctness and re-assess implementation for perf + assert!(size == 23); + format!("{val:0023o}") + } + + _ => { + panic!("copyin: unknown binary format {}", bf); + } + }; + + assert!(tmp_str.len() == size as usize); + + mem.write_mem(addr as usize, tmp_str.as_bytes()) +} + +fn copyout(mem: &mut M, addr: u64, size: u64, res: &mut u64) { + // NB: this code makes an assumption that we are working with LSB only architectures + const _STATIC_ASSERT_IS_LSB: [u8; (1 - u16::from_le_bytes([1, 0])) as usize] = []; // if this errors we are compiling to an MSB arch + + let mut buf = [0; 8]; + let readlen = size.min(8) as usize; + mem.read_mem(addr as usize, &mut buf[0..readlen]); + match size { + 1 => (), + 2 => (), + 4 => (), + 8 => (), + _ => panic!("copyout: bad argument size {:#x}", size), + } + + *res = u64::from_le_bytes(buf); +} + +// Takes a syz_in buffer containing raw syzkaller data from TKO, parses +// individual test cases from it into the provided addr buffer, and calls the +// provided exec function with a single test case; then continues from the +// beginning until all provided testcases have completed. +// +// It is expected that addr_size is minimum 0x1000000 bytes (or 4mb). +// syz_exec_mem must be at least EXEC_INPUT_REQ_SIZE in size. +// syz_input_buffer must be at least SUPPORTED_INPUT_SIZE in size. +pub fn exec_testcases_safe( + syz_exec_mem: M, + syz_input_buffer: &mut [u8], + exec: F, +) -> Result +where + F: Fn(&DecodedProgram, InputCase) -> InputResult + Send + Sync, +{ + let decoded = DecodedProgram::new(syz_exec_mem, syz_input_buffer, exec)?; + decoded.exec_instrs()?; + Ok(decoded.results.as_ref().clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::boxed::Box; + + #[test] + fn test_decode() { + const BUF: &[u8] = &[ + 0xce, 0xfa, 0xad, 0xeb, 0xfe, 0x0f, 0xdc, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]; + + let mut decoder = Decoder::new(BUF).unwrap(); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(0), + args: vec![], + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(1), + args: vec![] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 0, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 2 + }, + custom_copyout_index: Some(2), + args: vec![ + Arg::Const(wire::ArgConst { meta: 4, val: 2047 }), + Arg::Const(wire::ArgConst { + meta: 4, + val: 0x1_00000001 + }) + ] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(3), + args: vec![] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(4), + args: vec![] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 0, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 2 + }, + custom_copyout_index: Some(5), + args: vec![ + Arg::Const(wire::ArgConst { meta: 4, val: 5 }), + Arg::Const(wire::ArgConst { meta: 4, val: 4 }) + ] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(6), + args: vec![] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(7), + args: vec![] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 0, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 2 + }, + custom_copyout_index: Some(8), + args: vec![ + Arg::Const(wire::ArgConst { meta: 4, val: 8 }), + Arg::Const(wire::ArgConst { + meta: 4, + val: 0xFFFFFFFFFFFFFFFB + }) + ] + })) + ); + assert_eq!( + decoder.try_next().unwrap(), + Some(Instr::Call(InstrCall { + idx: 1, + rpid: 0, + wire: wire::InstrCall { + copyout_index: !0, + num_args: 0 + }, + custom_copyout_index: Some(9), + args: vec![] + })) + ); + assert_eq!(decoder.try_next().unwrap(), None); + } + + #[test] + fn test_copyin() { + let mut mem: [u8; 8] = [0; 8]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0x1234567890abcdef; + let size: u64 = 8; + let bf: u64 = 0; // binary_format_native + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + + let expected: [u8; 8] = [0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12]; + assert_eq!(mem, expected); + } + + #[test] + fn test_copyin_with_bitmask() { + let mut mem: [u8; 4] = [0; 4]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0x5; + let size: u64 = 1; + let bf: u64 = 0; // binary_format_native + let bf_off: u64 = 2; + let bf_len: u64 = 2; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + + let expected: [u8; 4] = [0x4, 0x0, 0x0, 0x0]; + assert_eq!(mem, expected); + } + + #[test] + #[should_panic(expected = "copyin: bad argument size 3")] + fn test_copyin_with_invalid_size() { + let mut mem: [u8; 8] = [0; 8]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0x1234567890abcdef; + let size: u64 = 3; + let bf: u64 = 0; // binary_format_native + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + } + + #[test] + #[should_panic(expected = "copyin: unknown binary format 5")] + fn test_copyin_with_unknown_binary_format() { + let mut mem: [u8; 8] = [0; 8]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0x1234567890abcdef; + let size: u64 = 8; + let bf: u64 = 5; + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + } + + #[test] + fn test_copyin_format_strdec() { + let mut mem: [u8; 20] = [0; 20]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0xffffffffffffffff; + let size: u64 = 20; + let bf: u64 = 2; // binary_format_strdec + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + + // 0xffffffffffffffff -> 34343736`34343831 35353930`37333730 00000000`35313631 (ascii dec) + let expected: [u8; 20] = [ + 0x31, 0x38, 0x34, 0x34, 0x36, 0x37, 0x34, 0x34, 0x30, 0x37, 0x33, 0x37, 0x30, 0x39, + 0x35, 0x35, 0x31, 0x36, 0x31, 0x35, + ]; + assert_eq!(mem, expected); + } + + #[test] + fn test_copyin_format_strhex() { + let mut mem: [u8; 18] = [0; 18]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0xffffffffffffffff; + let size: u64 = 18; + let bf: u64 = 3; // binary_format_strhex + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + + // 0xffffffffffffffff -> 0x66666666`66667830 66666666`66666666 (ascii hex) + let expected: [u8; 18] = [ + 0x30, 0x78, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, + ]; + assert_eq!(mem, expected); + } + + #[test] + fn test_copyin_format_stroct() { + let mut mem: [u8; 23] = [0; 23]; + let addr = mem.as_ptr() as u64; + let val: u64 = 0xffffffffffffffff; + let size: u64 = 23; + let bf: u64 = 4; // binary_format_stroct + let bf_off: u64 = 0; + let bf_len: u64 = 0; + + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + + // 0xffffffffffffffff -> 37373737`37373130 37373737`37373737 00373737`37373737 (ascii oct) + let expected: [u8; 23] = [ + 0x30, 0x31, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + ]; + assert_eq!(mem, expected); + } + + fn make_noop_prog_with_results( + mem: Box, + results: Arc, + ) -> DecodedProgram<'static, 'static> { + let noop_exec: Arc> = + Arc::new(|_: &DecodedProgram, _: InputCase| InputResult::default()); + DecodedProgram { + instr_vec: Arc::new(AtomicRefQueue::new(vec![])), + mem: Arc::new(Mutex::new(mem)), + exec: noop_exec, + results, + custom_results: Arc::new([const { ResT::new() }; K_MAX_COMMANDS]), + } + } + + // exec_single CopyIn Arg::Result path: result was successful, op_div and op_add are applied. + // results[0] = 200, op_div = 4, op_add = 10 → (200 / 4) + 10 = 60 + #[test] + fn test_exec_single_copyin_result_applies_op_div_and_op_add() { + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + results[0].mark_executed(true, 200); + + let mem = Box::new([0u8; 8]); + let addr = mem.as_ptr() as u64; + let prog = make_noop_prog_with_results(mem, results); + + let instr = Instr::CopyIn(InstrCopyIn { + wire: wire::InstrCopyIn { addr }, + rpid: 0, + arg: Arg::Result(wire::ArgResult { + meta: 4, // size=4, binary_format_native + idx: 0, + op_div: 4, + op_add: 10, + arg: 0xFFFFFFFF, + }), + }); + + prog.exec_single(instr).unwrap(); + + let mut buf = [0u8; 4]; + prog.mem.lock().read_mem(addr as usize, &mut buf); + let written = u32::from_le_bytes(buf); + assert_eq!(written, 60); // (200 / 4) + 10 + } + + // exec_single CopyIn Arg::Result path: op_div is 0 so division is skipped. + // results[0] = 100, op_div = 0, op_add = 7 → 100 + 7 = 107 + #[test] + fn test_exec_single_copyin_result_op_div_zero_skips_division() { + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + results[0].mark_executed(true, 100); + + let mem = Box::new([0u8; 8]); + let addr = mem.as_ptr() as u64; + let prog = make_noop_prog_with_results(mem, results); + + let instr = Instr::CopyIn(InstrCopyIn { + wire: wire::InstrCopyIn { addr }, + rpid: 0, + arg: Arg::Result(wire::ArgResult { + meta: 4, + idx: 0, + op_div: 0, + op_add: 7, + arg: 0xFFFFFFFF, + }), + }); + + prog.exec_single(instr).unwrap(); + + let mut buf = [0u8; 4]; + prog.mem.lock().read_mem(addr as usize, &mut buf); + let written = u32::from_le_bytes(buf); + assert_eq!(written, 107); // 100 + 7 + } + + // exec_single CopyIn Arg::Result path: result was NOT successful, falls back to ArgResult.arg default. + // results[0] not executed, arg (default) = 42 → 42 + #[test] + fn test_exec_single_copyin_result_uses_default_when_not_successful() { + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + // results[0] left unexecuted + + let mem = Box::new([0u8; 8]); + let addr = mem.as_ptr() as u64; + let prog = make_noop_prog_with_results(mem, results); + + let instr = Instr::CopyIn(InstrCopyIn { + wire: wire::InstrCopyIn { addr }, + rpid: 0, + arg: Arg::Result(wire::ArgResult { + meta: 4, + idx: 0, + op_div: 2, + op_add: 5, + arg: 42, + }), + }); + + prog.exec_single(instr).unwrap(); + + let mut buf = [0u8; 4]; + prog.mem.lock().read_mem(addr as usize, &mut buf); + let written = u32::from_le_bytes(buf); + assert_eq!(written, 42); // default, op_div/op_add not applied + } + + // exec_single Call Arg::Result path: result was successful, op_div and op_add are applied to the call arg. + // results[0] = 200, op_div = 4, op_add = 10 → exec receives (200 / 4) + 10 = 60 + #[test] + fn test_exec_single_call_result_applies_op_div_and_op_add() { + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + results[0].mark_executed(true, 200); + + let captured_arg = Arc::new(AtomicU64::new(0)); + let captured_clone = captured_arg.clone(); + let exec_fn: Arc> = + Arc::new(move |_: &DecodedProgram, input: InputCase| -> InputResult { + captured_clone.store(input.args[0], Ordering::SeqCst); + InputResult { + code: 0, + name: String::new(), + is_success: true, + } + }); + + let mem: Box = Box::new([0u8; 8]); + let prog = DecodedProgram { + instr_vec: Arc::new(AtomicRefQueue::new(vec![])), + mem: Arc::new(Mutex::new(mem)), + exec: exec_fn, + results, + custom_results: Arc::new([const { ResT::new() }; K_MAX_COMMANDS]), + }; + + let instr = Instr::Call(InstrCall { + wire: wire::InstrCall { + copyout_index: wire::COPYOUT_INDEX_INVALID as u64, + num_args: 1, + }, + rpid: 0, + idx: 0, + custom_copyout_index: Some(0), + args: vec![Arg::Result(wire::ArgResult { + meta: 4, + idx: 0, + op_div: 4, + op_add: 10, + arg: 0xFFFFFFFF, + })], + }); + + prog.exec_single(instr).unwrap(); + + let val = captured_arg.load(Ordering::SeqCst); + assert_eq!(val, 60); // (200 / 4) + 10 + } + + // exec_single Call Arg::Result path: result was NOT successful, the call is skipped entirely. + // results[0] not executed → exec function should never be invoked. + #[test] + fn test_exec_single_call_result_skips_when_not_successful() { + let results = Arc::new([const { ResT::new() }; K_MAX_COMMANDS]); + // results[0] left unexecuted + + let was_called = Arc::new(AtomicBool::new(false)); + let was_called_clone = was_called.clone(); + let exec_fn: Arc> = + Arc::new(move |_: &DecodedProgram, _: InputCase| -> InputResult { + was_called_clone.store(true, Ordering::SeqCst); + InputResult::default() + }); + + let mem: Box = Box::new([0u8; 8]); + let prog = DecodedProgram { + instr_vec: Arc::new(AtomicRefQueue::new(vec![])), + mem: Arc::new(Mutex::new(mem)), + exec: exec_fn, + results, + custom_results: Arc::new([const { ResT::new() }; K_MAX_COMMANDS]), + }; + + let instr = Instr::Call(InstrCall { + wire: wire::InstrCall { + copyout_index: wire::COPYOUT_INDEX_INVALID as u64, + num_args: 1, + }, + rpid: 0, + idx: 0, + custom_copyout_index: Some(0), + args: vec![Arg::Result(wire::ArgResult { + meta: 4, + idx: 0, + op_div: 2, + op_add: 5, + arg: 99, + })], + }); + + prog.exec_single(instr).unwrap(); + + assert!(!was_called.load(Ordering::SeqCst)); + } +} diff --git a/opentmk/inv_decoder/src/safememory.rs b/opentmk/inv_decoder/src/safememory.rs new file mode 100644 index 00000000000..4001b483239 --- /dev/null +++ b/opentmk/inv_decoder/src/safememory.rs @@ -0,0 +1,300 @@ +use alloc::{boxed::Box, format, string::String, vec::Vec}; + +/// This represents a virtual memory map that can be used to safely write/read from +/// memory for syzkaller calls that use executor memory +pub trait SafeMemoryMap: Send + Sync { + /// Writes some bytes to the memory map + /// + /// # Panics + /// If we do not do a full write to memory + #[inline] + fn write_mem(&mut self, base: usize, val: &[u8]) { + self.try_write_mem(base, val) + .unwrap_or_else(|e| panic!("{e}")); + } + + /// Reads some bytes from the memory map + /// + /// # Panics + /// If we do not do a full read from memory + #[inline] + fn read_mem(&mut self, base: usize, val: &mut [u8]) { + self.try_read_mem(base, val) + .unwrap_or_else(|e| panic!("{e}")); + } + + /// Attempts to write some bytes to the memory map, returning an error message + /// if it fails + #[inline] + fn try_write_mem(&mut self, base: usize, val: &[u8]) -> Result<(), String> { + let written = self.partial_write_mem(base, val); + if written == val.len() { + Ok(()) + } else { + Err(format!( + "SafeMemoryMap: did not do full write at 0x{base:016x}, written: {written} / {} bytes", + val.len() + )) + } + } + + /// Attempts to read some bytes from the memory map, returning an error message + /// if it fails + #[inline] + fn try_read_mem(&mut self, base: usize, val: &mut [u8]) -> Result<(), String> { + let read = self.partial_read_mem(base, val); + if read == val.len() { + Ok(()) + } else { + Err(format!( + "SafeMemoryMap: did not do full read at 0x{base:016x}, read: {read} / {} bytes", + val.len() + )) + } + } + + /// Writes some bytes to the memory map, returning the number of bytes actually + /// written + #[must_use] + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize; + + /// Reads some bytes from the memory map, returning the number of bytes + /// actually read + #[must_use] + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize; +} + +impl SafeMemoryMap for Box { + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + (**self).partial_write_mem(base, val) + } + + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + (**self).partial_read_mem(base, val) + } +} + +impl SafeMemoryMap for &mut T { + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + (**self).partial_write_mem(base, val) + } + + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + (**self).partial_read_mem(base, val) + } +} + +impl SafeMemoryMap for (&mut [u8], usize) { + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + let Some(off) = base.checked_sub(self.1) else { + return 0; + }; + let Some(arr_len) = self.0.len().checked_sub(off) else { + return 0; + }; + let written = val.len().min(arr_len); + self.0[off..off + written].copy_from_slice(&val[..written]); + written + } + + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + let Some(off) = base.checked_sub(self.1) else { + return 0; + }; + let Some(arr_len) = self.0.len().checked_sub(off) else { + return 0; + }; + let written = val.len().min(arr_len); + val[..written].copy_from_slice(&self.0[off..off + written]); + written + } +} + +impl SafeMemoryMap for (&mut [u8; SIZE], usize) { + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + (self.0 as &mut [u8], self.1).partial_write_mem(base, val) + } + + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + (self.0 as &mut [u8], self.1).partial_read_mem(base, val) + } +} + +impl SafeMemoryMap for (Vec, usize) { + #[inline] + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + (self.0.as_mut_slice(), self.1).partial_write_mem(base, val) + } + + #[inline] + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + (self.0.as_mut_slice(), self.1).partial_read_mem(base, val) + } +} + +/// Without an explicit address key value, we implicitly use the real address +/// base of the u8 slice to compute from. +impl SafeMemoryMap for [u8] { + #[inline] + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + let addr = self.as_ptr() as usize; + (self, addr).partial_write_mem(base, val) + } + + #[inline] + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + let addr = self.as_ptr() as usize; + (self, addr).partial_read_mem(base, val) + } +} + +/// Without an explicit address key value, we implicitly use the real address +/// base of the u8 slice to compute from. +impl SafeMemoryMap for [u8; LEN] { + #[inline] + fn partial_write_mem(&mut self, base: usize, val: &[u8]) -> usize { + (self as &mut [u8]).partial_write_mem(base, val) + } + + #[inline] + fn partial_read_mem(&mut self, base: usize, val: &mut [u8]) -> usize { + (self as &mut [u8]).partial_read_mem(base, val) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_round_trip_box() { + let mut buf = [0u8, 1, 2, 3]; + let mut buf = Box::new((&mut buf, 0xdead0000usize)); + buf.write_mem(0xdead0001, &[2]); + + let mut res = [0; 4]; + buf.read_mem(0xdead0000, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_round_trip_mut_box_box() { + let mut buf = [0u8, 1, 2, 3]; + let mut buf = Box::new((&mut buf, 0xdead0000usize)); + let mut buf = Box::new(&mut buf); + buf.write_mem(0xdead0001, &[2]); + + let mut res = [0; 4]; + buf.read_mem(0xdead0000, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_round_trip_u8_slice() { + let buf = &mut [0u8, 1, 2, 3]; + let base = buf.as_ptr() as usize; + buf.write_mem(base + 1, &[2]); + + let mut res = [0; 4]; + buf.read_mem(base, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_round_trip_u8_slice2() { + let buf = &mut [0u8, 1, 2, 3] as &mut [u8]; + let base = buf.as_ptr() as usize; + buf.write_mem(base + 1, &[2]); + + let mut res = [0; 4]; + buf.read_mem(base, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_round_trip_u8_slice_addr() { + let buf = &mut [0u8, 1, 2, 3] as &mut [u8]; + let mut buf = Box::new((buf, 0xdead0000usize)); + buf.write_mem(0xdead0001, &[2]); + + let mut res = [0; 4]; + buf.read_mem(0xdead0000, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_round_trip_u8_vec_addr() { + let mut buf = (vec![0u8, 1, 2, 3], 0xdead0000usize); + buf.write_mem(0xdead0001, &[2]); + + let mut res = [0; 4]; + buf.read_mem(0xdead0000, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_write() { + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + buf.write_mem(0xdead0001, &[2]); + + let mut res = [0; 4]; + buf.read_mem(0xdead0000, &mut res); + assert_eq!([0, 2, 2, 3], res); + } + + #[test] + fn test_write_partial() { + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert_eq!(3, buf.partial_write_mem(0xdead0001, &[2, 3, 4, 5])); + assert_eq!(&[0, 2, 3, 4], buf.0); + } + + #[test] + fn test_read_partial() { + let mut res = [0xff; 4]; + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert_eq!(3, buf.partial_read_mem(0xdead0001, &mut res)); + assert_eq!(res, [1, 2, 3, 0xff]); + } + + #[test] + #[should_panic] + fn test_write_panic() { + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + buf.write_mem(0xdeadbeef, &[2]); + } + + #[test] + #[should_panic] + fn test_read_panic() { + let mut res = [0xff]; + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + buf.read_mem(0xdeadbeef, &mut res); + } + + #[test] + fn test_write_fail() { + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert!(buf.try_write_mem(0xdeadbeef, &[2]).is_err()); + } + + #[test] + fn test_read_fail() { + let mut res = [0xff]; + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert!(buf.try_read_mem(0xdeadbeef, &mut res).is_err()); + } + + #[test] + fn test_try_write_success() { + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert!(buf.try_write_mem(0xdead0001, &[2]).is_ok()); + } + + #[test] + fn test_try_read_success() { + let mut res = [0xff]; + let mut buf = (&mut [0u8, 1, 2, 3], 0xdead0000usize); + assert!(buf.try_read_mem(0xdead0001, &mut res).is_ok()); + } +} diff --git a/opentmk/inv_decoder/src/wire.rs b/opentmk/inv_decoder/src/wire.rs new file mode 100644 index 00000000000..c66b321cc6e --- /dev/null +++ b/opentmk/inv_decoder/src/wire.rs @@ -0,0 +1,110 @@ +//! Structures representing on-the-wire data formats. +use super::*; + +pub const INSTR_EOF: i64 = -1; +pub const INSTR_COPYIN: i64 = -2; +pub const INSTR_COPYOUT: i64 = -3; + +pub const ARG_CONST: u64 = 0; +pub const ARG_RESULT: u64 = 1; +pub const ARG_DATA: u64 = 2; + +/// usize::MAX (aka -1) as a copyout index value +/// indicates we should skip any copying out of the call's result +pub const COPYOUT_INDEX_INVALID: usize = usize::MAX; + +/// The header of a syzkaller program. +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct ProgramHeader { + pub magic: u64, + /// Environment flags bitfield: + /// 0: debug + /// 1: coverage + /// 2: sandbox_setuid + /// 3: sandbox_namespace + /// 4: sandbox_android_untrusted_app + /// 5: enable_tun + /// 6: enable_net_dev + /// 7: enable_fault_injection + pub env_flags: u64, + /// Executor flags bitfield: + /// 0: collect_cover + /// 1: dedup_cover + /// 2: inject_fault + /// 3: collect_comps + /// 4: threaded + /// 5: collide + pub exec_flags: u64, + /// Process ID + pub pid: u64, + /// Inject a fault in the corresponding index in the call table + pub fault_call: u64, + /// Inject a fault in the nth call in the program + pub fault_nth: u64, + pub prog_size: u64, +} + +/// Copyin instruction +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct InstrCopyIn { + pub addr: u64, + // Followed by an argument (Arg*). +} + +/// Copyout instruction +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct InstrCopyOut { + /// The index into the results array. + pub index: u64, + /// The address of the memory to copy from. + pub addr: u64, + /// The size to copy, in bytes. + pub size: u64, +} + +/// Call instruction +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct InstrCall { + pub copyout_index: u64, + pub num_args: u64, + // Arguments array follows... +} + +/// Constant argument (ty = 0) +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct ArgConst { + /// Packed bitfield describing the constant argument. + pub meta: u64, + /// The value of the argument. + pub val: u64, +} + +/// Result argument (ty = 1) +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct ArgResult { + /// Packed bitfield describing the result argument. + pub meta: u64, + /// The index of the result argument. + pub idx: u64, + /// A value to divide the result by. + pub op_div: u64, + /// A value to add to the result. + pub op_add: u64, + /// The value to use if the result at `idx` was not initialized. + pub arg: u64, +} + +/// Data argument (ty = 2) +#[derive(IntoBytes, FromBytes, Immutable, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(C)] +pub struct ArgData { + /// Data size, in bytes. + pub size: u64, + // Data words follow... +} diff --git a/opentmk/inv_packet/Cargo.toml b/opentmk/inv_packet/Cargo.toml new file mode 100644 index 00000000000..5c039a9a253 --- /dev/null +++ b/opentmk/inv_packet/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "inv_packet" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { workspace = true, features = ["derive"]} diff --git a/opentmk/inv_packet/src/lib.rs b/opentmk/inv_packet/src/lib.rs new file mode 100644 index 00000000000..d011bd382d0 --- /dev/null +++ b/opentmk/inv_packet/src/lib.rs @@ -0,0 +1,86 @@ +//! This contains the packet API library used for communication between the inv-agent and opentmk +//! components. +//! +//! In general, a communication channel between the inv-agent and opentmk works first by +//! establishing a three-way-handshake (SYN, SYN-ACK, ACK) which is entirely just a magic number +//! being exchanged between the two parties. Currently we assume that the inv-agent would start the +//! communication. +//! +//! Then after that, packets of type [`OpenTMKPacket`], with corresponding magic header and footer +//! value (defined in [`COMMS_PACKET_HEADER_MAGIC`] and [`COMMS_PACKET_FOOTER_MAGIC`]) will be +//! exchanged as the inv-agent executes test cases. Usually this is done with sending a single +//! configuration packet followed by a back and forth exchange of test case packets with the +//! opentmk ack'ing each packet. +extern crate alloc; + +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; + +/// Magic value used in the SYN packet +pub const COMMS_SYN_MAGIC: u64 = 0x4142434445464748; +/// Magic value used in the SYN-ACK packet (opentmk acknowledging SYN) +pub const COMMS_SYN_ACK_MAGIC: u64 = 0x5152535455565758; +/// Magic value used in the ACK packet (agent acknowledging SYN-ACK) +pub const COMMS_ACK_MAGIC: u64 = 0x6162636465666768; +/// Magic value used as the header of a regular packet +pub const COMMS_PACKET_HEADER_MAGIC: u64 = 0xf0f1f2f3f4f5f6f7; +/// Magic value used as the footer of a regular packet +pub const COMMS_PACKET_FOOTER_MAGIC: u64 = 0xf0f1f2f3f4f5f6f7; + +/// Describes the specific serialization format used for encoding test cases to the opentmk +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum OpenTMKGrammarDeserializer { + /// No encoding -- just use raw bytes + None, + /// syz-decoder. Used in conjunction of syzkaller fuzzers + SyzDecoder, +} + +/// Packet used to configure the opentmk instance with +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct OpenTMKConfigurationPacket { + /// Specify the type of serialization format for future test cases + pub deserializer: OpenTMKGrammarDeserializer, + /// A serialization-specific mapping that describes how (if any) call codes + /// will correspond to specific functions + pub mapping: Vec, +} + +/// Packet used to run a test case +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct OpenTMKFuzzTest { + /// Specify a timeout (in microseconds) of the entire test case + pub timeout_usec: u64, + /// The encoded test case to run on the main thread + pub testcase_vcpu0: Vec, +} + +/// Packet used to relay error messages out +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct OpenTMKErrorPacket { + /// The message string + pub message: String, + /// Whether if the error is fatal and requires rebooting + pub fatal: bool, +} + +/// Packet used to acknowledge the completion of a single fuzz test case +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct OpenTMKAckPacket { + /// The error code (or zero if success) + pub code: u64, +} + +/// Represents any packets that may be transmitted to and from the TMK +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum OpenTMKPacket { + /// An [`OpenTMKConfigurationPacket`] configuration packet + Configuration(OpenTMKConfigurationPacket), + /// An [`OpenTMKFuzzTest`] test case + FuzzTest(OpenTMKFuzzTest), + /// An [`OpenTMKAckPacket`] acknowledgement to a test case + Ack(OpenTMKAckPacket), + /// An [`OpenTMKErrorPacket`] error message for any other catch-all errors. + Error(OpenTMKErrorPacket), +} diff --git a/opentmk/opentmk_invariant/Cargo.toml b/opentmk/opentmk_invariant/Cargo.toml new file mode 100644 index 00000000000..effacc3ec4d --- /dev/null +++ b/opentmk/opentmk_invariant/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tmk_invariant" +version = "0.1.0" +edition = "2024" + +[dependencies] +hvdef.workspace = true +log.workspace = true +opentmk_core.workspace = true +spin.workspace = true +zerocopy.workspace = true +serde = { workspace = true, features = ["derive"]} +postcard = { workspace = true, features = ["alloc"] } +inv_packet.workspace = true +inv_decoder.workspace = true + +[target.'cfg(target_os = "uefi")'.dependencies] +uefi = { workspace = true, features = ["alloc"] } + +[lints] +workspace = true diff --git a/opentmk/opentmk_invariant/src/comms/mod.rs b/opentmk/opentmk_invariant/src/comms/mod.rs new file mode 100644 index 00000000000..5943c522b16 --- /dev/null +++ b/opentmk/opentmk_invariant/src/comms/mod.rs @@ -0,0 +1,185 @@ +#[cfg(test)] +pub(crate) mod test; + +use crate::executor::ExecutorError; +use inv_packet::{ + COMMS_ACK_MAGIC, COMMS_PACKET_FOOTER_MAGIC, COMMS_PACKET_HEADER_MAGIC, COMMS_SYN_ACK_MAGIC, + COMMS_SYN_MAGIC, OpenTMKPacket, +}; + +#[allow(unused_imports)] +use crate::prelude::*; + +use opentmk_core::arch::serial::{InstrIoAccess, Serial, SerialPort}; + +pub(crate) trait SerialIo { + fn init(&mut self); + fn drain(&mut self); + fn write_byte(&mut self, byte: u8); + fn read_byte(&mut self) -> u8; +} + +pub(crate) struct OpenTmkSerialIo { + handle: Serial, +} + +impl OpenTmkSerialIo { + fn new(port: SerialPort) -> Self { + log::info!("creating serial port"); + Self { + handle: Serial::new(port, InstrIoAccess), + } + } +} + +impl SerialIo for OpenTmkSerialIo { + fn init(&mut self) { + self.handle.init(); + } + + fn drain(&mut self) { + self.handle.drain(); + } + + fn write_byte(&mut self, byte: u8) { + self.handle.write_byte(byte); + } + + fn read_byte(&mut self) -> u8 { + self.handle.read_byte() + } +} + +pub(crate) struct SerialCommsServer { + pub(crate) handle: T, + connected: bool, +} + +impl SerialCommsServer { + pub fn new(port: SerialPort) -> Self { + Self::new_with_transport(OpenTmkSerialIo::new(port)) + } +} + +impl SerialCommsServer { + pub(crate) fn new_with_transport(handle: T) -> Self { + Self { + handle, + connected: false, + } + } +} + +impl SerialCommsServer { + pub fn handshake(&mut self) -> Result<(), ExecutorError> { + if self.connected { + return Ok(()); + } + + self.handle.init(); + + // drain any data in the serial FIFO + self.handle.drain(); + + //1. Read a QWord from the host, it should be SerialCommsSynMagic + let syn = self.read_u64_blocking(); + if syn != COMMS_SYN_MAGIC { + log::error!("0x{:016x}", syn); + return Err(ExecutorError::HandshakeInvalidSynMagic); + } + + //2. Send SerialCommsSynAckMagic to host + self.send_u64_blocking(COMMS_SYN_ACK_MAGIC); + + //3. Read a QWORD from the host, it should be SerialCommsAckMagic + let ack = self.read_u64_blocking(); + if ack != COMMS_ACK_MAGIC { + log::error!("0x{:016x}", ack); + return Err(ExecutorError::HandshakeInvalidSynAckMagic); + } + + self.connected = true; + log::info!("Serial Comms sucessfully connected"); + + Ok(()) + } + + pub fn read_packet_blocking(&mut self) -> Result { + //1. Read packet header magic + let magic = self.read_u64_blocking(); + if magic != COMMS_PACKET_HEADER_MAGIC { + log::error!("0x{:016x}", magic); + return Err(ExecutorError::PacketInvalidHeaderMagic); + } + + //2. Read packet size + let payload_sz = self.read_u64_blocking(); + + //3. Read payload + let mut buffer: Vec = Vec::with_capacity(payload_sz as usize); + + for _ in 0..payload_sz { + buffer.push(self.read_u8_blocking()); + } + + //4. Read packet footer magic + let footer = self.read_u64_blocking(); + if footer != COMMS_PACKET_FOOTER_MAGIC { + log::error!("0x{:016x}", footer); + return Err(ExecutorError::PacketInvalidFooterMagic); + } + + let packet: OpenTMKPacket = match postcard::from_bytes(&buffer) { + Ok(p) => p, + Err(_) => return Err(ExecutorError::PacketPayloadDeserializeFailed), + }; + + Ok(packet) + } + + pub fn write_packet_blocking(&mut self, pkt: &OpenTMKPacket) -> Result<(), ExecutorError> { + let data = match postcard::to_allocvec(&pkt) { + Ok(p) => p, + Err(_) => return Err(ExecutorError::PacketPayloadSerializeFailed), + }; + + //1. Write packet header magic + self.send_u64_blocking(COMMS_PACKET_HEADER_MAGIC); + + //2. Write packet size + self.send_u64_blocking(data.len() as u64); + + //3. Write payload + for byte in data { + self.send_u8_blocking(byte); + } + + //4. Write packet footer magic + self.send_u64_blocking(COMMS_PACKET_FOOTER_MAGIC); + + Ok(()) + } + + fn send_u8_blocking(&mut self, d: u8) { + self.handle.write_byte(d); + } + + fn send_u64_blocking(&mut self, d: u64) { + for i in 0..8 { + self.send_u8_blocking((d >> (i * 8)) as u8); + } + } + + fn read_u8_blocking(&mut self) -> u8 { + self.handle.read_byte() + } + + fn read_u64_blocking(&mut self) -> u64 { + let mut val: u64 = 0; + for i in 0..8 { + let byte = self.read_u8_blocking() as u64; + val |= byte << (i * 8); + } + val + } +} diff --git a/opentmk/opentmk_invariant/src/comms/test.rs b/opentmk/opentmk_invariant/src/comms/test.rs new file mode 100644 index 00000000000..02764b6400b --- /dev/null +++ b/opentmk/opentmk_invariant/src/comms/test.rs @@ -0,0 +1,154 @@ +extern crate alloc; + +use super::*; +use alloc::collections::VecDeque; +use inv_packet::{OpenTMKAckPacket, OpenTMKErrorPacket, OpenTMKPacket}; + +#[derive(Default)] +pub(crate) struct MockSerialIo { + reads: VecDeque, + writes: Vec, + init_calls: usize, + drain_calls: usize, +} + +impl MockSerialIo { + pub(crate) fn with_reads(reads: Vec) -> Self { + Self { + reads: reads.into(), + writes: Vec::new(), + init_calls: 0, + drain_calls: 0, + } + } + + pub(crate) fn written_bytes(&self) -> &[u8] { + &self.writes + } + + pub(crate) fn init_calls(&self) -> usize { + self.init_calls + } + + pub(crate) fn drain_calls(&self) -> usize { + self.drain_calls + } +} + +impl SerialIo for MockSerialIo { + fn init(&mut self) { + self.init_calls += 1; + } + + fn drain(&mut self) { + self.drain_calls += 1; + } + + fn write_byte(&mut self, byte: u8) { + self.writes.push(byte); + } + + fn read_byte(&mut self) -> u8 { + self.reads + .pop_front() + .expect("mock serial transport was read past the queued bytes") + } +} + +fn append_u64(bytes: &mut Vec, value: u64) { + bytes.extend_from_slice(&value.to_le_bytes()); +} + +fn frame_packet(packet: &OpenTMKPacket) -> Vec { + let payload = postcard::to_allocvec(packet).expect("packet serialization should succeed"); + let mut bytes = Vec::new(); + append_u64(&mut bytes, COMMS_PACKET_HEADER_MAGIC); + append_u64(&mut bytes, payload.len() as u64); + bytes.extend_from_slice(&payload); + append_u64(&mut bytes, COMMS_PACKET_FOOTER_MAGIC); + bytes +} + +#[test] +fn handshake_success() { + // Case: a valid SYN/SYN-ACK/ACK exchange marks the transport connected and emits only SYN-ACK. + let mut reads = Vec::new(); + append_u64(&mut reads, COMMS_SYN_MAGIC); + append_u64(&mut reads, COMMS_ACK_MAGIC); + + let mut comms = SerialCommsServer::new_with_transport(MockSerialIo::with_reads(reads)); + comms.handshake().expect("handshake should succeed"); + + assert!(comms.connected); + assert_eq!( + comms.handle.written_bytes(), + &COMMS_SYN_ACK_MAGIC.to_le_bytes() + ); + assert_eq!(comms.handle.init_calls(), 1); + assert_eq!(comms.handle.drain_calls(), 1); +} + +#[test] +fn handshake_bad_syn() { + // Case: an unexpected first qword is rejected before any SYN-ACK bytes are written. + let mut reads = Vec::new(); + append_u64(&mut reads, 0xdead_beef_dead_beef); + + let mut comms = SerialCommsServer::new_with_transport(MockSerialIo::with_reads(reads)); + let err = comms.handshake().expect_err("handshake should fail"); + + assert_eq!(err, ExecutorError::HandshakeInvalidSynMagic); + assert!(!comms.connected); + assert!(comms.handle.written_bytes().is_empty()); + assert_eq!(comms.handle.init_calls(), 1); + assert_eq!(comms.handle.drain_calls(), 1); +} + +#[test] +fn valid_packet_deserialize() { + // Case: a correctly framed packet is deserialized into the original packet payload. + let expected = OpenTMKPacket::Error(OpenTMKErrorPacket { + message: String::from("boom"), + }); + + let mut comms = + SerialCommsServer::new_with_transport(MockSerialIo::with_reads(frame_packet(&expected))); + + let packet = comms + .read_packet_blocking() + .expect("packet read should succeed"); + assert_eq!(packet, expected); +} + +#[test] +fn invalid_footer() { + // Case: a packet with a corrupted footer is rejected after the payload bytes are read. + let packet = OpenTMKPacket::Ack(OpenTMKAckPacket { code: 7 }); + let payload = postcard::to_allocvec(&packet).expect("packet serialization should succeed"); + + let mut bytes = Vec::new(); + append_u64(&mut bytes, COMMS_PACKET_HEADER_MAGIC); + append_u64(&mut bytes, payload.len() as u64); + bytes.extend_from_slice(&payload); + append_u64(&mut bytes, 0x0102_0304_0506_0708); + + let mut comms = SerialCommsServer::new_with_transport(MockSerialIo::with_reads(bytes)); + let err = comms + .read_packet_blocking() + .expect_err("packet read should fail"); + + assert_eq!(err, ExecutorError::PacketInvalidFooterMagic); +} + +#[test] +fn packet_write_framing() { + // Case: writing a packet emits header, length, serialized payload, and footer in order. + let packet = OpenTMKPacket::Ack(OpenTMKAckPacket { code: 99 }); + let mut comms = SerialCommsServer::new_with_transport(MockSerialIo::default()); + + comms + .write_packet_blocking(&packet) + .expect("packet write should succeed"); + + assert_eq!(comms.handle.written_bytes(), frame_packet(&packet)); +} diff --git a/opentmk/opentmk_invariant/src/deserializer/mod.rs b/opentmk/opentmk_invariant/src/deserializer/mod.rs new file mode 100644 index 00000000000..3778bf2508f --- /dev/null +++ b/opentmk/opentmk_invariant/src/deserializer/mod.rs @@ -0,0 +1,16 @@ +pub mod syzlang; + +use spin::Mutex; + +use crate::{executor::ExecutorError, functions::FunctionRegistry, prelude::*}; + +use inv_packet::OpenTMKFuzzTest; + +pub trait Deserializer { + fn deserialize_and_execute( + &mut self, + testcase: &mut OpenTMKFuzzTest, + ) -> Result; + fn set_function_registry(&mut self, registry: Arc>); + fn set_mappings(&mut self, mappings: Vec) -> Result<(), ExecutorError>; +} diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs new file mode 100644 index 00000000000..c6a94b5e505 --- /dev/null +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -0,0 +1,178 @@ +#[cfg(test)] +mod test; + +use crate::{ + deserializer::Deserializer, + executor::ExecutorError, + functions::{FunctionRegistry, FuzzFunctionVariable}, + prelude::*, +}; +use inv_decoder::{InputCase, InputResult, SafeMemoryMap, exec_testcases_safe}; +use inv_packet::OpenTMKFuzzTest; +use spin::Mutex; + +// For now we are using the syz-decoder library, and unfortunately to get it to work here, is not clean as it has a different design. +// TODO: Refactor syz-decoder in a way to make this interface much cleaner +pub const SYZLANG_DESERIALIZER_ERROR_CODE: u64 = 0x133713381339; + +#[derive(Default)] +struct SyzlangState { + error_list: Vec, + function_registry: Arc>, + glob_mapping: Vec, +} + +impl SyzlangState { + /// Executes a testcase with this state object + fn exec_syzlang_testcase_line( + &mut self, + mem: &mut dyn SafeMemoryMap, + input_struct: InputCase, + ) -> InputResult { + // resolve the call number to a pseudo syscall + if self.glob_mapping.len() as u64 <= input_struct.call_num { + log::error!( + "[fatal] invalid call number {} received from syzlang", + input_struct.call_num, + ); + return InputResult { + code: SYZLANG_DESERIALIZER_ERROR_CODE, + name: String::default(), //never used! + is_success: false, + }; + } + + let handler_name = &self.glob_mapping[input_struct.call_num as usize]; + let input = SyzlangDeserializer::to_function_variables(&input_struct); + + match self + .function_registry + .lock() + .exec(mem, handler_name.clone(), input) + { + FuzzFunctionVariable::Void => (), + FuzzFunctionVariable::Int(_) => (), // TODO + FuzzFunctionVariable::Error(e) => { + let error_str = format!("Error recorded in function: {handler_name} error: {e}"); + log::error!("{error_str:?}"); + self.error_list.push(error_str); + + return InputResult { + code: SYZLANG_DESERIALIZER_ERROR_CODE, + name: String::default(), + is_success: false, + }; + } + } + + InputResult { + code: 0, + name: String::default(), + is_success: true, + } + } + + fn dump_errors(&mut self) -> Result<(), ExecutorError> { + if !self.error_list.is_empty() { + let resp = Err(ExecutorError::SyzlangDeserializerFailed( + self.error_list.join(", "), + )); + self.error_list.clear(); + return resp; + } + + Ok(()) + } +} + +pub struct SyzlangDeserializer { + tc_slice: Vec, + st: Mutex, + mem: (Vec, usize), +} + +impl SyzlangDeserializer { + pub fn new() -> Self { + Self { + tc_slice: vec![0; inv_decoder::SUPPORTED_INPUT_SIZE], + st: Default::default(), + mem: ( + vec![0; inv_decoder::EXEC_INPUT_REQ_SIZE], + inv_decoder::ADDR_SYZ_BEGIN as usize, + ), + } + } + + fn to_function_variables(input: &InputCase) -> Vec { + let mut resp = Vec::new(); + for arg in 0..input.num_args { + // for now everything is a u64 + resp.push(FuzzFunctionVariable::Int(input.args[arg as usize])); + } + resp + } +} + +impl Deserializer for SyzlangDeserializer { + fn set_function_registry(&mut self, registry: Arc>) { + self.st.lock().function_registry = registry; + } + + fn set_mappings(&mut self, mappings: Vec) -> Result<(), ExecutorError> { + // for syzlang the mappings are to map the syscall number + // to the string name of the pseudo syscall + // e.g. + // [0] => mmio_read + // [1] => mmio_write + // etc. + self.st.lock().glob_mapping = match postcard::from_bytes(&mappings) { + Ok(m) => m, + Err(_) => { + return Err(ExecutorError::DecoderMappingsDeserializeFailed); + } + }; + + Ok(()) + } + + fn deserialize_and_execute( + &mut self, + testcase: &mut OpenTMKFuzzTest, + ) -> Result { + self.tc_slice.fill(0); + + let src = &testcase.testcase_vcpu0.as_slice(); + self.tc_slice[..src.len()].copy_from_slice(src); + self.mem.0.as_mut_slice().fill(0); + + // Borrow choreography: + // - `&mut self.mem` and `&mut self.tc_slice` are disjoint fields (split borrow). + // - The closure captures `&self.st` only (Rust 2021 disjoint capture), not + // `&self`, so it doesn't conflict with the &mut borrows above. + // - `decoder` is `&DecodedProgram` (shared); `&mut decoder` borrows the local + // binding, not the program. SafeMemoryMap is impl'd for `&DecodedProgram` + // with interior mutability via the inner Mutex. + // - `self.st` is locked per-call inside the closure and again at + // `dump_errors()` below; safe because exec_testcases_safe is synchronous + // and the closure is dropped before we return here. We also need to + // ensure that `self.st` is Sync + Send (which means the underlying + // state should be `Send`) + let results = exec_testcases_safe(&mut self.mem, &mut self.tc_slice, |mut decoder, inp| { + self.st.lock().exec_syzlang_testcase_line(&mut decoder, inp) + }); + + // See if we hit any errors. + self.st.lock().dump_errors()?; + match results { + Ok(_) => { + // For now lets ignore the response data + // + // TODO: we should recover this and use it for minimization steps etc when we start + // doing more complicated fuzzing. it also needs reactoring since the string is + // never used! + Ok(0) + } + Err(e) => Err(ExecutorError::SyzlangDeserializerFailed(format!("{e:?}"))), + } + } +} diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs new file mode 100644 index 00000000000..a50a07cd341 --- /dev/null +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs @@ -0,0 +1,188 @@ +use spin::mutex::Mutex; + +use super::*; +use crate::functions::{FunctionRegistry, FuzzFunctionVariable}; +use inv_packet::OpenTMKFuzzTest; + +static SYZLANG_EXEC_TEST_LOCK: Mutex<()> = Mutex::new(()); +static RECORDED_CALLS: Mutex)>> = Mutex::new(Vec::new()); + +const PROGRAM_MAGIC: u64 = 0xbadc0ffeebadface; +const ARG_CONST: u64 = 0; +const ARG_DATA: u64 = 2; +const INSTR_EOF: u64 = u64::MAX; +const INSTR_COPYIN: u64 = u64::MAX - 1; +const COPYOUT_INDEX_INVALID: u64 = usize::MAX as u64; + +const MAGIC: &[u8; 8] = b"Leetspek"; + +fn record_call( + name: &str, + vars: Vec, +) -> Result { + let args = vars + .iter() + .map(|var| var.expect_int(name)) + .collect::, _>>()?; + RECORDED_CALLS.lock().push((String::from(name), args)); + Ok(FuzzFunctionVariable::Void) +} + +fn chkmagic( + mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let ptr = vars[0].expect_int("Pointer")? as usize; + let mut buf = [0; MAGIC.len()]; + mem.read_mem(ptr, &mut buf); + assert_eq!(&buf, MAGIC); + record_call("chkmagic", vec![]) +} + +fn record_func0( + _mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + record_call("func0", vars) +} + +fn record_func1( + _mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + record_call("func1", vars) +} + +fn clear_recorded_calls() { + RECORDED_CALLS.lock().clear(); +} + +fn recorded_calls() -> Vec<(String, Vec)> { + RECORDED_CALLS.lock().clone() +} + +#[derive(Copy, Clone)] +enum Insn<'s> { + Call(u64, &'s [u64]), + WriteMem(u64, &'s [u64]), +} + +fn build_testcase(insns: &[Insn<'_>]) -> OpenTMKFuzzTest { + let mut instructions = Vec::new(); + for insn in insns { + match *insn { + Insn::Call(call_num, args) => { + instructions.push(call_num); + instructions.push(COPYOUT_INDEX_INVALID); + instructions.push(args.len() as u64); + + for &arg in args { + instructions.push(ARG_CONST); + instructions.push(0u64); + instructions.push(arg); + } + } + Insn::WriteMem(addr, buf) => { + instructions.push(INSTR_COPYIN); + instructions.push(addr); + instructions.push(ARG_DATA); + instructions.push(buf.len() as u64 * 8); + instructions.extend_from_slice(buf); + } + } + } + instructions.push(INSTR_EOF); + + let mut prog = vec![ + PROGRAM_MAGIC, + 0u64, + 0u64, + 0u64, + 0u64, + 0u64, + instructions.len() as u64 * 8, + ]; + prog.append(&mut instructions); + + let mut bytes = Vec::with_capacity(prog.len() * 8); + for num in prog { + bytes.extend_from_slice(&num.to_le_bytes()); + } + + OpenTMKFuzzTest { + timeout_usec: 0, + testcase_vcpu0: bytes, + } +} + +fn install_function_registry(deserializer: &mut SyzlangDeserializer) { + let registry = Arc::new(Mutex::new(FunctionRegistry::default())); + let mut registry_lock = registry.lock(); + registry_lock.register("func0", record_func0); + registry_lock.register("func1", record_func1); + registry_lock.register("chkmagic", chkmagic); + drop(registry_lock); + deserializer.set_function_registry(registry); +} + +#[test] +fn deserialize_executes_functions_selected_by_mapping_and_call_number() { + // Needed because of syzkaller actually writing to some value + let _guard = SYZLANG_EXEC_TEST_LOCK.lock(); + + // Case: a decoded syzlang program dispatches each call number to the mapped function with decoded args. + clear_recorded_calls(); + + let mut deserializer = SyzlangDeserializer::new(); + install_function_registry(&mut deserializer); + deserializer + .set_mappings( + postcard::to_allocvec(&vec![String::from("func0"), String::from("func1")]) + .expect("mapping serialization should succeed"), + ) + .expect("mapping should succeed"); + + let mut testcase = build_testcase(&[Insn::Call(1, &[11, 22]), Insn::Call(0, &[99])]); + let result = deserializer + .deserialize_and_execute(&mut testcase) + .expect("deserialization should succeed"); + + assert_eq!(result, 0); + assert_eq!( + recorded_calls(), + vec![ + (String::from("func1"), vec![11, 22]), + (String::from("func0"), vec![99]), + ] + ); +} + +#[test] +fn deserialize_syzkaller_pointers() { + // Needed because of syzkaller actually writing to some value + let _guard = SYZLANG_EXEC_TEST_LOCK.lock(); + + // Case: a decoded syzlang program with memory pointers + clear_recorded_calls(); + + let ptr = inv_decoder::ADDR_SYZ_BEGIN; + let mut deserializer = SyzlangDeserializer::new(); + install_function_registry(&mut deserializer); + deserializer + .set_mappings( + postcard::to_allocvec(&vec![String::from("chkmagic")]) + .expect("mapping serialization should succeed"), + ) + .expect("mapping should succeed"); + + let mut testcase = build_testcase(&[ + Insn::WriteMem(ptr + 0x3, &[u64::from_le_bytes(*MAGIC)]), + Insn::Call(0, &[ptr + 3]), + ]); + let result = deserializer + .deserialize_and_execute(&mut testcase) + .expect("deserialization should succeed"); + + assert_eq!(result, 0); + assert_eq!(recorded_calls(), vec![(String::from("chkmagic"), vec![]),]); +} diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs new file mode 100644 index 00000000000..d10d92dbb36 --- /dev/null +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -0,0 +1,166 @@ +#[allow(unused_imports)] +use crate::prelude::*; +use crate::{ + comms::{OpenTmkSerialIo, SerialCommsServer, SerialIo}, + deserializer::{Deserializer, syzlang::SyzlangDeserializer}, + functions::{FunctionRegistry, FuzzFunction, hyperv, io_port}, +}; + +use inv_packet::{ + OpenTMKAckPacket, OpenTMKConfigurationPacket, OpenTMKErrorPacket, OpenTMKFuzzTest, + OpenTMKGrammarDeserializer, OpenTMKPacket, +}; + +use opentmk_core::arch::serial::SerialPort; +use spin::Mutex; + +#[cfg(test)] +mod test; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ExecutorError { + HandshakeInvalidSynMagic, + HandshakeInvalidSynAckMagic, + PacketInvalidHeaderMagic, + PacketInvalidFooterMagic, + PacketPayloadDeserializeFailed, + PacketPayloadSerializeFailed, + UnexpectedPacketReceived, + NoDeserializerEnabled, + #[allow(dead_code)] + SyzlangDeserializerFailed(String), + DecoderMappingsDeserializeFailed, + DeserializerUnset, +} + +pub(crate) struct Executor { + comms: SerialCommsServer, + deserializer_type: OpenTMKGrammarDeserializer, + deserializer: Option>, + fn_registry: Arc>, +} + +impl Executor { + pub fn new(port: SerialPort) -> Self { + Self::new_with_comms(SerialCommsServer::new(port)) + } +} + +impl Executor { + pub(crate) fn new_with_comms(comms: SerialCommsServer) -> Self { + Self { + comms, + deserializer_type: OpenTMKGrammarDeserializer::None, + deserializer: None, + fn_registry: Default::default(), + } + } + + pub fn initialize(&mut self) -> Result<(), ExecutorError> { + self.comms.handshake()?; + Ok(()) + } + + pub fn register_fuzz_functions(&mut self) { + static REGISTRY: &[(&str, FuzzFunction)] = &[ + ("port_write8", io_port::write_ioport_u8), + ("port_write16", io_port::write_ioport_u16), + ("port_write32", io_port::write_ioport_u32), + ("port_read8", io_port::read_ioport_u8), + ("port_read16", io_port::read_ioport_u16), + ("port_read32", io_port::read_ioport_u32), + ("hvcall", hyperv::hvcall), + ]; + let mut fn_registry = self.fn_registry.lock(); + for (name, func) in REGISTRY { + fn_registry.register(name, *func); + } + } + + pub fn run(&mut self) -> Result<(), ExecutorError> { + // enter the main run loop + loop { + self.process_next_packet()?; + } + } + + pub(crate) fn process_next_packet(&mut self) -> Result<(), ExecutorError> { + let pkt = self.comms.read_packet_blocking()?; + + let response_pkt = match pkt { + OpenTMKPacket::Configuration(cfg) => self.on_receive_configuration_packet(&cfg)?, + OpenTMKPacket::FuzzTest(mut fuzz) => self.on_receive_fuzz_test_packet(&mut fuzz)?, + OpenTMKPacket::Ack(a) => self.on_recieve_ack_packet(&a)?, + OpenTMKPacket::Error(a) => self.on_recieve_error_packet(&a)?, + }; + + if let Some(resp) = response_pkt { + self.comms.write_packet_blocking(&resp)?; + } + + Ok(()) + } + + pub fn on_receive_configuration_packet( + &mut self, + pkt: &OpenTMKConfigurationPacket, + ) -> Result, ExecutorError> { + self.deserializer_type = pkt.deserializer; + + match self.deserializer_type { + OpenTMKGrammarDeserializer::None => Err(ExecutorError::NoDeserializerEnabled)?, + OpenTMKGrammarDeserializer::SyzDecoder => { + self.deserializer = Some(Box::new(SyzlangDeserializer::new())); + } + }; + + self.deserializer + .as_mut() + .ok_or(ExecutorError::DeserializerUnset)? + .set_function_registry(self.fn_registry.clone()); + + self.deserializer + .as_mut() + .ok_or(ExecutorError::DeserializerUnset)? + .set_mappings(pkt.mapping.clone()) + .map(|_| ())?; + + log::info!( + "Setting active deserializer to {:?}", + &self.deserializer_type + ); + Ok(Some(OpenTMKPacket::Ack(OpenTMKAckPacket { code: 0 }))) + } + + pub fn on_receive_fuzz_test_packet( + &mut self, + pkt: &mut OpenTMKFuzzTest, + ) -> Result, ExecutorError> { + match self.deserializer.as_mut() { + None => Err(ExecutorError::NoDeserializerEnabled), + Some(t) => Ok(Some(match t.as_mut().deserialize_and_execute(pkt) { + Ok(code) => OpenTMKPacket::Ack(OpenTMKAckPacket { code }), + Err(e) => OpenTMKPacket::Error(OpenTMKErrorPacket { + message: format!("{:?}", e), + fatal: false, + }), + })), + } + } + + pub fn on_recieve_ack_packet( + &mut self, + _: &OpenTMKAckPacket, + ) -> Result, ExecutorError> { + log::error!("Received ACK packet incorrectly"); + Err(ExecutorError::UnexpectedPacketReceived) + } + + pub fn on_recieve_error_packet( + &mut self, + _: &OpenTMKErrorPacket, + ) -> Result, ExecutorError> { + log::error!("Received error packet incorrectly"); + Err(ExecutorError::UnexpectedPacketReceived) + } +} diff --git a/opentmk/opentmk_invariant/src/executor/test.rs b/opentmk/opentmk_invariant/src/executor/test.rs new file mode 100644 index 00000000000..62a8f00b3ba --- /dev/null +++ b/opentmk/opentmk_invariant/src/executor/test.rs @@ -0,0 +1,222 @@ +extern crate alloc; +use alloc::rc::Rc; +use core::cell::RefCell; + +use super::*; +use crate::comms::{SerialCommsServer, test::MockSerialIo}; +use inv_packet::{COMMS_PACKET_FOOTER_MAGIC, COMMS_PACKET_HEADER_MAGIC}; + +#[derive(Clone)] +struct TestDeserializerState { + execute_result: Result, + mappings_result: Result<(), ExecutorError>, + received_mappings: Vec, + execute_calls: usize, +} + +struct TestDeserializer { + state: Rc>, +} + +impl TestDeserializer { + fn new(state: Rc>) -> Self { + Self { state } + } +} + +impl Deserializer for TestDeserializer { + fn deserialize_and_execute( + &mut self, + _testcase: &mut OpenTMKFuzzTest, + ) -> Result { + let mut state = self.state.borrow_mut(); + state.execute_calls += 1; + state.execute_result.clone() + } + + fn set_function_registry(&mut self, _registry: Arc>) {} + + fn set_mappings(&mut self, mappings: Vec) -> Result<(), ExecutorError> { + let mut state = self.state.borrow_mut(); + state.received_mappings = mappings; + state.mappings_result.clone() + } +} + +fn frame_packet(packet: &OpenTMKPacket) -> Vec { + let payload = postcard::to_allocvec(packet).expect("packet serialization should succeed"); + let mut bytes = Vec::new(); + bytes.extend_from_slice(&COMMS_PACKET_HEADER_MAGIC.to_le_bytes()); + bytes.extend_from_slice(&(payload.len() as u64).to_le_bytes()); + bytes.extend_from_slice(&payload); + bytes.extend_from_slice(&COMMS_PACKET_FOOTER_MAGIC.to_le_bytes()); + bytes +} + +fn fuzz_packet() -> OpenTMKFuzzTest { + OpenTMKFuzzTest { + timeout_usec: 123, + testcase_vcpu0: vec![1, 2, 3, 4], + } +} + +fn test_state( + execute_result: Result, + mappings_result: Result<(), ExecutorError>, +) -> Rc> { + Rc::new(RefCell::new(TestDeserializerState { + execute_result, + mappings_result, + received_mappings: Vec::new(), + execute_calls: 0, + })) +} + +#[test] +fn no_deserializer_returns_error() { + // Case: fuzz packets are rejected until a deserializer has been configured. + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::default(), + )); + let mut packet = fuzz_packet(); + + let err = executor + .on_receive_fuzz_test_packet(&mut packet) + .expect_err("missing deserializer should fail"); + + assert_eq!(err, ExecutorError::NoDeserializerEnabled); +} + +#[test] +fn successful_fuzz_packet_returns_ack() { + // Case: successful testcase execution returns an Ack packet carrying the deserializer code. + let state = test_state(Ok(77), Ok(())); + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::default(), + )); + executor.deserializer = Some(Box::new(TestDeserializer::new(state.clone()))); + + let response = executor + .on_receive_fuzz_test_packet(&mut fuzz_packet()) + .expect("fuzz packet should succeed"); + + assert_eq!( + response, + Some(OpenTMKPacket::Ack(OpenTMKAckPacket { code: 77 })) + ); + assert_eq!(state.borrow().execute_calls, 1); +} + +#[test] +fn deserializer_failure_returns_error_packet() { + // Case: deserializer execution failures are surfaced as Error response packets. + let state = test_state( + Err(ExecutorError::SyzlangDeserializerFailed(String::from( + "decode failed", + ))), + Ok(()), + ); + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::default(), + )); + executor.deserializer = Some(Box::new(TestDeserializer::new(state))); + + let response = executor + .on_receive_fuzz_test_packet(&mut fuzz_packet()) + .expect("fuzz packet should still produce a response"); + + assert_eq!( + response, + Some(OpenTMKPacket::Error(OpenTMKErrorPacket { + message: String::from("SyzlangDeserializerFailed(\"decode failed\")"), + })) + ); +} + +#[test] +fn invalid_config_deserializer_returns_error() { + // Case: configuration packets that select no deserializer fail immediately. + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::default(), + )); + let config = OpenTMKConfigurationPacket { + deserializer: OpenTMKGrammarDeserializer::None, + mapping: Vec::new(), + }; + + let err = executor + .on_receive_configuration_packet(&config) + .expect_err("invalid configuration should fail"); + + assert_eq!(err, ExecutorError::NoDeserializerEnabled); +} + +#[test] +fn unexpected_ack_and_error_packets_are_rejected() { + // Case: executor-side Ack and Error packets are treated as protocol violations. + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::default(), + )); + + assert_eq!( + executor + .on_recieve_ack_packet(&OpenTMKAckPacket { code: 0 }) + .expect_err("ack packets should be rejected"), + ExecutorError::UnexpectedPacketReceived + ); + assert_eq!( + executor + .on_recieve_error_packet(&OpenTMKErrorPacket { + message: String::from("boom"), + }) + .expect_err("error packets should be rejected"), + ExecutorError::UnexpectedPacketReceived + ); +} + +#[test] +fn framed_fuzz_packet_writes_framed_ack() { + // Case: processing one framed fuzz packet from mocked comms writes a framed Ack response. + let state = test_state(Ok(11), Ok(())); + let packet = OpenTMKPacket::FuzzTest(fuzz_packet()); + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::with_reads(frame_packet(&packet)), + )); + executor.deserializer = Some(Box::new(TestDeserializer::new(state))); + + executor + .process_next_packet() + .expect("packet processing should succeed"); + + assert_eq!( + executor.comms.handle.written_bytes(), + frame_packet(&OpenTMKPacket::Ack(OpenTMKAckPacket { code: 11 })) + ); +} + +#[test] +fn framed_fuzz_packet_writes_framed_error() { + // Case: processing one framed fuzz packet writes a framed Error packet when execution fails. + let state = test_state( + Err(ExecutorError::SyzlangDeserializerFailed(String::from( + "test failure", + ))), + Ok(()), + ); + let packet = OpenTMKPacket::FuzzTest(fuzz_packet()); + let mut executor = Executor::new_with_comms(SerialCommsServer::new_with_transport( + MockSerialIo::with_reads(frame_packet(&packet)), + )); + executor.deserializer = Some(Box::new(TestDeserializer::new(state))); + + executor + .process_next_packet() + .expect("packet processing should succeed"); + + assert_eq!( + executor.comms.handle.written_bytes(), + frame_packet(&OpenTMKPacket::Error(OpenTMKErrorPacket { + message: String::from("SyzlangDeserializerFailed(\"test failure\")"), + })) + ); +} diff --git a/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs b/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs new file mode 100644 index 00000000000..d61fe64a120 --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs @@ -0,0 +1,44 @@ +//! Guest-side decoder for per-hypercall static metadata sent from the +//! host fuzzer. +//! +//! See `hvfuzztest/src/targets/hyperv.rs` on the producer side. The +//! struct definition here **must** stay byte-compatible with the +//! producer's `HvcallMeta` — they're intentionally duplicated to keep +//! `opentmk_invariant`'s dep graph small (TODO: unify in a shared crate +//! later). + +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +/// Per-call static metadata. Wire-compatible with +/// `hvfuzztest::targets::hyperv::HvcallMeta`. +#[repr(C)] +#[derive(Copy, Clone, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct HvcallMeta { + /// Hypercall code. + pub code: u16, + /// Fixed header size of the input struct, in bytes. + pub header_size: u16, + /// Variable-length array element size of the input struct, in + /// bytes. `0` if there is no trailing array. + pub element_size: u16, + /// Reserved; expected to be `0`. + pub _reserved: u16, +} + +const _: () = assert!(size_of::() == 8); + +/// Unpacks the `meta` argument value (a 64-bit integer carried in a +/// [`FuzzFunctionVariable::Int`](crate::functions::FuzzFunctionVariable::Int)) +/// into an [`HvcallMeta`]. +/// +/// The 8 bytes of the integer are reinterpreted directly via +/// `zerocopy` — no manual bit-shifts. Returns the decoded struct +/// (always succeeds, since 8 bytes always fit and there are no +/// validation constraints). +pub fn unpack_hvcall_meta(value: u64) -> HvcallMeta { + // `read_from_bytes` requires exactly 8 bytes for an 8-byte struct, + // which is what `to_le_bytes` always returns. So this never + // panics. + HvcallMeta::read_from_bytes(&value.to_le_bytes()) + .expect("HvcallMeta is 8 bytes (compiler-asserted)") +} diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs new file mode 100644 index 00000000000..49343f31725 --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -0,0 +1,82 @@ +use crate::functions::hvcall_meta::unpack_hvcall_meta; +use crate::functions::{FuzzFunctionVariable, VerifyFuzzVariables}; +#[allow(unused)] +use crate::prelude::*; +use hvdef::Vtl; +use inv_decoder::SafeMemoryMap; +use opentmk_core::context::HypercallPlatformTrait; +use opentmk_core::platform::hyperv::ctx::{HvTestCtx, HyperVHypercallConfig}; +use spin::Mutex; + +/// Future-proof for future multi-VP usage to ensure writing to input_page and +/// then dispatching the hypercall is done in one shot. Today we are running +/// single-threaded, and this is mainly used to keep rust happy. +static CALLS: Mutex<(HvTestCtx, bool)> = Mutex::new((HvTestCtx::new(), false)); + +/// Makes a hypervisor call from a fuzz function. +/// +/// The grammar invokes us with exactly three arguments: +/// +/// * `meta` — an `i64` carrying a packed +/// [`HvcallMeta`](crate::functions::hvcall_meta::HvcallMeta) with the +/// static (`code`, `header_size`, `element_size`) triple for this +/// hypercall. +/// * `in_page` — pointer to the syzkaller-generated input buffer. +/// * `in_page_len` — the byte size of `*in_page` +/// (`BYTE_SIZE("../in")` in the grammar). +/// +/// `rep` is computed at call-time as +/// `(in_page_len − header_size) / element_size`. +pub fn hvcall( + mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + // Verify and parse parameters. + let [meta, in_page, in_page_len] = vars.verify_num_params()?; + let meta = unpack_hvcall_meta(meta.expect_int("meta")?); + let in_page = in_page.expect_int("in_page")? as usize; + let in_page_len = in_page_len.expect_int("in_page_len")? as usize; + + // Read in the input page from `in_page` (only if any input is + // expected — `void`-input hypercalls send `in_page_len == 0`). + let mut hvc_lock = CALLS.lock(); + let (hvc, init) = &mut *hvc_lock; + if !*init { + hvc.init(Vtl::Vtl0) + .map_err(|e| format!("Failed to initialize HvTestCtx: {e}"))?; + *init = true; + } + + + let mut in_args = vec![0; in_page_len]; + match mem.try_read_mem(in_page, &mut in_args) { + Ok(_) => (), + Err(e) => log::info!("hvcall: Failed to read input: {e}"), + } + + // Compute rep from the static header/element sizes plus the + // grammar-supplied buffer size. + let header_size = meta.header_size as usize; + let element_size = meta.element_size as usize; + let rep_count = if element_size == 0 || in_page_len <= header_size { + None + } else { + Some((in_page_len - header_size) / element_size) + }; + + let cfg = HyperVHypercallConfig { + rep_start: None, + rep_count, + size: None, // TODO: certain calls require instead of or in addition to rep_count + fast_call: false, // TODO: we may like to change this in fuzzing + }; + + // Invoke actual call. + let result = hvc.hypercall(meta.code.into(), &in_args, &mut [], cfg); + match result { + Ok(_) => (), + Err(e) => log::info!("hvcall: Failed to make hypercall: {e}"), + } + + Ok(FuzzFunctionVariable::Void) +} diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs new file mode 100644 index 00000000000..58f8c7a8599 --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -0,0 +1,135 @@ +// UNSAFETY: This module contains unsafe code because we are doing raw I/O port via within a fuzzer +// context +#![expect(unsafe_code)] + +use crate::functions::{FuzzFunctionVariable, VerifyFuzzVariables}; +#[allow(unused_imports)] +use crate::prelude::*; + +use inv_decoder::SafeMemoryMap; + +use opentmk_core::arch; + +fn validate_ioport(port: u16) -> Option { + if (0x3E8..0x3F0).contains(&port) || (0x2E8..0x300).contains(&port) { + Some(port) + } else { + None + } +} + +pub fn write_ioport_u8( + _mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as u8; + + if let Some(port) = port { + unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::outb(port, value); + } + } + Ok(FuzzFunctionVariable::Void) +} + +pub fn read_ioport_u8( + mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as usize; + if let Some(port) = port { + // Do a sanity check first on whether if the memory address is valid + mem.try_write_mem(value, &[0])?; + + // Perform and write inb result + let res = unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::inb(port) + }; + mem.write_mem(value, &[res]); + } + Ok(FuzzFunctionVariable::Void) +} + +pub fn write_ioport_u16( + _mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as u16; + + if let Some(port) = port { + unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::outw(port, value); + } + } + + Ok(FuzzFunctionVariable::Void) +} + +pub fn read_ioport_u16( + mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as usize; + if let Some(port) = port { + // Do a sanity check first on whether if the memory address is valid + mem.try_write_mem(value, &[0; 2])?; + + // Perform and write inb result + let res = unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::inw(port) + }; + mem.write_mem(value, &res.to_le_bytes()); + } + Ok(FuzzFunctionVariable::Void) +} + +pub fn write_ioport_u32( + _mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as u32; + + if let Some(port) = port { + unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::outl(port, value); + } + } + + Ok(FuzzFunctionVariable::Void) +} + +pub fn read_ioport_u32( + mem: &mut dyn SafeMemoryMap, + vars: Vec, +) -> Result { + let [port, value] = vars.verify_num_params::<2>()?; + let port = validate_ioport(port.expect_int("Port")? as u16); + let value = value.expect_int("Value")? as usize; + if let Some(port) = port { + // Do a sanity check first on whether if the memory address is valid + mem.try_write_mem(value, &[0; 4])?; + + // Perform and write inb result + let res = unsafe { + // SAFETY: this is called within a fuzzer context. We assume all unsafe risks + arch::io::inl(port) + }; + mem.write_mem(value, &res.to_le_bytes()); + } + Ok(FuzzFunctionVariable::Void) +} diff --git a/opentmk/opentmk_invariant/src/functions/mod.rs b/opentmk/opentmk_invariant/src/functions/mod.rs new file mode 100644 index 00000000000..db017c3b821 --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/mod.rs @@ -0,0 +1,10 @@ +pub mod hvcall_meta; +pub mod hyperv; +pub mod io_port; +mod registry; +pub mod variable; + +pub use registry::FunctionRegistry; +pub use variable::FuzzFunction; +pub use variable::FuzzFunctionVariable; +pub use variable::VerifyFuzzVariables; diff --git a/opentmk/opentmk_invariant/src/functions/registry.rs b/opentmk/opentmk_invariant/src/functions/registry.rs new file mode 100644 index 00000000000..afd6a2ff1a8 --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/registry.rs @@ -0,0 +1,53 @@ +use inv_decoder::SafeMemoryMap; + +use crate::functions::{FuzzFunction, FuzzFunctionVariable}; + +#[allow(unused_imports)] +use crate::prelude::*; + +struct RegisteredFn { + name: String, + call: FuzzFunction, +} + +#[derive(Default)] +pub struct FunctionRegistry { + fns: Vec, +} + +impl FunctionRegistry { + pub fn register(&mut self, name: &str, f: FuzzFunction) { + self.fns.push(RegisteredFn { + name: String::from(name), + call: f, + }); + } + + pub fn exec( + &self, + mem: &mut dyn SafeMemoryMap, + function_name: String, + input: Vec, + ) -> FuzzFunctionVariable { + let mut func: Option<&RegisteredFn> = None; + + for f in &self.fns { + if f.name == function_name { + func = Some(f); + break; + } + } + + let func = match func { + None => { + return FuzzFunctionVariable::Error(format!( + "Invalid function name: {}", + &function_name + )); + } + Some(f) => f, + }; + + (func.call)(mem, input).into() + } +} diff --git a/opentmk/opentmk_invariant/src/functions/variable.rs b/opentmk/opentmk_invariant/src/functions/variable.rs new file mode 100644 index 00000000000..404e38ab31c --- /dev/null +++ b/opentmk/opentmk_invariant/src/functions/variable.rs @@ -0,0 +1,57 @@ +use inv_decoder::SafeMemoryMap; + +#[allow(unused_imports)] +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub enum FuzzFunctionVariable { + Void, + Int(u64), + Error(String), +} + +impl FuzzFunctionVariable { + pub fn name(&self) -> &str { + match self { + FuzzFunctionVariable::Void => "void", + FuzzFunctionVariable::Int(_) => "int", + FuzzFunctionVariable::Error(_) => "error", + } + } + + pub fn expect_int(&self, arg: &str) -> Result { + match self { + Self::Int(val) => Ok(*val), + _ => Err(format!("{arg} is not int but {}", self.name())), + } + } +} + +impl From> for FuzzFunctionVariable { + fn from(value: Result) -> Self { + match value { + Ok(val) => val, + Err(err) => FuzzFunctionVariable::Error(err), + } + } +} + +pub type FuzzFunction = + fn(&mut dyn SafeMemoryMap, Vec) -> Result; + +pub trait VerifyFuzzVariables { + fn verify_num_params(&self) -> Result<&[FuzzFunctionVariable; NUM], String>; +} + +impl VerifyFuzzVariables for Vec { + fn verify_num_params(&self) -> Result<&[FuzzFunctionVariable; NUM], String> { + if self.len() != NUM { + return Err(format!( + "Expected {:?} params, but received {:?}", + NUM, + self.len() + )); + } + Ok(self.first_chunk().unwrap()) + } +} diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs new file mode 100644 index 00000000000..1e77ff4ac06 --- /dev/null +++ b/opentmk/opentmk_invariant/src/main.rs @@ -0,0 +1,61 @@ +//! This is the main entrypoint for opentmk_invariant. +//! +//! Opentmk Invariant is a bare-bones operating system based off of opentmk +//! framework used to accept customly crafted program consisting of an encoded +//! series of functions that would invoke specific functions into this OS +#![cfg_attr(target_os = "uefi", no_main)] +#![cfg_attr(target_os = "uefi", no_std)] + +mod comms; +mod deserializer; +mod executor; +mod functions; +mod prelude; + +use crate::executor::Executor; + +use opentmk_core::arch::serial::SerialPort; + +#[cfg(target_os = "uefi")] +use uefi::println; + +#[cfg(target_os = "uefi")] +#[uefi::entry] +fn uefi_entry() -> uefi::Status { + _ = uefi::helpers::init(); + main(); + uefi::Status::ABORTED +} + +fn main() { + println!("Invariant OpenTMK kernel"); + + #[cfg(target_os = "uefi")] + { + // Note: println() will no longer work after this step + // since init() will exit boot services where enabled. + // use log from henceforth for SERIAL port 2 logging + match opentmk::uefi::init() { + Ok(_) => log::info!("OpenTMK initialization complete!"), + Err(e) => { + log::info!("OpenTMK initialization failed! - {:?}", e); + return; + } + }; + } + + let mut exec = Executor::new(SerialPort::COM1); + + if let Err(e) = exec.initialize() { + //TODO: we want to be able to catch these errors on the host side + log::error!("Executor initialization failed with error {:?}", e); + return; + } + + exec.register_fuzz_functions(); + + if let Err(e) = exec.run() { + //TODO: we want to be able to catch these errors on the host side + log::error!("Executor exited with an error - {:?}", e); + } +} diff --git a/opentmk/opentmk_invariant/src/prelude.rs b/opentmk/opentmk_invariant/src/prelude.rs new file mode 100644 index 00000000000..acdbe2c0811 --- /dev/null +++ b/opentmk/opentmk_invariant/src/prelude.rs @@ -0,0 +1,16 @@ +//! This is a extended prelude crate that imports a number of common rust API entities that +//! would've been imported from the `alloc` crate. +#[cfg(target_os = "uefi")] +extern crate alloc; +#[cfg(target_os = "uefi")] +pub use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + sync::Arc, + vec, + vec::Vec, +}; + +#[cfg(not(target_os = "uefi"))] +pub use std::sync::Arc; From 90ce39438cf449bf3a0fc10168408304a86674fd Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Fri, 17 Jul 2026 17:24:19 -0700 Subject: [PATCH 02/32] Fix tests --- opentmk/opentmk_invariant/src/comms/test.rs | 1 + opentmk/opentmk_invariant/src/executor/test.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/opentmk/opentmk_invariant/src/comms/test.rs b/opentmk/opentmk_invariant/src/comms/test.rs index 02764b6400b..bc62879920e 100644 --- a/opentmk/opentmk_invariant/src/comms/test.rs +++ b/opentmk/opentmk_invariant/src/comms/test.rs @@ -109,6 +109,7 @@ fn valid_packet_deserialize() { // Case: a correctly framed packet is deserialized into the original packet payload. let expected = OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("boom"), + fatal: false, }); let mut comms = diff --git a/opentmk/opentmk_invariant/src/executor/test.rs b/opentmk/opentmk_invariant/src/executor/test.rs index 62a8f00b3ba..362af5684dc 100644 --- a/opentmk/opentmk_invariant/src/executor/test.rs +++ b/opentmk/opentmk_invariant/src/executor/test.rs @@ -129,6 +129,7 @@ fn deserializer_failure_returns_error_packet() { response, Some(OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("SyzlangDeserializerFailed(\"decode failed\")"), + fatal: false, })) ); } @@ -168,6 +169,7 @@ fn unexpected_ack_and_error_packets_are_rejected() { executor .on_recieve_error_packet(&OpenTMKErrorPacket { message: String::from("boom"), + fatal: false, }) .expect_err("error packets should be rejected"), ExecutorError::UnexpectedPacketReceived @@ -217,6 +219,7 @@ fn framed_fuzz_packet_writes_framed_error() { executor.comms.handle.written_bytes(), frame_packet(&OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("SyzlangDeserializerFailed(\"test failure\")"), + fatal: false, })) ); } From 9f41a5458113d6f7ed630c740803f18baa33d191 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Thu, 23 Jul 2026 09:56:01 -0700 Subject: [PATCH 03/32] Soften the errors when non-fatal failures occur --- .../src/functions/io_port/mod.rs | 28 +++++++++---------- opentmk/opentmk_invariant/src/prelude.rs | 9 +----- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index 58f8c7a8599..02528a14e89 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -43,15 +43,15 @@ pub fn read_ioport_u8( let port = validate_ioport(port.expect_int("Port")? as u16); let value = value.expect_int("Value")? as usize; if let Some(port) = port { - // Do a sanity check first on whether if the memory address is valid - mem.try_write_mem(value, &[0])?; - // Perform and write inb result let res = unsafe { // SAFETY: this is called within a fuzzer context. We assume all unsafe risks arch::io::inb(port) }; - mem.write_mem(value, &[res]); + + if let Err(e) = mem.try_write_mem(value, &[res]) { + log::warn!("Failed to write inb output: {e}") + } } Ok(FuzzFunctionVariable::Void) } @@ -82,15 +82,15 @@ pub fn read_ioport_u16( let port = validate_ioport(port.expect_int("Port")? as u16); let value = value.expect_int("Value")? as usize; if let Some(port) = port { - // Do a sanity check first on whether if the memory address is valid - mem.try_write_mem(value, &[0; 2])?; - - // Perform and write inb result + // Perform and write inh result let res = unsafe { // SAFETY: this is called within a fuzzer context. We assume all unsafe risks arch::io::inw(port) }; - mem.write_mem(value, &res.to_le_bytes()); + + if let Err(e) = mem.try_write_mem(value, &res.to_le_bytes()) { + log::warn!("Failed to write inh output: {e}") + } } Ok(FuzzFunctionVariable::Void) } @@ -121,15 +121,15 @@ pub fn read_ioport_u32( let port = validate_ioport(port.expect_int("Port")? as u16); let value = value.expect_int("Value")? as usize; if let Some(port) = port { - // Do a sanity check first on whether if the memory address is valid - mem.try_write_mem(value, &[0; 4])?; - - // Perform and write inb result + // Perform and write inl result let res = unsafe { // SAFETY: this is called within a fuzzer context. We assume all unsafe risks arch::io::inl(port) }; - mem.write_mem(value, &res.to_le_bytes()); + + if let Err(e) = mem.try_write_mem(value, &res.to_le_bytes()) { + log::warn!("Failed to write inl output: {e}") + } } Ok(FuzzFunctionVariable::Void) } diff --git a/opentmk/opentmk_invariant/src/prelude.rs b/opentmk/opentmk_invariant/src/prelude.rs index acdbe2c0811..5c661d30cfc 100644 --- a/opentmk/opentmk_invariant/src/prelude.rs +++ b/opentmk/opentmk_invariant/src/prelude.rs @@ -3,14 +3,7 @@ #[cfg(target_os = "uefi")] extern crate alloc; #[cfg(target_os = "uefi")] -pub use alloc::{ - boxed::Box, - format, - string::{String, ToString}, - sync::Arc, - vec, - vec::Vec, -}; +pub use alloc::{boxed::Box, format, string::String, sync::Arc, vec, vec::Vec}; #[cfg(not(target_os = "uefi"))] pub use std::sync::Arc; From 474a058650241dfdfa581fa42e145d4fd3b2ae38 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Thu, 23 Jul 2026 10:00:21 -0700 Subject: [PATCH 04/32] Formatting --- opentmk/opentmk_invariant/src/functions/hyperv.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs index 49343f31725..fe70b5a0b99 100644 --- a/opentmk/opentmk_invariant/src/functions/hyperv.rs +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -47,7 +47,6 @@ pub fn hvcall( *init = true; } - let mut in_args = vec![0; in_page_len]; match mem.try_read_mem(in_page, &mut in_args) { Ok(_) => (), From 9190dbb47a721938fd187333cfb080d36e269e1f Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 27 Jul 2026 11:37:48 -0700 Subject: [PATCH 05/32] Fix no_std stuff for uefi build --- opentmk/inv_decoder/Cargo.toml | 2 +- opentmk/inv_packet/src/lib.rs | 3 ++- opentmk/opentmk_invariant/src/main.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/opentmk/inv_decoder/Cargo.toml b/opentmk/inv_decoder/Cargo.toml index 4fa8146726c..c44a87904fd 100644 --- a/opentmk/inv_decoder/Cargo.toml +++ b/opentmk/inv_decoder/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow.workspace = true +anyhow = { version = "1.0", default-features = false } num-traits.workspace = true spin.workspace = true zerocopy.workspace = true diff --git a/opentmk/inv_packet/src/lib.rs b/opentmk/inv_packet/src/lib.rs index d011bd382d0..7788ab38929 100644 --- a/opentmk/inv_packet/src/lib.rs +++ b/opentmk/inv_packet/src/lib.rs @@ -1,3 +1,4 @@ +#![no_std] //! This contains the packet API library used for communication between the inv-agent and opentmk //! components. //! @@ -13,7 +14,7 @@ //! opentmk ack'ing each packet. extern crate alloc; -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs index 1e77ff4ac06..c49622ccc2d 100644 --- a/opentmk/opentmk_invariant/src/main.rs +++ b/opentmk/opentmk_invariant/src/main.rs @@ -35,7 +35,7 @@ fn main() { // Note: println() will no longer work after this step // since init() will exit boot services where enabled. // use log from henceforth for SERIAL port 2 logging - match opentmk::uefi::init() { + match opentmk::uefi::init::init() { Ok(_) => log::info!("OpenTMK initialization complete!"), Err(e) => { log::info!("OpenTMK initialization failed! - {:?}", e); From 6b982c11ce32e141490f17da26f45ff79e196061 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 10:14:06 -0700 Subject: [PATCH 06/32] Fix cargo and parameter names --- Cargo.lock | 32 +++++++++---------- opentmk/opentmk_invariant/Cargo.toml | 2 +- .../opentmk_invariant/src/functions/hyperv.rs | 24 +++++++------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 497eabe9594..060ce48011b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5702,6 +5702,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "opentmk_invariant" +version = "0.1.0" +dependencies = [ + "hvdef", + "inv_decoder", + "inv_packet", + "log", + "opentmk_core", + "postcard", + "serde", + "spin", + "uefi", + "zerocopy", +] + [[package]] name = "opentmk_protocol" version = "0.0.0" @@ -8382,22 +8398,6 @@ dependencies = [ "x86defs", ] -[[package]] -name = "tmk_invariant" -version = "0.1.0" -dependencies = [ - "hvdef", - "inv_decoder", - "inv_packet", - "log", - "opentmk_core", - "postcard", - "serde", - "spin", - "uefi", - "zerocopy", -] - [[package]] name = "tmk_macros" version = "0.0.0" diff --git a/opentmk/opentmk_invariant/Cargo.toml b/opentmk/opentmk_invariant/Cargo.toml index effacc3ec4d..7d498f0583b 100644 --- a/opentmk/opentmk_invariant/Cargo.toml +++ b/opentmk/opentmk_invariant/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tmk_invariant" +name = "opentmk_invariant" version = "0.1.0" edition = "2024" diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs index fe70b5a0b99..f876b35b706 100644 --- a/opentmk/opentmk_invariant/src/functions/hyperv.rs +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -21,24 +21,24 @@ static CALLS: Mutex<(HvTestCtx, bool)> = Mutex::new((HvTestCtx::new(), false)); /// [`HvcallMeta`](crate::functions::hvcall_meta::HvcallMeta) with the /// static (`code`, `header_size`, `element_size`) triple for this /// hypercall. -/// * `in_page` — pointer to the syzkaller-generated input buffer. -/// * `in_page_len` — the byte size of `*in_page` +/// * `input` — pointer to the syzkaller-generated input buffer. +/// * `input_len` — the byte size of `*input` /// (`BYTE_SIZE("../in")` in the grammar). /// /// `rep` is computed at call-time as -/// `(in_page_len − header_size) / element_size`. +/// `(input_len − header_size) / element_size`. pub fn hvcall( mem: &mut dyn SafeMemoryMap, vars: Vec, ) -> Result { // Verify and parse parameters. - let [meta, in_page, in_page_len] = vars.verify_num_params()?; + let [meta, input, input_len] = vars.verify_num_params()?; let meta = unpack_hvcall_meta(meta.expect_int("meta")?); - let in_page = in_page.expect_int("in_page")? as usize; - let in_page_len = in_page_len.expect_int("in_page_len")? as usize; + let input = input.expect_int("input")? as usize; + let input_len = input_len.expect_int("input_len")? as usize; - // Read in the input page from `in_page` (only if any input is - // expected — `void`-input hypercalls send `in_page_len == 0`). + // Read in the input page from `input` (only if any input is + // expected — `void`-input hypercalls send `input_len == 0`). let mut hvc_lock = CALLS.lock(); let (hvc, init) = &mut *hvc_lock; if !*init { @@ -47,8 +47,8 @@ pub fn hvcall( *init = true; } - let mut in_args = vec![0; in_page_len]; - match mem.try_read_mem(in_page, &mut in_args) { + let mut in_args = vec![0; input_len]; + match mem.try_read_mem(input, &mut in_args) { Ok(_) => (), Err(e) => log::info!("hvcall: Failed to read input: {e}"), } @@ -57,10 +57,10 @@ pub fn hvcall( // grammar-supplied buffer size. let header_size = meta.header_size as usize; let element_size = meta.element_size as usize; - let rep_count = if element_size == 0 || in_page_len <= header_size { + let rep_count = if element_size == 0 || input_len <= header_size { None } else { - Some((in_page_len - header_size) / element_size) + Some((input_len - header_size) / element_size) }; let cfg = HyperVHypercallConfig { From d66cc3995c41fe76e1ba2d2240662df9d34ac160 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Tue, 28 Jul 2026 12:44:09 -0700 Subject: [PATCH 07/32] Fixes for PR tests Adds copyright headers and fix versioning --- Cargo.lock | 7 +++---- opentmk/inv_decoder/Cargo.toml | 11 ++++++++--- opentmk/inv_decoder/src/atomicrefqueue.rs | 3 +++ opentmk/inv_decoder/src/lib.rs | 3 +++ opentmk/inv_decoder/src/safememory.rs | 3 +++ opentmk/inv_decoder/src/wire.rs | 3 +++ opentmk/inv_packet/Cargo.toml | 10 ++++++++-- opentmk/inv_packet/src/lib.rs | 3 +++ opentmk/opentmk_invariant/Cargo.toml | 8 +++++--- opentmk/opentmk_invariant/src/comms/mod.rs | 3 +++ opentmk/opentmk_invariant/src/comms/test.rs | 3 +++ opentmk/opentmk_invariant/src/deserializer/mod.rs | 3 +++ .../opentmk_invariant/src/deserializer/syzlang/mod.rs | 3 +++ .../src/deserializer/syzlang/test.rs | 3 +++ opentmk/opentmk_invariant/src/executor/mod.rs | 3 +++ opentmk/opentmk_invariant/src/executor/test.rs | 3 +++ .../opentmk_invariant/src/functions/hvcall_meta.rs | 3 +++ opentmk/opentmk_invariant/src/functions/hyperv.rs | 3 +++ .../opentmk_invariant/src/functions/io_port/mod.rs | 3 +++ opentmk/opentmk_invariant/src/functions/mod.rs | 3 +++ opentmk/opentmk_invariant/src/functions/registry.rs | 3 +++ opentmk/opentmk_invariant/src/functions/variable.rs | 3 +++ opentmk/opentmk_invariant/src/main.rs | 4 ++++ opentmk/opentmk_invariant/src/prelude.rs | 3 +++ 24 files changed, 85 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 060ce48011b..5fcd4cdd7cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4043,7 +4043,7 @@ dependencies = [ [[package]] name = "inv_decoder" -version = "0.3.0" +version = "0.0.0" dependencies = [ "anyhow", "num-traits", @@ -4053,7 +4053,7 @@ dependencies = [ [[package]] name = "inv_packet" -version = "0.1.0" +version = "0.0.0" dependencies = [ "serde", ] @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "opentmk_invariant" -version = "0.1.0" +version = "0.0.0" dependencies = [ "hvdef", "inv_decoder", @@ -5712,7 +5712,6 @@ dependencies = [ "log", "opentmk_core", "postcard", - "serde", "spin", "uefi", "zerocopy", diff --git a/opentmk/inv_decoder/Cargo.toml b/opentmk/inv_decoder/Cargo.toml index c44a87904fd..f3d818426f7 100644 --- a/opentmk/inv_decoder/Cargo.toml +++ b/opentmk/inv_decoder/Cargo.toml @@ -1,8 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + [package] name = "inv_decoder" -version = "0.3.0" -authors = ["VRTD "] -edition = "2021" +edition.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,3 +13,6 @@ anyhow = { version = "1.0", default-features = false } num-traits.workspace = true spin.workspace = true zerocopy.workspace = true + +[lints] +workspace = true diff --git a/opentmk/inv_decoder/src/atomicrefqueue.rs b/opentmk/inv_decoder/src/atomicrefqueue.rs index ca44b0093d2..84cdb17146c 100644 --- a/opentmk/inv_decoder/src/atomicrefqueue.rs +++ b/opentmk/inv_decoder/src/atomicrefqueue.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use alloc::vec::Vec; diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 8742273b53c..7c48c67130e 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #![no_std] #[macro_use] diff --git a/opentmk/inv_decoder/src/safememory.rs b/opentmk/inv_decoder/src/safememory.rs index 4001b483239..598e3ce6a32 100644 --- a/opentmk/inv_decoder/src/safememory.rs +++ b/opentmk/inv_decoder/src/safememory.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use alloc::{boxed::Box, format, string::String, vec::Vec}; /// This represents a virtual memory map that can be used to safely write/read from diff --git a/opentmk/inv_decoder/src/wire.rs b/opentmk/inv_decoder/src/wire.rs index c66b321cc6e..07054430a55 100644 --- a/opentmk/inv_decoder/src/wire.rs +++ b/opentmk/inv_decoder/src/wire.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + //! Structures representing on-the-wire data formats. use super::*; diff --git a/opentmk/inv_packet/Cargo.toml b/opentmk/inv_packet/Cargo.toml index 5c039a9a253..24e61ae5068 100644 --- a/opentmk/inv_packet/Cargo.toml +++ b/opentmk/inv_packet/Cargo.toml @@ -1,7 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + [package] name = "inv_packet" -version = "0.1.0" -edition = "2024" +edition.workspace = true +rust-version.workspace = true [dependencies] serde = { workspace = true, features = ["derive"]} + +[lints] +workspace = true diff --git a/opentmk/inv_packet/src/lib.rs b/opentmk/inv_packet/src/lib.rs index 7788ab38929..9262a3da6ab 100644 --- a/opentmk/inv_packet/src/lib.rs +++ b/opentmk/inv_packet/src/lib.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #![no_std] //! This contains the packet API library used for communication between the inv-agent and opentmk //! components. diff --git a/opentmk/opentmk_invariant/Cargo.toml b/opentmk/opentmk_invariant/Cargo.toml index 7d498f0583b..e797b1441f0 100644 --- a/opentmk/opentmk_invariant/Cargo.toml +++ b/opentmk/opentmk_invariant/Cargo.toml @@ -1,7 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + [package] name = "opentmk_invariant" -version = "0.1.0" -edition = "2024" +edition.workspace = true +rust-version.workspace = true [dependencies] hvdef.workspace = true @@ -9,7 +12,6 @@ log.workspace = true opentmk_core.workspace = true spin.workspace = true zerocopy.workspace = true -serde = { workspace = true, features = ["derive"]} postcard = { workspace = true, features = ["alloc"] } inv_packet.workspace = true inv_decoder.workspace = true diff --git a/opentmk/opentmk_invariant/src/comms/mod.rs b/opentmk/opentmk_invariant/src/comms/mod.rs index 5943c522b16..4e9803cb06d 100644 --- a/opentmk/opentmk_invariant/src/comms/mod.rs +++ b/opentmk/opentmk_invariant/src/comms/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #[cfg(test)] pub(crate) mod test; diff --git a/opentmk/opentmk_invariant/src/comms/test.rs b/opentmk/opentmk_invariant/src/comms/test.rs index bc62879920e..345a754ce33 100644 --- a/opentmk/opentmk_invariant/src/comms/test.rs +++ b/opentmk/opentmk_invariant/src/comms/test.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + extern crate alloc; use super::*; diff --git a/opentmk/opentmk_invariant/src/deserializer/mod.rs b/opentmk/opentmk_invariant/src/deserializer/mod.rs index 3778bf2508f..aeb703a64ed 100644 --- a/opentmk/opentmk_invariant/src/deserializer/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + pub mod syzlang; use spin::Mutex; diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index c6a94b5e505..2e776a32ab0 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #[cfg(test)] mod test; diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs index a50a07cd341..54cab11b833 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/test.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use spin::mutex::Mutex; use super::*; diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index d10d92dbb36..77901105621 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #[allow(unused_imports)] use crate::prelude::*; use crate::{ diff --git a/opentmk/opentmk_invariant/src/executor/test.rs b/opentmk/opentmk_invariant/src/executor/test.rs index 362af5684dc..c7050b58f0c 100644 --- a/opentmk/opentmk_invariant/src/executor/test.rs +++ b/opentmk/opentmk_invariant/src/executor/test.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + extern crate alloc; use alloc::rc::Rc; use core::cell::RefCell; diff --git a/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs b/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs index d61fe64a120..5b58add9aa6 100644 --- a/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs +++ b/opentmk/opentmk_invariant/src/functions/hvcall_meta.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + //! Guest-side decoder for per-hypercall static metadata sent from the //! host fuzzer. //! diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs index f876b35b706..d24207c9e6f 100644 --- a/opentmk/opentmk_invariant/src/functions/hyperv.rs +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use crate::functions::hvcall_meta::unpack_hvcall_meta; use crate::functions::{FuzzFunctionVariable, VerifyFuzzVariables}; #[allow(unused)] diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index 02528a14e89..5fce331d885 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + // UNSAFETY: This module contains unsafe code because we are doing raw I/O port via within a fuzzer // context #![expect(unsafe_code)] diff --git a/opentmk/opentmk_invariant/src/functions/mod.rs b/opentmk/opentmk_invariant/src/functions/mod.rs index db017c3b821..5264538b1e6 100644 --- a/opentmk/opentmk_invariant/src/functions/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + pub mod hvcall_meta; pub mod hyperv; pub mod io_port; diff --git a/opentmk/opentmk_invariant/src/functions/registry.rs b/opentmk/opentmk_invariant/src/functions/registry.rs index afd6a2ff1a8..5e3fb2a565b 100644 --- a/opentmk/opentmk_invariant/src/functions/registry.rs +++ b/opentmk/opentmk_invariant/src/functions/registry.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use inv_decoder::SafeMemoryMap; use crate::functions::{FuzzFunction, FuzzFunctionVariable}; diff --git a/opentmk/opentmk_invariant/src/functions/variable.rs b/opentmk/opentmk_invariant/src/functions/variable.rs index 404e38ab31c..fff2c8e96c2 100644 --- a/opentmk/opentmk_invariant/src/functions/variable.rs +++ b/opentmk/opentmk_invariant/src/functions/variable.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + use inv_decoder::SafeMemoryMap; #[allow(unused_imports)] diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs index c49622ccc2d..741a07e3d85 100644 --- a/opentmk/opentmk_invariant/src/main.rs +++ b/opentmk/opentmk_invariant/src/main.rs @@ -1,8 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + //! This is the main entrypoint for opentmk_invariant. //! //! Opentmk Invariant is a bare-bones operating system based off of opentmk //! framework used to accept customly crafted program consisting of an encoded //! series of functions that would invoke specific functions into this OS + #![cfg_attr(target_os = "uefi", no_main)] #![cfg_attr(target_os = "uefi", no_std)] diff --git a/opentmk/opentmk_invariant/src/prelude.rs b/opentmk/opentmk_invariant/src/prelude.rs index 5c661d30cfc..d1f16f3bd94 100644 --- a/opentmk/opentmk_invariant/src/prelude.rs +++ b/opentmk/opentmk_invariant/src/prelude.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + //! This is a extended prelude crate that imports a number of common rust API entities that //! would've been imported from the `alloc` crate. #[cfg(target_os = "uefi")] From 2a2035ae82bff9bc033b7f834bef8f763ab07dc8 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 10:43:05 -0700 Subject: [PATCH 08/32] Fix some warnings --- opentmk/inv_decoder/src/atomicrefqueue.rs | 5 ----- opentmk/inv_decoder/src/lib.rs | 14 ++++++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/opentmk/inv_decoder/src/atomicrefqueue.rs b/opentmk/inv_decoder/src/atomicrefqueue.rs index 84cdb17146c..4857da2037c 100644 --- a/opentmk/inv_decoder/src/atomicrefqueue.rs +++ b/opentmk/inv_decoder/src/atomicrefqueue.rs @@ -16,11 +16,6 @@ pub(crate) struct AtomicRefQueue { start_idx: AtomicUsize, } -// AtomicRefQueue is safe to send between threads, as long as the entries are also Send. -// Same goes for sync. -unsafe impl Send for AtomicRefQueue where T: Send {} -unsafe impl Sync for AtomicRefQueue where T: Sync {} - impl AtomicRefQueue { /// Create a new AtomicRefQueue from a list of entries. pub(crate) fn new(list: Vec) -> Self { diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 7c48c67130e..aaffc10f84f 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -21,7 +21,7 @@ use core::{ }; use alloc::{string::String, sync::Arc, vec::Vec}; -use anyhow::{Context, bail}; +use anyhow::{Context, Result, bail}; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::atomicrefqueue::AtomicRefQueue; @@ -30,20 +30,18 @@ const K_MAX_COMMANDS: usize = 1000; pub const EXEC_INPUT_REQ_SIZE: usize = 0x1000000; -// Supported (min) input size (kMaxInput in executor.cc). +/// Supported (min) input size (kMaxInput in executor.cc). pub const SUPPORTED_INPUT_SIZE: usize = 8 << 20; -// Max supported args +/// Max supported args pub const MAX_ARGS: usize = 30; -// All syzkaller pointers are offsets from this presumed base address +/// All syzkaller pointers are offsets from this presumed base address pub const ADDR_SYZ_BEGIN: u64 = 0x20000000; -// Call failed +/// Call failed const _SYZKALLER_CALL_END_FAILED: u64 = 3; -pub type Result = anyhow::Result; - pub type TestcaseResults = [ResT; K_MAX_COMMANDS]; pub type Executor<'f> = @@ -89,7 +87,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { /// buffer and exec callback. fn new< M: SafeMemoryMap + 'm, - F: Fn(&DecodedProgram, InputCase) -> InputResult + Send + Sync + 'f, + F: Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + Send + Sync + 'f, >( mem: M, syz_input_buffer: &[u8], From f8ab2b4c255398aa9045d2a47becc350f609333c Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 10:49:42 -0700 Subject: [PATCH 09/32] Add docs --- opentmk/inv_decoder/src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index aaffc10f84f..993c7bf6c79 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +//! Decodes and executes syzkaller programs against a caller-provided memory map. + #![no_std] #[macro_use] @@ -28,6 +30,7 @@ use crate::atomicrefqueue::AtomicRefQueue; const K_MAX_COMMANDS: usize = 1000; +/// Required size, in bytes, of the syzkaller executor memory region. pub const EXEC_INPUT_REQ_SIZE: usize = 0x1000000; /// Supported (min) input size (kMaxInput in executor.cc). @@ -42,8 +45,10 @@ pub const ADDR_SYZ_BEGIN: u64 = 0x20000000; /// Call failed const _SYZKALLER_CALL_END_FAILED: u64 = 3; +/// Results produced by executing the calls in a syzkaller program. pub type TestcaseResults = [ResT; K_MAX_COMMANDS]; +/// Callback used to execute a decoded syzkaller input. pub type Executor<'f> = dyn Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + 'f + Send + Sync; @@ -552,10 +557,11 @@ impl<'a> Decoder<'a> { /// Represents a parsed syzkaller input. pub struct InputCase { + /// Syzkaller call number to execute. pub call_num: u64, /// argument array pub args: [u64; MAX_ARGS], - // Number of args set in args + /// Number of args set in `args`. pub num_args: u64, /// Prevent construction by our callers so we can ensure maximum SemVer flexibility. #[doc(hidden)] @@ -565,11 +571,15 @@ pub struct InputCase { /// Result of an executed syzkaller input #[derive(Default)] pub struct InputResult { + /// Value returned by the executed call. pub code: u64, + /// Name of the executed call. pub name: String, + /// Whether the call completed successfully. pub is_success: bool, } +/// Thread-safe result storage for a single executed call. pub struct ResT { executed: AtomicBool, val: AtomicU64, From c1210db870947d23cbb8113382232500c2a093e0 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 10:49:58 -0700 Subject: [PATCH 10/32] Fix remaining warnings in inv_decoder --- opentmk/inv_decoder/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 993c7bf6c79..c5392d900cc 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -479,7 +479,7 @@ impl<'a> Decoder<'a> { } else if hdr.prog_size == 0 { bail!("prog size is 0"); } else if hdr.prog_size - > ((SUPPORTED_INPUT_SIZE as u64) - (core::mem::size_of::() as u64)) + > ((SUPPORTED_INPUT_SIZE as u64) - (size_of::() as u64)) { bail!("input size too large! InputSz:{:#?}", hdr.prog_size); } @@ -806,21 +806,21 @@ fn copyout(mem: &mut M, addr: u64, size: u64, res: &m *res = u64::from_le_bytes(buf); } -// Takes a syz_in buffer containing raw syzkaller data from TKO, parses -// individual test cases from it into the provided addr buffer, and calls the -// provided exec function with a single test case; then continues from the -// beginning until all provided testcases have completed. -// -// It is expected that addr_size is minimum 0x1000000 bytes (or 4mb). -// syz_exec_mem must be at least EXEC_INPUT_REQ_SIZE in size. -// syz_input_buffer must be at least SUPPORTED_INPUT_SIZE in size. +/// Takes a syz_in buffer containing raw syzkaller data from TKO, parses +/// individual test cases from it into the provided addr buffer, and calls the +/// provided exec function with a single test case; then continues from the +/// beginning until all provided testcases have completed. +/// +/// It is expected that addr_size is minimum 0x1000000 bytes (or 4mb). +/// syz_exec_mem must be at least [`EXEC_INPUT_REQ_SIZE`] in size. +/// syz_input_buffer must be at least [`SUPPORTED_INPUT_SIZE`] in size. pub fn exec_testcases_safe( syz_exec_mem: M, syz_input_buffer: &mut [u8], exec: F, ) -> Result where - F: Fn(&DecodedProgram, InputCase) -> InputResult + Send + Sync, + F: Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + Send + Sync, { let decoded = DecodedProgram::new(syz_exec_mem, syz_input_buffer, exec)?; decoded.exec_instrs()?; From 0fc4e9d5dbc3633c3748f49eb733fee1c820492e Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 10:56:04 -0700 Subject: [PATCH 11/32] Fix clippy warnings --- opentmk/opentmk_invariant/src/comms/mod.rs | 2 +- opentmk/opentmk_invariant/src/executor/mod.rs | 2 -- opentmk/opentmk_invariant/src/functions/hyperv.rs | 2 +- opentmk/opentmk_invariant/src/functions/io_port/mod.rs | 2 +- opentmk/opentmk_invariant/src/functions/registry.rs | 2 +- opentmk/opentmk_invariant/src/functions/variable.rs | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/opentmk/opentmk_invariant/src/comms/mod.rs b/opentmk/opentmk_invariant/src/comms/mod.rs index 4e9803cb06d..4ea29123628 100644 --- a/opentmk/opentmk_invariant/src/comms/mod.rs +++ b/opentmk/opentmk_invariant/src/comms/mod.rs @@ -10,7 +10,7 @@ use inv_packet::{ COMMS_SYN_MAGIC, OpenTMKPacket, }; -#[allow(unused_imports)] +#[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; use opentmk_core::arch::serial::{InstrIoAccess, Serial, SerialPort}; diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index 77901105621..41736db1ee6 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#[allow(unused_imports)] use crate::prelude::*; use crate::{ comms::{OpenTmkSerialIo, SerialCommsServer, SerialIo}, @@ -30,7 +29,6 @@ pub(crate) enum ExecutorError { PacketPayloadSerializeFailed, UnexpectedPacketReceived, NoDeserializerEnabled, - #[allow(dead_code)] SyzlangDeserializerFailed(String), DecoderMappingsDeserializeFailed, DeserializerUnset, diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs index d24207c9e6f..80e9f351e45 100644 --- a/opentmk/opentmk_invariant/src/functions/hyperv.rs +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -3,7 +3,7 @@ use crate::functions::hvcall_meta::unpack_hvcall_meta; use crate::functions::{FuzzFunctionVariable, VerifyFuzzVariables}; -#[allow(unused)] +#[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; use hvdef::Vtl; use inv_decoder::SafeMemoryMap; diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index 5fce331d885..e05d13edad2 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -6,7 +6,7 @@ #![expect(unsafe_code)] use crate::functions::{FuzzFunctionVariable, VerifyFuzzVariables}; -#[allow(unused_imports)] +#[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; use inv_decoder::SafeMemoryMap; diff --git a/opentmk/opentmk_invariant/src/functions/registry.rs b/opentmk/opentmk_invariant/src/functions/registry.rs index 5e3fb2a565b..c94745aff53 100644 --- a/opentmk/opentmk_invariant/src/functions/registry.rs +++ b/opentmk/opentmk_invariant/src/functions/registry.rs @@ -5,7 +5,7 @@ use inv_decoder::SafeMemoryMap; use crate::functions::{FuzzFunction, FuzzFunctionVariable}; -#[allow(unused_imports)] +#[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; struct RegisteredFn { diff --git a/opentmk/opentmk_invariant/src/functions/variable.rs b/opentmk/opentmk_invariant/src/functions/variable.rs index fff2c8e96c2..c88db7a42cf 100644 --- a/opentmk/opentmk_invariant/src/functions/variable.rs +++ b/opentmk/opentmk_invariant/src/functions/variable.rs @@ -3,7 +3,7 @@ use inv_decoder::SafeMemoryMap; -#[allow(unused_imports)] +#[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; #[derive(Debug, Clone)] From a20c645c1d048feed1ece68ef5004b27bf6be139 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 11:24:39 -0700 Subject: [PATCH 12/32] Readd panic_handler from opentmk main code --- opentmk/opentmk_invariant/src/main.rs | 3 ++- opentmk/opentmk_invariant/src/rt.rs | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 opentmk/opentmk_invariant/src/rt.rs diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs index 741a07e3d85..e1b67c08a86 100644 --- a/opentmk/opentmk_invariant/src/main.rs +++ b/opentmk/opentmk_invariant/src/main.rs @@ -15,6 +15,7 @@ mod deserializer; mod executor; mod functions; mod prelude; +mod rt; use crate::executor::Executor; @@ -39,7 +40,7 @@ fn main() { // Note: println() will no longer work after this step // since init() will exit boot services where enabled. // use log from henceforth for SERIAL port 2 logging - match opentmk::uefi::init::init() { + match opentmk_core::uefi::init::init() { Ok(_) => log::info!("OpenTMK initialization complete!"), Err(e) => { log::info!("OpenTMK initialization failed! - {:?}", e); diff --git a/opentmk/opentmk_invariant/src/rt.rs b/opentmk/opentmk_invariant/src/rt.rs new file mode 100644 index 00000000000..91d034574fc --- /dev/null +++ b/opentmk/opentmk_invariant/src/rt.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![cfg(target_os = "uefi")] +#[panic_handler] +fn panic_handler(panic: &core::panic::PanicInfo<'_>) -> ! { + log::error!("Panic at runtime: {}", panic); + loop {} +} From 3cadde3704a1c832e85a996f7f85467806bede9c Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 17 Aug 2026 11:25:37 -0700 Subject: [PATCH 13/32] Fix clippy --- opentmk/opentmk_invariant/src/executor/mod.rs | 2 +- opentmk/opentmk_invariant/src/functions/registry.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index 41736db1ee6..ce374490c22 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -128,7 +128,7 @@ impl Executor { log::info!( "Setting active deserializer to {:?}", - &self.deserializer_type + self.deserializer_type ); Ok(Some(OpenTMKPacket::Ack(OpenTMKAckPacket { code: 0 }))) } diff --git a/opentmk/opentmk_invariant/src/functions/registry.rs b/opentmk/opentmk_invariant/src/functions/registry.rs index c94745aff53..b38aed8c81c 100644 --- a/opentmk/opentmk_invariant/src/functions/registry.rs +++ b/opentmk/opentmk_invariant/src/functions/registry.rs @@ -44,8 +44,7 @@ impl FunctionRegistry { let func = match func { None => { return FuzzFunctionVariable::Error(format!( - "Invalid function name: {}", - &function_name + "Invalid function name: {function_name}", )); } Some(f) => f, From adad0f176a2733a10d94e9ba19f230ae44568527 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 24 Aug 2026 15:52:00 -0700 Subject: [PATCH 14/32] use workspaced anyhow --- Cargo.toml | 2 +- flowey/flowey_hvlite/Cargo.toml | 2 +- opentmk/inv_decoder/Cargo.toml | 2 +- xtask/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af4c1a65606..c6977167151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -473,7 +473,7 @@ vnc_worker_defs = { path = "workers/vnc_worker_defs" } # crates.io # --- Error handling --- -anyhow = "1.0" +anyhow = { version = "1.0", default-features = false } thiserror = { version = "2", default-features = false } # --- Logging / tracing --- diff --git a/flowey/flowey_hvlite/Cargo.toml b/flowey/flowey_hvlite/Cargo.toml index 314058d0a53..a20cc7d068a 100644 --- a/flowey/flowey_hvlite/Cargo.toml +++ b/flowey/flowey_hvlite/Cargo.toml @@ -17,7 +17,7 @@ petri_artifacts_core.workspace = true petri_artifacts_vmm_test.workspace = true vmm_test_images = { workspace = true, features = ["serde", "clap"] } -anyhow.workspace = true +anyhow = { workspace = true, features = ["std"] } clap = { workspace = true, features = ["derive"] } log.workspace = true paste.workspace = true diff --git a/opentmk/inv_decoder/Cargo.toml b/opentmk/inv_decoder/Cargo.toml index f3d818426f7..9bf7d7409fb 100644 --- a/opentmk/inv_decoder/Cargo.toml +++ b/opentmk/inv_decoder/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = { version = "1.0", default-features = false } +anyhow.workspace = true num-traits.workspace = true spin.workspace = true zerocopy.workspace = true diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 8fdb8892d72..2aa7fca3bc3 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true [dependencies] vmm_test_images = { workspace = true, features = ["clap"] } -anyhow.workspace = true +anyhow = { workspace = true, features = ["std"] } cargo_toml.workspace = true ci_logger.workspace = true clap = { workspace = true, features = ["derive", "env"] } From da1713a31d8650c57f3b2d13073612bfff127601 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Thu, 27 Aug 2026 10:15:41 -0700 Subject: [PATCH 15/32] fix clippy issues --- opentmk/inv_decoder/src/atomicrefqueue.rs | 2 +- opentmk/inv_decoder/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opentmk/inv_decoder/src/atomicrefqueue.rs b/opentmk/inv_decoder/src/atomicrefqueue.rs index 4857da2037c..6cec1175f42 100644 --- a/opentmk/inv_decoder/src/atomicrefqueue.rs +++ b/opentmk/inv_decoder/src/atomicrefqueue.rs @@ -84,7 +84,7 @@ impl AtomicRefQueue { // Add tests for the AtomicRefQueue, ensuring that the vector is correctly populated and that // the pop_ref method returns the correct entries, and stops when the vector is all marked. #[cfg(test)] -#[allow(non_snake_case)] +#[expect(non_snake_case)] mod tests { use super::*; diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index c5392d900cc..16457075731 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -1143,7 +1143,7 @@ mod tests { results: Arc, ) -> DecodedProgram<'static, 'static> { let noop_exec: Arc> = - Arc::new(|_: &DecodedProgram, _: InputCase| InputResult::default()); + Arc::new(|_: &DecodedProgram<'_, '_>, _: InputCase| InputResult::default()); DecodedProgram { instr_vec: Arc::new(AtomicRefQueue::new(vec![])), mem: Arc::new(Mutex::new(mem)), @@ -1256,7 +1256,7 @@ mod tests { let captured_arg = Arc::new(AtomicU64::new(0)); let captured_clone = captured_arg.clone(); let exec_fn: Arc> = - Arc::new(move |_: &DecodedProgram, input: InputCase| -> InputResult { + Arc::new(move |_: &DecodedProgram<'_, '_>, input: InputCase| -> InputResult { captured_clone.store(input.args[0], Ordering::SeqCst); InputResult { code: 0, @@ -1307,7 +1307,7 @@ mod tests { let was_called = Arc::new(AtomicBool::new(false)); let was_called_clone = was_called.clone(); let exec_fn: Arc> = - Arc::new(move |_: &DecodedProgram, _: InputCase| -> InputResult { + Arc::new(move |_: &DecodedProgram<'_, '_>, _: InputCase| -> InputResult { was_called_clone.store(true, Ordering::SeqCst); InputResult::default() }); From c53ea89ffeef016287d5ca6e5269574fb32d9181 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Thu, 27 Aug 2026 10:44:42 -0700 Subject: [PATCH 16/32] Formatting --- opentmk/inv_decoder/src/lib.rs | 14 ++++++++------ opentmk/opentmk_invariant/src/comms/mod.rs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 16457075731..14f1575f7b2 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -1255,15 +1255,16 @@ mod tests { let captured_arg = Arc::new(AtomicU64::new(0)); let captured_clone = captured_arg.clone(); - let exec_fn: Arc> = - Arc::new(move |_: &DecodedProgram<'_, '_>, input: InputCase| -> InputResult { + let exec_fn: Arc> = Arc::new( + move |_: &DecodedProgram<'_, '_>, input: InputCase| -> InputResult { captured_clone.store(input.args[0], Ordering::SeqCst); InputResult { code: 0, name: String::new(), is_success: true, } - }); + }, + ); let mem: Box = Box::new([0u8; 8]); let prog = DecodedProgram { @@ -1306,11 +1307,12 @@ mod tests { let was_called = Arc::new(AtomicBool::new(false)); let was_called_clone = was_called.clone(); - let exec_fn: Arc> = - Arc::new(move |_: &DecodedProgram<'_, '_>, _: InputCase| -> InputResult { + let exec_fn: Arc> = Arc::new( + move |_: &DecodedProgram<'_, '_>, _: InputCase| -> InputResult { was_called_clone.store(true, Ordering::SeqCst); InputResult::default() - }); + }, + ); let mem: Box = Box::new([0u8; 8]); let prog = DecodedProgram { diff --git a/opentmk/opentmk_invariant/src/comms/mod.rs b/opentmk/opentmk_invariant/src/comms/mod.rs index 4ea29123628..d534f974954 100644 --- a/opentmk/opentmk_invariant/src/comms/mod.rs +++ b/opentmk/opentmk_invariant/src/comms/mod.rs @@ -102,7 +102,7 @@ impl SerialCommsServer { } self.connected = true; - log::info!("Serial Comms sucessfully connected"); + log::info!("Serial Comms successfully connected"); Ok(()) } From 1cbcc3ac8dd4d84c889dbb7f75de045db8bb0633 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Mon, 31 Aug 2026 17:29:57 -0700 Subject: [PATCH 17/32] remove fatal flag --- opentmk/inv_packet/src/lib.rs | 2 -- opentmk/opentmk_invariant/src/comms/test.rs | 1 - opentmk/opentmk_invariant/src/executor/mod.rs | 1 - opentmk/opentmk_invariant/src/executor/test.rs | 3 --- 4 files changed, 7 deletions(-) diff --git a/opentmk/inv_packet/src/lib.rs b/opentmk/inv_packet/src/lib.rs index 9262a3da6ab..3f82aead130 100644 --- a/opentmk/inv_packet/src/lib.rs +++ b/opentmk/inv_packet/src/lib.rs @@ -65,8 +65,6 @@ pub struct OpenTMKFuzzTest { pub struct OpenTMKErrorPacket { /// The message string pub message: String, - /// Whether if the error is fatal and requires rebooting - pub fatal: bool, } /// Packet used to acknowledge the completion of a single fuzz test case diff --git a/opentmk/opentmk_invariant/src/comms/test.rs b/opentmk/opentmk_invariant/src/comms/test.rs index 345a754ce33..3309da43bf9 100644 --- a/opentmk/opentmk_invariant/src/comms/test.rs +++ b/opentmk/opentmk_invariant/src/comms/test.rs @@ -112,7 +112,6 @@ fn valid_packet_deserialize() { // Case: a correctly framed packet is deserialized into the original packet payload. let expected = OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("boom"), - fatal: false, }); let mut comms = diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index ce374490c22..c9cf3589155 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -143,7 +143,6 @@ impl Executor { Ok(code) => OpenTMKPacket::Ack(OpenTMKAckPacket { code }), Err(e) => OpenTMKPacket::Error(OpenTMKErrorPacket { message: format!("{:?}", e), - fatal: false, }), })), } diff --git a/opentmk/opentmk_invariant/src/executor/test.rs b/opentmk/opentmk_invariant/src/executor/test.rs index c7050b58f0c..626c7700479 100644 --- a/opentmk/opentmk_invariant/src/executor/test.rs +++ b/opentmk/opentmk_invariant/src/executor/test.rs @@ -132,7 +132,6 @@ fn deserializer_failure_returns_error_packet() { response, Some(OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("SyzlangDeserializerFailed(\"decode failed\")"), - fatal: false, })) ); } @@ -172,7 +171,6 @@ fn unexpected_ack_and_error_packets_are_rejected() { executor .on_recieve_error_packet(&OpenTMKErrorPacket { message: String::from("boom"), - fatal: false, }) .expect_err("error packets should be rejected"), ExecutorError::UnexpectedPacketReceived @@ -222,7 +220,6 @@ fn framed_fuzz_packet_writes_framed_error() { executor.comms.handle.written_bytes(), frame_packet(&OpenTMKPacket::Error(OpenTMKErrorPacket { message: String::from("SyzlangDeserializerFailed(\"test failure\")"), - fatal: false, })) ); } From 679b75a7c9ace3ad234116eff89b913141225163 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 09:50:49 -0700 Subject: [PATCH 18/32] apply PR suggestions --- opentmk/inv_packet/Cargo.toml | 2 +- opentmk/inv_packet/src/lib.rs | 2 +- opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs | 7 +++++++ opentmk/opentmk_invariant/src/executor/mod.rs | 8 ++++---- opentmk/opentmk_invariant/src/executor/test.rs | 4 ++-- opentmk/opentmk_invariant/src/functions/hyperv.rs | 7 +++++++ 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/opentmk/inv_packet/Cargo.toml b/opentmk/inv_packet/Cargo.toml index 24e61ae5068..b9563ebff6c 100644 --- a/opentmk/inv_packet/Cargo.toml +++ b/opentmk/inv_packet/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] -serde = { workspace = true, features = ["derive"]} +serde = { workspace = true, features = ["derive", "alloc"]} [lints] workspace = true diff --git a/opentmk/inv_packet/src/lib.rs b/opentmk/inv_packet/src/lib.rs index 3f82aead130..6d0a006a5fd 100644 --- a/opentmk/inv_packet/src/lib.rs +++ b/opentmk/inv_packet/src/lib.rs @@ -30,7 +30,7 @@ pub const COMMS_ACK_MAGIC: u64 = 0x6162636465666768; /// Magic value used as the header of a regular packet pub const COMMS_PACKET_HEADER_MAGIC: u64 = 0xf0f1f2f3f4f5f6f7; /// Magic value used as the footer of a regular packet -pub const COMMS_PACKET_FOOTER_MAGIC: u64 = 0xf0f1f2f3f4f5f6f7; +pub const COMMS_PACKET_FOOTER_MAGIC: u64 = !COMMS_PACKET_HEADER_MAGIC; /// Describes the specific serialization format used for encoding test cases to the opentmk #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index 2e776a32ab0..77dbc231025 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -145,6 +145,13 @@ impl Deserializer for SyzlangDeserializer { self.tc_slice.fill(0); let src = &testcase.testcase_vcpu0.as_slice(); + if src.len() > self.tc_slice.len() { + return Err(ExecutorError::SyzlangDeserializerFailed(format!( + "Testcase input is too large: {} > {}", + src.len(), + self.tc_slice.len(), + ))); + } self.tc_slice[..src.len()].copy_from_slice(src); self.mem.0.as_mut_slice().fill(0); diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index c9cf3589155..acaa09edfae 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -91,8 +91,8 @@ impl Executor { let response_pkt = match pkt { OpenTMKPacket::Configuration(cfg) => self.on_receive_configuration_packet(&cfg)?, OpenTMKPacket::FuzzTest(mut fuzz) => self.on_receive_fuzz_test_packet(&mut fuzz)?, - OpenTMKPacket::Ack(a) => self.on_recieve_ack_packet(&a)?, - OpenTMKPacket::Error(a) => self.on_recieve_error_packet(&a)?, + OpenTMKPacket::Ack(a) => self.on_receive_ack_packet(&a)?, + OpenTMKPacket::Error(a) => self.on_receive_error_packet(&a)?, }; if let Some(resp) = response_pkt { @@ -148,7 +148,7 @@ impl Executor { } } - pub fn on_recieve_ack_packet( + pub fn on_receive_ack_packet( &mut self, _: &OpenTMKAckPacket, ) -> Result, ExecutorError> { @@ -156,7 +156,7 @@ impl Executor { Err(ExecutorError::UnexpectedPacketReceived) } - pub fn on_recieve_error_packet( + pub fn on_receive_error_packet( &mut self, _: &OpenTMKErrorPacket, ) -> Result, ExecutorError> { diff --git a/opentmk/opentmk_invariant/src/executor/test.rs b/opentmk/opentmk_invariant/src/executor/test.rs index 626c7700479..bbd6070dbe0 100644 --- a/opentmk/opentmk_invariant/src/executor/test.rs +++ b/opentmk/opentmk_invariant/src/executor/test.rs @@ -163,13 +163,13 @@ fn unexpected_ack_and_error_packets_are_rejected() { assert_eq!( executor - .on_recieve_ack_packet(&OpenTMKAckPacket { code: 0 }) + .on_receive_ack_packet(&OpenTMKAckPacket { code: 0 }) .expect_err("ack packets should be rejected"), ExecutorError::UnexpectedPacketReceived ); assert_eq!( executor - .on_recieve_error_packet(&OpenTMKErrorPacket { + .on_receive_error_packet(&OpenTMKErrorPacket { message: String::from("boom"), }) .expect_err("error packets should be rejected"), diff --git a/opentmk/opentmk_invariant/src/functions/hyperv.rs b/opentmk/opentmk_invariant/src/functions/hyperv.rs index 80e9f351e45..864176d9315 100644 --- a/opentmk/opentmk_invariant/src/functions/hyperv.rs +++ b/opentmk/opentmk_invariant/src/functions/hyperv.rs @@ -11,6 +11,8 @@ use opentmk_core::context::HypercallPlatformTrait; use opentmk_core::platform::hyperv::ctx::{HvTestCtx, HyperVHypercallConfig}; use spin::Mutex; +const HVCALL_SANE_LIMIT: usize = 0x100000; + /// Future-proof for future multi-VP usage to ensure writing to input_page and /// then dispatching the hypercall is done in one shot. Today we are running /// single-threaded, and this is mainly used to keep rust happy. @@ -50,6 +52,11 @@ pub fn hvcall( *init = true; } + if input_len > HVCALL_SANE_LIMIT { + log::error!("hvcall: input_len is too large: {input_len} > {HVCALL_SANE_LIMIT}"); + return Ok(FuzzFunctionVariable::Void); + } + let mut in_args = vec![0; input_len]; match mem.try_read_mem(input, &mut in_args) { Ok(_) => (), From 19725e8d468b4b3540039ba05b39b9e5d4dbd552 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:17:21 -0700 Subject: [PATCH 19/32] fix build for non-x86 arch builds --- opentmk/opentmk_invariant/src/comms/mod.rs | 45 +---------- opentmk/opentmk_invariant/src/executor/mod.rs | 39 +++++---- .../opentmk_invariant/src/functions/mod.rs | 1 + opentmk/opentmk_invariant/src/main.rs | 3 +- opentmk/opentmk_invariant/src/serial.rs | 80 +++++++++++++++++++ 5 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 opentmk/opentmk_invariant/src/serial.rs diff --git a/opentmk/opentmk_invariant/src/comms/mod.rs b/opentmk/opentmk_invariant/src/comms/mod.rs index d534f974954..5fae46631cb 100644 --- a/opentmk/opentmk_invariant/src/comms/mod.rs +++ b/opentmk/opentmk_invariant/src/comms/mod.rs @@ -4,7 +4,10 @@ #[cfg(test)] pub(crate) mod test; -use crate::executor::ExecutorError; +use crate::{ + executor::ExecutorError, + serial::{OpenTmkSerialIo, SerialIo, SerialPort}, +}; use inv_packet::{ COMMS_ACK_MAGIC, COMMS_PACKET_FOOTER_MAGIC, COMMS_PACKET_HEADER_MAGIC, COMMS_SYN_ACK_MAGIC, COMMS_SYN_MAGIC, OpenTMKPacket, @@ -13,46 +16,6 @@ use inv_packet::{ #[cfg_attr(not(target_os = "uefi"), expect(unused_imports))] use crate::prelude::*; -use opentmk_core::arch::serial::{InstrIoAccess, Serial, SerialPort}; - -pub(crate) trait SerialIo { - fn init(&mut self); - fn drain(&mut self); - fn write_byte(&mut self, byte: u8); - fn read_byte(&mut self) -> u8; -} - -pub(crate) struct OpenTmkSerialIo { - handle: Serial, -} - -impl OpenTmkSerialIo { - fn new(port: SerialPort) -> Self { - log::info!("creating serial port"); - Self { - handle: Serial::new(port, InstrIoAccess), - } - } -} - -impl SerialIo for OpenTmkSerialIo { - fn init(&mut self) { - self.handle.init(); - } - - fn drain(&mut self) { - self.handle.drain(); - } - - fn write_byte(&mut self, byte: u8) { - self.handle.write_byte(byte); - } - - fn read_byte(&mut self) -> u8 { - self.handle.read_byte() - } -} - pub(crate) struct SerialCommsServer { pub(crate) handle: T, connected: bool, diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index acaa09edfae..36e56187885 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -3,17 +3,20 @@ use crate::prelude::*; use crate::{ - comms::{OpenTmkSerialIo, SerialCommsServer, SerialIo}, + comms::SerialCommsServer, deserializer::{Deserializer, syzlang::SyzlangDeserializer}, - functions::{FunctionRegistry, FuzzFunction, hyperv, io_port}, + functions::{FunctionRegistry, hyperv}, + serial::{OpenTmkSerialIo, SerialIo, SerialPort}, }; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use crate::functions::io_port; + use inv_packet::{ OpenTMKAckPacket, OpenTMKConfigurationPacket, OpenTMKErrorPacket, OpenTMKFuzzTest, OpenTMKGrammarDeserializer, OpenTMKPacket, }; -use opentmk_core::arch::serial::SerialPort; use spin::Mutex; #[cfg(test)] @@ -63,19 +66,25 @@ impl Executor { } pub fn register_fuzz_functions(&mut self) { - static REGISTRY: &[(&str, FuzzFunction)] = &[ - ("port_write8", io_port::write_ioport_u8), - ("port_write16", io_port::write_ioport_u16), - ("port_write32", io_port::write_ioport_u32), - ("port_read8", io_port::read_ioport_u8), - ("port_read16", io_port::read_ioport_u16), - ("port_read32", io_port::read_ioport_u32), - ("hvcall", hyperv::hvcall), - ]; let mut fn_registry = self.fn_registry.lock(); - for (name, func) in REGISTRY { - fn_registry.register(name, *func); - } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + static X86_REGISTRY: &[(&str, crate::functions::FuzzFunction)] = &[ + ("port_write8", io_port::write_ioport_u8), + ("port_write16", io_port::write_ioport_u16), + ("port_write32", io_port::write_ioport_u32), + ("port_read8", io_port::read_ioport_u8), + ("port_read16", io_port::read_ioport_u16), + ("port_read32", io_port::read_ioport_u32), + ]; + + for (name, func) in X86_REGISTRY { + fn_registry.register(name, *func); + } + }; + + fn_registry.register("hvcall", hyperv::hvcall); } pub fn run(&mut self) -> Result<(), ExecutorError> { diff --git a/opentmk/opentmk_invariant/src/functions/mod.rs b/opentmk/opentmk_invariant/src/functions/mod.rs index 5264538b1e6..0f97d218475 100644 --- a/opentmk/opentmk_invariant/src/functions/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/mod.rs @@ -3,6 +3,7 @@ pub mod hvcall_meta; pub mod hyperv; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub mod io_port; mod registry; pub mod variable; diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs index e1b67c08a86..7ca180c4604 100644 --- a/opentmk/opentmk_invariant/src/main.rs +++ b/opentmk/opentmk_invariant/src/main.rs @@ -16,10 +16,11 @@ mod executor; mod functions; mod prelude; mod rt; +mod serial; use crate::executor::Executor; -use opentmk_core::arch::serial::SerialPort; +use serial::SerialPort; #[cfg(target_os = "uefi")] use uefi::println; diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs new file mode 100644 index 00000000000..59cc115cd90 --- /dev/null +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -0,0 +1,80 @@ +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub use opentmk_core::arch::serial::SerialPort; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use opentmk_core::arch::serial::{InstrIoAccess, Serial}; + +/// Copy of the x86 serial ports, used as a polyfill for those architectures +/// that are not currently supported yet +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +#[expect(unused)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SerialPort { + COM1, + COM2, + COM3, + COM4, +} + +pub(crate) trait SerialIo { + fn init(&mut self); + fn drain(&mut self); + fn write_byte(&mut self, byte: u8); + fn read_byte(&mut self) -> u8; +} + +pub(crate) struct OpenTmkSerialIo { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + handle: Serial, +} + +impl OpenTmkSerialIo { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + pub fn new(port: SerialPort) -> Self { + log::info!("creating serial port"); + Self { + handle: Serial::new(port, InstrIoAccess), + } + } + + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + pub fn new(_port: SerialPort) -> Self { + log::info!("creating serial port (dummy)"); + Self {} + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +impl SerialIo for OpenTmkSerialIo { + fn init(&mut self) { + self.handle.init(); + } + + fn drain(&mut self) { + self.handle.drain(); + } + + fn write_byte(&mut self, byte: u8) { + self.handle.write_byte(byte); + } + + fn read_byte(&mut self) -> u8 { + self.handle.read_byte() + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +impl SerialIo for OpenTmkSerialIo { + fn init(&mut self) {} + + fn drain(&mut self) { + todo!() + } + + fn write_byte(&mut self, _byte: u8) { + todo!() + } + + fn read_byte(&mut self) -> u8 { + todo!() + } +} From 000c8aa9aacce3ce6801fa62c77491e1d5be8788 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:32:29 -0700 Subject: [PATCH 20/32] round two of fixing PR suggestions --- opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs | 4 ++-- opentmk/opentmk_invariant/src/functions/io_port/mod.rs | 6 +++++- opentmk/opentmk_invariant/src/functions/registry.rs | 2 +- opentmk/opentmk_invariant/src/prelude.rs | 4 ++-- opentmk/opentmk_invariant/src/serial.rs | 6 +++++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index 77dbc231025..576f6e2ef2b 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -51,13 +51,13 @@ impl SyzlangState { match self .function_registry .lock() - .exec(mem, handler_name.clone(), input) + .exec(mem, &handler_name, input) { FuzzFunctionVariable::Void => (), FuzzFunctionVariable::Int(_) => (), // TODO FuzzFunctionVariable::Error(e) => { let error_str = format!("Error recorded in function: {handler_name} error: {e}"); - log::error!("{error_str:?}"); + log::error!("{error_str}"); self.error_list.push(error_str); return InputResult { diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index e05d13edad2..16643c28954 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -14,7 +14,11 @@ use inv_decoder::SafeMemoryMap; use opentmk_core::arch; fn validate_ioport(port: u16) -> Option { - if (0x3E8..0x3F0).contains(&port) || (0x2E8..0x300).contains(&port) { + if (0x3E8..0x3F0).contains(&port) // COM3 + || (0x2E8..0x2F0).contains(&port) // COM4 + || (0x2F8..0x300).contains(&port) // COM2 + // Ignores COM1 because we need that for fuzzer + { Some(port) } else { None diff --git a/opentmk/opentmk_invariant/src/functions/registry.rs b/opentmk/opentmk_invariant/src/functions/registry.rs index b38aed8c81c..4d64ce6f974 100644 --- a/opentmk/opentmk_invariant/src/functions/registry.rs +++ b/opentmk/opentmk_invariant/src/functions/registry.rs @@ -29,7 +29,7 @@ impl FunctionRegistry { pub fn exec( &self, mem: &mut dyn SafeMemoryMap, - function_name: String, + function_name: &str, input: Vec, ) -> FuzzFunctionVariable { let mut func: Option<&RegisteredFn> = None; diff --git a/opentmk/opentmk_invariant/src/prelude.rs b/opentmk/opentmk_invariant/src/prelude.rs index d1f16f3bd94..c97036a81e0 100644 --- a/opentmk/opentmk_invariant/src/prelude.rs +++ b/opentmk/opentmk_invariant/src/prelude.rs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! This is a extended prelude crate that imports a number of common rust API entities that -//! would've been imported from the `alloc` crate. +//! This is an extended prelude crate that imports a number of common rust API entities that +//! would otherwise be imported from the `alloc` crate. #[cfg(target_os = "uefi")] extern crate alloc; #[cfg(target_os = "uefi")] diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs index 59cc115cd90..fcca81cae1c 100644 --- a/opentmk/opentmk_invariant/src/serial.rs +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -62,9 +62,13 @@ impl SerialIo for OpenTmkSerialIo { } } +// Dummy transport for non x86 serial specifically to allow compilation to +// take place. Crashes so that we flag this issue early on. #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] impl SerialIo for OpenTmkSerialIo { - fn init(&mut self) {} + fn init(&mut self) { + todo!() + } fn drain(&mut self) { todo!() From ee90239eeb800b8f303de9cf84675a24c31b7a8c Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:35:35 -0700 Subject: [PATCH 21/32] formatting --- opentmk/opentmk_invariant/src/functions/io_port/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index 16643c28954..749d28bc119 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -15,10 +15,10 @@ use opentmk_core::arch; fn validate_ioport(port: u16) -> Option { if (0x3E8..0x3F0).contains(&port) // COM3 - || (0x2E8..0x2F0).contains(&port) // COM4 - || (0x2F8..0x300).contains(&port) // COM2 - // Ignores COM1 because we need that for fuzzer + || /* COM4 */ (0x2E8..0x2F0).contains(&port) + || /* COM2 */ (0x2F8..0x300).contains(&port) { + // Ignores COM1 because we need that for fuzzer Some(port) } else { None From 8bf5e0240e50f6dd17383e7e2962cc7e85afbf2c Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:43:16 -0700 Subject: [PATCH 22/32] fix breaking errors for deserializer --- .../opentmk_invariant/src/deserializer/syzlang/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index 576f6e2ef2b..68021312280 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -34,10 +34,13 @@ impl SyzlangState { ) -> InputResult { // resolve the call number to a pseudo syscall if self.glob_mapping.len() as u64 <= input_struct.call_num { - log::error!( - "[fatal] invalid call number {} received from syzlang", + let error_str = format!( + "invalid call number {} received from syzlang", input_struct.call_num, ); + log::error!("{error_str}"); + self.error_list.push(error_str); + return InputResult { code: SYZLANG_DESERIALIZER_ERROR_CODE, name: String::default(), //never used! @@ -56,7 +59,7 @@ impl SyzlangState { FuzzFunctionVariable::Void => (), FuzzFunctionVariable::Int(_) => (), // TODO FuzzFunctionVariable::Error(e) => { - let error_str = format!("Error recorded in function: {handler_name} error: {e}"); + let error_str = format!("{handler_name}: {e}"); log::error!("{error_str}"); self.error_list.push(error_str); From 31c4cacbe462c452f6204ad309b0ce2d13772a44 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:46:06 -0700 Subject: [PATCH 23/32] justify target_arch --- opentmk/opentmk_invariant/src/executor/mod.rs | 6 ++---- opentmk/opentmk_invariant/src/functions/mod.rs | 2 +- opentmk/opentmk_invariant/src/serial.rs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index 36e56187885..b86b6ce014c 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -9,9 +9,6 @@ use crate::{ serial::{OpenTmkSerialIo, SerialIo, SerialPort}, }; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use crate::functions::io_port; - use inv_packet::{ OpenTMKAckPacket, OpenTMKConfigurationPacket, OpenTMKErrorPacket, OpenTMKFuzzTest, OpenTMKGrammarDeserializer, OpenTMKPacket, @@ -68,8 +65,9 @@ impl Executor { pub fn register_fuzz_functions(&mut self) { let mut fn_registry = self.fn_registry.lock(); - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate { + use crate::functions::io_port; static X86_REGISTRY: &[(&str, crate::functions::FuzzFunction)] = &[ ("port_write8", io_port::write_ioport_u8), ("port_write16", io_port::write_ioport_u16), diff --git a/opentmk/opentmk_invariant/src/functions/mod.rs b/opentmk/opentmk_invariant/src/functions/mod.rs index 0f97d218475..16855b69a15 100644 --- a/opentmk/opentmk_invariant/src/functions/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/mod.rs @@ -3,7 +3,7 @@ pub mod hvcall_meta; pub mod hyperv; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate pub mod io_port; mod registry; pub mod variable; diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs index fcca81cae1c..5ba0532d45c 100644 --- a/opentmk/opentmk_invariant/src/serial.rs +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -1,11 +1,11 @@ -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate pub use opentmk_core::arch::serial::SerialPort; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate use opentmk_core::arch::serial::{InstrIoAccess, Serial}; /// Copy of the x86 serial ports, used as a polyfill for those architectures /// that are not currently supported yet -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate #[expect(unused)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum SerialPort { @@ -23,12 +23,12 @@ pub(crate) trait SerialIo { } pub(crate) struct OpenTmkSerialIo { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate handle: Serial, } impl OpenTmkSerialIo { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate pub fn new(port: SerialPort) -> Self { log::info!("creating serial port"); Self { @@ -36,14 +36,14 @@ impl OpenTmkSerialIo { } } - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate pub fn new(_port: SerialPort) -> Self { log::info!("creating serial port (dummy)"); Self {} } } -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate impl SerialIo for OpenTmkSerialIo { fn init(&mut self) { self.handle.init(); @@ -64,7 +64,7 @@ impl SerialIo for OpenTmkSerialIo { // Dummy transport for non x86 serial specifically to allow compilation to // take place. Crashes so that we flag this issue early on. -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate impl SerialIo for OpenTmkSerialIo { fn init(&mut self) { todo!() From caf70f2984dcd54040c16a7ec49d3875e578ba13 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:46:57 -0700 Subject: [PATCH 24/32] Add copyright header --- opentmk/opentmk_invariant/src/serial.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs index 5ba0532d45c..882dfb7adc8 100644 --- a/opentmk/opentmk_invariant/src/serial.rs +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate pub use opentmk_core::arch::serial::SerialPort; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate From f5382a54bb145982b74210c0139e10fa4a637203 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 10:53:40 -0700 Subject: [PATCH 25/32] fix more suggestions --- opentmk/opentmk_invariant/src/functions/io_port/mod.rs | 4 ++-- opentmk/opentmk_invariant/src/main.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs index 749d28bc119..76f9a5f0137 100644 --- a/opentmk/opentmk_invariant/src/functions/io_port/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/io_port/mod.rs @@ -89,14 +89,14 @@ pub fn read_ioport_u16( let port = validate_ioport(port.expect_int("Port")? as u16); let value = value.expect_int("Value")? as usize; if let Some(port) = port { - // Perform and write inh result + // Perform and write inw result let res = unsafe { // SAFETY: this is called within a fuzzer context. We assume all unsafe risks arch::io::inw(port) }; if let Err(e) = mem.try_write_mem(value, &res.to_le_bytes()) { - log::warn!("Failed to write inh output: {e}") + log::warn!("Failed to write inw output: {e}") } } Ok(FuzzFunctionVariable::Void) diff --git a/opentmk/opentmk_invariant/src/main.rs b/opentmk/opentmk_invariant/src/main.rs index 7ca180c4604..092de97eb0f 100644 --- a/opentmk/opentmk_invariant/src/main.rs +++ b/opentmk/opentmk_invariant/src/main.rs @@ -4,7 +4,7 @@ //! This is the main entrypoint for opentmk_invariant. //! //! Opentmk Invariant is a bare-bones operating system based off of opentmk -//! framework used to accept customly crafted program consisting of an encoded +//! framework used to accept custom-crafted program consisting of an encoded //! series of functions that would invoke specific functions into this OS #![cfg_attr(target_os = "uefi", no_main)] From 2e3202a69b068c015f7e2769c521d63600d19cde Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:05:05 -0700 Subject: [PATCH 26/32] remove anyhow usages --- Cargo.lock | 1 - Cargo.toml | 2 +- flowey/flowey_hvlite/Cargo.toml | 2 +- opentmk/inv_decoder/Cargo.toml | 1 - opentmk/inv_decoder/src/lib.rs | 293 +++++++++--------- .../src/deserializer/syzlang/mod.rs | 2 +- xtask/Cargo.toml | 2 +- 7 files changed, 155 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5fcd4cdd7cf..92256478657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4045,7 +4045,6 @@ dependencies = [ name = "inv_decoder" version = "0.0.0" dependencies = [ - "anyhow", "num-traits", "spin", "zerocopy", diff --git a/Cargo.toml b/Cargo.toml index c6977167151..af4c1a65606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -473,7 +473,7 @@ vnc_worker_defs = { path = "workers/vnc_worker_defs" } # crates.io # --- Error handling --- -anyhow = { version = "1.0", default-features = false } +anyhow = "1.0" thiserror = { version = "2", default-features = false } # --- Logging / tracing --- diff --git a/flowey/flowey_hvlite/Cargo.toml b/flowey/flowey_hvlite/Cargo.toml index a20cc7d068a..314058d0a53 100644 --- a/flowey/flowey_hvlite/Cargo.toml +++ b/flowey/flowey_hvlite/Cargo.toml @@ -17,7 +17,7 @@ petri_artifacts_core.workspace = true petri_artifacts_vmm_test.workspace = true vmm_test_images = { workspace = true, features = ["serde", "clap"] } -anyhow = { workspace = true, features = ["std"] } +anyhow.workspace = true clap = { workspace = true, features = ["derive"] } log.workspace = true paste.workspace = true diff --git a/opentmk/inv_decoder/Cargo.toml b/opentmk/inv_decoder/Cargo.toml index 9bf7d7409fb..18bdf0fc55f 100644 --- a/opentmk/inv_decoder/Cargo.toml +++ b/opentmk/inv_decoder/Cargo.toml @@ -9,7 +9,6 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow.workspace = true num-traits.workspace = true spin.workspace = true zerocopy.workspace = true diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 14f1575f7b2..14782a4cb2f 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -23,8 +23,7 @@ use core::{ }; use alloc::{string::String, sync::Arc, vec::Vec}; -use anyhow::{Context, Result, bail}; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{FromBytes, Immutable, IntoBytes, SizeError}; use crate::atomicrefqueue::AtomicRefQueue; @@ -52,6 +51,62 @@ pub type TestcaseResults = [ResT; K_MAX_COMMANDS]; pub type Executor<'f> = dyn Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + 'f + Send + Sync; +/// An enumeration of possible decoder errors +#[derive(Debug)] +pub enum DecoderError { + /// A bad magic value for the program header + BadMagic(u64), + /// The program size is zero + ZeroProgSize, + /// The program size exceeds the remaining size of the buffer + ProgSizeExceedsBuffer, + /// A truncated read occurred + TruncRead { + /// The context of the value being read + context: &'static str, + /// The number of bytes expected to read + expected: usize, + /// The actual number of bytes read + got: usize, + }, + /// A bad instruction code + BadInstruction(i64), + /// A bad argument code + BadArgType(u64), + /// The data of an argument is zero size + ZeroDataSize, + /// copy_in: a bitmask is non-zero for string binary format + CopyInUnsupportedStringMask, + /// copy_in: bad size value for a particular binary format + CopyInBadSize { + /// The binary format code + bf: u64, + /// The size that was set for this + size: u64, + }, + /// copy_in: bad format type + CopyInBadFormat(u64), + /// copy_out: bad size value + CopyOutBadSize(u64), + /// A data argument is passed to a function call argument directly + /// (unsupported) + UnsupportedArgData, + /// The copyout_index overflows/underflows K_MAX_COMMANDS + OverflowOutIndex(usize), + /// Some other error not captured here + Other(String), +} + +impl DecoderError { + fn trunc(context: &'static str, value: SizeError<&[u8], D>) -> Self { + Self::TruncRead { + context, + expected: size_of::(), + got: value.into_src().len(), + } + } +} + /// Instructions for a decoded syzkaller program alongside the required /// components to execute the program (e.g. the exec function and the results array). /// @@ -97,14 +152,13 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { mem: M, syz_input_buffer: &[u8], exec: F, - ) -> Result { - let mut decoder = - Decoder::new(syz_input_buffer).context("failed to instantiate decoder")?; + ) -> Result { + let mut decoder = Decoder::new(syz_input_buffer)?; let mut instr_vec_tmp: Vec = Vec::new(); { let mut call = None; let mut instrout: Vec = Vec::new(); - while let Some(instr) = decoder.try_next().context("failed to decode instruction")? { + while let Some(instr) = decoder.try_next()? { match instr { Instr::CopyOut(i) => { // CopyOuts always follow a call and are tied to calls, so we collect them @@ -179,7 +233,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { /// Check if the provided call was executed and returned success, based on the contents /// of the provided results array. - fn was_call_successful(&self, call: &InstrCall) -> bool { + fn was_call_successful(&self, call: &InstrCall) -> Result { let (copyout_index, results) = if call.wire.copyout_index != wire::COPYOUT_INDEX_INVALID as u64 { (call.wire.copyout_index as usize, &self.results) @@ -192,20 +246,17 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { }; if copyout_index >= K_MAX_COMMANDS { - panic!( - "copyout_call_results: result idx {:#x} overflows/underflows K_MAX_COMMANDS", - copyout_index - ); + return Err(DecoderError::OverflowOutIndex(copyout_index)); } // Get the result value from the results array. let r = &results[copyout_index]; - r.was_successful() + Ok(r.was_successful()) } /// Continues execution of the instructions contains in our own instruction vector - pub fn continue_execution(&self) -> Result<()> { + pub fn continue_execution(&self) -> Result<(), DecoderError> { // SAFETY: This should always be sound because we cannot reach this point without having a valid // execution function. // We can _only_ take an immutable borrow here because this function is reentrant and we may have @@ -214,17 +265,15 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { while let Some(entry) = instr_vec.pop_ref_conditional(|ent| self.is_instr_call_ready(ent)) { // Execute all the instructions, regardless of their type let instr_target = entry.get_inner_instr(); - self.exec_single(instr_target) - .context("failed to execute instruction")?; + self.exec_single(instr_target)?; // If the instr was a Call, it may have copyouts to execute. if let InstrEntry::Call(instr_call, copyouts) = entry { // Only execute copyouts if the call itself was successful, otherwise the values being // copied out may be invalid. - if self.was_call_successful(instr_call) { + if self.was_call_successful(instr_call)? { for copyout_entry in copyouts.iter() { - self.exec_single(Instr::CopyOut(*copyout_entry)) - .context("failed to execute instruction")?; + self.exec_single(Instr::CopyOut(*copyout_entry))?; } } } // No copyouts to process if the instruction was not a call @@ -234,21 +283,19 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { } /// Executes the instructions in the provided instruction vector. - fn exec_instrs(&self) -> Result<()> { + fn exec_instrs(&self) -> Result<(), DecoderError> { let instr_vec = self.instr_vec.clone(); while let Some(entry) = instr_vec.pop_ref_conditional(|ent| self.is_instr_call_ready(ent)) { // Execute all the instructions, regardless of their type - self.exec_single(entry.get_inner_instr()) - .context("failed to execute instruction")?; + self.exec_single(entry.get_inner_instr())?; // If the instr was a Call, it may have copyouts to execute. if let InstrEntry::Call(instr_call, copyouts) = entry { // Only execute copyouts if the call itself was successful, otherwise the values being // copied out may be invalid. - if self.was_call_successful(instr_call) { + if self.was_call_successful(instr_call)? { for copyout_entry in copyouts.iter() { - self.exec_single(Instr::CopyOut(*copyout_entry)) - .context("failed to execute instruction")?; + self.exec_single(Instr::CopyOut(*copyout_entry))?; } } } // No copyouts to process if the instruction was not a call @@ -260,7 +307,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { /// Executes the provided instruction. /// If the instruction is a call, the provided exec function is called with the provided input case /// and the results are stored in the results array if the call has a valid copyout index. - fn exec_single(&self, instr: Instr) -> Result<()> { + fn exec_single(&self, instr: Instr) -> Result<(), DecoderError> { /// The number of bytes to offset all data operations by. const COPYIN_OFFSET: u64 = 0; @@ -282,7 +329,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { bf, bf_off, bf_len, - ); + )?; } Arg::Result(a) => { let size = a.meta & 0xff; @@ -297,7 +344,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { a.arg }; - copyin(&mut *mem, i.wire.addr + COPYIN_OFFSET, val, size, bf, 0, 0); + copyin(&mut *mem, i.wire.addr + COPYIN_OFFSET, val, size, bf, 0, 0)?; } Arg::Data((a, d)) => { mem.write_mem( @@ -309,7 +356,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { Instr::CopyOut(i) => { let mut val = 0u64; - copyout(&mut *mem, i.wire.addr, i.wire.size, &mut val); + copyout(&mut *mem, i.wire.addr, i.wire.size, &mut val)?; let r = &self.results[i.wire.index as usize]; // Its assumed if we're executing a CopyOut, that the associated call was @@ -345,7 +392,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { args[n] = val; } - Arg::Data(_) => panic!("data argument for function call!?"), + Arg::Data(_) => return Err(DecoderError::UnsupportedArgData), } } @@ -378,10 +425,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { }; if copyout_index >= K_MAX_COMMANDS { - panic!( - "copyout_call_results: result idx {:#x} overflows/underflows K_MAX_COMMANDS", - copyout_index - ); + return Err(DecoderError::OverflowOutIndex(copyout_index)); } let r = &results[copyout_index]; @@ -470,22 +514,15 @@ struct Decoder<'a> { } impl<'a> Decoder<'a> { - fn new(mut buf: &'a [u8]) -> Result { - let hdr = read_struct::(&mut buf) - .context("failed to read program header")?; + fn new(mut buf: &'a [u8]) -> Result { + let hdr = read_struct::("program header", &mut buf)?; if hdr.magic != 0xbadc0ffeebadface { - bail!("bad execute request magic {:#?}", hdr.magic); + return Err(DecoderError::BadMagic(hdr.magic)); } else if hdr.prog_size == 0 { - bail!("prog size is 0"); - } else if hdr.prog_size - > ((SUPPORTED_INPUT_SIZE as u64) - (size_of::() as u64)) - { - bail!("input size too large! InputSz:{:#?}", hdr.prog_size); - } - - if hdr.prog_size > buf.len() as u64 { - bail!("syzkaller program is larger than input buffer"); + return Err(DecoderError::ZeroProgSize); + } else if hdr.prog_size > buf.len() as u64 { + return Err(DecoderError::ProgSizeExceedsBuffer); } Ok(Self { @@ -497,45 +534,30 @@ impl<'a> Decoder<'a> { } /// Fetch and decode the next instruction. Returns `None` if we have reached EOF. - pub fn try_next(&mut self) -> Result> { - let n = read_inc_input(&mut self.buf).context("unexpected end of input")? as i64; + pub fn try_next(&mut self) -> Result, DecoderError> { + let n = read_inc_input("instruction type", &mut self.buf)? as i64; match n { wire::INSTR_EOF => Ok(None), - wire::INSTR_COPYIN => { - let insn = read_struct::(&mut self.buf) - .context("failed to read instruction")?; + wire::INSTR_COPYIN => Ok(Some(Instr::CopyIn(InstrCopyIn { + rpid: self.hdr.pid, + wire: read_struct("instruction", &mut self.buf)?, + arg: read_arg(&mut self.buf)?, + }))), - let arg = read_arg(&mut self.buf).context("failed to read argument")?; + wire::INSTR_COPYOUT => Ok(Some(Instr::CopyOut(InstrCopyOut { + rpid: self.hdr.pid, + wire: read_struct("instruction", &mut self.buf)?, + }))), - Ok(Some(Instr::CopyIn(InstrCopyIn { - rpid: self.hdr.pid, - wire: insn, - arg, - }))) - } - - wire::INSTR_COPYOUT => { - let insn = read_struct::(&mut self.buf) - .context("failed to read instruction")?; - - Ok(Some(Instr::CopyOut(InstrCopyOut { - rpid: self.hdr.pid, - wire: insn, - }))) - } - - c if c < 0 => { - bail!("unknown instruction {n}"); - } + c if c < 0 => Err(DecoderError::BadInstruction(n)), _ => { - let insn = read_struct::(&mut self.buf) - .context("failed to read instruction")?; + let insn = read_struct::("instruction", &mut self.buf)?; let mut args = Vec::new(); for _i in 0..insn.num_args { - args.push(read_arg(&mut self.buf).context("failed to read argument")?); + args.push(read_arg(&mut self.buf)?); } let custom_copyout_index = if insn.copyout_index == wire::COPYOUT_INDEX_INVALID as u64 { @@ -633,49 +655,39 @@ impl ResT { } /// Read a 64-bit little-endian word from a slice and advance the slice's pointer. -/// -/// This will panic if the slice pointed to by `input_data` is smaller than 8 bytes. -fn read_inc_input(input_data: &mut &[u8]) -> Option { - let (s, t) = u64::read_from_prefix(input_data).ok()?; +fn read_inc_input(ctx: &'static str, input_data: &mut &[u8]) -> Result { + let (s, t) = u64::read_from_prefix(input_data).map_err(|e| DecoderError::trunc(ctx, e))?; *input_data = t; - Some(s) + Ok(s) } /// Read a struct out of `input_data` and advance the slice's pointer. -fn read_struct(input_data: &mut &[u8]) -> Option { - let (s, t) = T::read_from_prefix(input_data).ok()?; +fn read_struct(ctx: &'static str, input_data: &mut &[u8]) -> Result { + let (s, t) = T::read_from_prefix(input_data).map_err(|e| DecoderError::trunc(ctx, e))?; *input_data = t; - Some(s) + Ok(s) } /// Read out an argument from an input buffer. -fn read_arg(buf: &mut &[u8]) -> Result { - let typ = read_inc_input(buf).context("unexpected end of input")?; +fn read_arg(buf: &mut &[u8]) -> Result { + let typ = read_inc_input("argument type", buf)?; Ok(match typ { - wire::ARG_CONST => { - let arg = read_struct::(buf).context("failed to read argument")?; - - Arg::Const(arg) - } - wire::ARG_RESULT => { - let arg = read_struct::(buf).context("failed to read argument")?; - - Arg::Result(arg) - } + wire::ARG_CONST => Arg::Const(read_struct("argument", buf)?), + wire::ARG_RESULT => Arg::Result(read_struct("argument", buf)?), wire::ARG_DATA => { - let arg = read_struct::(buf).context("failed to read argument")?; + let arg = read_struct::("argument", buf)?; // Read out each data word. let cnt = arg.size.div_ceil(8); if cnt == 0 { - panic!("data argument with size of zero!?"); + return Err(DecoderError::ZeroDataSize); } let mut words = Vec::new(); for _i in 0..cnt { - words.push(read_inc_input(buf).context("unexpected end of input")?); + words.push(read_inc_input("argument data", buf)?); } Arg::Data((arg, words)) @@ -683,7 +695,7 @@ fn read_arg(buf: &mut &[u8]) -> Result { // arg_csum 0x3 => todo!(), // Catchall - _ => bail!("unsupported argument type: {typ}"), + _ => return Err(DecoderError::BadArgType(typ)), }) } @@ -728,9 +740,9 @@ fn copyin( bf: u64, bf_off: u64, bf_len: u64, -) { +) -> Result<(), DecoderError> { if bf != 0 && (bf_off != 0 || bf_len != 0) { - panic!("copyin: bitmask for string format invalid"); + return Err(DecoderError::CopyInUnsupportedStringMask); } let tmp_str = match bf { @@ -741,69 +753,66 @@ fn copyin( 2 => store_by_bitmask(mem, addr, val as u16, bf_off, bf_len), 4 => store_by_bitmask(mem, addr, val as u32, bf_off, bf_len), 8 => store_by_bitmask(mem, addr, val, bf_off, bf_len), - _ => { - panic!("copyin: bad argument size {}", size); - } + _ => return Err(DecoderError::CopyInBadSize { bf, size }), } - return; + return Ok(()); } // Case: binary_format_bigendian 1 => panic!("unhandled: bigendian binary format"), // Case: binary_format_strdec - 2 => { - // Converts 0xffffffffffffffff into 34343736`34343831 - // 35353930`37333730 00000000`35313631 - // TODO: verify endianness & correctness and re-assess implementation for perf - assert!(size == 20); - format!("{val:020}") - } + // Converts 0xffffffffffffffff into 34343736`34343831 + // 35353930`37333730 00000000`35313631 + // TODO: verify endianness & correctness and re-assess implementation for perf + 2 => format!("{val:020}"), // Case: binary_format_strhex - 3 => { - // Stores ascii of hex, e.g val 0xffffffffffffffff turns - // into 0x66666666`66667830 66666666`66666666 - // TODO: verify endianness & correctness and re-assess implementation for perf - assert!(size == 18); - format!("{val:#018x}") - } + // Stores ascii of hex, e.g val 0xffffffffffffffff turns + // into 0x66666666`66667830 66666666`66666666 + // TODO: verify endianness & correctness and re-assess implementation for perf + 3 => format!("{val:#018x}"), // Case: binary_format_stroct - 4 => { - // turns 0xffffffffffffffff into 37373737`37373130 - // 37373737`37373737 00373737`37373737 - // TODO: verify endianness & correctness and re-assess implementation for perf - assert!(size == 23); - format!("{val:0023o}") - } + // turns 0xffffffffffffffff into 37373737`37373130 + // 37373737`37373737 00373737`37373737 + // TODO: verify endianness & correctness and re-assess implementation for perf + 4 => format!("{val:0023o}"), - _ => { - panic!("copyin: unknown binary format {}", bf); - } + _ => return Err(DecoderError::CopyInBadFormat(bf)), }; - assert!(tmp_str.len() == size as usize); + if size as usize != tmp_str.len() { + return Err(DecoderError::CopyInBadSize { bf, size }); + } - mem.write_mem(addr as usize, tmp_str.as_bytes()) + mem.try_write_mem(addr as usize, tmp_str.as_bytes()) + .map_err(DecoderError::Other) } -fn copyout(mem: &mut M, addr: u64, size: u64, res: &mut u64) { +fn copyout( + mem: &mut M, + addr: u64, + size: u64, + res: &mut u64, +) -> Result<(), DecoderError> { // NB: this code makes an assumption that we are working with LSB only architectures const _STATIC_ASSERT_IS_LSB: [u8; (1 - u16::from_le_bytes([1, 0])) as usize] = []; // if this errors we are compiling to an MSB arch let mut buf = [0; 8]; let readlen = size.min(8) as usize; - mem.read_mem(addr as usize, &mut buf[0..readlen]); + mem.try_read_mem(addr as usize, &mut buf[0..readlen]) + .map_err(DecoderError::Other)?; match size { 1 => (), 2 => (), 4 => (), 8 => (), - _ => panic!("copyout: bad argument size {:#x}", size), + _ => return Err(DecoderError::CopyOutBadSize(size)), } *res = u64::from_le_bytes(buf); + Ok(()) } /// Takes a syz_in buffer containing raw syzkaller data from TKO, parses @@ -818,7 +827,7 @@ pub fn exec_testcases_safe( syz_exec_mem: M, syz_input_buffer: &mut [u8], exec: F, -) -> Result +) -> Result where F: Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + Send + Sync, { @@ -1028,7 +1037,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); let expected: [u8; 8] = [0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12]; assert_eq!(mem, expected); @@ -1044,7 +1053,7 @@ mod tests { let bf_off: u64 = 2; let bf_len: u64 = 2; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); let expected: [u8; 4] = [0x4, 0x0, 0x0, 0x0]; assert_eq!(mem, expected); @@ -1061,7 +1070,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); } #[test] @@ -1075,7 +1084,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); } #[test] @@ -1088,7 +1097,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); // 0xffffffffffffffff -> 34343736`34343831 35353930`37333730 00000000`35313631 (ascii dec) let expected: [u8; 20] = [ @@ -1108,7 +1117,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); // 0xffffffffffffffff -> 0x66666666`66667830 66666666`66666666 (ascii hex) let expected: [u8; 18] = [ @@ -1128,7 +1137,7 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len); + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); // 0xffffffffffffffff -> 37373737`37373130 37373737`37373737 00373737`37373737 (ascii oct) let expected: [u8; 23] = [ diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index 68021312280..e6065b065a9 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -54,7 +54,7 @@ impl SyzlangState { match self .function_registry .lock() - .exec(mem, &handler_name, input) + .exec(mem, handler_name, input) { FuzzFunctionVariable::Void => (), FuzzFunctionVariable::Int(_) => (), // TODO diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 2aa7fca3bc3..8fdb8892d72 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true [dependencies] vmm_test_images = { workspace = true, features = ["clap"] } -anyhow = { workspace = true, features = ["std"] } +anyhow.workspace = true cargo_toml.workspace = true ci_logger.workspace = true clap = { workspace = true, features = ["derive", "env"] } From 1efa8605d3771b4278c7ce0a8f360bae29bb0d2c Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:13:11 -0700 Subject: [PATCH 27/32] fix idx bug in atomic ref queue --- opentmk/inv_decoder/src/atomicrefqueue.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opentmk/inv_decoder/src/atomicrefqueue.rs b/opentmk/inv_decoder/src/atomicrefqueue.rs index 6cec1175f42..baa88cc7a8b 100644 --- a/opentmk/inv_decoder/src/atomicrefqueue.rs +++ b/opentmk/inv_decoder/src/atomicrefqueue.rs @@ -45,6 +45,8 @@ impl AtomicRefQueue { let mut update_start_idx = true; let start_idx = self.start_idx.load(Ordering::SeqCst); for (idx, entry) in self.data[start_idx..].iter().enumerate() { + let idx = idx + start_idx; + // Check if the entry is already marked without marking it first // If it is not, then ensure it also passes the conditional function check if !entry.1.load(Ordering::SeqCst) { From 4e32318bee6c41d3e3268ec3813b75ee2045eb7d Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:17:38 -0700 Subject: [PATCH 28/32] fix test cases --- opentmk/inv_decoder/src/lib.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 14782a4cb2f..8d05277e0e7 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -52,7 +52,7 @@ pub type Executor<'f> = dyn Fn(&DecodedProgram<'_, '_>, InputCase) -> InputResult + 'f + Send + Sync; /// An enumeration of possible decoder errors -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum DecoderError { /// A bad magic value for the program header BadMagic(u64), @@ -1060,7 +1060,6 @@ mod tests { } #[test] - #[should_panic(expected = "copyin: bad argument size 3")] fn test_copyin_with_invalid_size() { let mut mem: [u8; 8] = [0; 8]; let addr = mem.as_ptr() as u64; @@ -1070,11 +1069,13 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); + assert_eq!( + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len), + Err(DecoderError::CopyInBadSize { bf: 0, size: 3 }), + ); } #[test] - #[should_panic(expected = "copyin: unknown binary format 5")] fn test_copyin_with_unknown_binary_format() { let mut mem: [u8; 8] = [0; 8]; let addr = mem.as_ptr() as u64; @@ -1084,7 +1085,10 @@ mod tests { let bf_off: u64 = 0; let bf_len: u64 = 0; - copyin(&mut mem, addr, val, size, bf, bf_off, bf_len).unwrap(); + assert_eq!( + copyin(&mut mem, addr, val, size, bf, bf_off, bf_len), + Err(DecoderError::CopyInBadFormat(5)), + ); } #[test] From c2f46cf50dc53419b78bb5c0ac79e2c846a61c6e Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:19:28 -0700 Subject: [PATCH 29/32] Fix doc typo --- opentmk/inv_decoder/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index 8d05277e0e7..a1c112926a5 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -820,7 +820,7 @@ fn copyout( /// provided exec function with a single test case; then continues from the /// beginning until all provided testcases have completed. /// -/// It is expected that addr_size is minimum 0x1000000 bytes (or 4mb). +/// It is expected that addr_size is minimum 0x1000000 bytes (or 16MiB). /// syz_exec_mem must be at least [`EXEC_INPUT_REQ_SIZE`] in size. /// syz_input_buffer must be at least [`SUPPORTED_INPUT_SIZE`] in size. pub fn exec_testcases_safe( From c65607cdcb86eb01ec183373b3e29e178d95345e Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:20:58 -0700 Subject: [PATCH 30/32] formatting --- .../opentmk_invariant/src/deserializer/syzlang/mod.rs | 6 +----- opentmk/opentmk_invariant/src/executor/mod.rs | 3 ++- opentmk/opentmk_invariant/src/serial.rs | 9 ++++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs index e6065b065a9..fa9e8fa557e 100644 --- a/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs +++ b/opentmk/opentmk_invariant/src/deserializer/syzlang/mod.rs @@ -51,11 +51,7 @@ impl SyzlangState { let handler_name = &self.glob_mapping[input_struct.call_num as usize]; let input = SyzlangDeserializer::to_function_variables(&input_struct); - match self - .function_registry - .lock() - .exec(mem, handler_name, input) - { + match self.function_registry.lock().exec(mem, handler_name, input) { FuzzFunctionVariable::Void => (), FuzzFunctionVariable::Int(_) => (), // TODO FuzzFunctionVariable::Error(e) => { diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index b86b6ce014c..eb2fff15ec5 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -65,7 +65,8 @@ impl Executor { pub fn register_fuzz_functions(&mut self) { let mut fn_registry = self.fn_registry.lock(); - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + // xtask-fmt allow-target-arch sys-crate { use crate::functions::io_port; static X86_REGISTRY: &[(&str, crate::functions::FuzzFunction)] = &[ diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs index 882dfb7adc8..d7d42fe128b 100644 --- a/opentmk/opentmk_invariant/src/serial.rs +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +// xtask-fmt allow-target-arch sys-crate pub use opentmk_core::arch::serial::SerialPort; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +// xtask-fmt allow-target-arch sys-crate use opentmk_core::arch::serial::{InstrIoAccess, Serial}; /// Copy of the x86 serial ports, used as a polyfill for those architectures @@ -26,7 +28,8 @@ pub(crate) trait SerialIo { } pub(crate) struct OpenTmkSerialIo { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + // xtask-fmt allow-target-arch sys-crate handle: Serial, } From ef346e67f4cbf81584112a8c813778fc313e4917 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:35:09 -0700 Subject: [PATCH 31/32] exempt opentmk for target-arch warnings --- opentmk/opentmk_invariant/src/executor/mod.rs | 1 - opentmk/opentmk_invariant/src/functions/mod.rs | 2 +- opentmk/opentmk_invariant/src/serial.rs | 13 +++++-------- xtask/src/tasks/fmt/lints/cfg_target_arch.rs | 3 ++- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/opentmk/opentmk_invariant/src/executor/mod.rs b/opentmk/opentmk_invariant/src/executor/mod.rs index eb2fff15ec5..e670d07a10a 100644 --- a/opentmk/opentmk_invariant/src/executor/mod.rs +++ b/opentmk/opentmk_invariant/src/executor/mod.rs @@ -66,7 +66,6 @@ impl Executor { let mut fn_registry = self.fn_registry.lock(); #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - // xtask-fmt allow-target-arch sys-crate { use crate::functions::io_port; static X86_REGISTRY: &[(&str, crate::functions::FuzzFunction)] = &[ diff --git a/opentmk/opentmk_invariant/src/functions/mod.rs b/opentmk/opentmk_invariant/src/functions/mod.rs index 16855b69a15..0f97d218475 100644 --- a/opentmk/opentmk_invariant/src/functions/mod.rs +++ b/opentmk/opentmk_invariant/src/functions/mod.rs @@ -3,7 +3,7 @@ pub mod hvcall_meta; pub mod hyperv; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub mod io_port; mod registry; pub mod variable; diff --git a/opentmk/opentmk_invariant/src/serial.rs b/opentmk/opentmk_invariant/src/serial.rs index d7d42fe128b..ff1b6b425f9 100644 --- a/opentmk/opentmk_invariant/src/serial.rs +++ b/opentmk/opentmk_invariant/src/serial.rs @@ -2,15 +2,13 @@ // Licensed under the MIT License. #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -// xtask-fmt allow-target-arch sys-crate pub use opentmk_core::arch::serial::SerialPort; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -// xtask-fmt allow-target-arch sys-crate use opentmk_core::arch::serial::{InstrIoAccess, Serial}; /// Copy of the x86 serial ports, used as a polyfill for those architectures /// that are not currently supported yet -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] #[expect(unused)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum SerialPort { @@ -29,12 +27,11 @@ pub(crate) trait SerialIo { pub(crate) struct OpenTmkSerialIo { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - // xtask-fmt allow-target-arch sys-crate handle: Serial, } impl OpenTmkSerialIo { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub fn new(port: SerialPort) -> Self { log::info!("creating serial port"); Self { @@ -42,14 +39,14 @@ impl OpenTmkSerialIo { } } - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] pub fn new(_port: SerialPort) -> Self { log::info!("creating serial port (dummy)"); Self {} } } -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] // xtask-fmt allow-target-arch sys-crate +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] impl SerialIo for OpenTmkSerialIo { fn init(&mut self) { self.handle.init(); @@ -70,7 +67,7 @@ impl SerialIo for OpenTmkSerialIo { // Dummy transport for non x86 serial specifically to allow compilation to // take place. Crashes so that we flag this issue early on. -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] // xtask-fmt allow-target-arch sys-crate +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] impl SerialIo for OpenTmkSerialIo { fn init(&mut self) { todo!() diff --git a/xtask/src/tasks/fmt/lints/cfg_target_arch.rs b/xtask/src/tasks/fmt/lints/cfg_target_arch.rs index 2125306dd12..e9735cd3d4c 100644 --- a/xtask/src/tasks/fmt/lints/cfg_target_arch.rs +++ b/xtask/src/tasks/fmt/lints/cfg_target_arch.rs @@ -65,7 +65,7 @@ fn is_exempt(path: &Path) -> bool { // the whp/kvm crates are inherently arch-specific, as they contain // low-level bindings to a particular platform's virtualization APIs // - // The TMK-related crates run in the guest and are inherently arch-specific. + // The [open]TMK-related crates run in the guest and are inherently arch-specific. path.starts_with("guest_test_uefi") || path.starts_with("openhcl/openhcl_boot") || path.starts_with("openhcl/minimal_rt") @@ -74,6 +74,7 @@ fn is_exempt(path: &Path) -> bool { || path.starts_with("support") || path.starts_with("tmk/simple_tmk") || path.starts_with("tmk/tmk_core") + || path.starts_with("opentmk") || path.starts_with("vm/whp") || path.starts_with("vm/kvm") } From 9814ed2e1fbb95265e1be415f489e29e5452f6e7 Mon Sep 17 00:00:00 2001 From: Henry Wang Date: Wed, 2 Sep 2026 14:42:25 -0700 Subject: [PATCH 32/32] properly exit if an argument is not provided and we need to skip a call --- opentmk/inv_decoder/src/lib.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/opentmk/inv_decoder/src/lib.rs b/opentmk/inv_decoder/src/lib.rs index a1c112926a5..665586c6e73 100644 --- a/opentmk/inv_decoder/src/lib.rs +++ b/opentmk/inv_decoder/src/lib.rs @@ -369,6 +369,7 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { // Evaluate all input arguments. let mut args = [0u64; MAX_ARGS]; + let mut skip = false; for (n, arg) in i.args.iter().enumerate() { match arg { Arg::Const(a) => { @@ -383,7 +384,8 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { // The dependent call that's expected to fill this result argument value has either // not been executed or did not execute successfully, meaning the result value // is invalid. We will skip this call - return Ok(()); + skip = true; + break; } else { let mut v = r.val.load(Ordering::SeqCst); v = v.checked_div(a.op_div).unwrap_or(v); @@ -396,20 +398,29 @@ impl<'m, 'f> DecodedProgram<'m, 'f> { } } - let input_struct = InputCase { - call_num: i.idx as u64, - args, - num_args: i.args.len() as u64, - _priv: PhantomData, - }; - // Ensure that we don't keep a lock to the safe memory map // when we pass execution to the executor. That way we can // perform reentrancy as needed. drop(mem); - // Call the provided exec function now that we've parsed an input. - let exec_result = (self.exec)(self, input_struct); + let exec_result = if skip { + // Skipped function call because we couldn't fill out all + // the values needed from dependent calls + InputResult { + code: 0, + name: "".into(), + is_success: false, + } + } else { + // Call the provided exec function now that we've parsed an input. + let input_struct = InputCase { + call_num: i.idx as u64, + args, + num_args: i.args.len() as u64, + _priv: PhantomData, + }; + (self.exec)(self, input_struct) + }; // If the call has a valid associated copyout index, we need to set the result in the results array. let (copyout_index, results) =