After a commit, IndexFileDeleter deletes the old segments_N and then the files only it referenced with no directory fsync in between, so an OS crash that persists those unlinks out of order can leave a stale older commit that blocks IndexWriter

Finding LFV-201Area: index / IndexFileDeleter and util/FileDeleter (deletion of expired commits, crash durability)Priority: lowImpact: index unusableNovelty: known upstream

AI-assisted finding, confirmed by automated checks, not yet triaged by a human Lucene committer. This site files nothing upstream.

What it shows

You index with IndexWriter on an FSDirectory with default settings and commit; after the new commit is fsynced, the default deletion policy removes the previous commit, first its segments_N and then the files only it used. You expect that an OS crash or power loss at that point leaves the old commit either complete or gone, which is what the FileDeleter comments and the LUCENE-6616 CHANGES entry ('the index is never left in a state where an expired segments_N references non-existing files') describe. Instead nothing fsyncs the directory between the two deletion passes, so on a file system that persists those unlinks out of order the old segments_N can survive while its _N.si or compound files are gone; DirectoryReader still opens the newest commit, but new IndexWriter fails (CorruptSegmentInfoException on main), listCommits throws and CheckIndex reports corruption (shown in crash states built in software from the real operation sequence; no physical crash was run).

Expected vs actual

Files

Run it

# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble   (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar   # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar   # JUnit Platform console launcher (Maven Central)

javac -d classes -cp "$CP" $(find src -name '*.java')

# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
     -cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
     execute --select-class org.apache.lucene.index.TestStaleCommitAfterCrashDuringDeletes

Gradle alternative: copy the test into the matching module's src/test/ tree of the checkout and run ./gradlew -p lucene/<module> test --tests <class> -Ptests.seed=<seed> (add -Ptests.asserts=false for a run without assertions).

Feedback

Open an issue in lucene-ai-bug-hunt. Please do not open issues or pull requests in apache/lucene about this finding unless a Lucene committer agrees it should be filed.

Snapshot of 2026-11-03. AI-assisted; not yet triaged by a human Lucene committer. Feedback: open an issue in lucene-ai-bug-hunt.