Skip to content

Commit 727d528

Browse files
committed
fix: Crash on constraint application
fixes #618
1 parent 1bb76f6 commit 727d528

5 files changed

Lines changed: 133 additions & 23 deletions

File tree

crates/fff-core/src/constraints.rs

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(crate) trait Constrainable {
3737
fn write_file_name(&self, arena: ArenaPtr, out: &mut String);
3838
fn git_status(&self) -> Option<git2::Status>;
3939
fn write_relative_path(&self, arena: ArenaPtr, out: &mut String);
40+
fn is_overflow(&self) -> bool;
4041
}
4142

4243
/// Windows stores paths with `\\`; `/` comes from user queries.
@@ -163,12 +164,13 @@ pub(crate) fn apply_constraints<'a, T: Constrainable + Sync>(
163164
items: &'a [T],
164165
constraints: &[Constraint<'_>],
165166
arena: ArenaPtr,
167+
overflow_arena: ArenaPtr,
166168
) -> Option<Vec<&'a T>> {
167169
if constraints.is_empty() {
168170
return None;
169171
}
170-
let plan = ConstraintPlan::build(constraints, items, arena);
171-
Some(plan.run(items, arena))
172+
let plan = ConstraintPlan::build(constraints, items, arena, overflow_arena);
173+
Some(plan.run(items, arena, overflow_arena))
172174
}
173175

174176
#[cfg(feature = "zlob")]
@@ -217,6 +219,7 @@ impl<'q, 'c> ConstraintPlan<'q, 'c> {
217219
constraints: &'c [Constraint<'q>],
218220
items: &[T],
219221
arena: ArenaPtr,
222+
overflow_arena: ArenaPtr,
220223
) -> Self {
221224
let mut extensions = SmallVec::new();
222225
let mut rest = SmallVec::new();
@@ -227,22 +230,28 @@ impl<'q, 'c> ConstraintPlan<'q, 'c> {
227230
}
228231
}
229232
let has_pre_filter = !extensions.is_empty() || rest.iter().any(|&c| !is_glob_node(c));
230-
let glob = build_glob_strategy(&rest, has_pre_filter, items, arena);
233+
let glob = build_glob_strategy(&rest, has_pre_filter, items, arena, overflow_arena);
231234
Self {
232235
extensions,
233236
rest,
234237
glob,
235238
}
236239
}
237240

