Skip to main content

tightbeam/testing/
fuzz.rs

1//! AFL-powered fuzzing support
2//!
3//! CSP-guided fuzzing using `CspOracle` for intelligent state exploration.
4//! All fuzzing is powered by AFL.rs (<https://github.com/rust-fuzz/afl.rs>).
5//!
6//! # AFL Integration Architecture
7//!
8//! 1. **tb_scenario! with fuzz: afl** generates `fn main() { afl::fuzz!(|data: &[u8]| { ... }) }`
9//! 2. **AFL.rs** provides mutated byte arrays and tracks code coverage automatically
10//! 3. **CspOracle** interprets bytes as event choices, guiding AFL toward valid protocol states
11//! 4. **Coverage feedback** (automatic): AFL discovers new code paths without manual instrumentation
12//! 5. **IJON integration** (automatic with feature): Enable `testing-fuzz-ijon` for state-aware guidance
13//!
14//! # How AFL Discovers Coverage
15//!
16//! AFL.rs uses compile-time instrumentation (via LLVM) to track:
17//! - Basic block transitions (edge coverage)
18//! - Hit counts for each edge (frequency analysis)
19//! - New execution paths through code
20//!
21//! # Optional: IJON State-Space Guidance
22//!
23//! Enable the `testing-fuzz-ijon` feature to guide AFL toward unexplored CSP states:
24//!
25//! ```bash
26//! cargo afl build --test fuzzing --features "std,testing-fuzz,testing-fuzz-ijon"
27//! ```
28//!
29//! When enabled, the oracle automatically calls:
30//! - `afl::ijon_max!("csp_coverage", ...)` - maximize state+transition coverage
31//! - `afl::ijon_set!("csp_state", ...)` - track unique states visited
32//!
33//! This helps AFL prioritize inputs that explore new protocol states beyond
34//! code coverage.
35//!
36//! # Verifying AFL Integration
37//!
38//! ## Unit Tests
39//! Run the oracle unit tests to verify core functionality:
40//! ```bash
41//! cargo test --features "std,testing-fuzz,testing-csp" fuzz::tests
42//! ```
43//!
44//! Key verification tests:
45//! - `oracle_fuzz_from_bytes_reaches_terminal` - oracle interprets bytes correctly
46//! - `oracle_coverage_increases_during_fuzzing` - coverage tracking works
47//! - `oracle_track_state_differs_between_states` - state hashing is unique
48//! - `oracle_crash_context_provides_debug_info` - debugging info available
49//!
50//! ## IJON Feature Test
51//! Verify IJON feature compiles correctly:
52//! ```bash
53//! cargo test --features "std,testing-fuzz,testing-fuzz-ijon,testing-csp" oracle_ijon_feature_enabled
54//! ```
55//!
56//! Note: IJON macros (`afl::ijon_max!`, `afl::ijon_set!`) require AFL runtime
57//! and cannot be tested with `cargo check` or `cargo test`. They only work when
58//! code is executed inside `afl::fuzz!()` under AFL's runtime. The unit test
59//! verifies the oracle's coverage methods work correctly.
60//!
61//! ## AFL Runtime Verification
62//! To verify AFL actually sees IJON data at runtime:
63//!
64//! 1. Build with IJON enabled:
65//!    ```bash
66//!    RUSTFLAGS="--cfg fuzzing" cargo afl build --test fuzzing \
67//!      --features "std,testing-fuzz,testing-fuzz-ijon,testing-csp"
68//!    ```
69//!
70//! 2. Run AFL with verbose output:
71//!    ```bash
72//!    cargo afl fuzz -i built/fuzz/in -o built/fuzz/out \
73//!      target/debug/deps/fuzzing-* -- -V
74//!    ```
75//!
76//! 3. Check AFL UI for IJON metrics:
77//!    - Look for "csp_coverage" in maximization targets
78//!    - Look for "csp_state" in state tracking
79//!    - Coverage should increase faster with IJON enabled
80//!
81//! 4. Compare with/without IJON:
82//!    ```bash
83//!    # Without IJON (baseline)
84//!    RUSTFLAGS="--cfg fuzzing" cargo afl build --test fuzzing \
85//!      --features "std,testing-fuzz,testing-csp"
86//!    # Run for 60 seconds, note coverage
87//!
88//!    # With IJON (should find more states)
89//!    RUSTFLAGS="--cfg fuzzing" cargo afl build --test fuzzing \
90//!      --features "std,testing-fuzz,testing-fuzz-ijon,testing-csp"
91//!    # Run for 60 seconds, compare coverage
92//!    ```
93//!
94//! Expected: IJON build should discover more unique test cases and reach
95//! higher state coverage in the same time period.
96//!
97//! # Creating Fuzz Targets
98//!
99//! Simple usage - no manual IJON calls needed:
100//!
101//! ```ignore
102//! tb_scenario! {
103//!     fuzz: afl,
104//!     spec: MySpec,
105//!     csp: MyProcess,
106//!     environment Bare {
107//!         exec: |trace| {
108//!             // Oracle-guided fuzzing - IJON automatic with feature flag
109//!             trace.oracle().fuzz_from_bytes()?;
110//!
111//!             // Make assertions based on execution trace
112//!             for event in trace.oracle().trace() {
113//!                 trace.event(event.0);
114//!             }
115//!             Ok(())
116//!         }
117//!     }
118//! }
119//! ```
120
121#![cfg(all(feature = "std", feature = "testing-fuzz"))]
122
123use std::collections::hash_map::DefaultHasher;
124use std::collections::HashSet;
125use std::hash::{Hash, Hasher};
126use std::sync::{Arc, Mutex};
127
128use crate::testing::error::TestingError;
129use crate::testing::specs::csp::{Event, Process, State};
130
131/// Oracle-guided fuzz execution error
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum FuzzError {
134	/// No valid events available in a non-terminal state
135	Deadlock { state: State },
136	/// Input bytes ran out before reaching a terminal state
137	InputExhausted { state: State },
138	/// Oracle rejected an event it reported as valid (internal invariant)
139	EventRejected { state: State, event: Event },
140}
141
142impl core::fmt::Display for FuzzError {
143	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
144		match self {
145			Self::Deadlock { state } => {
146				write!(f, "deadlock: no valid events in non-terminal state {}", state.0)
147			}
148			Self::InputExhausted { state } => {
149				write!(f, "input exhausted before terminal state (stopped in {})", state.0)
150			}
151			Self::EventRejected { state, event } => {
152				write!(f, "oracle rejected valid event {} in state {}", event.0, state.0)
153			}
154		}
155	}
156}
157
158impl core::error::Error for FuzzError {}
159
160impl From<FuzzError> for TestingError {
161	fn from(error: FuzzError) -> Self {
162		match error {
163			FuzzError::Deadlock { state } => TestingError::FuzzDeadlock(state.0),
164			FuzzError::InputExhausted { .. } => TestingError::FuzzInputExhausted,
165			FuzzError::EventRejected { event, .. } => TestingError::FuzzEventRejected(event.0),
166		}
167	}
168}
169
170/// CSP state oracle for AFL-guided fuzzing
171///
172/// The oracle bridges AFL's byte-level mutation with CSP protocol semantics:
173///
174/// ## Core Functionality
175/// - **State Machine Tracking**: Maintains current state in CSP process
176/// - **Event Interpretation**: Maps input bytes -> valid event choices
177/// - **Coverage Metrics**: Tracks visited states/transitions for analysis
178/// - **Crash Triage**: Provides execution context when fuzz targets fail
179///
180/// ## AFL Integration
181/// - AFL automatically discovers code coverage (no manual instrumentation needed)
182/// - Oracle guides AFL toward valid protocol states (prevents random noise)
183/// - IJON-compatible methods available for state-space exploration
184///
185/// ## Usage Pattern
186/// ```ignore
187/// // In fuzz target (generated by tb_scenario! fuzz: afl):
188/// let trace = TraceCollector::with_fuzz_oracle(data, process);
189/// trace.oracle().fuzz_from_bytes()?;  // Run oracle-guided execution
190/// // AFL sees the code paths taken and mutates input accordingly
191/// ```
192#[derive(Debug, Clone)]
193pub struct CspOracle {
194	process: Process,
195	current_state: State,
196	visited_states: HashSet<State>,
197	visited_transitions: HashSet<(State, Event)>,
198	trace: Vec<Event>,
199}
200
201impl CspOracle {
202	/// Create a new CSP oracle from a process specification
203	pub fn new(process: Process) -> Self {
204		let initial = process.initial;
205		let mut visited_states = HashSet::new();
206		visited_states.insert(initial);
207
208		Self {
209			process,
210			current_state: initial,
211			visited_states,
212			visited_transitions: HashSet::new(),
213			trace: Vec::new(),
214		}
215	}
216
217	/// Get the current state
218	pub fn current_state(&self) -> State {
219		self.current_state
220	}
221
222	/// Get all visited states
223	pub fn visited_states(&self) -> &HashSet<State> {
224		&self.visited_states
225	}
226
227	/// Get all visited transitions
228	pub fn visited_transitions(&self) -> &HashSet<(State, Event)> {
229		&self.visited_transitions
230	}
231
232	/// Get the execution trace
233	pub fn trace(&self) -> &[Event] {
234		&self.trace
235	}
236
237	/// Check if current state is terminal
238	pub fn is_terminal(&self) -> bool {
239		self.process.is_terminal(self.current_state)
240	}
241
242	/// Get list of valid events from current state
243	///
244	/// Returns only observable events that can be taken from the current state.
245	/// Returns empty if in terminal state.
246	///
247	/// The list is sorted by event label (via `Process::enabled`), so the
248	/// byte -> index mapping in [`Self::fuzz_from_bytes`] is deterministic
249	/// across runs -- the contract AFL replay and corpus minimization rely on.
250	pub fn valid_events(&self) -> Vec<Event> {
251		if self.is_terminal() {
252			return Vec::new();
253		}
254
255		self.process
256			.enabled(self.current_state)
257			.iter()
258			.filter(|a| a.is_observable())
259			.map(|a| a.event)
260			.collect()
261	}
262
263	/// Attempt to take a transition with the given event
264	///
265	/// Returns `true` if transition succeeded, `false` if event not enabled.
266	/// Updates current state and tracking metrics on success.
267	///
268	/// Nondeterministic transitions resolve to the first target. Use
269	/// [`Self::step_with_target`] to select among multiple targets.
270	pub fn step(&mut self, event: &Event) -> bool {
271		self.step_with_target(event, 0)
272	}
273
274	/// Attempt a transition, resolving nondeterministic targets by choice byte
275	///
276	/// Targets are sorted by state name before indexing (`choice % targets`),
277	/// so the same choice byte selects the same target on every run.
278	pub fn step_with_target(&mut self, event: &Event, choice: u8) -> bool {
279		let mut next_states = self.process.step(self.current_state, event);
280
281		if next_states.is_empty() {
282			return false;
283		}
284
285		next_states.sort_unstable();
286
287		// Record transition
288		self.visited_transitions.insert((self.current_state, *event));
289		self.trace.push(*event);
290
291		self.current_state = next_states[(choice as usize) % next_states.len()];
292		self.visited_states.insert(self.current_state);
293
294		true
295	}
296
297	/// Reset oracle to initial state
298	pub fn reset(&mut self) {
299		let initial = self.process.initial;
300		self.current_state = initial;
301		self.visited_states.clear();
302		self.visited_states.insert(initial);
303		self.visited_transitions.clear();
304		self.trace.clear();
305	}
306
307	/// Get hash of current state for IJON tracking
308	///
309	/// Returns a stable 32-bit hash of the current CSP state that can be used
310	/// with AFL's IJON `ijon_set!()` macro to guide fuzzing toward unexplored states.
311	///
312	/// ## IJON Integration
313	/// ```ignore
314	/// // In fuzz target:
315	/// if oracle.fuzz_from_bytes(data).is_ok() {
316	///     afl::ijon_set!("state", oracle.track_state());
317	/// }
318	/// ```
319	///
320	/// This tells AFL: "I'm interested in reaching different state hash values."
321	/// AFL will prioritize inputs that produce new state hashes.
322	///
323	/// ## When to Use
324	/// - Protocol has large state space (many states)
325	/// - Code coverage alone doesn't distinguish states well
326	/// - Want to maximize state exploration beyond code paths
327	///
328	/// ## Note
329	/// IJON requires `cargo install afl` with IJON support and setting
330	/// `AFL_PRELOAD` environment variable. Standard AFL.rs works without this.
331	pub fn track_state(&self) -> u32 {
332		let mut hasher = DefaultHasher::new();
333		self.current_state.hash(&mut hasher);
334		hasher.finish() as u32
335	}
336
337	/// Get coverage score for IJON maximization
338	///
339	/// Returns a 64-bit score combining state and transition coverage:
340	/// - **Upper 32 bits**: Number of unique states visited
341	/// - **Lower 32 bits**: Number of unique transitions taken
342	///
343	/// ## IJON Integration
344	/// ```ignore
345	/// // In fuzz target:
346	/// if oracle.fuzz_from_bytes(data).is_ok() {
347	///     afl::ijon_max!("coverage", oracle.coverage_score());
348	/// }
349	/// ```
350	///
351	/// This tells AFL: "Maximize this coverage score."
352	/// AFL will prioritize inputs that increase the score (explore new states/transitions).
353	///
354	/// ## When to Use
355	/// - Want AFL to explicitly optimize for CSP coverage
356	/// - Protocol has complex state machine with many paths
357	/// - Code coverage metrics don't capture semantic coverage
358	///
359	/// ## Note
360	/// IJON requires `cargo install afl` with IJON support. Standard AFL.rs
361	/// discovers coverage automatically through code instrumentation.
362	pub fn coverage_score(&self) -> u64 {
363		((self.visited_states.len() as u64) << 32) | (self.visited_transitions.len() as u64)
364	}
365
366	/// Get state space coverage percentage
367	pub fn state_coverage(&self) -> f64 {
368		let total_states = self.process.states.len();
369		if total_states == 0 {
370			return 0.0;
371		}
372		(self.visited_states.len() as f64) / (total_states as f64) * 100.0
373	}
374
375	/// Get transition coverage statistics
376	pub fn transition_coverage(&self) -> (usize, usize) {
377		// Count total possible transitions
378		let mut total_transitions = 0;
379		for state in &self.process.states {
380			for event in self.process.observable.iter().chain(&self.process.hidden) {
381				if self.process.transitions.targets(*state, event).is_some() {
382					total_transitions += 1;
383				}
384			}
385		}
386
387		(self.visited_transitions.len(), total_transitions)
388	}
389
390	/// Get crash triage information for debugging failed fuzz runs
391	///
392	/// Returns a formatted string with execution context useful for understanding
393	/// why a fuzz target crashed or failed. Includes:
394	/// - Current state (where execution stopped)
395	/// - Event trace (sequence of events taken)
396	/// - Coverage statistics (states/transitions explored)
397	/// - Valid events (what could have been done at crash point)
398	pub fn crash_context(&self) -> String {
399		let (visited_trans, total_trans) = self.transition_coverage();
400		format!(
401			"AFL Crash Context:\n\
402             Current State: {:?}\n\
403             Terminal: {}\n\
404             Trace: {:?}\n\
405             State Coverage: {:.1}% ({}/{})\n\
406             Transition Coverage: {:.1}% ({}/{})\n\
407             Valid Events: {:?}",
408			self.current_state,
409			self.is_terminal(),
410			self.trace,
411			self.state_coverage(),
412			self.visited_states.len(),
413			self.process.states.len(),
414			if total_trans > 0 {
415				(visited_trans as f64 / total_trans as f64) * 100.0
416			} else {
417				0.0
418			},
419			visited_trans,
420			total_trans,
421			self.valid_events()
422		)
423	}
424
425	/// Run oracle-guided fuzzing from arbitrary byte input
426	///
427	/// **Core AFL Integration Point**: This method interprets AFL-provided bytes
428	/// as choices for which events to take at each state in the CSP process.
429	///
430	/// ## How It Works
431	/// 1. Reset oracle to initial state
432	/// 2. For each input byte:
433	///    - Get valid events at current state (sorted for deterministic replay)
434	///    - Use byte value to choose event: `choice = byte % valid_events.len()`
435	///    - When the chosen event has multiple target states, consume one more
436	///      byte to choose the target: `target = byte % targets.len()`
437	///    - Take transition with chosen event and target
438	///    - Update state and coverage tracking
439	///    - Report to IJON (if `testing-fuzz-ijon` feature enabled)
440	/// 3. Return `Ok(())` if terminal state reached, `Err` otherwise
441	///
442	/// ## AFL Coverage Discovery
443	/// AFL automatically sees:
444	/// - Which code paths are taken (edge coverage)
445	/// - How many valid events were at each state
446	/// - Whether execution reached terminal state
447	/// - Any panics/crashes during execution
448	///
449	/// ## IJON Integration (Automatic)
450	/// When built with `--features testing-fuzz-ijon`, the oracle automatically
451	/// reports CSP state exploration to AFL's IJON system:
452	/// - `ijon_max!("csp_coverage", ...)` - maximize state+transition coverage
453	/// - `ijon_set!("csp_state", ...)` - track unique states visited
454	///
455	/// This guides AFL toward unexplored protocol states beyond code coverage.
456	///
457	/// ## Returns
458	/// - `Ok(())`: Execution reached terminal state successfully
459	/// - [`FuzzError::Deadlock`]: No valid events available (stuck in non-terminal state)
460	/// - [`FuzzError::EventRejected`]: Internal oracle error (should not happen)
461	/// - [`FuzzError::InputExhausted`]: Ran out of input bytes before terminal state
462	///
463	/// ## Crash Triage
464	/// If this panics, check:
465	/// - `self.current_state()` - where execution stopped
466	/// - `self.trace()` - sequence of events taken
467	/// - `self.visited_states()` - states explored
468	pub fn fuzz_from_bytes(&mut self, input: &[u8]) -> Result<(), FuzzError> {
469		self.reset();
470
471		let mut byte_idx = 0;
472		while !self.is_terminal() && byte_idx < input.len() {
473			let valid = self.valid_events();
474			if valid.is_empty() {
475				return Err(FuzzError::Deadlock { state: self.current_state });
476			}
477
478			// Use input byte to choose which event to take
479			let choice = (input[byte_idx] as usize) % valid.len();
480			let event = valid[choice];
481
482			byte_idx += 1;
483
484			// Nondeterministic transitions consume one more byte so every
485			// target state stays reachable under AFL mutation.
486			let target_count = self.process.step(self.current_state, &event).len();
487			let target_choice = if target_count > 1 {
488				let Some(byte) = input.get(byte_idx) else {
489					return Err(FuzzError::InputExhausted { state: self.current_state });
490				};
491
492				byte_idx += 1;
493
494				*byte
495			} else {
496				0
497			};
498
499			if !self.step_with_target(&event, target_choice) {
500				return Err(FuzzError::EventRejected { state: self.current_state, event });
501			}
502		}
503
504		if self.is_terminal() {
505			Ok(())
506		} else {
507			Err(FuzzError::InputExhausted { state: self.current_state })
508		}
509	}
510}
511
512/// Fuzz context for integration with tb_scenario! macro
513///
514/// Provides ergonomic access to `CspOracle` within test scenarios.
515/// Created by `TraceCollector::with_fuzz_oracle()` and accessed via `trace.oracle()`.
516///
517/// ## Architecture
518/// - **TraceCollector** manages test assertions
519/// - **FuzzContext** wraps `CspOracle` with mutex for thread-safe access
520/// - **CspOracle** performs actual CSP-guided fuzzing
521///
522/// ## Usage
523/// ```ignore
524/// tb_scenario! {
525///     fuzz: afl,
526///     spec: MySpec,
527///     csp: MyProcess,
528///     environment Bare {
529///         exec: |trace| {
530///             // FuzzContext provides oracle access
531///             trace.oracle().fuzz_from_bytes()?;
532///
533///             // Can also query oracle state
534///             for (label, _) in trace.oracle().trace() {
535///                 trace.event(label);
536///             }
537///             Ok(())
538///         }
539///     }
540/// }
541/// ```
542///
543/// ## Advanced: Direct Byte Consumption
544/// The `fuzz_u8()`, `fuzz_u16()`, etc. methods are provided for custom
545/// fuzzing logic that needs structured data beyond oracle-guided execution.
546/// Most fuzz targets should use `fuzz_from_bytes()` instead.
547#[derive(Debug, Clone)]
548pub struct FuzzContext {
549	inner: Arc<Mutex<FuzzContextInner>>,
550}
551
552#[derive(Debug)]
553struct FuzzContextInner {
554	input: Vec<u8>,
555	cursor: usize,
556	oracle: CspOracle,
557}
558
559impl FuzzContext {
560	/// Create new fuzz context with input and CSP process
561	pub fn new(input: Vec<u8>, process: Process) -> Self {
562		Self {
563			inner: Arc::new(Mutex::new(FuzzContextInner {
564				input,
565				cursor: 0,
566				oracle: CspOracle::new(process),
567			})),
568		}
569	}
570
571	/// Run oracle-guided fuzzing from the input buffer
572	///
573	/// Interprets input bytes as choices for which events to take at each state.
574	/// Returns `Ok(())` if execution reaches terminal state. Deadlocks, oracle
575	/// rejections, and input exhaustion map to distinct [`TestingError`]
576	/// variants so a real deadlock finding is not reported as exhaustion.
577	pub fn fuzz_from_bytes(&self) -> Result<(), TestingError> {
578		let mut guard = self.inner.lock()?;
579		let input = guard.input.clone();
580
581		guard.oracle.fuzz_from_bytes(&input)?;
582
583		Ok(())
584	}
585
586	/// Get the execution trace of events
587	pub fn trace(&self) -> Vec<Event> {
588		self.inner.lock().map(|g| g.oracle.trace().to_vec()).unwrap_or_default()
589	}
590
591	/// Check if current state is terminal
592	pub fn is_terminal(&self) -> bool {
593		self.inner.lock().map(|g| g.oracle.is_terminal()).unwrap_or(false)
594	}
595
596	/// Get valid events from current state
597	pub fn valid_events(&self) -> Vec<Event> {
598		self.inner.lock().map(|g| g.oracle.valid_events()).unwrap_or_default()
599	}
600
601	/// Get current state
602	pub fn current_state(&self) -> Option<State> {
603		self.inner.lock().ok().map(|g| g.oracle.current_state())
604	}
605
606	/// Get crash context for debugging
607	///
608	/// Returns formatted debugging information about the current oracle state.
609	/// Useful when a fuzz target fails to understand what happened.
610	pub fn crash_context(&self) -> String {
611		self.inner
612			.lock()
613			.map(|g| g.oracle.crash_context())
614			.unwrap_or_else(|_| "Failed to acquire oracle lock".to_string())
615	}
616
617	/// Get current coverage score for IJON integration
618	///
619	/// Returns combined state+transition coverage metric. Used by `tb_scenario!`
620	/// macro when `testing-fuzz-ijon` feature is enabled.
621	pub fn coverage_score(&self) -> u64 {
622		self.inner.lock().map(|g| g.oracle.coverage_score()).unwrap_or(0)
623	}
624
625	/// Get current state hash for IJON integration
626	///
627	/// Returns stable hash of current CSP state. Used by `tb_scenario!`
628	/// macro when `testing-fuzz-ijon` feature is enabled.
629	pub fn track_state(&self) -> u32 {
630		self.inner.lock().map(|g| g.oracle.track_state()).unwrap_or(0)
631	}
632
633	/// Manually step CSP oracle with an event
634	///
635	/// Attempts to take a transition with the given event in the CSP state machine.
636	/// Returns `Ok(true)` if transition succeeded, `Ok(false)` if event not enabled.
637	///
638	/// ## Usage
639	/// ```ignore
640	/// // Manually step CSP state machine
641	/// trace.oracle().step_event(&Event::new("move_request"))?;
642	/// ```
643	///
644	/// ## Errors
645	/// Returns `Err(TestingError::FuzzInputLockPoisoned)` if mutex is poisoned.
646	pub fn step_event(&self, event: &Event) -> Result<bool, TestingError> {
647		let mut guard = self.inner.lock()?;
648		Ok(guard.oracle.step(event))
649	}
650
651	/// Consume and return a u8 from fuzz input
652	pub fn fuzz_u8(&self) -> Result<u8, TestingError> {
653		let mut guard = self.inner.lock()?;
654		if guard.cursor + 1 > guard.input.len() {
655			return Err(TestingError::FuzzInputExhausted);
656		}
657
658		let value = guard.input[guard.cursor];
659
660		guard.cursor += 1;
661
662		Ok(value)
663	}
664
665	/// Consume and return a u16 from fuzz input (big-endian)
666	pub fn fuzz_u16(&self) -> Result<u16, TestingError> {
667		let mut guard = self.inner.lock()?;
668		if guard.cursor + 2 > guard.input.len() {
669			return Err(TestingError::FuzzInputExhausted);
670		}
671
672		let bytes = [guard.input[guard.cursor], guard.input[guard.cursor + 1]];
673
674		guard.cursor += 2;
675
676		Ok(u16::from_be_bytes(bytes))
677	}
678
679	/// Consume and return a u32 from fuzz input (big-endian)
680	pub fn fuzz_u32(&self) -> Result<u32, TestingError> {
681		let mut guard = self.inner.lock()?;
682		if guard.cursor + 4 > guard.input.len() {
683			return Err(TestingError::FuzzInputExhausted);
684		}
685		let bytes = [
686			guard.input[guard.cursor],
687			guard.input[guard.cursor + 1],
688			guard.input[guard.cursor + 2],
689			guard.input[guard.cursor + 3],
690		];
691
692		guard.cursor += 4;
693
694		Ok(u32::from_be_bytes(bytes))
695	}
696
697	/// Consume and return a u64 from fuzz input (big-endian)
698	pub fn fuzz_u64(&self) -> Result<u64, TestingError> {
699		let mut guard = self.inner.lock()?;
700		if guard.cursor + 8 > guard.input.len() {
701			return Err(TestingError::FuzzInputExhausted);
702		}
703		let bytes = [
704			guard.input[guard.cursor],
705			guard.input[guard.cursor + 1],
706			guard.input[guard.cursor + 2],
707			guard.input[guard.cursor + 3],
708			guard.input[guard.cursor + 4],
709			guard.input[guard.cursor + 5],
710			guard.input[guard.cursor + 6],
711			guard.input[guard.cursor + 7],
712		];
713
714		guard.cursor += 8;
715
716		Ok(u64::from_be_bytes(bytes))
717	}
718
719	/// Consume and return N bytes from fuzz input
720	pub fn fuzz_bytes(&self, n: usize) -> Result<Vec<u8>, TestingError> {
721		let mut guard = self.inner.lock()?;
722		if guard.cursor + n > guard.input.len() {
723			return Err(TestingError::FuzzInputExhausted);
724		}
725
726		let bytes = guard.input[guard.cursor..guard.cursor + n].to_vec();
727
728		guard.cursor += n;
729
730		Ok(bytes)
731	}
732
733	/// Get raw fuzz input bytes
734	pub fn fuzz_input(&self) -> Result<Vec<u8>, TestingError> {
735		let guard = self.inner.lock()?;
736		Ok(guard.input.clone())
737	}
738
739	/// Check if N bytes are available in fuzz input
740	pub fn fuzz_has_bytes(&self, n: usize) -> Result<bool, TestingError> {
741		let guard = self.inner.lock()?;
742		Ok(guard.cursor + n <= guard.input.len())
743	}
744
745	/// Get remaining byte count in fuzz input
746	pub fn fuzz_remaining(&self) -> Result<usize, TestingError> {
747		let guard = self.inner.lock()?;
748		Ok(guard.input.len() - guard.cursor)
749	}
750
751	/// Peek at next u8 from fuzz input without consuming
752	pub fn fuzz_peek_u8(&self) -> Result<u8, TestingError> {
753		let guard = self.inner.lock()?;
754		if guard.cursor + 1 > guard.input.len() {
755			return Err(TestingError::FuzzInputExhausted);
756		}
757
758		Ok(guard.input[guard.cursor])
759	}
760
761	/// Peek at next N bytes from fuzz input without consuming
762	pub fn fuzz_peek_bytes(&self, n: usize) -> Result<Vec<u8>, TestingError> {
763		let guard = self.inner.lock()?;
764		if guard.cursor + n > guard.input.len() {
765			return Err(TestingError::FuzzInputExhausted);
766		}
767
768		Ok(guard.input[guard.cursor..guard.cursor + n].to_vec())
769	}
770}
771
772/// Generic test framework for fuzzing functionality
773/// Tests common patterns across oracle and context components
774mod tests {
775	use super::*;
776
777	// ===== Test Fixtures =====
778
779	/// Build a simple linear process: S0 --event--> S1 (terminal)
780	#[allow(dead_code)]
781	fn build_simple_process(event: &'static str) -> Process {
782		Process::builder("TestProc")
783			.initial_state(State("S0"))
784			.add_observable(event)
785			.add_transition(State("S0"), event, State("S1"))
786			.add_terminal(State("S1"))
787			.build()
788			.expect("fixture process builder has a valid initial state")
789	}
790
791	/// Build a two-step linear process: S0 --e1--> S1 --e2--> S2 (terminal)
792	#[allow(dead_code)]
793	fn build_two_step_process(e1: &'static str, e2: &'static str) -> Process {
794		Process::builder("TestProc")
795			.initial_state(State("S0"))
796			.add_observable(e1)
797			.add_observable(e2)
798			.add_transition(State("S0"), e1, State("S1"))
799			.add_transition(State("S1"), e2, State("S2"))
800			.add_terminal(State("S2"))
801			.build()
802			.expect("fixture process builder has a valid initial state")
803	}
804
805	/// Build a three-step linear process: S0 --a--> S1 --b--> S2 --c--> S3 (terminal)
806	#[allow(dead_code)]
807	fn build_three_step_process() -> Process {
808		Process::builder("TestProc")
809			.initial_state(State("S0"))
810			.add_observable("a")
811			.add_observable("b")
812			.add_observable("c")
813			.add_transition(State("S0"), "a", State("S1"))
814			.add_transition(State("S1"), "b", State("S2"))
815			.add_transition(State("S2"), "c", State("S3"))
816			.add_terminal(State("S3"))
817			.build()
818			.expect("fixture process builder has a valid initial state")
819	}
820
821	/// Build a choice process: S0 --choice--> {S1, S2} (both terminal)
822	#[allow(dead_code)]
823	fn build_choice_process() -> Process {
824		Process::builder("TestProc")
825			.initial_state(State("S0"))
826			.add_observable("choice")
827			.add_transition(State("S0"), "choice", State("S1"))
828			.add_transition(State("S0"), "choice", State("S2"))
829			.add_choice(State("S0"))
830			.add_terminal(State("S1"))
831			.add_terminal(State("S2"))
832			.build()
833			.expect("fixture process builder has a valid initial state")
834	}
835
836	/// Build a branching process: S0 --{a,b,c}--> {S1, S2, S3} (all terminal)
837	#[allow(dead_code)]
838	fn build_branching_process() -> Process {
839		Process::builder("TestProc")
840			.initial_state(State("S0"))
841			.add_observable("a")
842			.add_observable("b")
843			.add_observable("c")
844			.add_transition(State("S0"), "a", State("S1"))
845			.add_transition(State("S0"), "b", State("S2"))
846			.add_transition(State("S0"), "c", State("S3"))
847			.add_terminal(State("S1"))
848			.add_terminal(State("S2"))
849			.add_terminal(State("S3"))
850			.build()
851			.expect("fixture process builder has a valid initial state")
852	}
853
854	// ===== IJON Feature Tests =====
855
856	#[cfg(feature = "testing-fuzz-ijon")]
857	#[test]
858	fn oracle_ijon_feature_enabled() {
859		// This test verifies that the IJON feature flag compiles correctly.
860		// IJON macros only execute inside afl::fuzz!() at runtime, so we verify
861		// the feature enables the right code paths without actually calling IJON.
862		let proc = build_simple_process("go");
863		let mut oracle = CspOracle::new(proc);
864
865		// fuzz_from_bytes will skip IJON calls in test mode (cfg(test) is true)
866		let input = vec![0];
867		let result = oracle.fuzz_from_bytes(&input);
868		assert!(result.is_ok(), "IJON feature should not break fuzzing");
869		assert_eq!(oracle.visited_states().len(), 2);
870		assert_eq!(oracle.visited_transitions().len(), 1);
871
872		// Verify IJON-related methods work
873		let _ = oracle.track_state();
874		let _ = oracle.coverage_score();
875	}
876
877	/// Generate tests for oracle core functionality
878	macro_rules! generate_oracle_core_tests {
879		($module_name:ident) => {
880			mod $module_name {
881				#[test]
882				fn tracks_state_transitions() {
883					let mut oracle = super::CspOracle::new(super::build_two_step_process("go", "stop"));
884					assert_eq!(oracle.current_state(), super::State("S0"));
885					assert_eq!(oracle.visited_states().len(), 1);
886					assert!(!oracle.is_terminal());
887
888					let valid = oracle.valid_events();
889					assert_eq!(valid.len(), 1);
890					assert_eq!(valid[0].0, "go");
891
892					assert!(oracle.step(&super::Event("go")));
893					assert_eq!(oracle.current_state(), super::State("S1"));
894					assert_eq!(oracle.visited_states().len(), 2);
895					assert_eq!(oracle.visited_transitions().len(), 1);
896					assert_eq!(oracle.trace().len(), 1);
897
898					assert!(oracle.step(&super::Event("stop")));
899					assert_eq!(oracle.current_state(), super::State("S2"));
900					assert!(oracle.is_terminal());
901					assert_eq!(oracle.valid_events().len(), 0);
902				}
903
904				#[test]
905				fn rejects_invalid_events() {
906					let mut oracle = super::CspOracle::new(super::build_simple_process("valid"));
907					assert!(!oracle.step(&super::Event("invalid")));
908					assert_eq!(oracle.current_state(), super::State("S0"));
909					assert_eq!(oracle.visited_transitions().len(), 0);
910				}
911
912				#[test]
913				fn oracle_reset() {
914					let mut oracle = super::CspOracle::new(super::build_simple_process("go"));
915					oracle.step(&super::Event("go"));
916					assert_eq!(oracle.current_state(), super::State("S1"));
917
918					oracle.reset();
919					assert_eq!(oracle.current_state(), super::State("S0"));
920					assert_eq!(oracle.visited_states().len(), 1);
921					assert_eq!(oracle.visited_transitions().len(), 0);
922					assert_eq!(oracle.trace().len(), 0);
923				}
924
925				#[test]
926				fn oracle_with_choice_points() {
927					let mut oracle = super::CspOracle::new(super::build_choice_process());
928					let valid = oracle.valid_events();
929					assert_eq!(valid.len(), 1);
930					assert_eq!(valid[0].0, "choice");
931
932					assert!(oracle.step(&super::Event("choice")));
933					assert_eq!(oracle.current_state(), super::State("S1"));
934				}
935			}
936		};
937	}
938
939	/// Generate tests for coverage and metrics
940	macro_rules! generate_coverage_tests {
941		($module_name:ident) => {
942			mod $module_name {
943				#[test]
944				fn oracle_coverage_metrics() {
945					let mut oracle = super::CspOracle::new(super::build_two_step_process("a", "b"));
946					let coverage = oracle.state_coverage();
947					assert!((coverage - 33.33).abs() < 0.1);
948
949					oracle.step(&super::Event("a"));
950					let coverage = oracle.state_coverage();
951					assert!((coverage - 66.66).abs() < 0.1);
952
953					oracle.step(&super::Event("b"));
954					let coverage = oracle.state_coverage();
955					assert!((coverage - 100.0).abs() < 0.1);
956
957					let (visited, _total) = oracle.transition_coverage();
958					assert_eq!(visited, 2);
959				}
960
961				#[test]
962				fn oracle_track_state_is_stable() {
963					let proc = super::build_simple_process("go");
964					let oracle1 = super::CspOracle::new(proc.clone());
965					let oracle2 = super::CspOracle::new(proc);
966					assert_eq!(oracle1.track_state(), oracle2.track_state());
967				}
968
969				#[test]
970				fn oracle_coverage_score_increases() {
971					let mut oracle = super::CspOracle::new(super::build_simple_process("go"));
972					let score1 = oracle.coverage_score();
973
974					oracle.step(&super::Event("go"));
975					let score2 = oracle.coverage_score();
976					assert!(score2 > score1);
977				}
978			}
979		};
980	}
981
982	/// Generate tests for fuzzing from bytes
983	macro_rules! generate_fuzzing_tests {
984		($module_name:ident) => {
985			mod $module_name {
986				#[test]
987				fn oracle_fuzz_from_bytes_reaches_terminal() {
988					let mut oracle = super::CspOracle::new(super::build_simple_process("go"));
989					let input = vec![0];
990					assert!(oracle.fuzz_from_bytes(&input).is_ok());
991					assert_eq!(oracle.current_state(), super::State("S1"));
992					assert!(oracle.is_terminal());
993				}
994
995				#[test]
996				fn oracle_fuzz_from_bytes_multiple_transitions() {
997					let mut oracle = super::CspOracle::new(super::build_two_step_process("a", "b"));
998					let input = vec![0, 0];
999					assert!(oracle.fuzz_from_bytes(&input).is_ok());
1000					assert_eq!(oracle.current_state(), super::State("S2"));
1001					assert_eq!(oracle.trace().len(), 2);
1002				}
1003
1004				#[test]
1005				fn oracle_fuzz_from_bytes_fails_on_insufficient_input() {
1006					let mut oracle = super::CspOracle::new(super::build_two_step_process("a", "b"));
1007					let input = vec![0];
1008					assert!(matches!(
1009						oracle.fuzz_from_bytes(&input),
1010						Err(super::FuzzError::InputExhausted { .. })
1011					));
1012				}
1013
1014				#[test]
1015				fn oracle_deadlock_reported_as_deadlock() {
1016					let process = super::Process::builder("TestProc")
1017						.initial_state(super::State("S0"))
1018						.add_observable("go")
1019						.add_transition(super::State("S0"), "go", super::State("Stuck"))
1020						.build()
1021						.expect("fixture process builder has a valid initial state");
1022
1023					let mut oracle = super::CspOracle::new(process);
1024					assert!(matches!(
1025						oracle.fuzz_from_bytes(&[0, 0]),
1026						Err(super::FuzzError::Deadlock { state: super::State("Stuck") })
1027					));
1028				}
1029
1030				#[test]
1031				fn oracle_byte_mapping_deterministic_across_oracles() {
1032					let traces: Vec<Vec<super::Event>> = (0..8)
1033						.map(|_| {
1034							let mut oracle = super::CspOracle::new(super::build_branching_process());
1035							oracle.fuzz_from_bytes(&[1]).expect("branching process reaches terminal");
1036							oracle.trace().to_vec()
1037						})
1038						.collect();
1039
1040					for trace in &traces {
1041						assert_eq!(trace, &traces[0]);
1042					}
1043				}
1044
1045				#[test]
1046				fn oracle_coverage_increases_during_fuzzing() {
1047					let mut oracle = super::CspOracle::new(super::build_two_step_process("a", "b"));
1048					let initial_score = oracle.coverage_score();
1049
1050					let input = vec![0, 0];
1051					let _ = oracle.fuzz_from_bytes(&input);
1052					let final_score = oracle.coverage_score();
1053					assert!(final_score > initial_score);
1054
1055					assert_eq!(oracle.visited_states().len(), 3);
1056					assert_eq!(oracle.visited_transitions().len(), 2);
1057				}
1058
1059				#[test]
1060				fn oracle_track_state_differs_between_states() {
1061					let mut oracle = super::CspOracle::new(super::build_simple_process("go"));
1062					let hash_s0 = oracle.track_state();
1063
1064					oracle.step(&super::Event("go"));
1065
1066					let hash_s1 = oracle.track_state();
1067					assert_ne!(hash_s0, hash_s1);
1068				}
1069
1070				#[test]
1071				fn oracle_crash_context_provides_debug_info() {
1072					let mut oracle = super::CspOracle::new(super::build_simple_process("go"));
1073					oracle.step(&super::Event("go"));
1074
1075					let context = oracle.crash_context();
1076					assert!(context.contains("Current State:"));
1077					assert!(context.contains("S1"));
1078					assert!(context.contains("Terminal: true"));
1079					assert!(context.contains("Trace:"));
1080					assert!(context.contains("Coverage:"));
1081				}
1082			}
1083		};
1084	}
1085
1086	/// Generate tests for FuzzContext integration
1087	macro_rules! generate_context_tests {
1088		($module_name:ident) => {
1089			mod $module_name {
1090				#[test]
1091				fn fuzz_context_executes_oracle() {
1092					let proc = super::build_simple_process("go");
1093					let ctx = super::FuzzContext::new(vec![0], proc);
1094					assert!(ctx.fuzz_from_bytes().is_ok());
1095					assert!(ctx.is_terminal());
1096					assert_eq!(ctx.trace().len(), 1);
1097				}
1098
1099				#[test]
1100				fn fuzz_context_ijon_accessors() {
1101					let proc = super::build_two_step_process("a", "b");
1102					let ctx = super::FuzzContext::new(vec![0, 0], proc);
1103
1104					let initial_score = ctx.coverage_score();
1105					let initial_hash = ctx.track_state();
1106
1107					let _ = ctx.fuzz_from_bytes();
1108
1109					let final_score = ctx.coverage_score();
1110					let final_hash = ctx.track_state();
1111					assert!(final_score > initial_score);
1112					assert_ne!(initial_hash, final_hash);
1113				}
1114
1115				#[test]
1116				fn fuzz_context_crash_context() {
1117					let proc = super::build_simple_process("go");
1118					let ctx = super::FuzzContext::new(vec![0], proc);
1119					let _ = ctx.fuzz_from_bytes();
1120
1121					let context = ctx.crash_context();
1122					assert!(context.contains("AFL Crash Context"));
1123					assert!(context.contains("Current State:"));
1124					assert!(context.contains("S1"));
1125					assert!(context.contains("Coverage:"));
1126				}
1127
1128				#[test]
1129				fn fuzz_context_thread_safe_clone() {
1130					let proc = super::build_simple_process("go");
1131					let ctx1 = super::FuzzContext::new(vec![0], proc);
1132					let ctx2 = ctx1.clone();
1133
1134					let _ = ctx1.fuzz_from_bytes();
1135					assert_eq!(ctx1.current_state(), ctx2.current_state());
1136				}
1137			}
1138		};
1139	}
1140
1141	/// Generate tests for advanced fuzzing behavior
1142	macro_rules! generate_advanced_tests {
1143		($module_name:ident) => {
1144			mod $module_name {
1145				#[test]
1146				fn oracle_input_modulo_selection() {
1147					let proc = super::build_branching_process();
1148					let oracle_ref = super::CspOracle::new(proc.clone());
1149					let valid = oracle_ref.valid_events();
1150					assert_eq!(valid.len(), 3);
1151
1152					let state_for_choice: Vec<super::State> = (0..3)
1153						.map(|choice| {
1154							let mut oracle = super::CspOracle::new(proc.clone());
1155							oracle.fuzz_from_bytes(&[choice as u8]).unwrap();
1156							oracle.current_state()
1157						})
1158						.collect();
1159
1160					let test_cases = vec![
1161						(0, 0, "byte=0: 0 % 3 = 0 -> first event"),
1162						(1, 1, "byte=1: 1 % 3 = 1 -> second event"),
1163						(2, 2, "byte=2: 2 % 3 = 2 -> third event"),
1164						(3, 0, "byte=3: 3 % 3 = 0 -> wraps to first"),
1165						(255, 0, "byte=255: 255 % 3 = 0 -> wraps to first"),
1166					];
1167
1168					for (input_byte, expected_choice, desc) in test_cases {
1169						let mut oracle = super::CspOracle::new(proc.clone());
1170						assert!(oracle.fuzz_from_bytes(&[input_byte]).is_ok());
1171						assert_eq!(oracle.current_state(), state_for_choice[expected_choice], "{}", desc);
1172					}
1173				}
1174
1175				#[test]
1176				fn oracle_choice_point_fuzzing() {
1177					let proc = super::build_choice_process();
1178					let mut oracle = super::CspOracle::new(proc);
1179					assert!(oracle.fuzz_from_bytes(&[0, 0]).is_ok());
1180					assert_eq!(oracle.current_state(), super::State("S1"));
1181				}
1182
1183				#[test]
1184				fn oracle_target_byte_reaches_second_nondeterministic_target() {
1185					let proc = super::build_choice_process();
1186					let mut oracle = super::CspOracle::new(proc);
1187					assert!(oracle.fuzz_from_bytes(&[0, 1]).is_ok());
1188					assert_eq!(oracle.current_state(), super::State("S2"));
1189				}
1190
1191				#[test]
1192				fn oracle_reset_between_fuzz_runs() {
1193					let proc = super::build_simple_process("go");
1194					let mut oracle = super::CspOracle::new(proc);
1195					assert!(oracle.fuzz_from_bytes(&[0]).is_ok());
1196					assert_eq!(oracle.current_state(), super::State("S1"));
1197					assert_eq!(oracle.trace().len(), 1);
1198					assert!(oracle.fuzz_from_bytes(&[0]).is_ok());
1199					assert_eq!(oracle.current_state(), super::State("S1"));
1200					assert_eq!(oracle.trace().len(), 1);
1201				}
1202
1203				#[test]
1204				fn oracle_exhaustive_state_exploration() {
1205					let proc = super::build_three_step_process();
1206					let mut oracle = super::CspOracle::new(proc);
1207
1208					let input = vec![0, 0, 0];
1209					assert!(oracle.fuzz_from_bytes(&input).is_ok());
1210					assert_eq!(oracle.visited_states().len(), 4);
1211					assert_eq!(oracle.visited_transitions().len(), 3);
1212					assert_eq!(oracle.state_coverage(), 100.0);
1213				}
1214
1215				#[test]
1216				fn fuzz_context_concurrent_access() {
1217					let proc = super::build_simple_process("go");
1218					let ctx = std::sync::Arc::new(super::FuzzContext::new(vec![0], proc));
1219
1220					let handles: Vec<_> = (0..4)
1221						.map(|_| {
1222							let ctx_clone = std::sync::Arc::clone(&ctx);
1223							std::thread::spawn(move || {
1224								let _ = ctx_clone.coverage_score();
1225								let _ = ctx_clone.track_state();
1226								let _ = ctx_clone.is_terminal();
1227								let _ = ctx_clone.current_state();
1228							})
1229						})
1230						.collect();
1231
1232					for handle in handles {
1233						handle.join().expect("Thread should not panic");
1234					}
1235				}
1236			}
1237		};
1238	}
1239
1240	// Generate test modules
1241	generate_oracle_core_tests!(oracle_core);
1242	generate_coverage_tests!(coverage);
1243	generate_fuzzing_tests!(fuzzing);
1244	generate_context_tests!(context);
1245	generate_advanced_tests!(advanced);
1246}