From c5494b19166c24dc57f5a859c2172a38b4178090 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 21 Aug 2023 07:34:45 +0200 Subject: [PATCH] fuzz the test command --- .github/workflows/CICD.yml | 7 + fuzz/Cargo.toml | 15 +- fuzz/fuzz_targets/fuzz_test.rs | 285 +++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 fuzz/fuzz_targets/fuzz_test.rs diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 589126b6f4c..78b1faa557d 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -142,6 +142,13 @@ jobs: ## Run it cd fuzz cargo +nightly fuzz run fuzz_date -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 + - name: Run fuzz_test for XX seconds + continue-on-error: true + shell: bash + run: | + ## Run it + cd fuzz + cargo +nightly fuzz run fuzz_test -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 - name: Run fuzz_parse_glob for XX seconds shell: bash run: | diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c8565d69127..91a85b45a0f 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,12 +9,13 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" +libc = "0.2" +rand = { version = "0.8", features = ["small_rng"] } -[dependencies.uucore] -path = "../src/uucore/" +uucore = { path = "../src/uucore/" } +uu_date = { path = "../src/uu/date/" } +uu_test = { path = "../src/uu/test/" } -[dependencies.uu_date] -path = "../src/uu/date/" # Prevent this from interfering with workspaces [workspace] @@ -26,6 +27,12 @@ path = "fuzz_targets/fuzz_date.rs" test = false doc = false +[[bin]] +name = "fuzz_test" +path = "fuzz_targets/fuzz_test.rs" +test = false +doc = false + [[bin]] name = "fuzz_parse_glob" path = "fuzz_targets/fuzz_parse_glob.rs" diff --git a/fuzz/fuzz_targets/fuzz_test.rs b/fuzz/fuzz_targets/fuzz_test.rs new file mode 100644 index 00000000000..f4ecdb5f98c --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_test.rs @@ -0,0 +1,285 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use uu_test::uumain; + +use rand::seq::SliceRandom; +use rand::Rng; +use std::ffi::OsString; + +use libc::{dup, dup2, STDOUT_FILENO}; +use std::process::Command; + +#[derive(PartialEq, Debug, Clone)] +enum ArgType { + STRING, + STRINGSTRING, + INTEGER, + INTEGERINTEGER, + FILE, + FILEFILE, + // Add any other types as needed +} + +fn run_gnu_test(args: &[OsString]) -> Result<(String, i32), std::io::Error> { + let mut command = Command::new("test"); + for arg in args { + command.arg(arg); + } + let output = command.output()?; + let exit_status = output.status.code().unwrap_or(-1); // Capture the exit status code + Ok(( + String::from_utf8_lossy(&output.stdout).to_string(), + exit_status, + )) +} + +fn generate_random_string(max_length: usize) -> String { + let mut rng = rand::thread_rng(); + let valid_utf8: Vec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + .chars() + .collect(); + let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence + let mut result = String::new(); + + for _ in 0..rng.gen_range(1..=max_length) { + if rng.gen_bool(0.9) { + let ch = valid_utf8.choose(&mut rng).unwrap(); + result.push(*ch); + } else { + let ch = invalid_utf8.choose(&mut rng).unwrap(); + if let Some(c) = char::from_u32(*ch as u32) { + result.push(c); + } + } + } + + result +} + +#[derive(Debug, Clone)] +struct TestArg { + arg: String, + arg_type: ArgType, +} + +fn generate_random_path(rng: &mut dyn rand::RngCore) -> &'static str { + match rng.gen_range(0..=3) { + 0 => "/dev/null", + 1 => "/dev/random", + 2 => "/tmp", + _ => "/dev/urandom", + } +} + +fn generate_test_args() -> Vec { + vec![ + TestArg { + arg: "-z".to_string(), + arg_type: ArgType::STRING, + }, + TestArg { + arg: "-n".to_string(), + arg_type: ArgType::STRING, + }, + TestArg { + arg: "=".to_string(), + arg_type: ArgType::STRINGSTRING, + }, + TestArg { + arg: "!=".to_string(), + arg_type: ArgType::STRINGSTRING, + }, + TestArg { + arg: "-eq".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-ne".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-gt".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-ge".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-lt".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-le".to_string(), + arg_type: ArgType::INTEGERINTEGER, + }, + TestArg { + arg: "-f".to_string(), + arg_type: ArgType::FILE, + }, + TestArg { + arg: "-d".to_string(), + arg_type: ArgType::FILE, + }, + TestArg { + arg: "-e".to_string(), + arg_type: ArgType::FILE, + }, + TestArg { + arg: "-ef".to_string(), + arg_type: ArgType::FILEFILE, + }, + TestArg { + arg: "-nt".to_string(), + arg_type: ArgType::FILEFILE, + }, + ] +} + +fn generate_test_arg() -> String { + let mut rng = rand::thread_rng(); + let test_args = generate_test_args(); + let mut arg = String::new(); + + let choice = rng.gen_range(0..=5); + + match choice { + 0 => { + arg.push_str(&rng.gen_range(-100..=100).to_string()); + } + 1 | 2 | 3 => { + let test_arg = test_args + .choose(&mut rng) + .expect("Failed to choose a random test argument"); + if test_arg.arg_type == ArgType::INTEGER { + arg.push_str(&format!( + "{} {} {}", + &rng.gen_range(-100..=100).to_string(), + test_arg.arg, + &rng.gen_range(-100..=100).to_string() + )); + } else if test_arg.arg_type == ArgType::STRINGSTRING { + let random_str = generate_random_string(rng.gen_range(1..=10)); + let random_str2 = generate_random_string(rng.gen_range(1..=10)); + + arg.push_str(&format!( + "{} {} {}", + &random_str, test_arg.arg, &random_str2 + )); + } else if test_arg.arg_type == ArgType::STRING { + let random_str = generate_random_string(rng.gen_range(1..=10)); + arg.push_str(&format!("{} {}", test_arg.arg, &random_str)); + } else if test_arg.arg_type == ArgType::FILEFILE { + let path = generate_random_path(&mut rng); + let path2 = generate_random_path(&mut rng); + arg.push_str(&format!("{} {} {}", path, test_arg.arg, path2)); + } else if test_arg.arg_type == ArgType::FILE { + let path = generate_random_path(&mut rng); + arg.push_str(&format!("{} {}", test_arg.arg, path)); + } + } + 4 => { + let random_str = generate_random_string(rng.gen_range(1..=10)); + arg.push_str(&random_str); + } + _ => { + let path = generate_random_path(&mut rng); + + let file_test_args: Vec = test_args + .iter() + .filter(|ta| ta.arg_type == ArgType::FILE) + .cloned() + .collect(); + + if let Some(test_arg) = file_test_args.choose(&mut rng) { + arg.push_str(&format!("{}{}", test_arg.arg, path)); + } + } + } + + arg +} + +fuzz_target!(|_data: &[u8]| { + let mut rng = rand::thread_rng(); + let max_args = rng.gen_range(1..=6); + let mut args = vec![OsString::from("test")]; + let uumain_exit_status; + + for _ in 0..max_args { + args.push(OsString::from(generate_test_arg())); + } + + // Save the original stdout file descriptor + let original_stdout_fd = unsafe { dup(STDOUT_FILENO) }; + println!("Running test {:?}", &args[1..]); + // Create a pipe to capture stdout + let mut pipe_fds = [-1; 2]; + unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }; + + { + // Redirect stdout to the write end of the pipe + unsafe { dup2(pipe_fds[1], STDOUT_FILENO) }; + + // Run uumain with the provided arguments + uumain_exit_status = uumain(args.clone().into_iter()); + + // Restore original stdout + unsafe { dup2(original_stdout_fd, STDOUT_FILENO) }; + unsafe { libc::close(original_stdout_fd) }; + } + // Close the write end of the pipe + unsafe { libc::close(pipe_fds[1]) }; + + // Read captured output from the read end of the pipe + let mut captured_output = Vec::new(); + let mut read_buffer = [0; 1024]; + loop { + let bytes_read = unsafe { + libc::read( + pipe_fds[0], + read_buffer.as_mut_ptr() as *mut libc::c_void, + read_buffer.len(), + ) + }; + if bytes_read <= 0 { + break; + } + captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]); + } + + // Close the read end of the pipe + unsafe { libc::close(pipe_fds[0]) }; + + // Convert captured output to a string + let my_output = String::from_utf8_lossy(&captured_output) + .to_string() + .trim() + .to_owned(); + + // Run GNU test with the provided arguments and compare the output + match run_gnu_test(&args[1..]) { + Ok((gnu_output, gnu_exit_status)) => { + let gnu_output = gnu_output.trim().to_owned(); + println!("gnu_exit_status {}", gnu_exit_status); + println!("uumain_exit_status {}", uumain_exit_status); + if my_output != gnu_output || uumain_exit_status != gnu_exit_status { + println!("Discrepancy detected!"); + println!("Test: {:?}", &args[1..]); + println!("My output: {}", my_output); + println!("GNU output: {}", gnu_output); + println!("My exit status: {}", uumain_exit_status); + println!("GNU exit status: {}", gnu_exit_status); + panic!(); + } else { + println!( + "Outputs and exit statuses matched for expression {:?}", + &args[1..] + ); + } + } + Err(_) => { + println!("GNU test execution failed for expression {:?}", &args[1..]); + } + } +});