From 70e8ea24a8ec9e3dfe484227463fdb97ee341227 Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 19 Nov 2025 01:00:48 +0000 Subject: [PATCH] impl vercmp fuzzer --- Cargo.toml | 10 ++- fuzz/atom/meson.build | 1 + fuzz/atom/vercmp/fuzz.rs | 112 ++++++++++++++++++++++++++++++++++ fuzz/atom/vercmp/gencorpus.rs | 43 +++++++++++++ fuzz/atom/vercmp/meson.build | 6 ++ scripts/vercmp.py | 14 +++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 fuzz/atom/vercmp/fuzz.rs create mode 100644 fuzz/atom/vercmp/gencorpus.rs create mode 100644 fuzz/atom/vercmp/meson.build create mode 100755 scripts/vercmp.py diff --git a/Cargo.toml b/Cargo.toml index 0663f32..a92311c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,12 @@ name = "atom_parser_gencorpus" [[test]] path = "fuzz/atom/parser/fuzz.rs" -name = "atom_parser_fuzz" \ No newline at end of file +name = "atom_parser_fuzz" + +[[bin]] +path = "fuzz/atom/vercmp/gencorpus.rs" +name = "atom_vercmp_gencorpus" + +[[test]] +path = "fuzz/atom/vercmp/fuzz.rs" +name = "atom_vercmp_fuzz" \ No newline at end of file diff --git a/fuzz/atom/meson.build b/fuzz/atom/meson.build index 57b0131..6822370 100644 --- a/fuzz/atom/meson.build +++ b/fuzz/atom/meson.build @@ -1 +1,2 @@ subdir('parser') +subdir('vercmp') diff --git a/fuzz/atom/vercmp/fuzz.rs b/fuzz/atom/vercmp/fuzz.rs new file mode 100644 index 0000000..98483cb --- /dev/null +++ b/fuzz/atom/vercmp/fuzz.rs @@ -0,0 +1,112 @@ +use core::slice; +use gentoo_utils::{ + Parseable, + atom::{Atom, Version}, +}; +use mon::{Parser, ParserFinishedError, input::InputIter}; +use std::{ + cmp::Ordering, + io::{BufRead, BufReader, Write}, + process::{ChildStdin, ChildStdout, Command, Stdio}, + sync::{LazyLock, Mutex}, +}; + +struct PyProcess { + stdin: Mutex, + stdout: Mutex>, + buffer: Mutex, +} + +#[allow(clippy::missing_safety_doc, clippy::needless_return)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn LLVMFuzzerTestOneInput(input: *const u8, len: usize) -> i32 { + static PY_PROCESS: LazyLock = LazyLock::new(|| { + #[allow(clippy::zombie_processes)] + let mut proc = Command::new("vercmp.py") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("failed to spawn vercmp.py"); + + let stdin = Mutex::new(proc.stdin.take().unwrap()); + let stdout = Mutex::new(BufReader::new(proc.stdout.take().unwrap())); + + PyProcess { + stdin, + stdout, + buffer: Mutex::new(String::new()), + } + }); + + let control = Version::parser() + .parse_finished(InputIter::new("1.2.0a_alpha1_beta2-r1-8")) + .unwrap(); + + let slice = unsafe { slice::from_raw_parts(input, len) }; + + if slice.iter().any(|b| !b.is_ascii_graphic()) { + return -1; + } + + let str = match str::from_utf8(slice) { + Ok(str) => str, + Err(_) => return -1, + }; + + let version_str = match str.split_ascii_whitespace().nth(0) { + Some(lhs) => lhs, + None => return -1, + }; + + dbg!(version_str); + + let version = match Version::parser().parse_finished(InputIter::new(version_str)) { + Ok(a) => a, + Err(_) => return -1, + }; + + let gentoo_utils = control.cmp(&version); + + let portage_result = portage_vercmp(&PY_PROCESS, &control, &version); + + match portage_result { + Ok(portage) if portage == gentoo_utils => { + eprintln!("agreement on {control} cmp {version} == {portage:?}"); + } + Ok(portage) => { + panic!( + "disagreement on {control} == {version}:\nportage:{portage:?} gentoo_utils:{gentoo_utils:?}" + ) + } + Err(_) => { + panic!("parsed invalid versions: {control} | {version}") + } + } + + return 0; +} + +fn portage_vercmp(pyproc: &PyProcess, a: &Version, b: &Version) -> Result { + let mut stdin = pyproc.stdin.lock().expect("failed to get stdin lock"); + let mut stdout = pyproc.stdout.lock().expect("failed to get stdout lock"); + let mut buffer = pyproc.buffer.lock().expect("failed to get buffer lock"); + + writeln!(&mut stdin, "{a} {b}").expect("failed to write line to python process"); + + stdin.flush().unwrap(); + + buffer.clear(); + + stdout + .read_line(&mut buffer) + .expect("failed to read line from python process"); + + match buffer.as_str().trim() { + "0" => Ok(Ordering::Equal), + "1" => Ok(Ordering::Greater), + "-1" => Ok(Ordering::Less), + "err" => Err(()), + other => panic!("unexpected result from python: {other}"), + } +} diff --git a/fuzz/atom/vercmp/gencorpus.rs b/fuzz/atom/vercmp/gencorpus.rs new file mode 100644 index 0000000..6d5eeef --- /dev/null +++ b/fuzz/atom/vercmp/gencorpus.rs @@ -0,0 +1,43 @@ +use std::{ + env, + error::Error, + fs::{self, OpenOptions}, + io::Write, + path::PathBuf, +}; + +use gentoo_utils::ebuild::repo::Repo; + +fn main() -> Result<(), Box> { + let corpus_dir = PathBuf::from( + env::args() + .nth(1) + .expect("expected corpus directory as first argument"), + ); + + fs::create_dir_all(&corpus_dir)?; + + let repo = Repo::new("/var/db/repos/gentoo"); + let mut versions = Vec::new(); + + for category in repo.categories()? { + for ebuild in category?.ebuilds()? { + let version = ebuild?.version().clone(); + + versions.push(version); + } + } + + for (i, version) in versions.iter().enumerate() { + let path = corpus_dir.as_path().join(i.to_string()); + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(path)?; + + write!(file, "{version}")?; + } + + Ok(()) +} diff --git a/fuzz/atom/vercmp/meson.build b/fuzz/atom/vercmp/meson.build new file mode 100644 index 0000000..51b237b --- /dev/null +++ b/fuzz/atom/vercmp/meson.build @@ -0,0 +1,6 @@ +fuzzers += { + 'atom_vercmp': [ + meson.current_source_dir() / 'gencorpus.rs', + meson.current_source_dir() / 'fuzz.rs', + ], +} diff --git a/scripts/vercmp.py b/scripts/vercmp.py new file mode 100755 index 0000000..f02cb41 --- /dev/null +++ b/scripts/vercmp.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +import sys +import portage + +for line in sys.stdin.buffer: + a, b = line.decode().split(" ") + + try: + sys.stdout.buffer.write(f"{portage.vercmp(a, b)}\n".encode()) + except: + sys.stdout.buffer.write(b"err\n") + finally: + sys.stdout.buffer.flush()