A Lucene commit is a small file, segments_N, that names every file of every segment. It is crash-safe only if those files are on disk before segments_N is. IndexSplitter.split writes a real commit in the destination but skips that step for the files it copied.
- Writing a file puts its bytes in the operating system's page cache; the kernel writes them to disk later.
fsync(in Lucene,Directory.sync) forces them to disk now, and a directory fsync makes new file names durable. IndexWriter.commitwritespending_segments_N, fsyncs every file the commit names, renames it tosegments_Nand fsyncs the directory. Only then is the commit durable.- After a crash, a reader opens the newest
segments_Non disk and every file it names. If one of them lost its bytes, the open fails.
split() copies three files, the commit fsyncs only the directory and segments_1, and in a simulated crash the copies are lost and the destination no longer opens. Select the still to open it at full size. Generated with AI, checked against the code.- Your index has one segment,
_0, with one document, stored as 3 files (_0.si,_0.cfs,_0.cfe). You copy it into a new index:new IndexSplitter(src).split(dest, new String[] {"_0"}), orIndexSplitter <src> <dest> _0on the command line. - You expect what every Lucene commit guarantees: before the new
segments_1is durable, every file it names has been fsynced, so the new index survives an OS crash or power loss.IndexWriter.commitworks this way. - Instead
split()copies the 3 files withFiles.copyand callsSegmentInfos.commit, which fsyncs onlypending_segments_1(renamed tosegments_1) and the directory. 3 of the 4 files the commit names were never fsynced; their bytes may still be only in the OS page cache. - If the machine crashes or loses power before the kernel writes them back (on Linux, dirty data is written back after about 30 s by default),
segments_1can survive while the copies come back empty. In Lucene's crash model,DirectoryReader.open(dest)then fails withCorruptSegmentInfoExceptionon_0.si. - The source index is untouched, so the split can usually be re-run. Data is lost only if the segment was already removed from the source with
IndexSplitter <src> -d _0and its files deleted. Ending the process does not trigger it:Runtime.haltright after a split left 5 of 5 destinations intact.
Only users of the experimental IndexSplitter tool in lucene/misc (opt-in, rarely used); IndexWriter commits are not affected. It needs an OS crash, kernel panic or power loss soon after a split; the missing sync itself does not depend on index size. The split destination is lost, not the source, unless the segment was already removed from the source.
Mechanism, terms and numbers424 words
IndexSplitter.split (lucene/misc, also the command line IndexSplitter <srcDir> <destDir> <segments>+) copies every file of the chosen segments into the destination with Files.copy and then publishes a commit there with SegmentInfos.commit. That commit fsyncs only the new segments_N and the directory; nothing fsyncs the copied files. So the destination's commit can be on stable storage while the segment files it names are only in the OS page cache, which breaks the rule every other Lucene commit path follows (IndexWriter.commit and the NRT replicator sync the referenced files first). After an OS crash or power loss before writeback, the durable commit can name empty or damaged files, and then the destination does not open: in Lucene's own crash model DirectoryReader.open fails with CorruptSegmentInfoException. The source index is untouched. Present since the tool was added in 3.0.0 (2009), through 10.6.0 and main; the fix is to sync the copied files before the commit.
- It opens the destination as an
FSDirectoryand builds a newSegmentInfos(IndexSplitter.java:109-140). - For each chosen segment it copies every file of
SegmentCommitInfo.files()withFiles.copy(srcFile, destFile)(:141-147, the copy at:146).Files.copynever fsyncs. - It calls
destInfos.changed()anddestInfos.commit(destFSDir)(:149-150). There is nosynccall anywhere in the class.
commit(dir)isprepareCommit(dir)thenfinishCommit(dir)(SegmentInfos.java:1031-1034; javadoc "Writes and syncs to the Directory dir").prepareCommit:dir.syncMetaData()(:970), thenwrite(dir), which createspending_segments_Nand fsyncs only that file (:620).finishCommit: renames it tosegments_N(:1005) and fsyncs the directory again (:1007).
IndexWriter.startCommit calls prepareCommit (IndexWriter.java:5974), then directory.sync(toSync.files(false)) (:5992-5993), and only later finishCommit. So segments_N becomes durable only after every file it names is. ReplicaNode does the same ("fsync all index files we are now referencing", ReplicaNode.java:364-373). split() skips that middle step.
copy _0.cfe, copy _0.si, copy _0.cfs; fsync the directory; write and fsync pending_segments_1; rename to segments_1; fsync the directory. The commit names _0.cfe, _0.cfs, _0.si and segments_1; only segments_1 was fsynced. In a post-crash image with the 3 copies left empty, zero-filled or deleted (three of the six damage modes of MockDirectoryWrapper.crash; the probe builds these images itself), DirectoryReader.open fails with CorruptSegmentInfoException: segment info file: _0.si cannot be read - it may be missing or corrupt. Before any crash the destination opens with maxDoc 1.
Exposure and likelihood340 words
- Who. Operators who run the
@lucene.experimentalIndexSplitter(IndexSplitter.java:44) from code or the command line. Normal indexing is not affected:IndexWriter.commitandclose()fsync every referenced file in every version run, andIndexWriter.addIndexes(Directory)pluscommitleaves no referenced file unsynced. - Trigger. An OS crash, kernel panic, power loss or hard reset of the machine or VM after
split()returns and before the kernel writes the copies back. On Linux, dirty data is written back once it is about 30 s old by default (vm.dirty_expire_centisecs = 3000on the test host). Ending the process is not enough: afterRuntime.halt(0)right aftersplit(), 5 of 5 destinations opened intact in a fresh JVM, since the copies stay in the OS page cache; akill -9or JVM crash ends the process the same way (not run). How likely a real crash is to catch copies not yet written back depends on the file system, memory pressure and timing. - Outcome. The destination's newest (and only) commit names lost files, so it does not open. The tested crash states fail with an exception at open; no silent wrong results were seen.
- Severity. Usually recoverable:
split()never modifies the source, so the split can be re-run. Data is lost only when the source no longer has the segment's files either, for example in the follow-up the tool supports: split, thenIndexSplitter <src> -d <seg>(usage textIndexSplitter.java:58, code:100-107; upstreamTestIndexSplitter.java:97-98splits and then removes the segment this way), then anIndexWriteron the source deletes the unreferenced files, then the crash hits before the copies are on disk. - File-system dependence (not tested). On ext4 with delayed allocation, a new file whose data was never written back can come back empty after a crash; where
Files.copyclones extents (copy_file_range on XFS or btrfs), the copy may survive. Only the crash model was used, on ext4; no real power cut was induced. - Likelihood. Rare: the experimental tool plus a machine-level crash in a short window; nobody has reported it since 2009.
One JUnit test in lucene/misc: an index with one segment of one empty document is split into a directory whose file system records every fsync, and the test asserts that every segment file of the destination's commit was fsynced. It simulates no crash, so it is deterministic. It fails with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.misc.index;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.FileSystem;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.tests.mockfile.FilterFileChannel;
import org.apache.lucene.tests.mockfile.FilterFileSystemProvider;
import org.apache.lucene.tests.util.LuceneTestCase;
@LuceneTestCase.SuppressFileSystems("*")
public class TestIndexSplitterFsync extends LuceneTestCase {
public void testSplitFsyncsCopiedFiles() throws Exception {
Path src = createTempDir("src");
try (Directory dir = FSDirectory.open(src);
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
w.addDocument(new Document());
}
Path dest = createTempDir("dest");
Set<String> fsynced = new HashSet<>();
FilterFileSystemProvider fs =
new FilterFileSystemProvider("fsynctracking://", dest.getFileSystem()) {
@Override
public FileChannel newFileChannel(
Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
throws IOException {
return new FilterFileChannel(super.newFileChannel(path, options, attrs)) {
@Override
public void force(boolean metaData) throws IOException {
super.force(metaData);
fsynced.add(path.getFileName().toString());
}
};
}
};
new IndexSplitter(src).split(fs.wrapPath(dest), new String[] {"_0"});
Set<String> notFsynced;
try (Directory dir = FSDirectory.open(dest)) {
notFsynced = new TreeSet<>(SegmentInfos.readLatestCommit(dir).files(false));
}
notFsynced.removeAll(fsynced);
// expected: the split's commit references only fsynced files; actual: the copied files are not
assertTrue(
"segment files of the split's commit never fsynced: " + notFsynced, notFsynced.isEmpty());
}
}
# $REPRO holds TestIndexSplitterFsync.java; $LUCENE is a Lucene checkout
cp $REPRO/TestIndexSplitterFsync.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/index/
cd $LUCENE
./gradlew :lucene:misc:test --tests org.apache.lucene.misc.index.TestIndexSplitterFsync -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
- Expected
- Every segment file of the split's commit has been fsynced, as for any Lucene commit: the test passes.
- Observed
AssertionError: segment files of the split's commit never fsynced: [_0.cfe, _0.cfs, _0.si]; onlysegments_1and the directory were fsynced.
More files, environment, full output308 words
Public API only: IndexSplitter, SegmentInfos.readLatestCommit and the test-framework mockfile classes. split() takes a Path and opens its own FSDirectory, so MockDirectoryWrapper cannot see its writes; the test wraps the destination path in a FilterFileSystemProvider whose FileChannel.force records the file name, and @SuppressFileSystems("*") keeps the framework's own mock file systems out. Two limits: only the destination is wrapped, so the test's Files.copy crosses file-system providers and copies through streams rather than the JDK's native same-file-system copy (neither fsyncs: the independent reproducer's same-provider tracker and its no-wrapper flight-recorder check see no force on the copies either); and it checks the commit's segment files (files(false)), not segments_1 itself. Seeds DEADBEEF and 5EED1234 (5 iterations) fail the same way; the seed sets only the framework's randomization (codec, locale and similar).
checkGoogleJavaFormat rejects its one unused import, java.nio.file.FileSystem; that is the only format issue, and the version without it was run too (the failure is then reported at line 72 instead of 73).
What a real crash does next is not part of the test. In Lucene's crash model (the same split, with the 3 copies left empty, zero-filled or deleted), opening the destination fails with CorruptSegmentInfoException on _0.si on main, and with CorruptIndexException on 10.5.2 and 9.12.3.
| lucene | main 645d4fe14a20 and 10.6.0: fails, assertions on and off; 10.5.2 and 9.12.3: fails; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 |
# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on (assertions off: the same)
TestIndexSplitterFsync > testSplitFsyncsCopiedFiles FAILED
java.lang.AssertionError: segment files of the split's commit never fsynced: [_0.cfe, _0.cfs, _0.si]
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C2ADA7351EE6702E]:0)
at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
at junit@4.13.2/org.junit.Assert.assertTrue(Assert.java:42)
at org.apache.lucene.misc.index.TestIndexSplitterFsync.testSplitFsyncsCopiedFiles(TestIndexSplitterFsync.java:73)
# repro harness, the same message: main -ea, main -da, 10.6.0 -ea, 10.6.0 -da, 10.5.2 -ea, 9.12.3 -ea (1 run, 1 failed each)
# with the fix: the test passes (gradle, assertions on and off)- Lucene uses
Directory.sync"to prevent a machine/OS crash from corrupting the index", andIndexWriter.commit"syncs all referenced index files". - Every other code path that publishes a
segments_Nover files it wrote or copied syncs them first (IndexWriter,ReplicaNode). - The tool's javadoc lists its limits but not durability; the 2009 issue that added it never mentions fsync.
The full argument256 words
- The commit contract.
Directory.java:105-108:sync"Ensures that any writes to these files are moved to stable storage (made durable)." and "Lucene uses this to properly commit changes to the index, to prevent a machine/OS crash from corrupting the index."IndexWriter.java:4188-4191:commit"syncs all referenced index files, such that a reader will see the changes and the index updates will survive an OS or machine crash or power loss." - Lucene's crash model.
MockDirectoryWrapper.java:512: "Simulates a crash of OS or machine by overwriting unsynced files." - Every other publisher syncs first.
IndexWriter.startCommit(IndexWriter.java:5992-5993) andReplicaNode(:364-373). Among the non-testSegmentInfos.commitcallers,split()is the only one that writes or copies segment data right before its commit:IndexSplitter.removeand CheckIndex's exorcise only rewrite the commit over files already in the index, andIndexRearrangercommits segments that its ownIndexWriterhas just committed and closed (IndexRearranger.java:126-130,:170-173). - No carve-out. The class javadoc (
IndexSplitter.java:35-44) lists the tool's limits (whole segments only,FSDirectoryonly, segments are easy to remove by accident), not durability. TheIndexWriter.java:1513-1516note that offline splitting tools are used "at your own risk!" is about breaking up atomically added document blocks. UpstreamTestIndexSplitterchecks only the split result in-process. - History: an omission. LUCENE-1959 (#3034, 2009) never mentions fsync, sync, crash or durability (body and all comments searched); the tool went in as "Progress not perfection!". None of the 84 commits that touch the file (all branches, its four paths, the introducing commit included) added or removed a sync.
The devil's-advocate check150 words
The strongest case for "intended": IndexSplitter is an experimental command-line tool documented as "basic file-level copying" that promises no durability; SegmentInfos.commit promises only to write and sync the segments file, so callers must sync their own files; the crash state is synthetic.
Why it fails: the tool publishes a real Lucene commit point, with both directory fsyncs, and nothing says this commit is exempt from Lucene's commit rule; @lucene.experimental is about API stability. The narrow reading of SegmentInfos.commit only decides where the fix goes (in IndexSplitter, as IndexWriter and ReplicaNode do it). The crash state is Lucene's own model, and the repro asserts only the contract. Nothing found (javadoc, test, CHANGES, issue or list thread) calls non-durable split output intended; every verifier rejected the intended reading, and those that rated its strength called it moderate. Maintainers may still treat it as a small robustness fix to an experimental tool.
Recommended A. Sync the copied files before the commit
Collect the names of the files split() copies and call destFSDir.sync(copied) before destInfos.commit(destFSDir), the step IndexWriter and the NRT replicator already take: three statements and a comment, plus two imports. The repro passes, as do TestIndexSplitter (assertions on and off) and all 317 lucene/misc tests. Workaround today: run sync after a split.
List<String> copied = new ArrayList<>();
for (String n : segs) {
// ... build the destination's SegmentCommitInfo, unchanged ...
for (final String srcName : files) {
// ... resolve srcFile and destFile, unchanged ...
Files.copy(srcFile, destFile);
copied.add(srcName);
}
}
// the commit below must only reference files whose bytes are on stable storage
destFSDir.sync(copied);
destInfos.changed();
destInfos.commit(destFSDir);All options and trade-offs4 options ยท 482 words
One fix was built and run through the module's tests; the alternatives were not run.
In IndexSplitter.split, collect every copied name and call destFSDir.sync(copied) before destInfos.commit(destFSDir) (shown condensed; the patch also adds the ArrayList and List imports). FSDirectory.sync fsyncs each named file; the commit's own directory fsyncs then make the names durable. The copied names are all of SegmentCommitInfo.files(): the segment's files plus any live-docs, field-infos and doc-values update files.
List<String> copied = new ArrayList<>();
for (String n : segs) {
// ... build the destination's SegmentCommitInfo, unchanged ...
for (final String srcName : files) {
// ... resolve srcFile and destFile, unchanged ...
Files.copy(srcFile, destFile);
copied.add(srcName);
}
}
// the commit below must only reference files whose bytes are on stable storage
destFSDir.sync(copied);
destInfos.changed();
destInfos.commit(destFSDir);Trade-offs
One fsync per copied file, once per split, in an offline tool. No behaviour change otherwise.
What the checks validated
- Scratch worktree of main
645d4fe14a20(removed afterwards), seed DEADBEEF: stock, the test fails through gradle (assertions on and off); with A it passes, together with upstreamTestIndexSplitter, with-Ptests.asserts=trueandfalse. - All of
:lucene:misc:testwith A (assertions on): 317 tests, 9 skipped, 0 failures. :lucene:misc:checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMainandforbiddenApisTestpass with A and the test (without its unused import); the build's optional eclint check is not configured on the host and was skipped.- Earlier, with the same fix shadowed in: the hunt's, the independent reproducer's and the cross-model reviewer's tests, a no-wrapper flight-recorder check (then forcing the three copied files) and upstream
TestIndexSplitterpass.
Copy with destFSDir.copyFrom(fsDir, srcName, srcName, IOContext.DEFAULT) instead of Files.copy, then sync as in A. It would also answer the request deferred in LUCENE-1959 to use the Directory abstraction instead of java.io.
Trade-offs
A larger change that replaces the JDK's file copy with Lucene's stream copy; the sync is still needed, so it does not replace A. Not run.
Fix every caller at once inside the core method.
Trade-offs
Not recommended. IndexWriter never calls SegmentInfos.commit (it calls prepareCommit, IndexWriter.java:5974, syncs the files itself, :5992-5993, then calls finishCommit, :4295), so it would not be affected, but ReplicaNode would sync twice: it runs dir.sync(indexFiles) and then infos.commit(dir) (ReplicaNode.java:365, :373). Putting the sync into prepareCommit or finishCommit instead would double-sync in IndexWriter. It would also break split() as written: the SegmentInfo that split() builds (IndexSplitter.java:118-131) never gets its file set, so SegmentInfo.files() throws IllegalStateException ("files were not computed yet", SegmentInfo.java:206-212). Read, not run.
A javadoc note telling callers to sync the destination themselves.
Trade-offs
Not recommended: it leaves a commit point that breaks Lucene's own commit rule.
Downstream, a public code search of the default branches found no use of Lucene's IndexSplitter in Elasticsearch, OpenSearch or Solr; Solr splits shards with its own SolrIndexSplitter.
Code reading while building a file-system crash model of Lucene's commit protocol: listing every code path outside IndexWriter that publishes a commit showed that IndexSplitter.split never syncs the files it copies. A JUnit test with an fsync-recording file system confirmed it on the stock jars.
The method in full146 words
Method: code reading (non-formal), in a hunt on commit and crash recovery.
- The hunt built one file-system crash model for Lucene's commit protocol: file data is durable only after its own fsync, a new name only after a directory fsync, renames are atomic. A crash-state enumerator and a TLA+ model checked
IndexWriter,NRTCachingDirectoryandPersistentSnapshotDeletionPolicyworkloads against it; they did not coverIndexSplitter. - To list every code path outside
IndexWriterthat publishes a commit point, the finder read the callers ofSegmentInfos.commitand saw thatIndexSplitter.splitcopies files and commits without syncing them. - It wrote a JUnit test on the stock jars: a file system that records
FileChannel.forceper file, plus a post-crash image built under the model's rules. The test failed on main, 10.5.2 and 9.12.3 and passed with the fix sketch. It was later cut down to the one test shown here.
No prior report: GitHub issues and PRs, the JIRA archive, the Lucene mailing lists, downstream repos and the web (2026-10-26), refreshed on GitHub 2026-10-31. Related but different: open #7044 (LUCENE-5982), misc index tools should take the write lock.
Searches5 searches
Related: #7044 / LUCENE-5982 ("Tools in misc/ like index splitters need to obtain write.lock", open since 2014), the same tool family and the same worry about accidental corruption, but it is about a concurrent writer, never mentions fsync or crashes, and its fix would not fix this. #2120 / LUCENE-1044 and #4491 / LUCENE-3418 set up and repaired fsync on commit for IndexWriter and are precedent for the contract. The Lucene.NET port has the same copy-then-commit code, with no report there.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26 | 33 GraphQL searches (IndexSplitter with fsync, sync, crash, durable, corrupt, power, Files.copy; "not fsynced"; SegmentInfos.commit with fsync; misc tools write.lock) plus a scripted prior-report search | no report; 13 IndexSplitter items, all about other defects; #7044 related |
| Open PRs (apache/lucene) | 2026-10-26, refreshed 2026-10-31 | touched files of all 413 open PRs; then the 55 open PRs updated since 2026-10-26 | none touches IndexSplitter.java; #16571 touches SegmentInfos.java with no sync or commit change |
| JIRA archive and mailing lists | 2026-10-26 | JIRA REST queries and the full comments of 6 issues; dev@, java-user@, issues@, commits@ and Solr lists, 4 threads read in full | no durability discussion of IndexSplitter, LUCENE-1959 included |
| Downstream repos and the web | 2026-10-26 | apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch, Lucene.NET and GitHub-wide; 6 web searches | no report; the Lucene.NET port has the same pattern |
| GitHub refresh (apache/lucene) | 2026-10-31 | 8 issue/PR queries; fsync items updated since 2026-10-26 | nothing new |
Present since the tool was added in 3.0.0 (LUCENE-1959, 2009); none of the 84 commits (all branches) that touch the file added a sync. It fails on 13 releases from 3.0.0 to 10.5.2, on 10.6.0 and on main. Not a regression.
Versions tested7 rows
Introduced by 2fc8e01d9e1 (svn r823153, 2009-10-08), "LUCENE-1959: add IndexSplitter tool to pull segment files out of an index into another" (JIRA LUCENE-1959, migrated as #3034; svn era, no PR). The first version copied with a FileInputStream/FileOutputStream loop and then committed, syncing only the segments file. Later rewrites of the copy and commit lines (LUCENE-2805, LUCENE-4055, LUCENE-5945's switch to Files.copy, LUCENE-9600's move to org.apache.lucene.misc.index in 9.0.0) kept the omission.
Before 4.8.0 (LUCENE-5588) Lucene fsynced no directories, IndexWriter included, so for 3.0-4.7 the "names are durable" half of the crash picture is the same assumption IndexWriter relied on then. Minor releases not listed were not run; their code is unchanged.
| Version or commit | Verdict | Result |
|---|---|---|
| 2.9.0, 2.9.4 | no class | no IndexSplitter (the port fails to compile only at the IndexSplitter lines) |
| 3.0.0 (first release with the tool) | affected | _1.cfs never fsynced; the crash image fails to open (read past EOF) |
| 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.1.0 | affected | the copied files never fsynced; the crash image fails to open (fsync tracer) |
| 9.12.3, 10.5.2 | affected | the hunt's test and the tracer fail; the one-test repro fails |
| 10.6.0 (the latest release) | affected | the one-test repro fails, assertions on and off |
main 645d4fe14a20 (and ea57360912ed) | affected | the one-test repro fails through gradle and the harness, assertions on and off |
| main + fix A | not affected | the repro passes; all 317 lucene/misc tests pass |
An independent reproducer, cross-model reviews (OpenAI GPT-5.6) with two different lenses, an intended-behaviour devil's advocate and an adjudication (confirmed, high) all found it real; each ran a repro, except one reading-only review, for which a separate run re-ran the original test.
Checks and verdicts239 words
Each check had its own lens; the adjudication re-ran the reproducer's class with a new seed. The registry's credibility audit (2026-10-26) kept the row.
| Check | Verdict | What it found |
|---|---|---|
| independent reproducer | real, high for the mechanism | Own 3-test class fails 2 of 3 on main (assertions on and off), 10.5.2 and 9.12.3, every iteration of a 5-iteration run; the crash image fails to open in all three damage modes; the addIndexes control passes; a no-wrapper flight-recorder check shows only the directory and pending_segments_1 forced. |
| cross-model review, refute lens | real, high | Ran its own 2-test class (fails 1 of 2 under three settings while its direct Directory.sync control passes); with the fix 2 of 2 and upstream TestIndexSplitter pass. Intended argument moderate. |
| cross-model review, works-as-designed lens | real, medium | Reading only; found no documented carve-out. A separate run re-ran the hunt's test: it fails, and passes with the fix. |
| intended-behaviour devil's advocate | real, low severity | Built the case for intended (experimental tool, narrow SegmentInfos.commit, synthetic crash) and rejected it; re-ran the hunt's test on main and 10.5.2. |
| adjudication | confirmed, high | Re-ran the reproducer's class with seed 5EED1234: 2 of 3 fail with assertions on and off, control passing; listed the caveats (no real crash induced, implicit contract, destination only). |
| security screen | none | Machine-level crash only; a JVM halt right after the split left 5 of 5 destinations intact. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 451 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,394 words
- Registry title
- IndexSplitter.split publishes a durable commit in the destination index without fsyncing the segment files it copied, so an OS crash or power loss soon after a split can leave a destination whose newest commit references empty files and cannot be opened
- Area
- misc / index tools (IndexSplitter.split, SegmentInfos.commit durability)
- Priority
- Low, rank 910: IndexSplitter never fsyncs the copied segment files, so an OS crash soon after a split can leave an unopenable destination, but it is a rarely used tool and the source stays intact.
- Impact class
- index unusable
- Impact note
- Operators who run the @lucene.experimental IndexSplitter command-line tool from lucene/misc (opt-in and rarely used; the default IndexWriter path is not affected) and whose machine then has an OS crash, kernel panic or power loss before the kernel writes the copied files back (about 30 s by default on Linux) can be left, at any index size and in every release since 3.0.0, with a split destination whose newest commit references empty or damaged segment files, so DirectoryReader.open fails (in Lucene's own crash model; no real power cut was induced, and whether a real crash loses the copies depends on the file system); the source index is untouched, so the destination can normally be re-created, and data is lost only if the operator has already removed the segment from the source with 'IndexSplitter <src> -d' and an IndexWriter has since deleted the source's unreferenced files.
- Summary
- You copy segments into a new index with IndexSplitter (new IndexSplitter(src).split(dest, segments), or the command line 'IndexSplitter <src> <dest> <segments>'), and the machine crashes or loses power shortly after the call returns. You expect what every Lucene commit is built for (Directory.sync javadoc: Lucene syncs files 'to prevent a machine/OS crash from corrupting the index'; IndexWriter.commit syncs all referenced files first): the destination's commit only references files whose bytes are on stable storage. Instead split() copies the segment files with Files.copy and never fsyncs them, and SegmentInfos.commit then fsyncs only the new segments_N and the directory, so the commit and the copied file names can survive the crash while the copied .si, .cfs and .cfe data does not, and opening the destination fails (CorruptSegmentInfoException on _1.si on main, CorruptIndexException on 10.5.2 and 9.12.3, in the simulated post-crash state).
- Introduced
- 2fc8e01d9e1ae0eae040f27a2bcd044c23d27369 (svn r823153, 'LUCENE-1959: add IndexSplitter tool to pull segment files out of an index into another', committed by mikemccand from a patch contributed on JIRA LUCENE-1959, migrated as apache/lucene#3034; svn era, no PR) [read: blame on main and git log --all -G'sync|fsync|force\(' over all four historical paths of IndexSplitter.java in the full-history clone: 0 of the 84 commits that touch the file add or remove a sync; the first version copied with a FileInputStream/FileOutputStream loop and then called destInfos.commit(destFSDir), which synced only the segments file] [exec: 2.9.0 and 2.9.4 have no IndexSplitter (the 3.x port fails to compile only at the IndexSplitter import and use), 3.0.0 fails]; later in-place rewrites of the copy and commit lines (LUCENE-2805, LUCENE-4055, LUCENE-5945 switched to Files.copy, LUCENE-9600 moved the class to org.apache.lucene.misc.index) kept the omission (2009-10-08)
- First affected version
- 3.0.0 [exec] (released 2009-11-25; contrib/CHANGES.txt 3.0.0 lists LUCENE-1959, and lucene-misc-3.0.0.jar contains org/apache/lucene/index/IndexSplitter.class); 2.9.x and earlier have no IndexSplitter [exec: compile error only at the IndexSplitter lines on 2.9.0 and 2.9.4; the 2.9.1-2.9.4 releases come from the 2.9 branch, which never got the class]
- Affected versions
- every release from 3.0.0 through 10.6.0, and main ea57360912ed. [exec] The one-test repro fails on the 10.6.0 jars, -ea and -da (2026-10-31). 13 releases fail, each with the copied files never fsynced and a post-crash image that a reader cannot open: 3.0.0, 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0 and 9.1.0 (version-portable main() repro with a libc-level fsync tracer), 9.12.3 and 10.5.2 (finder JUnit and tracer), plus main (JUnit and tracer, -ea and -da); in every version the control (IndexWriter.commit in the source directory) fsynced every referenced file, and with the fix shadowed the tracer passes on main; the independent reproduction also showed the missing forces on main, 10.5.2 and 9.12.3 with the JDK's own jdk.FileForce flight-recorder event and no file-system wrapper. [read] the other releases are inferred from unchanged code. Before 4.8.0 (LUCENE-5588) Lucene fsynced no directories, IndexWriter included, so for 3.0-4.7 the 'file names are durable' half of the crash image is the same assumption IndexWriter relied on then. The class is org.apache.lucene.index.IndexSplitter (lucene-misc) up to 8.11.4 and org.apache.lucene.misc.index.IndexSplitter since 9.0.0. Not fixed upstream: main and branch_10x 1309534296e3 have no sync [read]; 10.6.0 is executed above. All runs on aarch64 with JDK 25.0.3; old releases ran on that JDK too, which does not change which files Lucene asks to fsync.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, read-only, plus the finder's search: a scripted prior-report search over the dev@, java-user@, issues@ and commits@ lists, the JIRA archive, apache/lucene issues, PRs and commits, Elasticsearch, OpenSearch and Solr, and the web; 33 GitHub GraphQL searches on apache/lucene and 11 on apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch, Lucene.NET and GitHub-wide; JIRA REST queries with the full comments of LUCENE-1959, -3188, -5982, -3418, -1044 and -2919 scanned; extra solr-user@, users@solr, dev@solr and general@ list queries and 4 threads read in full; the touched files of all 413 open apache/lucene PRs, none of which touches IndexSplitter.java). Closest related, different defect: open #7044 / LUCENE-5982 (2014) 'Tools in misc/ like index splitters need to obtain write.lock' (same tool family and the same worry about accidental corruption, but about mutual exclusion with a writer; no mention of fsync or crashes, and its fix would not fix this); contract precedent: LUCENE-1044 (#2120) and LUCENE-3418 (#4491) for IndexWriter. The Lucene.NET port (IndexSplitter.cs) has the same copy-then-commit pattern, with no report there.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the one-test repro through gradle in a scratch worktree (assertions on and off; removed afterwards) and through the hunt repro harness (assertions on and off); the fix through gradle (the repro andTestIndexSplitter, assertions on and off; all of:lucene:misc:test, 317 tests; the module's Google Java Format, ECJ lint and forbidden-APIs checks). The release jars with the one-test repro: 10.6.0 (assertions on and off), 10.5.2 and 9.12.3. The hunt ran on mainea57360912ed(the same lines), with history runs on 13 releases from 3.0.0 to 10.5.2, and 2.9.0 and 2.9.4 (no IndexSplitter). Upstream main2f424fd428da(2026-10-30) has no change toIndexSplitter.javaorSegmentInfos.javasince645d4fe14a20.
- Finding record
lucene/misc/src/java/org/apache/lucene/misc/index/IndexSplitter.java:35-44 (class javadoc: 'basic file-level copying', @lucene.experimental, warning about removing segments), :100-106 (remove, the '-d' path), :109-152 (split), :141-147 (Files.copy loop), :149-150 (destInfos.changed(); destInfos.commit(destFSDir); no sync)lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java:1025-1034 (commit = prepareCommit + finishCommit, 'Writes and syncs to the Directory dir'), :970 and :1007 (syncMetaData in prepareCommit and finishCommit), :605-630 (write syncs only pending_segments_N, at :620)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:4188-4201 (commit javadoc: 'syncs all referenced index files ... will survive an OS or machine crash or power loss'), :5993 (directory.sync(filesToSync) in startCommit), :1513-1516 (offline splitting tools break up document blocks, 'at your own risk')lucene/core/src/java/org/apache/lucene/store/Directory.java:105-112 (sync javadoc: 'moved to stable storage', 'to prevent a machine/OS crash from corrupting the index')lucene/replicator/src/java/org/apache/lucene/replicator/nrt/ReplicaNode.java:364-373 ('fsync all index files we are now referencing', dir.sync(indexFiles), then infos.commit(dir))lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java (crash(): 'Simulates a crash of OS or machine by overwriting unsynced files')SECURITY.md (Scope and Threat Model; Index files are trusted; Other out-of-scope reports; Reporting out-of-scope issues publicly; In-scope reports)
- The finding in 30 seconds
lucene/misc/src/java/org/apache/lucene/misc/index/IndexSplitter.java:109-152 (split; copy loop :141-147; commit :149-150)lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java:605-630, :966-972, :996-1023, :1025-1034 (write, prepareCommit, finishCommit, commit)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:5974, :5990-5993 (startCommit: prepareCommit, then directory.sync(filesToSync))lucene/replicator/src/java/org/apache/lucene/replicator/nrt/ReplicaNode.java:364-373
- Who is affected
lucene/misc/src/java/org/apache/lucene/misc/index/IndexSplitter.java:44, :58, :100-107; lucene/misc/src/test/org/apache/lucene/misc/index/TestIndexSplitter.java:97-98
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/store/Directory.java:104-112lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:4188-4201, :1513-1516, :5990-5993lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java:512-513lucene/replicator/src/java/org/apache/lucene/replicator/nrt/ReplicaNode.java:364-373lucene/misc/src/java/org/apache/lucene/misc/index/IndexSplitter.java:35-44
- Suggested fix
lucene/core/src/java/org/apache/lucene/store/FSDirectory.java:244-251, :295-297lucene/core/src/java/org/apache/lucene/index/SegmentInfo.java:206-212; lucene/misc/src/java/org/apache/lucene/misc/index/IndexSplitter.java:118-131lucene/core/src/java/org/apache/lucene/store/Directory.java:180 (copyFrom)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:4295, :5974, :5992-5993; lucene/replicator/src/java/org/apache/lucene/replicator/nrt/ReplicaNode.java:365, :373 (option C)
- TestIndexSplitterFsync.java (one test):
repro/LFV-191/src/org/apache/lucene/misc/index/TestIndexSplitterFsync.java(76 lines)
The alt text follows the scene's summary card as of 2026-10-31.
assets/LFV-191/bug.png(451 KB)
