|
| 1 | +// This file is part of the uutils coreutils package. |
| 2 | +// |
| 3 | +// For the full copyright and license information, please view the LICENSE |
| 4 | +// file that was distributed with this source code. |
| 5 | +// spell-checker:ignore parens |
| 6 | + |
| 7 | +#![no_main] |
| 8 | +use libfuzzer_sys::fuzz_target; |
| 9 | +use uu_expr::uumain; |
| 10 | + |
| 11 | +use rand::seq::SliceRandom; |
| 12 | +use rand::Rng; |
| 13 | +use std::ffi::OsString; |
| 14 | + |
| 15 | +use libc::{dup, dup2, STDOUT_FILENO}; |
| 16 | +use std::process::Command; |
| 17 | +mod fuzz_common; |
| 18 | +use crate::fuzz_common::is_gnu_cmd; |
| 19 | + |
| 20 | +static CMD_PATH: &str = "expr"; |
| 21 | + |
| 22 | +fn run_gnu_expr(args: &[OsString]) -> Result<(String, i32), std::io::Error> { |
| 23 | + is_gnu_cmd(CMD_PATH)?; // Check if it's a GNU implementation |
| 24 | + |
| 25 | + let mut command = Command::new(CMD_PATH); |
| 26 | + for arg in args { |
| 27 | + command.arg(arg); |
| 28 | + } |
| 29 | + let output = command.output()?; |
| 30 | + let exit_code = output.status.code().unwrap_or(-1); |
| 31 | + if output.status.success() { |
| 32 | + Ok(( |
| 33 | + String::from_utf8_lossy(&output.stdout).to_string(), |
| 34 | + exit_code, |
| 35 | + )) |
| 36 | + } else { |
| 37 | + Err(std::io::Error::new( |
| 38 | + std::io::ErrorKind::Other, |
| 39 | + format!("GNU expr execution failed with exit code {}", exit_code), |
| 40 | + )) |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +fn generate_random_string(max_length: usize) -> String { |
| 45 | + let mut rng = rand::thread_rng(); |
| 46 | + let valid_utf8: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
| 47 | + .chars() |
| 48 | + .collect(); |
| 49 | + let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence |
| 50 | + let mut result = String::new(); |
| 51 | + |
| 52 | + for _ in 0..rng.gen_range(1..=max_length) { |
| 53 | + if rng.gen_bool(0.9) { |
| 54 | + let ch = valid_utf8.choose(&mut rng).unwrap(); |
| 55 | + result.push(*ch); |
| 56 | + } else { |
| 57 | + let ch = invalid_utf8.choose(&mut rng).unwrap(); |
| 58 | + if let Some(c) = char::from_u32(*ch as u32) { |
| 59 | + result.push(c); |
| 60 | + } |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + result |
| 65 | +} |
| 66 | + |
| 67 | +fn generate_expr(max_depth: u32) -> String { |
| 68 | + let mut rng = rand::thread_rng(); |
| 69 | + let ops = ["+", "-", "*", "/", "%", "<", ">", "=", "&", "|"]; |
| 70 | + |
| 71 | + let mut expr = String::new(); |
| 72 | + let mut depth = 0; |
| 73 | + let mut last_was_operator = false; |
| 74 | + |
| 75 | + while depth <= max_depth { |
| 76 | + if last_was_operator || depth == 0 { |
| 77 | + // Add a number |
| 78 | + expr.push_str(&rng.gen_range(1..=100).to_string()); |
| 79 | + last_was_operator = false; |
| 80 | + } else { |
| 81 | + // 90% chance to add an operator followed by a number |
| 82 | + if rng.gen_bool(0.9) { |
| 83 | + let op = *ops.choose(&mut rng).unwrap(); |
| 84 | + expr.push_str(&format!(" {} ", op)); |
| 85 | + last_was_operator = true; |
| 86 | + } |
| 87 | + // 10% chance to add a random string (potentially invalid syntax) |
| 88 | + else { |
| 89 | + let random_str = generate_random_string(rng.gen_range(1..=10)); |
| 90 | + expr.push_str(&random_str); |
| 91 | + last_was_operator = false; |
| 92 | + } |
| 93 | + } |
| 94 | + depth += 1; |
| 95 | + } |
| 96 | + |
| 97 | + // Ensure the expression ends with a number if it ended with an operator |
| 98 | + if last_was_operator { |
| 99 | + expr.push_str(&rng.gen_range(1..=100).to_string()); |
| 100 | + } |
| 101 | + |
| 102 | + expr |
| 103 | +} |
| 104 | + |
| 105 | +fuzz_target!(|_data: &[u8]| { |
| 106 | + let mut rng = rand::thread_rng(); |
| 107 | + let expr = generate_expr(rng.gen_range(0..=20)); |
| 108 | + let mut args = vec![OsString::from("expr")]; |
| 109 | + args.extend(expr.split_whitespace().map(OsString::from)); |
| 110 | + |
| 111 | + // Save the original stdout file descriptor |
| 112 | + let original_stdout_fd = unsafe { dup(STDOUT_FILENO) }; |
| 113 | + |
| 114 | + // Create a pipe to capture stdout |
| 115 | + let mut pipe_fds = [-1; 2]; |
| 116 | + unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }; |
| 117 | + let uumain_exit_code; |
| 118 | + { |
| 119 | + // Redirect stdout to the write end of the pipe |
| 120 | + unsafe { dup2(pipe_fds[1], STDOUT_FILENO) }; |
| 121 | + |
| 122 | + // Run uumain with the provided arguments |
| 123 | + uumain_exit_code = uumain(args.clone().into_iter()); |
| 124 | + |
| 125 | + // Restore original stdout |
| 126 | + unsafe { dup2(original_stdout_fd, STDOUT_FILENO) }; |
| 127 | + unsafe { libc::close(original_stdout_fd) }; |
| 128 | + } |
| 129 | + // Close the write end of the pipe |
| 130 | + unsafe { libc::close(pipe_fds[1]) }; |
| 131 | + |
| 132 | + // Read captured output from the read end of the pipe |
| 133 | + let mut captured_output = Vec::new(); |
| 134 | + let mut read_buffer = [0; 1024]; |
| 135 | + loop { |
| 136 | + let bytes_read = unsafe { |
| 137 | + libc::read( |
| 138 | + pipe_fds[0], |
| 139 | + read_buffer.as_mut_ptr() as *mut libc::c_void, |
| 140 | + read_buffer.len(), |
| 141 | + ) |
| 142 | + }; |
| 143 | + if bytes_read <= 0 { |
| 144 | + break; |
| 145 | + } |
| 146 | + captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]); |
| 147 | + } |
| 148 | + |
| 149 | + // Close the read end of the pipe |
| 150 | + unsafe { libc::close(pipe_fds[0]) }; |
| 151 | + |
| 152 | + // Convert captured output to a string |
| 153 | + let rust_output = String::from_utf8_lossy(&captured_output) |
| 154 | + .to_string() |
| 155 | + .trim() |
| 156 | + .to_owned(); |
| 157 | + |
| 158 | + // Run GNU expr with the provided arguments and compare the output |
| 159 | + match run_gnu_expr(&args[1..]) { |
| 160 | + Ok((gnu_output, gnu_exit_code)) => { |
| 161 | + let gnu_output = gnu_output.trim().to_owned(); |
| 162 | + if uumain_exit_code != gnu_exit_code { |
| 163 | + println!("Expression: {}", expr); |
| 164 | + println!("Rust code: {}", uumain_exit_code); |
| 165 | + println!("GNU code: {}", gnu_exit_code); |
| 166 | + panic!("Different error codes"); |
| 167 | + } |
| 168 | + if rust_output != gnu_output { |
| 169 | + println!("Expression: {}", expr); |
| 170 | + println!("Rust output: {}", rust_output); |
| 171 | + println!("GNU output: {}", gnu_output); |
| 172 | + panic!("Different output between Rust & GNU"); |
| 173 | + } else { |
| 174 | + println!( |
| 175 | + "Outputs matched for expression: {} => Result: {}", |
| 176 | + expr, rust_output |
| 177 | + ); |
| 178 | + } |
| 179 | + } |
| 180 | + Err(_) => { |
| 181 | + println!("GNU expr execution failed for expression: {}", expr); |
| 182 | + } |
| 183 | + } |
| 184 | +}); |
0 commit comments