238-
fn run<'a, T: Constrainable + Sync>(&self, items: &'a [T], arena: ArenaPtr) -> Vec<&'a T> {
241+
fn run<'a, T: Constrainable + Sync>(
242+
&self,
243+
items: &'a [T],
244+
arena: ArenaPtr,
245+
overflow_arena: ArenaPtr,
246+
) -> Vec<&'a T> {
239247
if items.len() >= PAR_THRESHOLD {
240248
use rayon::prelude::*;
241249
items
242250
.par_iter()
243251
.enumerate()
244252
.map_init(ConstraintsBuffers::new, |scratch, (i, item)| {
245-
self.matches(item, i, arena, scratch).then_some(item)
253+
self.matches(item, i, arena, overflow_arena, scratch)
254+
.then_some(item)
246255
})
247256
.flatten()
248257
.collect()
@@ -251,7 +260,10 @@ impl<'q, 'c> ConstraintPlan<'q, 'c> {
251260
items
252261
.iter()
253262
.enumerate()
254-
.filter_map(|(i, item)| self.matches(item, i, arena, &mut scratch).then_some(item))
263+
.filter_map(|(i, item)| {
264+
self.matches(item, i, arena, overflow_arena, &mut scratch)
265+
.then_some(item)
266+
})
255267
.collect()
256268
}
257269
}
@@ -262,8 +274,17 @@ impl<'q, 'c> ConstraintPlan<'q, 'c> {
262274
item: &T,
263275
index: usize,
264276
arena: ArenaPtr,
277+
overflow_arena: ArenaPtr,
265278
scratch: &mut ConstraintsBuffers,
266279
) -> bool {
280+
// Overflow items' path chunks live in the overflow arena; reading them
281+
// through the base arena dereferences a wrong (possibly null) pointer.
282+
let arena = if item.is_overflow() {
283+
overflow_arena
284+
} else {
285+
arena
286+
};
287+
267288
if !self.passes_extensions(item, arena, scratch) {
268289
return false;
269290
}
@@ -442,14 +463,15 @@ fn build_glob_strategy<T: Constrainable>(
442463
has_pre_filter: bool,
443464
items: &[T],
444465
arena: ArenaPtr,
466+
overflow_arena: ArenaPtr,
445467
) -> GlobStrategy {
446468
if !contains_glob(rest) {
447469
return GlobStrategy::None;
448470
}
449471
if has_pre_filter {
450472
return GlobStrategy::Inline(compile_globs(rest));
451473
}
452-
let buf = PathBuffer::collect(items, arena);
474+
let buf = PathBuffer::collect(items, arena, overflow_arena);
453475
let path_refs = buf.as_strs();
454476
GlobStrategy::Prepass(precompute_masks(rest, &path_refs))
455477
}
@@ -477,13 +499,18 @@ struct PathBuffer {
477499
}
478500

479501
impl PathBuffer {
480-
fn collect<T: Constrainable>(items: &[T], arena: ArenaPtr) -> Self {
502+
fn collect<T: Constrainable>(items: &[T], arena: ArenaPtr, overflow_arena: ArenaPtr) -> Self {
481503
let mut bytes = Vec::<u8>::new();
482504
let mut offsets = Vec::with_capacity(items.len());
483505
let mut tmp = String::with_capacity(64);
484506
for item in items {
507+
let item_arena = if item.is_overflow() {
508+
overflow_arena
509+
} else {
510+
arena
511+
};
485512
let start = bytes.len();
486-
item.write_relative_path(arena, &mut tmp);
513+
item.write_relative_path(item_arena, &mut tmp);
487514
bytes.extend_from_slice(tmp.as_bytes());
488515
#[cfg(windows)]
489516
for b in &mut bytes[start..] {
@@ -607,6 +634,10 @@ mod tests {
607634
fn git_status(&self) -> Option<git2::Status> {
608635
None
609636
}
637+
638+
fn is_overflow(&self) -> bool {
639+
false
640+
}
610641
}
611642

612643
#[test]
@@ -830,13 +861,13 @@ mod tests {
830861
let mismatch = [Constraint::FilePath("트.c")];
831862

832863
let exact_items = [item.clone()];
833-
let exact_matches =
834-
apply_constraints(&exact_items, &exact, arena_ptr).expect("constraints applied");
864+
let exact_matches = apply_constraints(&exact_items, &exact, arena_ptr, arena_ptr)
865+
.expect("constraints applied");
835866
assert_eq!(exact_matches.len(), 1);
836867

837868
let mismatch_items = [item];
838-
let mismatch_matches =
839-
apply_constraints(&mismatch_items, &mismatch, arena_ptr).expect("constraints applied");
869+
let mismatch_matches = apply_constraints(&mismatch_items, &mismatch, arena_ptr, arena_ptr)
870+
.expect("constraints applied");
840871
assert!(mismatch_matches.is_empty());
841872
}
842873

@@ -888,7 +919,7 @@ mod tests {
888919

889920
// Not(Glob("**/*.rs")) should exclude .rs files
890921
let constraints = vec![Constraint::Not(Box::new(Constraint::Glob("**/*.rs")))];
891-
let result = apply_constraints(&items, &constraints, arena_ptr).unwrap();
922+
let result = apply_constraints(&items, &constraints, arena_ptr, arena_ptr).unwrap();
892923
let paths: Vec<&str> = result.iter().map(|i| i.relative_path).collect();
893924
assert!(
894925
!paths.contains(&"src/main.rs"),
@@ -926,15 +957,15 @@ mod tests {
926957
];
927958

928959
let mixed = vec![Constraint::Extension("rs"), Constraint::Glob("src/**")];
929-
let mixed_paths: Vec<&str> = apply_constraints(&items, &mixed, arena_ptr)
960+
let mixed_paths: Vec<&str> = apply_constraints(&items, &mixed, arena_ptr, arena_ptr)
930961
.unwrap()
931962
.iter()
932963
.map(|i| i.relative_path)
933964
.collect();
934965
assert_eq!(mixed_paths, vec!["src/main.rs"]);
935966

936967
let pure_glob = vec![Constraint::Glob("src/**")];
937-
let glob_paths: Vec<&str> = apply_constraints(&items, &pure_glob, arena_ptr)
968+
let glob_paths: Vec<&str> = apply_constraints(&items, &pure_glob, arena_ptr, arena_ptr)
938969
.unwrap()
939970
.iter()
940971
.map(|i| i.relative_path)
@@ -968,7 +999,7 @@ mod tests {
968999
Constraint::Extension("rs"),
9691000
Constraint::Not(Box::new(Constraint::Glob("vendor/**"))),
9701001
];
971-
let paths: Vec<&str> = apply_constraints(&items, &constraints, arena_ptr)
1002+
let paths: Vec<&str> = apply_constraints(&items, &constraints, arena_ptr, arena_ptr)
9721003
.unwrap()
9731004
.iter()
9741005
.map(|i| i.relative_path)

crates/fff-core/src/grep.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,7 @@ pub(crate) fn multi_grep_search<'a>(
10181018
base_file_count,
10191019
options,
10201020
arena,
1021+
overflow_arena,
10211022
);
10221023

10231024
// If constraints yielded 0 files and we had FilePath constraints,
@@ -1032,6 +1033,7 @@ pub(crate) fn multi_grep_search<'a>(
10321033
base_file_count,
10331034
options,
10341035
arena,
1036+
overflow_arena,
10351037
);
10361038
files_to_search = retry_files;
10371039
filtered_file_count = retry_count;
@@ -1440,12 +1442,18 @@ fn prefilter_files<'a>(
14401442
base_count: usize,
14411443
options: &GrepSearchOptions,
14421444
arena: crate::simd_path::ArenaPtr,
1445+
overflow_arena: crate::simd_path::ArenaPtr,
14431446
) -> (Vec<&'a FileItem>, usize) {
14441447
let max_file_size = options.max_file_size;
14451448
let plan = if constraints.is_empty() {
14461449
None
14471450
} else {
1448-
Some(ConstraintPlan::build(constraints, files, arena))
1451+
Some(ConstraintPlan::build(
1452+
constraints,
1453+
files,
1454+
arena,
1455+
overflow_arena,
1456+
))
14491457
};
14501458

14511459
let mut scratch = ConstraintsBuffers::new();
@@ -1482,7 +1490,7 @@ fn prefilter_files<'a>(
14821490
continue;
14831491
}
14841492
if let Some(plan) = plan.as_ref()
1485-
&& !plan.matches(f, file_idx, arena, &mut scratch)
1493+
&& !plan.matches(f, file_idx, arena, overflow_arena, &mut scratch)
14861494
{
14871495
continue;
14881496
}
@@ -1514,7 +1522,7 @@ fn prefilter_files<'a>(
15141522
continue;
15151523
}
15161524
if let Some(ref p) = plan
1517-
&& !p.matches(f, boundary + offset, arena, &mut scratch)
1525+
&& !p.matches(f, boundary + offset, arena, overflow_arena, &mut scratch)
15181526
{
15191527
continue;
15201528
}
@@ -1533,7 +1541,7 @@ fn prefilter_files<'a>(
15331541
continue;
15341542
}
15351543
if let Some(ref p) = plan
1536-
&& !p.matches(f, idx, arena, &mut scratch)
1544+
&& !p.matches(f, idx, arena, overflow_arena, &mut scratch)
15371545
{
15381546
continue;
15391547
}
@@ -2047,6 +2055,7 @@ pub(crate) fn grep_search<'a>(
20472055
base_count,
20482056
options,
20492057
arena,
2058+
overflow_arena,
20502059
);
20512060

20522061
if files_to_search.is_empty()
@@ -2060,6 +2069,7 @@ pub(crate) fn grep_search<'a>(
20602069
base_count,
20612070
options,
20622071
arena,
2072+
overflow_arena,
20632073
);
20642074

20652075
files_to_search = retry_files;
@@ -2179,6 +2189,7 @@ pub(crate) fn grep_search<'a>(
21792189
bigram_boundary,
21802190
options,
21812191
arena,
2192+
overflow_arena,
21822193
);
21832194

