fix: prevent use-after-free in TantivyIndexWrapper::finish() (#49845)

## Summary

Fix a use-after-free bug in `TantivyIndexWrapper::finish()` that causes
intermittent SEGFAULT (signal 139) in `json_stats_test` CI builds.

**Root cause**: The Rust FFI function `tantivy_finish_index` always
consumes and frees the `IndexWriterWrapper` via `Box::from_raw`,
regardless of success or failure. But the C++ side only set `writer_ =
nullptr` **after** the `AssertInfo` check:

```cpp
auto res = RustResultWrapper(tantivy_finish_index(writer_)); // Rust frees writer_
AssertInfo(res.result_->success, ...);  // throws on error
writer_ = nullptr;   // never reached if AssertInfo throws
```

When `finish()` returned an error (e.g., I/O issues during commit),
`AssertInfo` threw an exception while `writer_` still pointed to
already-freed Rust memory. The destructor chain then called
`tantivy_free_index_writer(writer_)` on the dangling pointer →
**use-after-free → SEGFAULT**.

**Fix**: Null `writer_` before the FFI call so the destructor safely
skips `free()` even when `finish()` throws.

**Observed in CI**: ciloop #6762 (SIGSEGV, rc=139) and #6684 (assertion
failure), both in `json_stats_test`. Intermittent (~1.8% failure rate).

issue: https://github.com/milvus-io/milvus/issues/49135

## Test plan

- [ ] `json_stats_test` no longer crashes with SIGSEGV in CI
- [ ] Existing C++ unit tests pass

Signed-off-by: Li Liu <li.liu@zilliz.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: Li Liu <li.liu@zilliz.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Li Liu
2026-05-16 08:36:27 +08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 66fa592752
commit 9f501f9bea
+9 -2
View File
@@ -678,11 +678,18 @@ struct TantivyIndexWrapper {
return;
}
auto res = RustResultWrapper(tantivy_finish_index(writer_));
// Null writer_ before the FFI call because tantivy_finish_index
// always consumes (frees) the Rust-side IndexWriterWrapper via
// Box::from_raw, regardless of success or failure. If we leave
// writer_ pointing at the now-freed memory and the AssertInfo
// below throws, the destructor path will call free() on a
// dangling pointer (use-after-free / SEGFAULT).
auto w = writer_;
writer_ = nullptr;
auto res = RustResultWrapper(tantivy_finish_index(w));
AssertInfo(res.result_->success,
"failed to finish index: {}",
res.result_->error);
writer_ = nullptr;
finished_ = true;
}