libafl/schedulers/
testcase_score.rs

1//! The `TestcaseScore` is an evaluator providing scores of corpus items.
2use alloc::string::{String, ToString};
3
4use libafl_bolts::{HasLen, HasRefCnt};
5use num_traits::Zero;
6
7use crate::{
8    Error, HasMetadata,
9    corpus::{Corpus, SchedulerTestcaseMetadata, Testcase},
10    feedbacks::MapIndexesMetadata,
11    schedulers::{
12        minimizer::{IsFavoredMetadata, TopRatedsMetadata},
13        powersched::{BaseSchedule, SchedulerMetadata},
14    },
15    state::HasCorpus,
16};
17
18/// Compute the favor factor of a [`Testcase`]. Higher is better.
19pub trait TestcaseScore<I, S> {
20    /// Computes the favor factor of a [`Testcase`]. Higher is better.
21    fn compute(state: &S, entry: &mut Testcase<I>) -> Result<f64, Error>;
22}
23
24/// Multiply the testcase size with the execution time.
25/// This favors small and quick testcases.
26#[derive(Debug, Clone)]
27pub struct LenTimeMulTestcaseScore {}
28
29impl<I, S> TestcaseScore<I, S> for LenTimeMulTestcaseScore
30where
31    S: HasCorpus<I>,
32    I: HasLen,
33{
34    #[expect(clippy::cast_precision_loss)]
35    fn compute(state: &S, entry: &mut Testcase<I>) -> Result<f64, Error> {
36        // TODO maybe enforce entry.exec_time().is_some()
37        Ok(entry.exec_time().map_or(1, |d| d.as_millis()) as f64
38            * entry.load_len(state.corpus())? as f64)
39    }
40}
41
42/// Constants for powerschedules
43const POWER_BETA: f64 = 1.0;
44const MAX_FACTOR: f64 = POWER_BETA * 32.0;
45const HAVOC_MAX_MULT: f64 = 64.0;
46
47/// The power assigned to each corpus entry
48/// This result is used for power scheduling
49#[derive(Debug, Clone)]
50pub struct CorpusPowerTestcaseScore {}
51
52impl<I, S> TestcaseScore<I, S> for CorpusPowerTestcaseScore
53where
54    S: HasCorpus<I> + HasMetadata,
55{
56    /// Compute the `power` we assign to each corpus entry
57    #[expect(clippy::cast_precision_loss, clippy::too_many_lines)]
58    fn compute(state: &S, entry: &mut Testcase<I>) -> Result<f64, Error> {
59        let psmeta = state.metadata::<SchedulerMetadata>()?;
60
61        let fuzz_mu = if let Some(strat) = psmeta.strat() {
62            if *strat.base() == BaseSchedule::COE {
63                let corpus = state.corpus();
64                let mut n_paths = 0;
65                let mut v = 0.0;
66                let cur_index = state.corpus().current().unwrap();
67                for id in corpus.ids() {
68                    let n_fuzz_entry = if cur_index == id {
69                        entry
70                            .metadata::<SchedulerTestcaseMetadata>()?
71                            .n_fuzz_entry()
72                    } else {
73                        corpus
74                            .get(id)?
75                            .borrow()
76                            .metadata::<SchedulerTestcaseMetadata>()?
77                            .n_fuzz_entry()
78                    };
79                    v += libm::log2(f64::from(psmeta.n_fuzz()[n_fuzz_entry]));
80                    n_paths += 1;
81                }
82
83                if n_paths == 0 {
84                    return Err(Error::unknown(String::from("Queue state corrput")));
85                }
86
87                v /= f64::from(n_paths);
88                v
89            } else {
90                0.0
91            }
92        } else {
93            0.0
94        };
95
96        let mut perf_score = 100.0;
97        let q_exec_us = entry
98            .exec_time()
99            .ok_or_else(|| Error::key_not_found("exec_time not set".to_string()))?
100            .as_nanos() as f64;
101
102        let avg_exec_us = psmeta.exec_time().as_nanos() as f64 / psmeta.cycles() as f64;
103        let avg_bitmap_size = if psmeta.bitmap_entries() == 0 {
104            1
105        } else {
106            psmeta.bitmap_size() / psmeta.bitmap_entries()
107        };
108
109        let favored = entry.has_metadata::<IsFavoredMetadata>();
110        let tcmeta = entry.metadata::<SchedulerTestcaseMetadata>()?;
111
112        if q_exec_us * 0.1 > avg_exec_us {
113            perf_score = 10.0;
114        } else if q_exec_us * 0.2 > avg_exec_us {
115            perf_score = 25.0;
116        } else if q_exec_us * 0.5 > avg_exec_us {
117            perf_score = 50.0;
118        } else if q_exec_us * 0.75 > avg_exec_us {
119            perf_score = 75.0;
120        } else if q_exec_us * 4.0 < avg_exec_us {
121            perf_score = 300.0;
122        } else if q_exec_us * 3.0 < avg_exec_us {
123            perf_score = 200.0;
124        } else if q_exec_us * 2.0 < avg_exec_us {
125            perf_score = 150.0;
126        }
127
128        let q_bitmap_size = tcmeta.bitmap_size() as f64;
129        if q_bitmap_size * 0.3 > avg_bitmap_size as f64 {
130            perf_score *= 3.0;
131        } else if q_bitmap_size * 0.5 > avg_bitmap_size as f64 {
132            perf_score *= 2.0;
133        } else if q_bitmap_size * 0.75 > avg_bitmap_size as f64 {
134            perf_score *= 1.5;
135        } else if q_bitmap_size * 3.0 < avg_bitmap_size as f64 {
136            perf_score *= 0.25;
137        } else if q_bitmap_size * 2.0 < avg_bitmap_size as f64 {
138            perf_score *= 0.5;
139        } else if q_bitmap_size * 1.5 < avg_bitmap_size as f64 {
140            perf_score *= 0.75;
141        }
142
143        if tcmeta.handicap() >= 4 {
144            perf_score *= 4.0;
145            // tcmeta.set_handicap(tcmeta.handicap() - 4);
146        } else if tcmeta.handicap() > 0 {
147            perf_score *= 2.0;
148            // tcmeta.set_handicap(tcmeta.handicap() - 1);
149        }
150
151        if tcmeta.depth() >= 4 && tcmeta.depth() < 8 {
152            perf_score *= 2.0;
153        } else if tcmeta.depth() >= 8 && tcmeta.depth() < 14 {
154            perf_score *= 3.0;
155        } else if tcmeta.depth() >= 14 && tcmeta.depth() < 25 {
156            perf_score *= 4.0;
157        } else if tcmeta.depth() >= 25 {
158            perf_score *= 5.0;
159        }
160
161        let mut factor: f64 = 1.0;
162
163        // COE and Fast schedule are fairly different from what are described in the original thesis,
164        // This implementation follows the changes made in this pull request https://github.com/AFLplusplus/AFLplusplus/pull/568
165        if let Some(strat) = psmeta.strat() {
166            match strat.base() {
167                BaseSchedule::EXPLORE => {
168                    // Nothing happens in EXPLORE
169                }
170                BaseSchedule::EXPLOIT => {
171                    factor = MAX_FACTOR;
172                }
173                BaseSchedule::COE => {
174                    if libm::log2(f64::from(psmeta.n_fuzz()[tcmeta.n_fuzz_entry()])) > fuzz_mu
175                        && !favored
176                    {
177                        // Never skip favorites.
178                        factor = 0.0;
179                    }
180                }
181                BaseSchedule::FAST => {
182                    if entry.scheduled_count() != 0 {
183                        let lg = libm::log2(f64::from(psmeta.n_fuzz()[tcmeta.n_fuzz_entry()]));
184
185                        match lg {
186                            f if f < 2.0 => {
187                                factor = 4.0;
188                            }
189                            f if (2.0..4.0).contains(&f) => {
190                                factor = 3.0;
191                            }
192                            f if (4.0..5.0).contains(&f) => {
193                                factor = 2.0;
194                            }
195                            f if (6.0..7.0).contains(&f) => {
196                                if !favored {
197                                    factor = 0.8;
198                                }
199                            }
200                            f if (7.0..8.0).contains(&f) => {
201                                if !favored {
202                                    factor = 0.6;
203                                }
204                            }
205                            f if f >= 8.0 => {
206                                if !favored {
207                                    factor = 0.4;
208                                }
209                            }
210                            _ => {
211                                factor = 1.0;
212                            }
213                        }
214
215                        if favored {
216                            factor *= 1.15;
217                        }
218                    }
219                }
220                BaseSchedule::LIN => {
221                    factor = (entry.scheduled_count() as f64)
222                        / f64::from(psmeta.n_fuzz()[tcmeta.n_fuzz_entry()] + 1);
223                }
224                BaseSchedule::QUAD => {
225                    factor = ((entry.scheduled_count() * entry.scheduled_count()) as f64)
226                        / f64::from(psmeta.n_fuzz()[tcmeta.n_fuzz_entry()] + 1);
227                }
228            }
229        }
230
231        if let Some(strat) = psmeta.strat() {
232            if *strat.base() != BaseSchedule::EXPLORE {
233                if factor > MAX_FACTOR {
234                    factor = MAX_FACTOR;
235                }
236
237                perf_score *= factor / POWER_BETA;
238            }
239        }
240
241        // Lower bound if the strat is not COE.
242        if let Some(strat) = psmeta.strat() {
243            if *strat.base() == BaseSchedule::COE && perf_score < 1.0 {
244                perf_score = 1.0;
245            }
246        }
247
248        // Upper bound
249        if perf_score > HAVOC_MAX_MULT * 100.0 {
250            perf_score = HAVOC_MAX_MULT * 100.0;
251        }
252
253        if entry.objectives_found() > 0 && psmeta.strat().is_some_and(|s| s.avoid_crash()) {
254            perf_score *= 0.00;
255        }
256
257        Ok(perf_score)
258    }
259}
260
261/// The weight for each corpus entry
262/// This result is used for corpus scheduling
263#[derive(Debug, Clone)]
264pub struct CorpusWeightTestcaseScore {}
265
266impl<I, S> TestcaseScore<I, S> for CorpusWeightTestcaseScore
267where
268    S: HasCorpus<I> + HasMetadata,
269{
270    /// Compute the `weight` used in weighted corpus entry selection algo
271    #[expect(clippy::cast_precision_loss)]
272    fn compute(state: &S, entry: &mut Testcase<I>) -> Result<f64, Error> {
273        let mut weight = 1.0;
274        let psmeta = state.metadata::<SchedulerMetadata>()?;
275
276        let tcmeta = entry.metadata::<SchedulerTestcaseMetadata>()?;
277        // This means that this testcase has never gone through the calibration stage before1,
278        // In this case we'll just return the default weight
279        // This methoud is called in corpus's on_add() method. Fuzz_level is zero at that time.
280        if entry.scheduled_count() == 0 || psmeta.cycles() == 0 {
281            return Ok(weight);
282        }
283
284        let q_exec_us = entry
285            .exec_time()
286            .ok_or_else(|| Error::key_not_found("exec_time not set".to_string()))?
287            .as_nanos() as f64;
288        let favored = entry.has_metadata::<IsFavoredMetadata>();
289
290        let avg_exec_us = psmeta.exec_time().as_nanos() as f64 / psmeta.cycles() as f64;
291        let avg_bitmap_size = psmeta.bitmap_size_log() / psmeta.bitmap_entries() as f64;
292
293        let q_bitmap_size = tcmeta.bitmap_size() as f64;
294
295        if let Some(ps) = psmeta.strat() {
296            match ps.base() {
297                BaseSchedule::FAST | BaseSchedule::COE | BaseSchedule::LIN | BaseSchedule::QUAD => {
298                    let hits = psmeta.n_fuzz()[tcmeta.n_fuzz_entry()];
299                    if hits > 0 {
300                        weight /= libm::log10(f64::from(hits)) + 1.0;
301                    }
302                }
303                _ => (),
304            }
305        }
306
307        weight *= avg_exec_us / q_exec_us;
308        weight *= if avg_bitmap_size.is_zero() {
309            // This can happen when the bitmap size of the target is as small as 1.
310            1.0
311        } else {
312            libm::log2(q_bitmap_size).max(1.0) / avg_bitmap_size
313        };
314
315        let tc_ref = match entry.metadata_map().get::<MapIndexesMetadata>() {
316            Some(meta) => meta.refcnt() as f64,
317            None => 0.0,
318        };
319
320        let avg_top_size = match state.metadata::<TopRatedsMetadata>() {
321            Ok(m) => m.map().len() as f64,
322            Err(e) => {
323                return Err(Error::key_not_found(format!(
324                    "{e:?} You have to use Minimizer scheduler with this.",
325                )));
326            }
327        };
328
329        weight *= 1.0 + (tc_ref / avg_top_size);
330
331        if favored {
332            weight *= 5.0;
333        }
334
335        // was it fuzzed before?
336        if entry.scheduled_count() == 0 {
337            weight *= 2.0;
338        }
339
340        if entry.objectives_found() > 0 && psmeta.strat().is_some_and(|s| s.avoid_crash()) {
341            weight *= 0.00;
342        }
343
344        assert!(weight.is_normal());
345
346        Ok(weight)
347    }
348}