21842195
if files_to_search.is_empty()
@@ -2191,6 +2202,7 @@ pub(crate) fn grep_search<'a>(
21912202
bigram_boundary,
21922203
options,
21932204
arena,
2205+
overflow_arena,
21942206
);
21952207
files_to_search = retry_files;
21962208
filtered_file_count = retry_count;

crates/fff-core/src/score.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ pub(crate) fn fuzzy_match_and_score_dirs<'a>(
276276
let working_dirs: Vec<&DirItem> = if parsed_query.constraints.is_empty() {
277277
dirs.iter().collect()
278278
} else {
279-
match apply_constraints(dirs, &parsed_query.constraints, arena) {
279+
match apply_constraints(dirs, &parsed_query.constraints, arena, arena) {
280280
Some(filtered) if !filtered.is_empty() => filtered,
281281
Some(_) => return (vec![], vec![], 0),
282282
None => dirs.iter().collect(),
@@ -488,7 +488,7 @@ fn match_and_score_in_arena<'a>(
488488
let working_files: FileItems<'a> = if parsed.constraints.is_empty() {
489489
FileItems::All(files)
490490
} else {
491-
match apply_constraints(files, &parsed.constraints, arena) {
491+
match apply_constraints(files, &parsed.constraints, arena, arena) {
492492
Some(filtered) if !filtered.is_empty() => FileItems::Filtered(filtered),
493493
Some(_) => {
494494
return vec![];

crates/fff-core/src/types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ impl Constrainable for DirItem {
205205
fn git_status(&self) -> Option<git2::Status> {
206206
None
207207
}
208+
209+
#[inline]
210+
fn is_overflow(&self) -> bool {
211+
DirItem::is_overflow(self)
212+
}
208213
}
209214

210215
#[derive(Debug)]
@@ -756,6 +761,11 @@ impl Constrainable for FileItem {
756761
fn git_status(&self) -> Option<git2::Status> {
757762
self.git_status
758763
}
764+
765+
#[inline]
766+
fn is_overflow(&self) -> bool {
767+
FileItem::is_overflow(self)
768+
}
759769
}
760770

761771
#[derive(Debug, Clone, Default)]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::fs;
2+
3+
use fff_search::file_picker::FilePicker;
4+
use fff_search::grep::{GrepMode, GrepSearchOptions};
5+
use fff_search::{AiGrepConfig, FFFQuery, FilePickerOptions};
6+
use tempfile::TempDir;
7+
8+
// bug pinning https://github.com/dmtrKovalenko/fff/issues/618
9+
#[test]
10+
fn grep_path_constraint_on_overflow_file_does_not_segfault() {
11+
let tmp = TempDir::new().expect("tempdir");
12+
let base = tmp.path();
13+
let spec_dir = base.join("specs");
14+
fs::create_dir_all(&spec_dir).expect("mkdir specs");
15+
16+
let file = spec_dir.join("annotation-plan.md");
17+
fs::write(&file, "dependency\n").expect("write test file");
18+
19+
// Intentionally do NOT call `collect_files()`. This leaves the base path
20+
// arena unset/null. `handle_create_or_modify` then adds the file as an
21+
// overflow file whose path chunks live in the overflow arena.
22+
let mut picker = FilePicker::new(FilePickerOptions {
23+
base_path: base.to_string_lossy().to_string(),
24+
enable_mmap_cache: false,
25+
watch: false,
26+
enable_home_dir_scanning: true,
27+
..Default::default()
28+
})
29+
.expect("create picker");
30+
31+
let is_overflow = picker
32+
.handle_create_or_modify(&file)
33+
.expect("add overflow file")
34+
.is_overflow();
35+
assert!(is_overflow, "file must be added to the overflow arena");
36+
37+
// Mirrors the `ffgrep({ pattern: "dependency", path: "specs/annotation-plan.md" })`
38+
// call that crashed: in AI grep mode the path token becomes a FilePath
39+
// constraint and `dependency` becomes the grep text.
40+
let parsed = FFFQuery::parse("specs/annotation-plan.md dependency", AiGrepConfig);
41+
42+
let opts = GrepSearchOptions {
43+
mode: GrepMode::PlainText,
44+
page_limit: 20,
45+
smart_case: true,
46+
..Default::default()
47+
};
48+
49+
// original issue:
50+
// Debug builds typically abort with Rust's unsafe precondition check at
51+
// simd_path.rs: ChunkedString::write_to_string. Release builds may SIGSEGV
52+
// in memmove from an address like 0x30 (null arena + chunk_index * 16).
53+
let result = picker.grep(&parsed, &opts);
54+
55+
assert_eq!(result.files.len(), 1, "the overflow file should match");
56+
assert_eq!(result.matches.len(), 1, "`dependency` should match once");
57+
}

0 commit comments

Comments
 (0)