diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 78b1faa557d..65903299f4f 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -149,6 +149,13 @@ jobs: ## Run it cd fuzz cargo +nightly fuzz run fuzz_test -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 + - name: Run fuzz_expr for XX seconds + continue-on-error: true + shell: bash + run: | + ## Run it + cd fuzz + cargo +nightly fuzz run fuzz_expr -- -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 91a85b45a0f..549f9a6b762 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -15,6 +15,7 @@ rand = { version = "0.8", features = ["small_rng"] } uucore = { path = "../src/uucore/" } uu_date = { path = "../src/uu/date/" } uu_test = { path = "../src/uu/test/" } +uu_expr = { path = "../src/uu/expr/" } # Prevent this from interfering with workspaces @@ -27,6 +28,12 @@ path = "fuzz_targets/fuzz_date.rs" test = false doc = false +[[bin]] +name = "fuzz_expr" +path = "fuzz_targets/fuzz_expr.rs" +test = false +doc = false + [[bin]] name = "fuzz_test" path = "fuzz_targets/fuzz_test.rs" diff --git a/fuzz/fuzz_targets/fuzz_expr.rs b/fuzz/fuzz_targets/fuzz_expr.rs new file mode 100644 index 00000000000..b3a88e62883 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_expr.rs @@ -0,0 +1,170 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore parens + +#![no_main] +use libfuzzer_sys::fuzz_target; +use uu_expr::uumain; + +use rand::seq::SliceRandom; +use rand::Rng; +use std::ffi::OsString; + +use libc::{dup, dup2, STDOUT_FILENO}; +use std::process::Command; + +fn run_gnu_expr(args: &[OsString]) -> Result { + let mut command = Command::new("expr"); + for arg in args { + command.arg(arg); + } + let output = command.output()?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "GNU expr execution failed", + )) + } +} + +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 +} + +fn generate_expr(max_depth: u32) -> String { + let mut rng = rand::thread_rng(); + let ops = ["+", "-", "*", "/", "%", "<", ">", "=", "&", "|"]; + let parens = ["(", ")"]; + + let mut expr = String::new(); + let mut depth = 0; + + while depth <= max_depth { + let choice = rng.gen_range(0..=1); + + match choice { + 0 => { + expr.push_str(&rng.gen_range(1..=100).to_string()); + depth += 1; + } + 1 => { + if depth > 0 { + let op = *ops.choose(&mut rng).unwrap(); + expr.push_str(&format!(" {} {} ", op, rng.gen_range(1..=100).to_string())); + depth += 1; + } + } + 2 => { + expr.push_str(&format!( + "{}{}{}", + parens[0], + rng.gen_range(1..=100).to_string(), + parens[1] + )); + depth += 1; + } + _ => { + let random_str = generate_random_string(rng.gen_range(1..=10)); + expr.push_str(&random_str); + depth += 1; + } + } + } + + expr +} + +fuzz_target!(|_data: &[u8]| { + let mut rng = rand::thread_rng(); + let expr = generate_expr(rng.gen_range(0..=20)); // Increase max_depth to generate longer expressions + let args = vec![OsString::from("expr"), OsString::from(&expr)]; + + // Save the original stdout file descriptor + let original_stdout_fd = unsafe { dup(STDOUT_FILENO) }; + + // 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(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 expr with the provided arguments and compare the output + match run_gnu_expr(&args[1..]) { + Ok(gnu_output) => { + let gnu_output = gnu_output.trim().to_owned(); + if my_output != gnu_output { + println!("Discrepancy detected!"); + println!("Expression: {}", expr); + println!("My output: {}", my_output); + println!("GNU output: {}", gnu_output); + panic!(); + } else { + println!("Outputs matched for expression: {}", expr); + } + } + Err(_) => { + println!("GNU expr execution failed for expression: {}", expr); + } + } +});