diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 3b1515e6924..19a10523e12 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -96,6 +96,7 @@ jobs: - { name: fuzz_parse_time, should_pass: true } - { name: fuzz_seq_parse_number, should_pass: false } - { name: fuzz_non_utf8_paths, should_pass: true } + - { name: fuzz_dirname, should_pass: true } steps: - uses: actions/checkout@v6 @@ -117,7 +118,7 @@ jobs: - name: Run ${{ matrix.test-target.name }} for XX seconds id: run_fuzzer shell: bash - continue-on-error: ${{ !matrix.test-target.name.should_pass }} + continue-on-error: ${{ !matrix.test-target.should_pass }} run: | mkdir -p fuzz/stats STATS_FILE="fuzz/stats/${{ matrix.test-target.name }}.txt" @@ -155,7 +156,7 @@ jobs: echo "Runs: $(grep -q "stat::number_of_executed_units" "$STATS_FILE" && grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}' || echo "unknown")" echo "Execution Rate: $(grep -q "stat::average_exec_per_sec" "$STATS_FILE" && grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}' || echo "unknown") execs/sec" echo "New Units: $(grep -q "stat::new_units_added" "$STATS_FILE" && grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}' || echo "unknown")" - echo "Expected: ${{ matrix.test-target.name.should_pass }}" + echo "Expected: ${{ matrix.test-target.should_pass }}" if grep -q "SUMMARY: " "$STATS_FILE"; then echo "Status: $(grep "SUMMARY: " "$STATS_FILE" | head -1)" else diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 1ec35d31418..31215113e01 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1617,6 +1617,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "uu_dirname" +version = "0.6.0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + [[package]] name = "uu_echo" version = "0.6.0" @@ -1798,6 +1807,7 @@ dependencies = [ "uu_cksum", "uu_cut", "uu_date", + "uu_dirname", "uu_echo", "uu_env", "uu_expr", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d3c987f229a..6d5e2d4d602 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -41,6 +41,7 @@ uu_split = { path = "../src/uu/split" } uu_tr = { path = "../src/uu/tr" } uu_env = { path = "../src/uu/env" } uu_cksum = { path = "../src/uu/cksum" } +uu_dirname = { path = "../src/uu/dirname" } [[bin]] name = "fuzz_date" @@ -149,3 +150,9 @@ name = "fuzz_non_utf8_paths" path = "fuzz_targets/fuzz_non_utf8_paths.rs" test = false doc = false + +[[bin]] +name = "fuzz_dirname" +path = "fuzz_targets/fuzz_dirname.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_dirname.rs b/fuzz/fuzz_targets/fuzz_dirname.rs new file mode 100644 index 00000000000..bfb127a5a5a --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_dirname.rs @@ -0,0 +1,211 @@ +// 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. + +#![no_main] +use libfuzzer_sys::fuzz_target; +use uu_dirname::uumain; + +use rand::Rng; +use rand::prelude::IndexedRandom; +use std::ffi::OsString; + +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; + +static CMD_PATH: &str = "dirname"; + +fn generate_dirname_args() -> Vec { + let mut rng = rand::rng(); + let mut args = Vec::new(); + + // 20% chance to include -z/--zero flag + if rng.random_bool(0.2) { + if rng.random_bool(0.5) { + args.push("-z".to_string()); + } else { + args.push("--zero".to_string()); + } + } + + // 30% chance to use one of the specific issue #8924 cases + if rng.random_bool(0.3) { + let issue_cases = [ + "foo//.", + "foo/./", + "foo/bar/./", + "bar//.", + "test/./", + "a/b/./", + "x//.", + "dir/subdir/./", + ]; + args.push(issue_cases.choose(&mut rng).unwrap().to_string()); + } else { + // Generate 1-3 path arguments normally + let num_paths = rng.random_range(1..=3); + for _ in 0..num_paths { + args.push(generate_path()); + } + } + + args +} + +fn generate_path() -> String { + let mut rng = rand::rng(); + + // Different types of paths to test + let path_type = rng.random_range(0..15); + + match path_type { + // Simple paths + 0 => generate_random_string(rng.random_range(1..=20)), + + // Paths with slashes + 1 => { + let mut path = String::new(); + let components = rng.random_range(1..=5); + for i in 0..components { + if i > 0 { + path.push('/'); + } + path.push_str(&generate_random_string(rng.random_range(1..=10))); + } + path + } + + // Root path + 2 => "/".to_string(), + + // Absolute paths + 3 => { + let mut path = "/".to_string(); + let components = rng.random_range(1..=4); + for _ in 0..components { + path.push_str(&generate_random_string(rng.random_range(1..=8))); + path.push('/'); + } + // Remove trailing slash sometimes + if rng.random_bool(0.5) && path.len() > 1 { + path.pop(); + } + path + } + + // Paths ending with "/." (specific case from issue #8924) + 4 => { + let base = if rng.random_bool(0.3) { + "/".to_string() + } else { + format!("/{}", generate_random_string(rng.random_range(1..=10))) + }; + format!("{}.", base) + } + + // Paths with multiple slashes + 5 => { + let base = generate_random_string(rng.random_range(1..=10)); + format!( + "///{}//{}", + base, + generate_random_string(rng.random_range(1..=8)) + ) + } + + // Paths with dots + 6 => { + let components = [".", "..", "...", "...."]; + let chosen = components.choose(&mut rng).unwrap(); + if rng.random_bool(0.5) { + format!("/{}", chosen) + } else { + chosen.to_string() + } + } + + // Single character paths + 7 => { + let chars = ['a', 'x', '1', '-', '_', '.']; + chars.choose(&mut rng).unwrap().to_string() + } + + // Empty string (edge case) + 8 => "".to_string(), + + // Issue #8924 specific cases: paths like "foo//." + 9 => { + let base = generate_random_string(rng.random_range(1..=10)); + format!("{}//.", base) + } + + // Issue #8924 specific cases: paths like "foo/./" + 10 => { + let base = generate_random_string(rng.random_range(1..=10)); + format!("{}/./", base) + } + + // Issue #8924 specific cases: paths like "foo/bar/./" + 11 => { + let base1 = generate_random_string(rng.random_range(1..=8)); + let base2 = generate_random_string(rng.random_range(1..=8)); + format!("{}/{}/./", base1, base2) + } + + // More complex patterns with ./ and multiple slashes + 12 => { + let base = generate_random_string(rng.random_range(1..=10)); + let patterns = ["/./", "//./", "//.//", "/.//"]; + let pattern = patterns.choose(&mut rng).unwrap(); + format!("{}{}", base, pattern) + } + + // Patterns with .. and multiple slashes + 13 => { + let base = generate_random_string(rng.random_range(1..=10)); + let patterns = ["/..", "//..", "/../", "//..//"]; + let pattern = patterns.choose(&mut rng).unwrap(); + format!("{}{}", base, pattern) + } + + // Complex paths with special cases + _ => { + let special_endings = [".", "..", "/.", "/..", "//", "/", "/./.", "//.", "./"]; + let base = generate_random_string(rng.random_range(1..=15)); + let ending = special_endings.choose(&mut rng).unwrap(); + format!("{}{}", base, ending) + } + } +} + +fuzz_target!(|_data: &[u8]| { + let dirname_args = generate_dirname_args(); + let mut args = vec![OsString::from("dirname")]; + args.extend(dirname_args.iter().map(OsString::from)); + + let rust_result = generate_and_run_uumain(&args, uumain, None); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { + 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( + "dirname", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, + ); +});