1use std::borrow::Cow;
2
3use libafl::prelude::*;
4use libafl_bolts::prelude::*;
5
6use super::utils::{
7 choose, choose_iter, choose_term, choose_term_filtered_mut, choose_term_path_filtered,
8 find_all_sub_term_filtered, find_all_term_filtered, find_term, find_term_mut, reservoir_sample,
9 Choosable, TermConstraints, TracePath,
10};
11use crate::algebra::atoms::Function;
12use crate::algebra::signature::Signature;
13use crate::algebra::{DYTerm, Subterms, Term, TermType};
14use crate::fuzzer::term_zoo::TermZoo;
15use crate::protocol::{ProtocolBehavior, ProtocolTypes};
16use crate::put_registry::PutRegistry;
17use crate::trace::{Action, Spawner, Trace, TraceContext};
18
19#[derive(Clone, Copy, Debug)]
20pub struct MutationConfig {
21 pub fresh_zoo_after: u64,
22 pub max_result_trace_length: usize,
23 pub min_trace_length: usize,
24 pub term_constraints: TermConstraints,
28 pub with_bit_level: bool,
29 pub with_dy: bool,
30 pub with_focus: bool,
32 pub scope_weights: ScopeWeights,
35}
36
37impl Default for MutationConfig {
38 fn default() -> Self {
40 Self {
41 fresh_zoo_after: 100000,
42 max_result_trace_length: 20,
43 min_trace_length: 2,
44 term_constraints: TermConstraints::default(),
45 with_bit_level: false,
46 with_dy: true,
47 with_focus: true,
48 scope_weights: ScopeWeights::default(),
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug)]
60pub struct ScopeWeights {
61 pub global: usize,
62 pub step: usize,
63 pub individual: usize,
64}
65
66impl Default for ScopeWeights {
67 fn default() -> Self {
68 Self {
69 global: 1,
75 step: 2,
76 individual: 3,
77 }
78 }
79}
80
81impl ScopeWeights {
82 #[must_use]
83 pub const fn new(global: usize, step: usize, individual: usize) -> Self {
84 Self {
85 global,
86 step,
87 individual,
88 }
89 }
90
91 const fn total(self) -> usize {
94 self.global
95 .saturating_add(self.step)
96 .saturating_add(self.individual)
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
102pub enum MutationScope {
103 Global,
105 Step,
107 Individual,
109}
110
111impl MutationScope {
112 fn choose<R: Rand>(weights: ScopeWeights, rand: &mut R) -> Self {
115 let total = weights.total();
116 if total == 0 {
117 return Self::Individual;
118 }
119 let draw = rand.between(0, total - 1);
120 if draw < weights.global {
121 Self::Global
122 } else if draw < weights.global.saturating_add(weights.step) {
123 Self::Step
124 } else {
125 Self::Individual
126 }
127 }
128}
129
130pub fn apply_scoped_mutation<PT: ProtocolTypes, R: Rand>(
151 trace: &mut Trace<PT>,
152 to_mutate_path: &TracePath,
153 representative: &Term<PT>,
154 new_term: &Term<PT>,
155 config: &MutationConfig,
156 rand: &mut R,
157) -> MutationResult {
158 let targets = scoped_targets(trace, to_mutate_path, representative, config, rand);
159 apply_to_targets(
160 trace,
161 &targets,
162 new_term,
163 representative,
164 config.with_bit_level,
165 &config.term_constraints,
166 )
167}
168
169fn replacement_within_caps<PT: ProtocolTypes>(
191 trace: &Trace<PT>,
192 targets: &[TracePath],
193 representative_size: usize,
194 new_size: usize,
195 constraints: &TermConstraints,
196) -> bool {
197 if new_size <= representative_size {
198 return true; }
200 let delta = new_size - representative_size;
201
202 let mut per_step: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
204 for (step_index, _) in targets {
205 *per_step.entry(*step_index).or_insert(0) += 1;
206 }
207 for (step_index, count) in &per_step {
208 let cur = match trace.steps.get(*step_index).map(|s| &s.action) {
209 Some(Action::Input(input)) => input.recipe.size(),
210 _ => 0,
211 };
212 if cur + count * delta > constraints.max_result_term_size {
213 return false;
214 }
215 }
216
217 true
221}
222
223fn payloads_within_caps<PT: ProtocolTypes>(
236 trace: &Trace<PT>,
237 targets: &[TracePath],
238 representative_payloads: usize,
239 new_payloads: usize,
240 constraints: &TermConstraints,
241) -> bool {
242 if new_payloads <= representative_payloads {
243 return true; }
245 let delta = new_payloads - representative_payloads;
246
247 let mut per_step: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
251 for (step_index, _) in targets {
252 *per_step.entry(*step_index).or_insert(0) += 1;
253 }
254 for (step_index, count) in &per_step {
255 let cur = match trace.steps.get(*step_index).map(|s| &s.action) {
256 Some(Action::Input(input)) => input.recipe.count_payloads(),
257 _ => 0,
258 };
259 if cur + count * delta > constraints.threshold_max_payloads_per_term {
260 return false;
261 }
262 }
263 true
264}
265
266pub fn apply_to_targets<PT: ProtocolTypes>(
277 trace: &mut Trace<PT>,
278 targets: &[TracePath],
279 new_term: &Term<PT>,
280 representative: &Term<PT>,
281 with_bit: bool,
282 constraints: &TermConstraints,
283) -> MutationResult {
284 if !replacement_within_caps(
285 trace,
286 targets,
287 representative.size(),
288 new_term.size(),
289 constraints,
290 ) {
291 log::debug!("[Mutation] rejected: would exceed result-size caps");
292 return MutationResult::Skipped;
293 }
294 if with_bit {
299 let new_payloads = new_term.count_payloads();
300 if new_payloads > 0
301 && !payloads_within_caps(
302 trace,
303 targets,
304 representative.count_payloads(),
305 new_payloads,
306 constraints,
307 )
308 {
309 log::debug!("[Mutation] rejected: would exceed per-term payload budget");
310 return MutationResult::Skipped;
311 }
312 }
313
314 let mut mutated = false;
315 for target in targets {
316 if let Some(term_mut) = find_term_mut(trace, target) {
317 term_mut.mutate(new_term.clone());
318 mutated = true;
319 }
320 }
321
322 if mutated {
323 MutationResult::Mutated
324 } else {
325 MutationResult::Skipped
326 }
327}
328
329pub fn scoped_targets<PT: ProtocolTypes, R: Rand>(
338 trace: &Trace<PT>,
339 to_mutate_path: &TracePath,
340 representative: &Term<PT>,
341 config: &MutationConfig,
342 rand: &mut R,
343) -> Vec<TracePath> {
344 let scope = MutationScope::choose(config.scope_weights, rand);
345 let (chosen_step, _) = to_mutate_path;
346
347 let filter = |term: &Term<PT>| {
349 term.get_type_shape() == representative.get_type_shape() && term == representative
350 };
351 let constraints = TermConstraints::no_constraint();
354
355 let mut targets: Vec<TracePath> = match scope {
356 MutationScope::Individual => vec![],
358 MutationScope::Step => match trace.steps.get(*chosen_step).map(|step| &step.action) {
360 Some(Action::Input(input)) => {
361 find_all_sub_term_filtered(&input.recipe, filter, &constraints)
362 .into_iter()
363 .map(|term_path| (*chosen_step, term_path))
364 .collect()
365 }
366 _ => vec![],
367 },
368 MutationScope::Global => find_all_term_filtered(trace, filter, &constraints),
369 };
370 targets.retain(|path| path != to_mutate_path);
372 targets.push(to_mutate_path.clone());
373
374 log::debug!(
375 "[Mutation] scope {scope:?} selected {} occurrence(s)",
376 targets.len()
377 );
378 targets
379}
380
381impl MutationConfig {
382 pub fn default_with_bit() -> Self {
383 MutationConfig {
384 with_bit_level: true,
385 with_focus: false,
386 ..Self::default()
387 }
388 }
389}
390pub type DyMutations<'harness, PT, PB, S> = tuple_list_type!(
391RepeatMutator<S>,
393 SkipMutator<S>,
394 ReplaceReuseMutator<S>,
395 ReplaceMatchMutator<S, PT>,
396 RemoveAndLiftMutator<S>,
397 GenerateMutator<'harness, S, PB>,
398 SwapMutator<S>,
399);
400
401#[must_use]
402pub fn dy_mutations<'harness, S, PT: ProtocolTypes, PB>(
403 mutation_config: MutationConfig,
404 signature: &'static Signature<PT>,
405 put_registry: &'harness PutRegistry<PB>,
406) -> DyMutations<'harness, PT, PB, S>
407where
408 S: HasCorpus<Trace<PT>> + HasMetadata + HasMaxSize + HasRand,
409 PB: ProtocolBehavior<ProtocolTypes = PT>,
410{
411 let MutationConfig {
412 fresh_zoo_after,
413 max_result_trace_length,
414 min_trace_length,
415 term_constraints,
416 with_dy,
417 ..
418 } = mutation_config;
419
420 tuple_list!(
421 RepeatMutator::new(max_result_trace_length, with_dy),
422 SkipMutator::new(min_trace_length, with_dy),
423 ReplaceReuseMutator::new(mutation_config),
424 ReplaceMatchMutator::new(mutation_config, signature),
425 RemoveAndLiftMutator::new(term_constraints, with_dy),
426 GenerateMutator::new(
427 0,
428 fresh_zoo_after,
429 None,
430 signature,
431 put_registry,
432 mutation_config,
433 ), SwapMutator::new(term_constraints, with_dy),
435 )
436}
437
438pub struct SwapMutator<S>
447where
448 S: HasRand,
449{
450 constraints: TermConstraints,
451 phantom_s: std::marker::PhantomData<S>,
452 with_dy: bool,
453}
454
455impl<S> SwapMutator<S>
456where
457 S: HasRand,
458{
459 #[must_use]
460 pub const fn new(constraints: TermConstraints, with_dy: bool) -> Self {
461 Self {
462 constraints,
463 phantom_s: std::marker::PhantomData,
464 with_dy,
465 }
466 }
467}
468
469impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for SwapMutator<S>
470where
471 S: HasRand,
472{
473 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
474 log::debug!("[DY] Start mutate with {}", self.name());
475 if !self.with_dy {
476 return Ok(MutationResult::Skipped);
477 }
478 let _a = BytesInsertMutator;
479 let rand = state.rand_mut();
480 if let Some((term_a, trace_path_a)) = choose(trace, &self.constraints, rand) {
481 if let Some(trace_path_b) = choose_term_path_filtered(
482 trace,
483 |term: &Term<PT>| term.get_type_shape() == term_a.get_type_shape(),
484 &self.constraints,
485 rand,
486 ) {
487 let step_a_size = match &trace.steps[trace_path_a.0].action {
488 crate::trace::Action::Input(input) => input.recipe.size(),
489 crate::trace::Action::Output(_) => 0,
490 };
491 let step_b_size = match &trace.steps[trace_path_b.0].action {
492 crate::trace::Action::Input(input) => input.recipe.size(),
493 crate::trace::Action::Output(_) => 0,
494 };
495
496 let term_a_cloned = term_a.clone();
497 let term_a_size = term_a.size();
498
499 if let Some(term_b_mut) = find_term_mut(trace, &trace_path_b) {
500 let term_b_size = term_b_mut.size();
501
502 if step_b_size + term_a_size <= 500 + term_b_size
505 && step_a_size + term_b_size <= 500 + term_a_size
506 {
507 log::debug!(
508 "[Mutation] Mutate SwapMutator on terms\n{} and\n {}",
509 term_a_cloned,
510 term_b_mut
511 );
512 let term_b_cloned = term_b_mut.clone();
513 term_b_mut.mutate(term_a_cloned);
514 if let Some(trace_a_mut) = find_term_mut(trace, &trace_path_a) {
515 trace_a_mut.mutate(term_b_cloned);
516 }
517 return Ok(MutationResult::Mutated);
518 } else {
519 log::debug!("[SwapMutator] Skipped as it would exceed max_term_size.");
520 }
521 }
522 }
523 }
524 log::debug!(" Skipped {}", self.name());
525 Ok(MutationResult::Skipped)
526 }
527
528 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
529 Ok(())
530 }
531}
532impl<S> Named for SwapMutator<S>
533where
534 S: HasRand,
535{
536 fn name(&self) -> &Cow<'static, str> {
537 &Cow::Borrowed("SwapMutator")
538 }
539}
540
541pub struct RemoveAndLiftMutator<S>
552where
553 S: HasRand,
554{
555 constraints: TermConstraints,
556 phantom_s: std::marker::PhantomData<S>,
557 with_dy: bool,
558}
559
560impl<S> RemoveAndLiftMutator<S>
561where
562 S: HasRand,
563{
564 #[must_use]
565 pub const fn new(constraints: TermConstraints, with_dy: bool) -> Self {
566 Self {
567 constraints,
568 phantom_s: std::marker::PhantomData,
569 with_dy,
570 }
571 }
572}
573
574impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for RemoveAndLiftMutator<S>
575where
576 S: HasRand,
577{
578 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
579 log::debug!("[DY] Start mutate with {}", self.name());
580 if !self.with_dy {
581 return Ok(MutationResult::Skipped);
582 }
583 let rand = state.rand_mut();
584 let filter = |term: &Term<PT>| {
585 term.is_symbolic() && match &term.term {
587 DYTerm::Variable(_) => false,
588 DYTerm::Application(_, subterms) =>
589 {
590 subterms
591 .find_subterm(|subterm| match &subterm.term {
592 DYTerm::Variable(_) => false,
593 DYTerm::Application(_, grand_subterms) => {
594 grand_subterms.find_subterm_same_shape(subterm).is_some()
595 }
596 })
597 .is_some()
598 }
599 }
600 };
601 if let Some(to_mutate) = choose_term_filtered_mut(trace, filter, &self.constraints, rand) {
602 log::debug!(
603 "[Mutation] Mutate RemoveAndLiftMutator on term\n{}",
604 to_mutate
605 );
606 match &mut to_mutate.term {
607 DYTerm::Variable(_) => {
609 log::debug!(" Skipped {}", self.name());
610 Ok(MutationResult::Skipped)
611 }
612 DYTerm::Application(_, ref mut subterms) => {
613 if let Some(((subterm_index, _), grand_subterm)) = choose_iter(
614 subterms.filter_grand_subterms(|subterm, grand_subterm| {
615 subterm.get_type_shape() == grand_subterm.get_type_shape()
616 }),
617 rand,
618 ) {
619 let grand_subterm_cloned = grand_subterm.clone();
620 subterms.push(grand_subterm_cloned);
621 subterms.swap_remove(subterm_index);
622 return Ok(MutationResult::Mutated);
623 }
624 log::debug!(" Skipped {}", self.name());
625 Ok(MutationResult::Skipped)
626 }
627 }
628 } else {
629 log::debug!(" Skipped {}", self.name());
630 Ok(MutationResult::Skipped)
631 }
632 }
633
634 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
635 Ok(())
636 }
637}
638
639impl<S> Named for RemoveAndLiftMutator<S>
640where
641 S: HasRand,
642{
643 fn name(&self) -> &Cow<'static, str> {
644 &Cow::Borrowed("RemoveAndLiftMutator")
645 }
646}
647
648pub struct ReplaceMatchMutator<S, PT: ProtocolTypes>
654where
655 S: HasRand,
656{
657 config: MutationConfig,
658 signature: &'static Signature<PT>,
659 phantom_s: std::marker::PhantomData<S>,
660}
661
662impl<S, PT: ProtocolTypes> ReplaceMatchMutator<S, PT>
663where
664 S: HasRand,
665{
666 #[must_use]
667 pub const fn new(config: MutationConfig, signature: &'static Signature<PT>) -> Self {
668 Self {
669 config: MutationConfig {
670 term_constraints: TermConstraints {
671 must_be_symbolic: true,
673 ..config.term_constraints
674 },
675 ..config
676 },
677 signature,
678 phantom_s: std::marker::PhantomData,
679 }
680 }
681}
682
683impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for ReplaceMatchMutator<S, PT>
684where
685 S: HasRand,
686{
687 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
688 log::debug!("[DY] Start mutate with {}", self.name());
689 if !self.config.with_dy {
690 return Ok(MutationResult::Skipped);
691 }
692 let rand = state.rand_mut();
693 let Some(to_mutate_path) =
694 choose_term_path_filtered(trace, |_| true, &self.config.term_constraints, rand)
695 else {
696 log::debug!(" Skipped {}", self.name());
697 return Ok(MutationResult::Skipped);
698 };
699 let Some(to_mutate) = find_term(trace, &to_mutate_path).cloned() else {
700 log::debug!(" Skipped {}", self.name());
701 return Ok(MutationResult::Skipped);
702 };
703 log::debug!("[Mutation] ReplaceMatchMutator on term\n{}", to_mutate);
704
705 let new_term = match &to_mutate.term {
707 DYTerm::Variable(variable) => {
708 let Some((shape, dynamic_fn)) = self.signature.functions.choose_filtered(
709 |(shape, _)| variable.typ == shape.return_type && shape.is_constant(),
710 rand,
711 ) else {
712 log::debug!(" Skipped {}", self.name());
713 return Ok(MutationResult::Skipped);
714 };
715 Term::from(DYTerm::Application(
716 Function::new(shape.clone(), dynamic_fn.clone()),
717 Vec::new(),
718 ))
719 }
720 DYTerm::Application(func, _) => {
721 let Some((shape, dynamic_fn)) = self.signature.functions.choose_filtered(
722 |(shape, _)| {
723 func.shape() != shape
724 && func.shape().return_type == shape.return_type
725 && func.shape().argument_types == shape.argument_types
726 },
727 rand,
728 ) else {
729 log::debug!(" Skipped {}", self.name());
730 return Ok(MutationResult::Skipped);
731 };
732 let mut new_term = to_mutate.clone();
734 if let DYTerm::Application(new_func, _) = &mut new_term.term {
735 new_func.change_function(shape.clone(), dynamic_fn.clone());
736 }
737 new_term
738 }
739 };
740
741 Ok(apply_scoped_mutation(
742 trace,
743 &to_mutate_path,
744 &to_mutate,
745 &new_term,
746 &self.config,
747 rand,
748 ))
749 }
750
751 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
752 Ok(())
753 }
754}
755
756impl<S, PT: ProtocolTypes> Named for ReplaceMatchMutator<S, PT>
757where
758 S: HasRand,
759{
760 fn name(&self) -> &Cow<'static, str> {
761 &Cow::Borrowed("ReplaceMatchMutator")
762 }
763}
764
765pub struct ReplaceReuseMutator<S>
769where
770 S: HasRand,
771{
772 config: MutationConfig,
773 phantom_s: std::marker::PhantomData<S>,
774}
775
776impl<S> ReplaceReuseMutator<S>
777where
778 S: HasRand,
779{
780 #[must_use]
781 pub const fn new(config: MutationConfig) -> Self {
782 Self {
783 config,
784 phantom_s: std::marker::PhantomData,
785 }
786 }
787}
788
789impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for ReplaceReuseMutator<S>
790where
791 S: HasRand,
792{
793 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
794 log::debug!("[DY] Start mutate with {}", self.name());
795 if !self.config.with_dy {
796 return Ok(MutationResult::Skipped);
797 }
798 let rand = state.rand_mut();
799 if let Some(replacement) = choose_term(trace, &self.config.term_constraints, rand).cloned()
800 {
801 if let Some(to_replace_path) = choose_term_path_filtered(
802 trace,
803 |term: &Term<PT>| term.get_type_shape() == replacement.get_type_shape(),
804 &self.config.term_constraints,
805 rand,
806 ) {
807 let Some(to_replace) = find_term(trace, &to_replace_path).cloned() else {
808 log::debug!(" Skipped {}", self.name());
809 return Ok(MutationResult::Skipped);
810 };
811 let targets =
814 scoped_targets(trace, &to_replace_path, &to_replace, &self.config, rand);
815 log::debug!(
819 "[Mutation] Mutate ReplaceReuseMutator on terms\n {} and\n{}",
820 to_replace,
821 replacement
822 );
823 return Ok(apply_to_targets(
824 trace,
825 &targets,
826 &replacement,
827 &to_replace,
828 self.config.with_bit_level,
829 &self.config.term_constraints,
830 ));
831 }
832 }
833 log::debug!(" Skipped {}", self.name());
834 Ok(MutationResult::Skipped)
835 }
836
837 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
838 Ok(())
839 }
840}
841
842impl<S> Named for ReplaceReuseMutator<S>
843where
844 S: HasRand,
845{
846 fn name(&self) -> &Cow<'static, str> {
847 &Cow::Borrowed("ReplaceReuseMutator")
848 }
849}
850
851pub struct SkipMutator<S>
853where
854 S: HasRand,
855{
856 min_trace_length: usize,
857 phantom_s: std::marker::PhantomData<S>,
858 with_dy: bool,
859}
860
861impl<S> SkipMutator<S>
862where
863 S: HasRand,
864{
865 #[must_use]
866 pub const fn new(min_trace_length: usize, with_dy: bool) -> Self {
867 Self {
868 min_trace_length,
869 phantom_s: std::marker::PhantomData,
870 with_dy,
871 }
872 }
873}
874impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for SkipMutator<S>
875where
876 S: HasRand,
877{
878 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
879 log::debug!("[DY] Start mutate with {}", self.name());
880 if !self.with_dy {
881 return Ok(MutationResult::Skipped);
882 }
883 let steps = &mut trace.steps;
884 let length = steps.len();
885 if length <= self.min_trace_length {
886 log::debug!(" Skipped {}", self.name());
887 return Ok(MutationResult::Skipped);
888 }
889 if length == 0 {
890 log::debug!(" Skipped {}", self.name());
891 return Ok(MutationResult::Skipped);
892 }
893 let remove_index = state.rand_mut().between(0, length - 1);
894 log::debug!("[Mutation] Mutate SkipMutator on step {remove_index}");
895 steps.remove(remove_index);
896 Ok(MutationResult::Mutated)
897 }
898
899 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
900 Ok(())
901 }
902}
903impl<S> Named for SkipMutator<S>
904where
905 S: HasRand,
906{
907 fn name(&self) -> &Cow<'static, str> {
908 &Cow::Borrowed("SkipMutator")
909 }
910}
911
912pub struct RepeatMutator<S>
914where
915 S: HasRand,
916{
917 max_result_trace_length: usize,
918 phantom_s: std::marker::PhantomData<S>,
919 with_dy: bool,
920}
921
922impl<S> RepeatMutator<S>
923where
924 S: HasRand,
925{
926 #[must_use]
927 pub const fn new(max_result_trace_length: usize, with_dy: bool) -> Self {
928 Self {
929 max_result_trace_length,
930 phantom_s: std::marker::PhantomData,
931 with_dy,
932 }
933 }
934}
935impl<S, PT: ProtocolTypes> Mutator<Trace<PT>, S> for RepeatMutator<S>
936where
937 S: HasRand,
938{
939 fn mutate(&mut self, state: &mut S, trace: &mut Trace<PT>) -> Result<MutationResult, Error> {
940 log::debug!("[DY] Start mutate with {}", self.name());
941 if !self.with_dy {
942 return Ok(MutationResult::Skipped);
943 }
944 let steps = &trace.steps;
945 let length = steps.len();
946 if length >= self.max_result_trace_length {
947 log::debug!(" Skipped {}", self.name());
948 return Ok(MutationResult::Skipped);
949 }
950 if length == 0 {
951 log::debug!(" Skipped {}", self.name());
952 return Ok(MutationResult::Skipped);
953 }
954 let insert_index = state.rand_mut().between(0, length);
955 let Some(step) = state.rand_mut().choose(steps) else {
956 return Ok(MutationResult::Skipped);
957 };
958 let step = step.clone();
959 log::debug!("[Mutation] Mutate RepeatMutator on step {insert_index}");
960 trace.steps.insert(insert_index, step);
961 Ok(MutationResult::Mutated)
962 }
963
964 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
965 Ok(())
966 }
967}
968impl<S> Named for RepeatMutator<S>
969where
970 S: HasRand,
971{
972 fn name(&self) -> &Cow<'static, str> {
973 &Cow::Borrowed("RepeatMutator")
974 }
975}
976
977pub struct GenerateMutator<'a, S, PB: ProtocolBehavior>
979where
980 S: HasRand,
981{
982 mutation_counter: u64,
983 refresh_zoo_after: u64,
984 config: MutationConfig,
985 zoo: Option<TermZoo<PB>>,
986 signature: &'static Signature<PB::ProtocolTypes>,
987 put_registry: &'a PutRegistry<PB>,
988 phantom_s: std::marker::PhantomData<S>,
989}
990impl<'a, S, PB: ProtocolBehavior> GenerateMutator<'a, S, PB>
991where
992 S: HasRand,
993{
994 #[must_use]
995 #[allow(clippy::too_many_arguments)]
996 pub const fn new(
997 mutation_counter: u64,
998 refresh_zoo_after: u64,
999 zoo: Option<TermZoo<PB>>,
1000 signature: &'static Signature<PB::ProtocolTypes>,
1001 put_registry: &'a PutRegistry<PB>,
1002 config: MutationConfig,
1003 ) -> Self {
1004 Self {
1005 mutation_counter,
1006 refresh_zoo_after,
1007 config,
1008 zoo,
1009 signature,
1010 put_registry,
1011 phantom_s: std::marker::PhantomData,
1012 }
1013 }
1014}
1015impl<'a, S, PB: ProtocolBehavior> Mutator<Trace<PB::ProtocolTypes>, S>
1016 for GenerateMutator<'a, S, PB>
1017where
1018 S: HasRand,
1019{
1020 fn mutate(
1021 &mut self,
1022 state: &mut S,
1023 trace: &mut Trace<PB::ProtocolTypes>,
1024 ) -> Result<MutationResult, Error> {
1025 log::debug!("[DY] Start mutate with {}", self.name());
1026 if !self.config.with_dy {
1027 return Ok(MutationResult::Skipped);
1028 }
1029 let rand = state.rand_mut();
1030 let (to_mutate_path, to_mutate, new_term) = {
1031 if let Some((to_mutate, to_mutate_path)) =
1032 reservoir_sample(trace, |_| true, &self.config.term_constraints, rand)
1033 {
1034 log::debug!("[Mutation] Mutate GenerateMutator on term\n{}", to_mutate);
1035 self.mutation_counter += 1;
1036 let zoo = if self.mutation_counter % self.refresh_zoo_after == 0 {
1037 log::debug!("[Mutation] Mutate GenerateMutator: refresh zoo");
1038 let spawner = Spawner::new(self.put_registry.clone());
1039 let ctx = TraceContext::new(spawner); self.zoo.insert(TermZoo::generate(
1041 &ctx,
1042 self.signature,
1043 rand,
1044 self.config.term_constraints.zoo_gen_how_many,
1045 self.config.term_constraints.zoo_max_depth,
1046 ))
1047 } else {
1048 self.zoo.get_or_insert_with(|| {
1049 let spawner = Spawner::new(self.put_registry.clone());
1050 let ctx = TraceContext::new(spawner); TermZoo::generate(
1052 &ctx,
1053 self.signature,
1054 rand,
1055 self.config.term_constraints.zoo_gen_how_many,
1056 self.config.term_constraints.zoo_max_depth,
1057 )
1058 })
1059 };
1060 if let Some(new_term) = zoo.choose_filtered(
1061 |term| {
1062 to_mutate.get_type_shape() == term.get_type_shape() && *to_mutate != **term
1064 },
1065 rand,
1066 ) {
1067 log::debug!(
1068 "Found to_mutate and new_term: {}\n ----------------\n{}",
1069 to_mutate,
1070 new_term
1071 );
1072 (to_mutate_path, to_mutate.clone(), new_term.clone())
1074 } else {
1075 return Ok(MutationResult::Skipped);
1076 }
1077 } else {
1078 return Ok(MutationResult::Skipped);
1079 }
1080 };
1081
1082 Ok(apply_scoped_mutation(
1084 trace,
1085 &to_mutate_path,
1086 &to_mutate,
1087 &new_term,
1088 &self.config,
1089 rand,
1090 ))
1091 }
1092
1093 fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
1094 Ok(())
1095 }
1096}
1097
1098impl<'a, S, PB: ProtocolBehavior> Named for GenerateMutator<'a, S, PB>
1099where
1100 S: HasRand,
1101{
1102 fn name(&self) -> &Cow<'static, str> {
1103 &Cow::Borrowed("GenerateMutator")
1104 }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use std::collections::{HashMap, HashSet};
1110
1111 use libafl::corpus::InMemoryCorpus;
1112 use libafl::mutators::{MutationResult, Mutator};
1113 use libafl::state::StdState;
1114 use libafl_bolts::rands::{RomuDuoJrRand, StdRand};
1115
1116 use super::*;
1117 use crate::agent::AgentName;
1118 use crate::algebra::dynamic_function::DescribableFunction;
1119 use crate::algebra::test_signature::{TestTrace, *};
1120 use crate::algebra::DYTerm;
1121 use crate::fuzzer::utils::{choose_term_path, TracePath};
1122 use crate::term;
1123 use crate::trace::{Action, InputAction, Step};
1124
1125 fn create_state(
1126 ) -> StdState<InMemoryCorpus<TestTrace>, TestTrace, RomuDuoJrRand, InMemoryCorpus<TestTrace>>
1127 {
1128 let rand = StdRand::with_seed(1235);
1129 let corpus: InMemoryCorpus<TestTrace> = InMemoryCorpus::new();
1130 StdState::new(rand, corpus, InMemoryCorpus::new(), &mut (), &mut ()).unwrap()
1131 }
1132
1133 #[test_log::test]
1135 fn test_repeat_mutator() {
1136 let mut state = create_state();
1137
1138 let mut mutator = RepeatMutator::new(15, true);
1139
1140 fn check_is_encrypt12(step: &Step<TestProtocolTypes>) -> bool {
1141 if let Action::Input(input) = &step.action {
1142 if input.recipe.name() == fn_encrypt12.name() {
1143 return true;
1144 }
1145 }
1146 false
1147 }
1148
1149 loop {
1150 let mut trace = setup_simple_trace();
1151 mutator.mutate(&mut state, &mut trace).unwrap();
1152
1153 let length = trace.steps.len();
1154 if let Some(last) = trace.steps.get(length - 1) {
1155 if check_is_encrypt12(last) {
1156 if let Some(step) = trace.steps.get(length - 2) {
1157 if check_is_encrypt12(step) {
1158 break;
1159 }
1160 }
1161 }
1162 }
1163 }
1164 }
1165
1166 #[test_log::test]
1172 fn test_scope_extent() {
1173 let mut rand = StdRand::with_seed(11);
1174
1175 let duplicated: Term<TestProtocolTypes> = term! { fn_signature_algorithm_extension };
1177 let recipe_a: Term<TestProtocolTypes> = term! {
1178 fn_client_extensions_append(
1179 (fn_client_extensions_append(
1180 (fn_client_extensions_append(
1181 fn_client_extensions_new,
1182 fn_signature_algorithm_extension
1183 )),
1184 fn_signature_algorithm_extension
1185 )),
1186 fn_signature_algorithm_extension
1187 )
1188 };
1189 let recipe_b: Term<TestProtocolTypes> = term! {
1190 fn_client_extensions_append(
1191 (fn_client_extensions_append(
1192 fn_client_extensions_new,
1193 fn_signature_algorithm_extension
1194 )),
1195 fn_signature_algorithm_extension
1196 )
1197 };
1198 let build_trace = || Trace {
1199 steps: vec![
1200 Step {
1201 agent: AgentName::first(),
1202 action: Action::Input(InputAction {
1203 recipe: recipe_a.clone(),
1204 precomputations: vec![],
1205 }),
1206 },
1207 Step {
1208 agent: AgentName::first(),
1209 action: Action::Input(InputAction {
1210 recipe: recipe_b.clone(),
1211 precomputations: vec![],
1212 }),
1213 },
1214 ],
1215 ..setup_simple_trace()
1216 };
1217 let replacement: Term<TestProtocolTypes> = term! { fn_ec_point_formats_extension };
1218
1219 let remaining = |trace: &Trace<TestProtocolTypes>| {
1221 (0..2)
1222 .map(|step| match &trace.steps[step].action {
1223 Action::Input(input) => (&input.recipe)
1224 .into_iter()
1225 .filter(|t| **t == duplicated)
1226 .count(),
1227 Action::Output(_) => 0,
1228 })
1229 .collect::<Vec<_>>()
1230 };
1231
1232 let chosen_path: TracePath = (0, vec![1]); let sanity = build_trace();
1234 assert_eq!(remaining(&sanity), vec![3, 2], "test fixture");
1235
1236 for (weights, expected, label) in [
1237 (ScopeWeights::new(0, 0, 1), vec![2, 2], "individual"),
1238 (ScopeWeights::new(0, 1, 0), vec![0, 2], "step"),
1239 (ScopeWeights::new(1, 0, 0), vec![0, 0], "global"),
1240 ] {
1241 let mut trace = build_trace();
1242 let targets = scoped_targets(
1243 &trace,
1244 &chosen_path,
1245 &duplicated,
1246 &MutationConfig {
1247 scope_weights: weights,
1248 ..MutationConfig::default()
1249 },
1250 &mut rand,
1251 );
1252 assert_eq!(
1253 apply_to_targets(
1254 &mut trace,
1255 &targets,
1256 &replacement,
1257 &duplicated,
1258 true,
1259 &TermConstraints::no_constraint(),
1260 ),
1261 MutationResult::Mutated,
1262 "{label}: should have mutated"
1263 );
1264 assert_eq!(
1265 remaining(&trace),
1266 expected,
1267 "{label}: unexpected extent of the replacement (per-step occurrences left)"
1268 );
1269 }
1270 }
1271
1272 #[test_log::test]
1279 fn test_payload_budget_is_per_term_not_average() {
1280 let trace = setup_simple_trace();
1281 let step = trace
1282 .steps
1283 .iter()
1284 .position(|s| matches!(s.action, Action::Input(_)))
1285 .expect("the fixture has at least one input step");
1286 let base = match &trace.steps[step].action {
1289 Action::Input(input) => input.recipe.count_payloads(),
1290 Action::Output(_) => unreachable!(),
1291 };
1292
1293 let concentrated: Vec<TracePath> = (0..4).map(|i| (step, vec![i])).collect();
1295 let single: Vec<TracePath> = vec![(step, vec![0])];
1296 let constraints = TermConstraints {
1298 threshold_max_payloads_per_term: base + 3,
1299 ..TermConstraints::no_constraint()
1300 };
1301
1302 assert!(
1306 !payloads_within_caps(&trace, &concentrated, base, base + 1, &constraints),
1307 "four +1-payload replacements in one recipe must exceed the per-term budget"
1308 );
1309 assert!(
1311 payloads_within_caps(&trace, &single, base, base + 1, &constraints),
1312 "a single growing replacement stays within the per-term budget"
1313 );
1314 assert!(payloads_within_caps(
1316 &trace,
1317 &concentrated,
1318 7,
1319 7,
1320 &constraints
1321 ));
1322 assert!(payloads_within_caps(
1323 &trace,
1324 &concentrated,
1325 9,
1326 2,
1327 &constraints
1328 ));
1329 }
1330
1331 #[test_log::test]
1334 fn test_count_payloads_matches_all_payloads_len() {
1335 let trace = setup_simple_trace();
1336 for step in &trace.steps {
1337 let Action::Input(input) = &step.action else {
1338 continue;
1339 };
1340 for sub in &input.recipe {
1342 assert_eq!(sub.count_payloads(), sub.all_payloads().len());
1343 }
1344 let mut with_payload = input.recipe.clone();
1346 with_payload.add_payload(vec![0u8; 4]);
1347 assert_eq!(
1348 with_payload.count_payloads(),
1349 with_payload.all_payloads().len()
1350 );
1351 assert_eq!(with_payload.count_payloads(), 1);
1352 }
1353 }
1354
1355 #[test_log::test]
1362 fn test_replacement_within_caps_rejects_growth() {
1363 let trace = setup_simple_trace();
1364 let step = trace
1365 .steps
1366 .iter()
1367 .position(|s| matches!(s.action, Action::Input(_)))
1368 .expect("the fixture has at least one input step");
1369 let cur = match &trace.steps[step].action {
1370 Action::Input(input) => input.recipe.size(),
1371 Action::Output(_) => unreachable!(),
1372 };
1373 let targets: Vec<TracePath> = vec![(step, vec![0])]; let (repr, new) = (1, 6);
1377
1378 let generous = TermConstraints {
1380 max_result_term_size: cur + 100,
1381 ..TermConstraints::no_constraint()
1382 };
1383 assert!(replacement_within_caps(
1384 &trace, &targets, repr, new, &generous
1385 ));
1386
1387 let tight_step = TermConstraints {
1389 max_result_term_size: cur + 2,
1390 ..TermConstraints::no_constraint()
1391 };
1392 assert!(!replacement_within_caps(
1393 &trace,
1394 &targets,
1395 repr,
1396 new,
1397 &tight_step
1398 ));
1399
1400 let below_seed = TermConstraints {
1407 max_result_term_size: 0,
1408 ..TermConstraints::no_constraint()
1409 };
1410 assert!(replacement_within_caps(&trace, &targets, 4, 4, &below_seed)); assert!(replacement_within_caps(&trace, &targets, 9, 2, &below_seed)); }
1413
1414 #[test_log::test]
1418 fn test_scoped_mutation_always_mutates_chosen_occurrence() {
1419 let mut rand = StdRand::with_seed(7);
1420
1421 for weights in [
1422 ScopeWeights::new(1, 0, 0), ScopeWeights::new(0, 1, 0), ScopeWeights::new(0, 0, 1), ScopeWeights::default(),
1426 ] {
1427 let mut trace = setup_simple_trace();
1428 let path =
1429 choose_term_path_filtered(&trace, |_| true, &TermConstraints::default(), &mut rand)
1430 .unwrap();
1431 let chosen = find_term(&trace, &path).unwrap().clone();
1432
1433 let Some(replacement) = trace
1435 .steps
1436 .iter()
1437 .filter_map(|step| match &step.action {
1438 Action::Input(input) => Some(&input.recipe),
1439 Action::Output(_) => None,
1440 })
1441 .flat_map(|recipe| recipe.into_iter())
1442 .find(|term| term.get_type_shape() == chosen.get_type_shape() && **term != chosen)
1443 .cloned()
1444 else {
1445 continue; };
1447
1448 let targets = scoped_targets(
1449 &trace,
1450 &path,
1451 &chosen,
1452 &MutationConfig {
1453 scope_weights: weights,
1454 ..MutationConfig::default()
1455 },
1456 &mut rand,
1457 );
1458 let result = apply_to_targets(
1459 &mut trace,
1460 &targets,
1461 &replacement,
1462 &chosen,
1463 true,
1464 &TermConstraints::no_constraint(),
1465 );
1466
1467 assert_eq!(
1468 result,
1469 MutationResult::Mutated,
1470 "weights {weights:?} should have mutated"
1471 );
1472 assert_eq!(
1473 find_term(&trace, &path).unwrap(),
1474 &replacement,
1475 "weights {weights:?}: the chosen occurrence must carry the replacement"
1476 );
1477 }
1478 }
1479
1480 #[test_log::test]
1483 fn test_scope_weights() {
1484 let mut rand = StdRand::with_seed(42);
1485
1486 let draw = |w: ScopeWeights, rand: &mut _| {
1487 let mut seen = std::collections::HashSet::new();
1488 for _ in 0..200 {
1489 seen.insert(MutationScope::choose(w, rand));
1490 }
1491 seen
1492 };
1493
1494 let all = draw(ScopeWeights::default(), &mut rand);
1496 assert_eq!(all.len(), 3, "uniform weights should draw all three scopes");
1497
1498 let no_step = draw(ScopeWeights::new(1, 0, 1), &mut rand);
1500 assert!(!no_step.contains(&MutationScope::Step));
1501 assert!(no_step.contains(&MutationScope::Global));
1502 assert!(no_step.contains(&MutationScope::Individual));
1503
1504 let only_individual = draw(ScopeWeights::new(0, 0, 1), &mut rand);
1506 assert_eq!(only_individual, HashSet::from([MutationScope::Individual]));
1507
1508 let zeroed = draw(ScopeWeights::new(0, 0, 0), &mut rand);
1510 assert_eq!(zeroed, HashSet::from([MutationScope::Individual]));
1511 }
1512
1513 #[test_log::test]
1514 fn test_replace_match_mutator() {
1515 let _server = AgentName::first();
1516 let mut state = create_state();
1517 let mut mutator =
1518 ReplaceMatchMutator::new(MutationConfig::default_with_bit(), &TEST_SIGNATURE);
1519
1520 loop {
1521 let mut trace = setup_simple_trace();
1522 mutator.mutate(&mut state, &mut trace).unwrap();
1523
1524 if let Some(last) = trace.steps.iter().last() {
1525 match &last.action {
1526 Action::Input(input) => match &input.recipe.term {
1527 DYTerm::Variable(_) => {}
1528 DYTerm::Application(_, subterms) => {
1529 if let Some(last_subterm) = subterms.iter().last() {
1530 if last_subterm.name() == fn_seq_1.name() {
1531 break;
1532 }
1533 }
1534 }
1535 },
1536 Action::Output(_) => {}
1537 }
1538 }
1539 }
1540 }
1541
1542 #[test_log::test]
1543 fn test_remove_lift_mutator() {
1544 let mut state = create_state();
1546 let _server = AgentName::first();
1547 let mut mutator = RemoveAndLiftMutator::new(TermConstraints::default(), true);
1548
1549 fn sum_extension_appends(trace: &TestTrace) -> usize {
1551 trace.count_functions_by_name(fn_client_extensions_append.name())
1552 }
1553
1554 loop {
1555 let mut trace = setup_simple_trace();
1556 let before_mutation = sum_extension_appends(&trace);
1557 let result = mutator.mutate(&mut state, &mut trace).unwrap();
1558
1559 if result == MutationResult::Mutated {
1560 let after_mutation = sum_extension_appends(&trace);
1561 if after_mutation < before_mutation {
1562 break;
1564 }
1565 }
1566 }
1567 }
1568
1569 #[test_log::test]
1570 fn test_replace_reuse_mutator() {
1571 let mut state = create_state();
1572 let _server = AgentName::first();
1573 let mut mutator = ReplaceReuseMutator::new(MutationConfig::default_with_bit());
1574
1575 fn count_client_hello(trace: &TestTrace) -> usize {
1576 trace.count_functions_by_name(fn_client_hello.name())
1577 }
1578
1579 fn count_finished(trace: &TestTrace) -> usize {
1580 trace.count_functions_by_name(fn_finished.name())
1581 }
1582
1583 loop {
1584 let mut trace = setup_simple_trace();
1585 let result = mutator.mutate(&mut state, &mut trace).unwrap();
1586
1587 if result == MutationResult::Mutated {
1588 let client_hellos = count_client_hello(&trace);
1589 let finishes = count_finished(&trace);
1590 if client_hellos == 2 && finishes == 0 {
1591 break;
1593 }
1594 }
1595 }
1596 }
1597
1598 #[test_log::test]
1599 fn test_skip_mutator() {
1600 let mut state = create_state();
1601 let _server = AgentName::first();
1602 let mut mutator = SkipMutator::new(2, true);
1603
1604 loop {
1605 let mut trace = setup_simple_trace();
1606 let before_len = trace.steps.len();
1607 mutator.mutate(&mut state, &mut trace).unwrap();
1608
1609 if before_len - 1 == trace.steps.len() {
1610 break;
1611 }
1612 }
1613 }
1614
1615 #[test_log::test]
1616 fn test_swap_mutator() {
1617 let mut state = create_state();
1618 let mut mutator = SwapMutator::new(TermConstraints::default(), true);
1619
1620 loop {
1621 let mut trace = setup_simple_trace();
1622 mutator.mutate(&mut state, &mut trace).unwrap();
1623
1624 let is_first_not_ch = if let Some(first) = trace.steps.first() {
1625 match &first.action {
1626 Action::Input(input) => Some(input.recipe.name() != fn_client_hello.name()),
1627 Action::Output(_) => None,
1628 }
1629 } else {
1630 None
1631 };
1632
1633 let is_next_not_fn_client_key_exchange = if let Some(next) = trace.steps.get(1) {
1634 match &next.action {
1635 Action::Input(input) => {
1636 Some(input.recipe.name() != fn_client_key_exchange.name())
1637 }
1638 Action::Output(_) => None,
1639 }
1640 } else {
1641 None
1642 };
1643
1644 if let Some(first) = is_first_not_ch {
1645 if let Some(second) = is_next_not_fn_client_key_exchange {
1646 if first && second {
1647 break;
1648 }
1649 }
1650 }
1651 }
1652 }
1653
1654 #[test_log::test]
1655 fn test_find_term() {
1656 let mut rand = StdRand::with_seed(45);
1657 let mut trace = setup_simple_trace();
1658 let term_size = trace.count_functions();
1659
1660 let mut stats: HashSet<TracePath> = HashSet::new();
1661
1662 for _ in 0..10000 {
1663 let path = choose_term_path(&trace, &TermConstraints::default(), &mut rand).unwrap();
1664 find_term_mut(&mut trace, &path).unwrap();
1665 stats.insert(path);
1666 }
1667
1668 assert_eq!(term_size, stats.len());
1669 }
1670
1671 #[test_log::test]
1672 fn test_reservoir_sample_randomness() {
1673 fn std_deviation(data: &[u32]) -> Option<f32> {
1675 fn mean(data: &[u32]) -> Option<f32> {
1676 let sum = data.iter().sum::<u32>() as f32;
1677 let count = data.len();
1678
1679 match count {
1680 positive if positive > 0 => Some(sum / count as f32),
1681 _ => None,
1682 }
1683 }
1684
1685 match (mean(data), data.len()) {
1686 (Some(data_mean), count) if count > 0 => {
1687 let variance = data
1688 .iter()
1689 .map(|value| {
1690 let diff = data_mean - (*value as f32);
1691
1692 diff * diff
1693 })
1694 .sum::<f32>()
1695 / count as f32;
1696
1697 Some(variance.sqrt())
1698 }
1699 _ => None,
1700 }
1701 }
1702
1703 let trace = setup_simple_trace();
1704 let term_size = trace.count_functions();
1705
1706 let mut rand = StdRand::with_seed(45);
1707 let mut stats: HashMap<u32, u32> = HashMap::new();
1708
1709 for _ in 0..10000 {
1710 let term = choose(&trace, &TermConstraints::default(), &mut rand).unwrap();
1711
1712 let id = term.0.resistant_id();
1713
1714 let count: u32 = *stats.get(&id).unwrap_or(&0);
1715 stats.insert(id, count + 1);
1716 }
1717
1718 let std_dev =
1719 std_deviation(stats.values().cloned().collect::<Vec<u32>>().as_slice()).unwrap();
1720 assert!(std_dev < 30.0);
1724 assert_eq!(term_size, stats.len());
1725 }
1726}