From 39feb8cd2198ca37c3f8cbd5a822bd771905072c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 23 Mar 2026 11:33:07 +0800 Subject: [PATCH] Add a test that reproduces a timeout issue in `gix-merge`. The problem was detected by Google OSS-Fuzz, and occurs because of window-size problem in imara-diff's preprocessing. `imara-diff` is the place that can address the issue. --- Cargo.lock | 21 +++++ gix-merge/Cargo.toml | 1 + ...-minimized-gix-merge-blob-6377298803884032 | Bin 0 -> 101386 bytes gix-merge/tests/merge/blob/builtin_driver.rs | 72 ++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 gix-merge/tests/fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032 diff --git a/Cargo.lock b/Cargo.lock index cb5b7f54e9d..9a543e11b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.8.2" @@ -920,6 +929,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1982,6 +2002,7 @@ dependencies = [ name = "gix-merge" version = "0.13.0" dependencies = [ + "arbitrary", "bstr", "document-features", "gix-command", diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index db988b1c146..7210bfc54d1 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -50,6 +50,7 @@ gix-odb = { path = "../gix-odb" } gix-utils = { path = "../gix-utils" } termtree = "1.0.0" pretty_assertions = "1.4.0" +arbitrary = { version = "1.4.2", features = ["derive"] } [package.metadata.docs.rs] all-features = true diff --git a/gix-merge/tests/fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032 b/gix-merge/tests/fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032 new file mode 100644 index 0000000000000000000000000000000000000000..666825a10bb42bd3244541645a0ee74fc6211d4c GIT binary patch literal 101386 zcmeI*KTBIt7zf}hR4CWg?olYwp}9sW6isfr2s*SB6jZFkMMEpZC~8T-(aEWEm(tJC z!P5FcN@v&Jf`hbIZD}T{1k(I@4mWVlx$nvOy-z+s!Y~Yjk&&N4Fd1ay;la~p{h+W@ zdl{CJTyv*ZKbUQ9m&adM4)ZDwOkY^Ba?!FG`i95}$o7HSdi+*dDDPR`<)} z&AsgTl1p;y8*#o^n95h;e3FkJ<EQy0C{+Q&VYUibTb#ZBRvA$V*vsE3BpERfc)GTFV YZ2tN3;__M+ZE0<3dHz}MDpz*ZFVnHrq5uE@ literal 0 HcmV?d00001 diff --git a/gix-merge/tests/merge/blob/builtin_driver.rs b/gix-merge/tests/merge/blob/builtin_driver.rs index 2172f86a5e5..1a6c8ec424f 100644 --- a/gix-merge/tests/merge/blob/builtin_driver.rs +++ b/gix-merge/tests/merge/blob/builtin_driver.rs @@ -27,6 +27,7 @@ fn binary() { } mod text { + use arbitrary::Arbitrary; use bstr::ByteSlice; use gix_merge::blob::{ builtin_driver, @@ -34,6 +35,7 @@ mod text { Resolution, }; use pretty_assertions::assert_str_eq; + use std::num::NonZero; const DIVERGING: &[&str] = &[ // Somehow, on in zdiff mode, it's different, and I wasn't able to figure out the rule properly. @@ -140,6 +142,76 @@ mod text { } } + /// This test reproduces what the fuzzer does, allowing it to accept `Arbitrary` input produced by the fuzzer. + #[test] + fn clusterfuzz_timeout_regression() { + #[derive(Debug, Arbitrary)] + struct FuzzCtx<'a> { + base: &'a [u8], + ours: &'a [u8], + theirs: &'a [u8], + marker_size: NonZero, + } + fn run_fuzz_case(ours: &[u8], base: &[u8], theirs: &[u8], marker_size: NonZero) { + let mut out = Vec::new(); + let mut input = imara_diff::intern::InternedInput::default(); + for diff_algorithm in [ + imara_diff::Algorithm::Histogram, + imara_diff::Algorithm::Myers, + imara_diff::Algorithm::MyersMinimal, + ] { + let mut options = builtin_driver::text::Options { + diff_algorithm, + conflict: Default::default(), + }; + for (left, right) in [(ours, theirs), (theirs, ours)] { + let resolution = gix_merge::blob::builtin_driver::text( + &mut out, + &mut input, + Default::default(), + left, + base, + right, + options, + ); + if resolution == Resolution::Conflict { + for conflict in [ + Conflict::ResolveWithOurs, + Conflict::ResolveWithTheirs, + Conflict::ResolveWithUnion, + Conflict::Keep { + style: ConflictStyle::Diff3, + marker_size, + }, + Conflict::Keep { + style: ConflictStyle::ZealousDiff3, + marker_size, + }, + ] { + options.conflict = conflict; + gix_merge::blob::builtin_driver::text( + &mut out, + &mut input, + Default::default(), + left, + base, + right, + options, + ); + } + } + } + } + } + + let ctx = FuzzCtx::arbitrary(&mut arbitrary::Unstructured::new(include_bytes!( + "../../fixtures/clusterfuzz-testcase-minimized-gix-merge-blob-6377298803884032" + ))) + .expect("testcase matches the historical fuzz target input layout"); + + run_fuzz_case(ctx.ours, ctx.base, ctx.theirs, ctx.marker_size); + } + #[test] fn run_baseline() -> crate::Result { let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?;