diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index c29294d4d..b96f7e4ef 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -49,6 +49,10 @@ jobs: - { name: fuzz_printf, should_pass: false } - { name: fuzz_echo, should_pass: true } - { name: fuzz_seq, should_pass: false } + - { name: fuzz_sort, should_pass: false } + - { name: fuzz_wc, should_pass: false } + - { name: fuzz_cut, should_pass: false } + - { name: fuzz_split, should_pass: false } - { name: fuzz_parse_glob, should_pass: true } - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3d5862c69..076e274ec 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,6 +20,10 @@ uu_expr = { path = "../src/uu/expr/" } uu_printf = { path = "../src/uu/printf/" } uu_echo = { path = "../src/uu/echo/" } uu_seq = { path = "../src/uu/seq/" } +uu_sort = { path = "../src/uu/sort/" } +uu_wc = { path = "../src/uu/wc/" } +uu_cut = { path = "../src/uu/cut/" } +uu_split = { path = "../src/uu/split/" } # Prevent this from interfering with workspaces [workspace] @@ -49,6 +53,30 @@ path = "fuzz_targets/fuzz_seq.rs" test = false doc = false +[[bin]] +name = "fuzz_sort" +path = "fuzz_targets/fuzz_sort.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_split" +path = "fuzz_targets/fuzz_split.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_cut" +path = "fuzz_targets/fuzz_cut.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_wc" +path = "fuzz_targets/fuzz_wc.rs" +test = false +doc = false + [[bin]] name = "fuzz_expr" path = "fuzz_targets/fuzz_expr.rs" diff --git a/fuzz/fuzz_targets/fuzz_cut.rs b/fuzz/fuzz_targets/fuzz_cut.rs new file mode 100644 index 000000000..fa5f8fcc4 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cut.rs @@ -0,0 +1,87 @@ +// 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_cut::uumain; + +use rand::Rng; +use std::ffi::OsString; + +mod fuzz_common; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +}; +static CMD_PATH: &str = "cut"; + +fn generate_cut_args() -> String { + let mut rng = rand::thread_rng(); + let arg_count = rng.gen_range(1..=6); + let mut args = Vec::new(); + + for _ in 0..arg_count { + if rng.gen_bool(0.1) { + args.push(generate_random_string(rng.gen_range(1..=20))); + } else { + match rng.gen_range(0..=4) { + 0 => args.push(String::from("-b") + &rng.gen_range(1..=10).to_string()), + 1 => args.push(String::from("-c") + &rng.gen_range(1..=10).to_string()), + 2 => args.push(String::from("-d,") + &generate_random_string(1)), // Using a comma as a default delimiter + 3 => args.push(String::from("-f") + &rng.gen_range(1..=5).to_string()), + _ => (), + } + } + } + + args.join(" ") +} + +fn generate_delimited_data(count: usize) -> String { + let mut rng = rand::thread_rng(); + let mut lines = Vec::new(); + + for _ in 0..count { + let fields = (0..rng.gen_range(1..=5)) + .map(|_| generate_random_string(rng.gen_range(1..=10))) + .collect::>() + .join(","); + lines.push(fields); + } + + lines.join("\n") +} + +fuzz_target!(|_data: &[u8]| { + let cut_args = generate_cut_args(); + let mut args = vec![OsString::from("cut")]; + args.extend(cut_args.split_whitespace().map(OsString::from)); + + let input_lines = generate_delimited_data(10); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "cut", + &format!("{:?}", &args[1..]), + Some(&input_lines), + &rust_result, + &gnu_result, + false, // Set to true if you want to fail on stderr diff + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_sort.rs b/fuzz/fuzz_targets/fuzz_sort.rs new file mode 100644 index 000000000..3520bbaef --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_sort.rs @@ -0,0 +1,86 @@ +// 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_sort::uumain; + +use rand::Rng; +use std::env; +use std::ffi::OsString; + +mod fuzz_common; +use crate::fuzz_common::CommandResult; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, +}; +static CMD_PATH: &str = "sort"; + +fn generate_sort_args() -> String { + let mut rng = rand::thread_rng(); + + let arg_count = rng.gen_range(1..=5); + let mut args = Vec::new(); + + for _ in 0..arg_count { + match rng.gen_range(0..=4) { + 0 => args.push(String::from("-r")), // Reverse the result of comparisons + 1 => args.push(String::from("-n")), // Compare according to string numerical value + 2 => args.push(String::from("-f")), // Fold lower case to upper case characters + 3 => args.push(generate_random_string(rng.gen_range(1..=10))), // Random string (to simulate file names) + _ => args.push(String::from("-k") + &rng.gen_range(1..=5).to_string()), // Sort via a specified field + } + } + + args.join(" ") +} + +fn generate_random_lines(count: usize) -> String { + let mut rng = rand::thread_rng(); + let mut lines = Vec::new(); + + for _ in 0..count { + lines.push(generate_random_string(rng.gen_range(1..=20))); + } + + lines.join("\n") +} + +fuzz_target!(|_data: &[u8]| { + let sort_args = generate_sort_args(); + let mut args = vec![OsString::from("sort")]; + args.extend(sort_args.split_whitespace().map(OsString::from)); + + // Generate random lines to sort + let input_lines = generate_random_lines(10); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); + + // TODO remove once uutils sort supports localization + env::set_var("LC_COLLATE", "C"); + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "sort", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, // Set to true if you want to fail on stderr diff + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_split.rs b/fuzz/fuzz_targets/fuzz_split.rs new file mode 100644 index 000000000..876c8dd21 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_split.rs @@ -0,0 +1,105 @@ +// 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_split::uumain; + +use rand::Rng; +use std::ffi::OsString; + +mod fuzz_common; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +}; +static CMD_PATH: &str = "split"; + +fn generate_split_args() -> String { + let mut rng = rand::thread_rng(); + let mut args = Vec::new(); + + match rng.gen_range(0..=9) { + 0 => { + args.push(String::from("-a")); // Suffix length + args.push(rng.gen_range(1..=8).to_string()); + } + 1 => { + args.push(String::from("--additional-suffix")); + args.push(generate_random_string(5)); // Random suffix + } + 2 => { + args.push(String::from("-b")); // Bytes per output file + args.push(rng.gen_range(1..=1024).to_string() + "K"); + } + 3 => { + args.push(String::from("-C")); // Line bytes + args.push(rng.gen_range(1..=1024).to_string()); + } + 4 => args.push(String::from("-d")), // Use numeric suffixes + 5 => args.push(String::from("-x")), // Use hex suffixes + 6 => { + args.push(String::from("-l")); // Number of lines per output file + args.push(rng.gen_range(1..=1000).to_string()); + } + 7 => { + args.push(String::from("--filter")); + args.push(String::from("cat > /dev/null")); // Example filter command + } + 8 => { + args.push(String::from("-t")); // Separator + args.push(String::from("\n")); // Newline as separator + } + 9 => args.push(String::from("--verbose")), // Verbose + _ => (), + } + + args.join(" ") +} + +// Function to generate a random string of lines +fn generate_random_lines(count: usize) -> String { + let mut rng = rand::thread_rng(); + let mut lines = Vec::new(); + + for _ in 0..count { + lines.push(generate_random_string(rng.gen_range(1..=20))); + } + + lines.join("\n") +} + +fuzz_target!(|_data: &[u8]| { + let split_args = generate_split_args(); + let mut args = vec![OsString::from("split")]; + args.extend(split_args.split_whitespace().map(OsString::from)); + + let input_lines = generate_random_lines(10); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "split", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, // Set to true if you want to fail on stderr diff + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_wc.rs b/fuzz/fuzz_targets/fuzz_wc.rs new file mode 100644 index 000000000..dc85bbc35 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_wc.rs @@ -0,0 +1,99 @@ +// 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_wc::uumain; + +use rand::Rng; +use std::ffi::OsString; + +mod fuzz_common; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +}; +static CMD_PATH: &str = "wc"; + +fn generate_wc_args() -> String { + let mut rng = rand::thread_rng(); + let arg_count = rng.gen_range(1..=6); + let mut args = Vec::new(); + + for _ in 0..arg_count { + // Introduce a chance to add invalid arguments + if rng.gen_bool(0.1) { + args.push(generate_random_string(rng.gen_range(1..=20))); + } else { + match rng.gen_range(0..=5) { + 0 => args.push(String::from("-c")), + 1 => args.push(String::from("-m")), + 2 => args.push(String::from("-l")), + 3 => args.push(String::from("-L")), + 4 => args.push(String::from("-w")), + // TODO + 5 => { + args.push(String::from("--files0-from")); + if rng.gen_bool(0.5) { + args.push(generate_random_string(50)); // Longer invalid file name + } else { + args.push(generate_random_string(5)); + } + } + _ => (), + } + } + } + + args.join(" ") +} + +// Function to generate a random string of lines, including invalid ones +fn generate_random_lines(count: usize) -> String { + let mut rng = rand::thread_rng(); + let mut lines = Vec::new(); + + for _ in 0..count { + if rng.gen_bool(0.1) { + lines.push(generate_random_string(rng.gen_range(1000..=5000))); // Very long invalid line + } else { + lines.push(generate_random_string(rng.gen_range(1..=20))); + } + } + + lines.join("\n") +} + +fuzz_target!(|_data: &[u8]| { + let wc_args = generate_wc_args(); + let mut args = vec![OsString::from("wc")]; + args.extend(wc_args.split_whitespace().map(OsString::from)); + + let input_lines = generate_random_lines(10); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "wc", + &format!("{:?}", &args[1..]), + Some(&input_lines), + &rust_result, + &gnu_result, + false, // Set to true if you want to fail on stderr diff + ); +});