|
| 1 | +/* |
| 2 | +Copyright 2025 The Hyperlight Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | +#![no_main] |
| 17 | + |
| 18 | +#[cfg(not(feature = "trace"))] |
| 19 | +compile_error!("feature `trace` must be enabled to fuzz guest trace event estimation"); |
| 20 | + |
| 21 | +use hyperlight_common::flatbuffer_wrappers::guest_trace_data::{ |
| 22 | + EventKeyValue, EventsBatchDecoder, EventsBatchEncoder, EventsDecoder, EventsEncoder, |
| 23 | + GuestEvent, estimate_event, |
| 24 | +}; |
| 25 | +use libfuzzer_sys::arbitrary::{Arbitrary, Result as FuzzResult, Unstructured}; |
| 26 | +use libfuzzer_sys::fuzz_target; |
| 27 | + |
| 28 | +const MAX_STRING_LEN: usize = 1 << 10; // 1024 bytes |
| 29 | +const MAX_FIELDS: usize = 32; |
| 30 | + |
| 31 | +/// Wrapper around GuestEvent to implement Arbitrary |
| 32 | +#[derive(Debug)] |
| 33 | +struct EventInput(GuestEvent); |
| 34 | + |
| 35 | +impl EventInput { |
| 36 | + /// Consumes the wrapper and returns the inner GuestEvent |
| 37 | + fn into_inner(self) -> GuestEvent { |
| 38 | + self.0 |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +impl<'a> Arbitrary<'a> for EventInput { |
| 43 | + fn arbitrary(u: &mut Unstructured<'a>) -> FuzzResult<Self> { |
| 44 | + // Choose a variant of GuestEvent to generate |
| 45 | + let discriminator = u.arbitrary::<u8>()? % 5; |
| 46 | + |
| 47 | + // Generate each variant with appropriate random data |
| 48 | + let event = match discriminator { |
| 49 | + 0 => GuestEvent::OpenSpan { |
| 50 | + id: u.arbitrary::<u64>()?, |
| 51 | + parent_id: arbitrary_parent(u)?, |
| 52 | + name: limited_string(u, MAX_STRING_LEN)?, |
| 53 | + target: limited_string(u, MAX_STRING_LEN)?, |
| 54 | + tsc: u.arbitrary::<u64>()?, |
| 55 | + fields: arbitrary_fields(u)?, |
| 56 | + }, |
| 57 | + 1 => GuestEvent::CloseSpan { |
| 58 | + id: u.arbitrary::<u64>()?, |
| 59 | + tsc: u.arbitrary::<u64>()?, |
| 60 | + }, |
| 61 | + 2 => GuestEvent::LogEvent { |
| 62 | + parent_id: u.arbitrary::<u64>()?, |
| 63 | + name: limited_string(u, MAX_STRING_LEN)?, |
| 64 | + tsc: u.arbitrary::<u64>()?, |
| 65 | + fields: arbitrary_fields(u)?, |
| 66 | + }, |
| 67 | + 3 => GuestEvent::EditSpan { |
| 68 | + id: u.arbitrary::<u64>()?, |
| 69 | + fields: arbitrary_fields(u)?, |
| 70 | + }, |
| 71 | + _ => GuestEvent::GuestStart { |
| 72 | + tsc: u.arbitrary::<u64>()?, |
| 73 | + }, |
| 74 | + }; |
| 75 | + |
| 76 | + Ok(EventInput(event)) |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +/// Generates an optional parent ID |
| 81 | +fn arbitrary_parent(u: &mut Unstructured<'_>) -> FuzzResult<Option<u64>> { |
| 82 | + let has_parent = u.arbitrary::<bool>()?; |
| 83 | + if has_parent { |
| 84 | + Ok(Some(u.arbitrary::<u64>()?)) |
| 85 | + } else { |
| 86 | + Ok(None) |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +/// Generates a String with a maximum length of `max_len` |
| 91 | +fn limited_string(u: &mut Unstructured<'_>, max_len: usize) -> FuzzResult<String> { |
| 92 | + let bytes = u.arbitrary::<&[u8]>()?; |
| 93 | + let s = std::str::from_utf8(bytes) |
| 94 | + // Fallback to repeating 'x' if not valid UTF-8 |
| 95 | + .unwrap_or(&"x".repeat(bytes.len() % max_len)) |
| 96 | + .chars() |
| 97 | + .take(max_len) |
| 98 | + .collect::<String>(); |
| 99 | + |
| 100 | + Ok(s) |
| 101 | +} |
| 102 | + |
| 103 | +/// Generates a vector of EventKeyValue pairs |
| 104 | +fn arbitrary_fields(u: &mut Unstructured<'_>) -> FuzzResult<Vec<EventKeyValue>> { |
| 105 | + let field_count = (u.arbitrary::<u8>()? as usize).min(MAX_FIELDS); |
| 106 | + let mut fields = Vec::with_capacity(field_count); |
| 107 | + for _ in 0..field_count { |
| 108 | + let key = limited_string(u, MAX_STRING_LEN)?; |
| 109 | + let value = limited_string(u, MAX_STRING_LEN)?; |
| 110 | + fields.push(EventKeyValue { key, value }); |
| 111 | + } |
| 112 | + Ok(fields) |
| 113 | +} |
| 114 | + |
| 115 | +/// Encodes a GuestEvent into a byte vector |
| 116 | +fn encode(event: &GuestEvent) -> Vec<u8> { |
| 117 | + // Use the estimate plus some slack to avoid reallocation during encoding |
| 118 | + let mut encoder = EventsBatchEncoder::new(estimate_event(event).saturating_add(256), |_| {}); |
| 119 | + encoder.encode(event); |
| 120 | + encoder.finish().to_vec() |
| 121 | +} |
| 122 | + |
| 123 | +/// Decodes a byte slice into a GuestEvent |
| 124 | +fn decode(data: &[u8]) -> Option<GuestEvent> { |
| 125 | + let decoder = EventsBatchDecoder {}; |
| 126 | + let mut events = decoder.decode(data).ok()?; |
| 127 | + |
| 128 | + if events.len() == 1 { |
| 129 | + events.pop() |
| 130 | + } else { |
| 131 | + None |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +/// Asserts that the estimated size is within acceptable bounds of the actual size |
| 136 | +/// Allows for a 10% slack or minimum of 128 bytes |
| 137 | +fn assert_estimate_bounds(actual: usize, estimate: usize) { |
| 138 | + assert!( |
| 139 | + estimate >= actual, |
| 140 | + "estimate {} smaller than actual {}", |
| 141 | + estimate, |
| 142 | + actual, |
| 143 | + ); |
| 144 | + |
| 145 | + let slack = (actual / 10).max(128); |
| 146 | + let upper_bound = actual + slack; |
| 147 | + assert!( |
| 148 | + estimate <= upper_bound, |
| 149 | + "estimate {} larger than allowable upper bound {} (actual {})", |
| 150 | + estimate, |
| 151 | + upper_bound, |
| 152 | + actual, |
| 153 | + ); |
| 154 | +} |
| 155 | + |
| 156 | +fuzz_target!(|input: EventInput| { |
| 157 | + let event = input.into_inner(); |
| 158 | + |
| 159 | + // Get the size estimate |
| 160 | + let estimate = estimate_event(&event); |
| 161 | + // Encode the event |
| 162 | + let encoded_data = encode(&event); |
| 163 | + // Assert that the estimate is within bounds |
| 164 | + assert_estimate_bounds(encoded_data.len(), estimate); |
| 165 | + |
| 166 | + // Decode the event back |
| 167 | + let decoded = decode(&encoded_data).expect("decoding failed"); |
| 168 | + // Assert that the decoded event matches the original |
| 169 | + assert_eq!(event, decoded); |
| 170 | +}); |
0 commit comments