- Index sorting keeps the documents of every segment in the order of a sort, here a long field
sort. addDocumentsindexes a block, children first and the parent last; the block must stay together through sorting and merging.- To find where blocks end, IndexWriter writes an internal mark, the parent field named by
IndexWriterConfig#setParentField, on the last document of every block. A block sorts by its parent's value.
- Sort the index on a long field
sortand setsetParentField("_parent"). Add one block withaddDocuments: a child (sort=9) and its parent (sort=1), and commit. IndexWriter marks the parent in_parent; the whole block sorts by its parent's value, 1. - Call
updateNumericDocValue(id:child, "_parent", -1)and close the writer. You expect anIllegalArgumentException: a document that contains_parentis rejected as "a reserved field", and an update of the sort field is rejected too. - Instead the call is accepted. The child now carries a parent mark, so the segment reads as two blocks,
[child](sort 9) then[parent](sort 1), which is out of order:CheckIndexreportssegment has indexSort=<long: "sort"> but docID=0 sorts after docID=1. - Removing a parent's mark instead (
updateDocValueswith a null value) is accepted too. If that block was the last one of its segment and every document there has a sort value, the nextforceMergethrowsEOFExceptionand closes the writer; once the update has been committed, writers opened later fail the same way, while reads still work. Otherwise the orphaned documents silently join the next block. - It fails with assertions on and off. Affected: every release from 9.10.0 (the first with
setParentField) to 10.6.0, and main. Only an application that writes its own configured parent field can trigger it.
Applications that combine index sorting, document blocks and setParentField (opt-in; required for blocks with a sort since 10.0) and that send a doc-values update naming their own parent field. Untrusted documents cannot: the document path rejects the field. Unlikely, but silent at the call; it surfaces as a broken segment or a writer that can no longer merge.
Mechanism, terms and numbers835 words
With an index sort and IndexWriterConfig#setParentField, IndexWriter writes a reserved numeric doc-values mark on the last document of every block, and CheckIndex and merges use those marks to find block boundaries. A document that contains the field is rejected as "a reserved field", but IndexWriter's doc-values update methods check only index-sort fields, so an application can overwrite or erase a mark. Giving a child a mark splits its block inside an already sorted segment: one updateNumericDocValue call on a committed 2-document block leaves a segment that CheckIndex rejects (segment has indexSort=<long: "sort"> but docID=0 sorts after docID=1), with no merge. Removing a parent's mark leaves its block unmarked: if that block was the last one of its segment and every document there has a sort value, the next merge throws EOFException and closes the writer, and once the update is committed later writers fail the same way; otherwise the orphans silently join the next block. Every release from 9.10.0 to 10.6.0 and main is affected. Only an application that writes the field it configured as its parent field can trigger it, and cross-model reviews call that out of contract, so it reads best as missing input validation. Low priority; a 16-line check fixes it.
- Index sort:
IndexWriterConfig#setIndexSort. Every segment keeps its documents in that order, andCheckIndexchecks it. - Document block: documents added together with
addDocuments, children first and the parent last. IndexWriter guarantees that sorting and merging never reorder or split a block. - Parent field:
IndexWriterConfig#setParentField("_parent"). IndexWriter writes_parent = -1on the last document of every block (IndexingChain.java:166-169,:604-609,:645-651). A block sorts by its parent's value. From 10.0.0 a parent field is required to combine blocks with an index sort (MIGRATE.md:875-881).
- A document that names the parent field is rejected:
"_parent" is a reserved field and should not be added to any document(IndexingChain.java:616-619,:748-750). - The doc-values update methods have no such check.
updateNumericDocValue(IndexWriter.java:1962),updateBinaryDocValue(:1996),updateSortedNumericDocValue(:2037) andbuildDocValuesUpdate(:2081, which servesupdateDocValues,softUpdateDocument(s)andtryUpdateDocValue) reject only index-sort fields (:1965,:2003,:2041,:2098). Once a block has been indexed,_parentis an existing doc-values-only NUMERIC field, whichFieldInfos.verifyOrCreateDvOnlyField(FieldInfos.java:562-647) accepts. CheckIndex.testSortwalks only the documents that carry the parent field and compares each block's parent with the next (CheckIndex.java:1255-1282). Merges build block boundaries from the field's current values and give each document the sort value of the next marked document (MultiSorter.java:52-63,:166).
Before the update the segment is doc 0 = child (sort=9, no mark) and doc 1 = parent (sort=1, _parent=-1): one block, trivially sorted. updateNumericDocValue(id:child, "_parent", -1) gives doc 0 a mark, and closing the writer commits it as a doc-values generation of the same segment. testSort now sees two blocks, compares 9 with 1 and reports segment has indexSort=<long: "sort"> but docID=0 sorts after docID=1. Nothing re-sorts the segment. If the marked child had its parent's sort value, CheckIndex would stay clean.
updateDocValues(term, new NumericDocValuesField("_parent", (Long) null)) on a parent is accepted too. Its block's documents then have no mark of their own, and the next merge gives them the sort value of the next marked document of their segment (MultiSorter.java:166). What follows depends on the layout:
- The block was the last one of its segment, and every document there has a sort value (the layout of every earlier test that threw):
nextSetBitreturnsNO_MORE_DOCS, and the firstforceMerge(1)reads past the end of the sort values and throwsRuntimeException(EOFException)(fromDirectReaderviaIndexSorter$LongSorter; a plainByteBuffersDirectorythrowsIndexOutOfBoundsException). The writer closes with a tragic exception. If the update was committed before the merge, writers opened later fail their firstforceMerge(1)the same way (three tried), while readers still open and search; if the merge ran before any commit, the tragic close drops the update and a new writer merges fine. - Another block follows in the same segment, or some documents have no sort value: nothing throws and CheckIndex stays clean, but the orphans silently become members of the next block.
After a mark is added and a merge runs, a block join can attribute a child to another root.
Without an index sort, a verifier's control stayed clean through later merges; merge-time reorderers such as BPIndexReorderer, which also read the mark, were not tested. Changing a parent's mark to another non-null value is harmless. Once a block exists, updateBinaryDocValue and updateSortedNumericDocValue already fail with a type-mismatch message, because the field is NUMERIC. On an index with no documents yet, updateDocValues even creates _parent with the type it is given; after a binary one, the writer's next block fails with cannot change field "_parent" from doc values type=BINARY to inconsistent doc values type=NUMERIC.
Exposure and likelihood291 words
- Who is exposed. Only applications that (a) use index sorting with document blocks and
setParentField, and (b) send a doc-values update (updateNumericDocValue,updateDocValues,softUpdateDocument(s)ortryUpdateDocValue) that names the field they configured as the parent field. The default configuration has no parent field. Untrusted documents cannot trigger it (the document path rejects the field), but an application that passes user-chosen field names to these update methods would pass the trigger on; that was not ruled out. - Silent at the call. The update returns a sequence number like any other. The damage shows later: CheckIndex rejects the committed segment, or the next merge fails and closes the writer.
- Lasting, once committed. In the tested remove-a-mark layouts (the unmarked block last in its segment, a sort value on every document,
forceMerge(1)), once the update had been committed each writer opened later failed its firstforceMergeand closed, while reads kept working (three reopened writers tried). When the merge ran before a commit, the tragic close dropped the update and a new writer merged fine. A natural merge chosen by the merge policy was not tested. - Wrong block membership. After a mark is added and a merge runs,
ToParentBlockJoinQuerymapped a child to another block's root; a removed mark that does not make the merge fail silently regroups documents into the next block. - Scale. Two documents and one call; deterministic.
- How likely. Low: no legitimate workflow needs to write the parent field. A code read of Elasticsearch, OpenSearch and Solr found no default-configuration path that sends such an update (Solr only if an administrator declares its internal
_is_root_field as an in-place-updatable docValues-only field; not executed). - Not claimed. Wrong top hits from sort-based early termination were never verified.
One JUnit test on the public API: a 2-document block in a sorted index, one updateNumericDocValue on the parent field, then CheckIndex's index-sort check on the committed segment. No merge, one thread.
/*
* 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.index;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
public class TestParentFieldDocValuesUpdate extends LuceneTestCase {
public void testUpdateParentFieldOfChildKeepsIndexSorted() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
IndexWriterConfig iwc =
new IndexWriterConfig()
.setCodec(TestUtil.getDefaultCodec())
.setIndexSort(new Sort(new SortField("sort", SortField.Type.LONG)))
.setParentField("_parent");
try (IndexWriter w = new IndexWriter(dir, iwc)) {
Document child = new Document();
child.add(new StringField("id", "child", Field.Store.NO));
child.add(new NumericDocValuesField("sort", 9));
Document parent = new Document();
parent.add(new NumericDocValuesField("sort", 1));
w.addDocuments(List.of(child, parent));
w.commit();
try {
w.updateNumericDocValue(new Term("id", "child"), "_parent", -1);
} catch (IllegalArgumentException e) {
return;
}
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
CodecReader segment = (CodecReader) reader.leaves().get(0).reader();
// expected: the update is rejected (IllegalArgumentException) or the segment stays in index-sort order; actual: out of order
assertNull(CheckIndex.testSort(segment, iwc.getIndexSort(), null, false).error);
}
}
}
}
cp $REPRO/TestParentFieldDocValuesUpdate.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestParentFieldDocValuesUpdate -Ptests.seed=DEADBEEF -Ptests.asserts=true
# same failure with -Ptests.asserts=false
- Expected
- The update is rejected with
IllegalArgumentException, or the segment stays in index-sort order. - Observed
- The update is accepted and
CheckIndex.testSortreportssegment has indexSort=<long: "sort"> but docID=0 sorts after docID=1(assertions on and off).
More files, environment, full output238 words
Public API only (IndexWriter, IndexWriterConfig, CheckIndex.testSort, ByteBuffersDirectory; the test framework's LuceneTestCase and TestUtil.getDefaultCodec()). The test sits in org.apache.lucene.index next to the core tests but uses no package-private API. It passes if the update is rejected with IllegalArgumentException (the fixed behaviour) or if the segment stays in index-sort order. The update call itself does not throw on stock code; only the sort check fails.
The same test is the suggested regression test. With two lint-only edits (the unused catch parameter e renamed to _, the 133-column comment wrapped) it passes the core lint and still fails on stock main and passes with the fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails with assertions on and off; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
# gradle, scratch worktree of 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (environment lines cut)
TestParentFieldDocValuesUpdate > testUpdateParentFieldOfChildKeepsIndexSorted FAILED
java.lang.AssertionError: expected null, but was:<org.apache.lucene.index.CheckIndex$CheckIndexException: segment has indexSort=<long: "sort"> but docID=0 sorts after docID=1>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:20EFBA983838F9D8]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotNull(Assert.java:756)
at org.junit.Assert.assertNull(Assert.java:738)
at org.junit.Assert.assertNull(Assert.java:748)
at org.apache.lucene.index.TestParentFieldDocValuesUpdate.testUpdateParentFieldOfChildKeepsIndexSorted(TestParentFieldDocValuesUpdate.java:57)
1 test completed, 1 failed
# -Ptests.asserts=false: the same failure and message
# with the fix: the update is rejected with IllegalArgumentException and the test passes (1 test, 0 failures)
# the remove-a-mark case (the hunt's second test, main ea57360912ed, -ea; its unmarked block is the last of its segment): the first forceMerge(1) after the update
java.lang.RuntimeException: java.io.EOFException
org.apache.lucene.util.packed.DirectReader$DirectPackedReader1.get(DirectReader.java:204)
org.apache.lucene.codecs.lucene90.Lucene90DocValuesProducer$5.longValue(Lucene90DocValuesProducer.java:1040)
org.apache.lucene.index.IndexSorter$LongSorter.lambda$getComparableProviders$0(IndexSorter.java:298)
org.apache.lucene.index.IndexSorter$ComparableValues$1.setTopValue(IndexSorter.java:103)
[...] MultiSorter- Lucene enforces the reservation elsewhere: a document that contains the field is rejected as "a reserved field".
- Updates of index-sort fields are rejected, a guard added as a bug fix (LUCENE-7581: "would lead to corruption").
- The mark exists "to maintain the API guarantee that the document order of a block is not altered by the IndexWriter" (
setParentFieldjavadoc).
The full argument258 words
- Reserved, enforced by validation.
IndexingChain.java:616-619throws for a document that names the field;TestIndexSorting.testBlockContainsParentFieldasserts the message. The update API is a second public write path to the same field without that check. - The javadoc states the purpose (
IndexWriterConfig.java:579-589): IndexWriter "will add an internal field to every root document", "In order to maintain the API guarantee that the document order of a block is not altered by the IndexWriter a marker for parent documents is required." - The same class is guarded, as a bug. LUCENE-7581 (2016) made IndexWriter reject doc-values updates of index-sort fields; its CHANGES entry says "since this would lead to corruption". #16565 (2026-08) added the missing guard to
updateBinaryDocValue.TestIndexSorting.testBadDVUpdateasserts all four entry points. - The design discussion treats the field as IndexWriter's (#12829 review): its author called it "a field name that IW fully controls it's type and content" and "a purely internal field", and wrote "you can't use the parent field I think that is reserved for the top level"; a reviewer asked "are there any other restrictions on the use of the field? Could we clear up the javadoc to be more explicit?" and another answered "+1 to tighten up the language". Doc-values updates were not discussed. The #15960 commit message says the reserved mechanism existed "to guard the field name from external use".
- Nothing permits it. No javadoc, MIGRATE entry, CHANGES entry or test covers a doc-values update of the parent field. The update javadoc (
IndexWriter.java:1950-1961) asks only for an existing doc-values-only field, which_parentis.
The devil's-advocate check253 words
The cross-model reviews (OpenAI GPT-5.6, refute and works-as-designed lenses) made the strongest case: the parent field is documented as internal (IndexWriterConfig.java:579-589; MIGRATE.md:879, "The internally used parent field"), IndexWriter owns its values, and the application chose the name itself. Writing it through a generic API is mutation of writer-owned metadata, garbage in, garbage out; a missing check makes this hardening, not a correctness bug. Both rated it "not real", medium, while reproducing it and agreeing with the root cause and the fix. The cross-model fact check of this doc rated this case strong: a maintainer could reasonably close it as out of contract while still accepting the check as hardening.
Why it does not settle it: Lucene answers "callers might write the reserved field" with validation, already on the document path, and it fixed the closest analogue (updates of index-sort fields) as a bug; those fields belong to the application, so the precedent shows how Lucene treats updates that break the sort invariant, not that writer-owned fields are in contract. The update methods document their preconditions and _parent meets them; nothing in their javadoc excludes fields named in the writer config, and the other such field, the soft-deletes field, is updated through these very methods (a designed use), so the javadoc gives callers no hint. And the failure is silent at the call. What survives is the scope: it is missing input validation, with low likelihood, and only an application writing its own parent field triggers it. Verdict (adjudication): confirmed, medium.
Recommended A. Reject the parent field in the four update entry points
Reject the configured parent field in IndexWriter's four doc-values update entry points with IllegalArgumentException, as the document path already does (+16 lines in IndexWriter.java). The repro, 12 related suites (383 tests) and the whole core index test package pass, and the change passes the core lint.
private void ensureNotParentField(String field) {
String parentField = config.getParentField();
if (parentField != null && parentField.equals(field)) {
throw new IllegalArgumentException(
"\"" + field + "\" is a reserved field and its doc values cannot be updated");
}
}
// called first in updateNumericDocValue, updateBinaryDocValue and
// updateSortedNumericDocValue, and for each field in buildDocValuesUpdateAll options and trade-offs4 options ยท 706 words
Option A was run through gradle in a scratch worktree of 645d4fe14a20. B and C are alternatives that were not run. Hardening MultiSorter is not a fix: the add-a-mark case breaks the segment at commit, with no merge.
A private ensureNotParentField(field) helper in IndexWriter, called first in updateNumericDocValue, updateBinaryDocValue and updateSortedNumericDocValue, and for every field in buildDocValuesUpdate (which serves updateDocValues, softUpdateDocument(s) and tryUpdateDocValue). The message mirrors the document path's "is a reserved field".
private void ensureNotParentField(String field) {
String parentField = config.getParentField();
if (parentField != null && parentField.equals(field)) {
throw new IllegalArgumentException(
"\"" + field + "\" is a reserved field and its doc values cannot be updated");
}
}
// called first in updateNumericDocValue, updateBinaryDocValue and
// updateSortedNumericDocValue, and for each field in buildDocValuesUpdateTrade-offs
- Runs before
verifyOrCreateDvOnlyField, so the binary and sorted-numeric methods now say "reserved field" instead of the type-mismatch message, andupdateDocValuescannot create the field with another type on an index without documents (executed). - Changes which error comes first:
updateBinaryDocValue(parentField, null)now gets the reserved-field error instead of today's "cannot update a field to a null value" (executed), and anupdateDocValuesfield named after the parent field is rejected before its doc-values type is checked. - The null-safe comparison keeps today's behaviour for a writer without a parent field and for a null field name.
- Rejects whether or not an index sort is set, like the document path. Without a sort a verifier's control stayed clean (merge-time reorderers such as
BPIndexReorderer, which also read the mark, were not tested), so a maintainer could limit the check to sorted indexes; rejecting always is simpler, keeps the field writer-owned and also protects code that reads the mark without an index sort. - Behaviour change: an application that writes its parent field through these methods now gets an exception; no upstream test or javadoc relies on that call.
- Open PR #16810 edits the same four guard blocks (it merges the index-sort checks into one helper).
- Worth adding: one sentence in the
setParentFieldand update javadocs that the parent field cannot be updated (a reviewer asked for more explicit javadoc in #12829).
What the checks validated
- Gradle in a scratch worktree of
645d4fe14a20(--offline, seed DEADBEEF). Stock: the repro fails with assertions on and off. - With the fix: the repro plus
TestIndexSorting,TestNumericDocValuesUpdates,TestBinaryDocValuesUpdates,TestSortedNumericDocValuesUpdates,TestMixedDocValuesUpdates,TestIndexWriter,TestAddIndexes,TestColumnBatchIndexing,TestIndexOptions,TestSoftDeletesRetentionMergePolicyandTestSoftDeletesDirectoryReaderWrapperpass with assertions on and off (12 suites, 383 tests, 0 failures, 3 skipped). - The whole
org.apache.lucene.indextest package with the fix (215 suites, 1396 tests, 0 failures, 56 skipped), and two parent-field suites in other modules:TestBPIndexReorderer(8 tests) andTestIndexSortBackwardsCompatibility(68 tests). - Lint with the fix:
checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMainandforbiddenApisTestpass once the repro gets two lint-only edits (the unused catch parameter renamed to_, the long comment wrapped); with the edits the repro still fails on stock code and passes with the fix. - Earlier, the hunt's equivalent fix sketch on main
ea57360912edpassed the finder's 2 tests, the first verifier's 5 (two seeds), 4 more probe tests and 242 existing tests.
All four update paths call it (IndexWriter.java:1964, :2010, :2048, :2097) and it already knows parentFieldName (FieldInfos.java:384-390), so one check covers them.
Trade-offs
Puts an IndexWriter policy into a schema-verification method; for the binary and sorted-numeric methods the index-sort message still comes first.
#16810 replaces the four copies of the index-sort check with one ensureNotIndexSortField helper; generalising it to "fields IndexWriter owns" (sort fields of the config and of existing segments, plus the parent field) keeps one guard.
Trade-offs
Ties this fix to an open, unreviewed PR.
It would change only the remove-a-mark case (the merge-time end state shared with LFV-011).
Trade-offs
The add-a-mark case breaks the segment at commit, with no merge involved, so the update must be rejected anyway.
A code read of Elasticsearch, OpenSearch and Solr found no default-configuration path that sends a doc-values update to the parent field, so the exposure is applications that pass their own parent-field name to IndexWriter's doc-values update methods.
While building an Alloy 6.2 model of index sorting with document blocks, the finder read IndexWriter's doc-values update guards and saw that they do not reject the parent field. The Alloy check MergeAfterParentDVUpdate then found a 2-document counterexample, and a JUnit test on the stock jars confirmed it. Semi-formal: the model encoded a hypothesis from code reading.
The method in full150 words
Method: an Alloy 6.2 model plus code reading, then JUnit (the hunt's "semi-formal" tier).
- A hunt unit (index sorting with document blocks) modelled the flush sort and the merge of blocks in Alloy (
SortBlocks.als). While modelling which inputs can change parent marks, the finder read IndexWriter's update guards (IndexWriter.java:1962-2110at the time) and wrote the predicategoodInputsAfterParentDVUpdate: marks may differ from the block structure after a doc-values update. - The check
MergeAfterParentDVUpdate(blocks preserved by the merge) was satisfiable: a 2-document counterexample in which a parent mark no longer matches its block. - The finder's JUnit tests (add a mark; remove a mark) then failed on the stock jars of main, 10.5.2, 9.12.3 and 9.10.0, with assertions on and off, and passed with a fix sketch.
- It counts as semi-formal because the model encoded a hypothesis formed while reading the code and the tool then showed its consequence.
No prior report found (searched 2026-10-26, refreshed 2026-11-01) in GitHub issues, PRs and commits, the mailing lists, the JIRA archive or downstream repositories. Closest: #16809 / #16810, the same update guards for a different field.
Searches4 searches
No issue, PR, commit, list thread, JIRA entry or downstream report describes doc-values updates of the parent field. #16809 (open issue) and its fix PR #16810 (open) cover index-sort fields that are updatable when the writer config has no sort: the same four guard blocks and the same CheckIndex symptom, but a different field, trigger and fix, and #16810 adds no parent-field check. Precedents for treating this class as a bug: #8632 (LUCENE-7581) and #16565.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-26 | a scripted prior-report search run (20 issue/PR and 3 commit queries) and 25 manual searches: setParentField, getParentField, "reserved field", "sorts after docID", the exact rejection message, MultiSorter EOFException, "parent field" with each update method name | related only: #16809 / #16810, #16565, #8632, #12829, #13016, #15960, #13340 / #13341 |
| Open PRs touching the fix files | 2026-10-26 | the file lists of all 412 open PRs; 31 touch the fix files, and the changed lines of the 18 non-test ones were grepped for the parent field; #16810 read in full | no PR adds a parent-field check to the update paths |
| Mailing lists, JIRA archive, downstream repositories, web | 2026-10-26 | 20 list queries (dev@, java-user@, issues@, commits@), 4 JIRA queries, 2 + 5 downstream queries (Elasticsearch, OpenSearch, Solr), 6 web queries | no report; only notification traffic for the related PRs |
| GitHub, refresh | 2026-11-01 | items created since 2026-10-25 for "parent field", setParentField, updateDocValues, "reserved field" | #16947 (merged 2026-10-30, disables sort-based early termination with document blocks, fixes #16887): a different bug; #16809 / #16810 still open |
Introduced with the parent field itself by #12829 (df6bd25ce44, merged 2024-01-11; first released in 9.10.0 through its backport 0aa88910ca9). Every release from 9.10.0 to 10.6.0 and main fails; 9.9.2 has no setParentField.
Versions tested8 rows
#12829 ("Add support for index sorting with document blocks") created the parent field and the document-path "reserved field" check, but added nothing to the doc-values update methods, whose only guard is LUCENE-7581's index-sort-field check (2016). Later changes near the field (#13016, #13128, #15960, #16565, #16581) close nothing. Not a regression; not fixed on any branch as of upstream main 2f424fd428da (2026-10-30).
| Version or commit | Verdict | Result |
|---|---|---|
| 9.9.2 | not affected | setParentField does not exist; the repro does not compile |
branch_9x 0aa88910ca9 (#12829 backport) vs parent 00e2fe6cacb | commit | the backport fails (finder's 2 tests, the first verifier's repro); its parent cannot compile the repro |
| 9.10.0 | affected | the minimal test fails (-ea); the finder's tests fail with -ea and -da |
| 9.11.0 - 9.12.3 | affected | the finder's tests fail on every release; the minimal test fails on 9.12.3 |
| 10.0.0 - 10.5.2 | affected | the finder's tests fail on every release; the minimal test fails on 10.0.0 and 10.5.2 |
| 10.6.0 | affected | the minimal test fails with -ea and -da |
main 645d4fe14a20 | affected | the minimal test fails with -ea and -da (standalone and gradle) |
fix A on 645d4fe14a20 | not affected | the minimal test passes; the update is rejected with IllegalArgumentException |
The finder, independent verifiers, cross-model reviews and an adjudication all reproduced it. The cross-model reviews (OpenAI GPT-5.6) rated it "not real" because the field is internal and called the check hardening; the adjudication confirmed it as missing input validation.
Checks and verdicts420 words
Every check reproduced the failure and agreed with the root cause and the fix; the split is only about whether writing an internal field is in contract.
| Check | Verdict | What it found |
|---|---|---|
| finder (Alloy model, then JUnit) | candidate | Alloy found a 2-document counterexample; its 2 JUnit tests fail on main (-ea, -da), 10.5.2, 9.12.3 and 9.10.0 and pass with its fix sketch. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | not real, medium | Its own test reproduces the CheckIndex failure (-ea, -da, two more seeds) and passes with the fix; it agrees with the root cause and the fix but rates the trigger out of contract. |
| independent reproducer | real | Own test with a different layout: 2 of 5 fail on main (-ea, -da), 10.5.2, 9.12.3 and 9.10.0, 3 controls pass, fix 5/5; corrected the claim (no merge needed; value- and layout-dependent). |
| cross-model review, works-as-designed lens (OpenAI GPT-5.6, executed) | not real, medium | Own test fails 1 of 3 with both controls passing; the fix passes it and 143 existing tests; same contract objection; concedes the update javadocs do not restate the reservation. |
| intended-behaviour devil's advocate | real, medium | Own test fails 1 of 4; controls: a reserved-field document is rejected, a soft-deletes update is accepted by design, no damage without an index sort. |
| adjudication | confirmed, medium | Re-ran the independent reproduction with a fresh seed (2 of 5 fail, 0 of 5 with the fix) and the finder repro (2 of 2 fail); the cross-model review's objection is about labelling, not reality. |
| history and security screen | 9.10.0 first affected; not security-sensitive | 38 runs on 9.9.2, the 19 releases from 9.10.0 to 10.5.2, main and two commits; a security probe on main (-ea, -da) and 10.5.2. |
| minimal repro and this doc | verified; fix validated | The one-test repro fails on 645d4fe14a20 (-ea, -da), 10.6.0 and four older releases; with fix A it and the core index tests pass. |
| fact checks of this doc (one by an independent reviewer from another model family, OpenAI GPT-5.6) | reproduced; corrections applied | Re-ran the repro and fix A with assertions on and off. Its probes narrowed the remove-a-mark case: the merge fails only when the unmarked block is the last of its segment with a sort value on every document, and later writers fail only once the update is committed. A later probe ran the path on an index without documents. |
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 415 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,190 words
- Registry title
- IndexWriter accepts doc-values updates of the reserved parent field; with index sorting and document blocks this leaves a segment that fails CheckIndex, or an index whose merges fail
- Area
- index / IndexWriter doc-values updates
- Priority
- Low, rank 1163: IndexWriter accepts doc-values updates of the reserved parent field, which can damage index-sorted block indexes, but only through out-of-contract misuse, with a cheap reject fix.
- Impact class
- data corruption
- Impact note
- Only applications that use opt-in index sorting with IndexWriterConfig#setParentField and document blocks and that themselves send a doc-values update naming the field they configured as the parent field (documented as internal; the cross-model reviews call this out of contract), through updateNumericDocValue or a numeric field in updateDocValues, softUpdateDocument(s) or tryUpdateDocValue; untrusted documents cannot trigger it, no default-configuration path was found in Elasticsearch, OpenSearch or Solr (code reading), ordinary scale, released 9.10.0-10.6.0 and main.
- Summary
- You use index sorting with a parent field (IndexWriterConfig#setParentField) and document blocks, and call IndexWriter#updateNumericDocValue or #updateDocValues on that parent field, for example to give a child document a parent mark or to set a parent's mark to null. You expect the call to be rejected with IllegalArgumentException, as IndexWriter rejects doc-values updates of index-sort fields and any document that contains the parent field ('is a reserved field'), or else the index to stay valid. Instead the update is accepted: a child given a mark can put the segment out of index-sort order, so CheckIndex fails ('segment has indexSort=<long: "sort"> but docID=1 sorts after docID=2') as soon as the update is committed, and a removed mark makes the next merge throw EOFException in MultiSorter and close the writer when the unmarked block was the last of its segment and every document there has a sort value, and otherwise silently moves the orphaned documents into the next block.
- Introduced
- df6bd25ce44 (#12829 'Add support for index sorting with document blocks', main, first released in 10.0.0); its branch_9x backport 0aa88910ca9 shipped first, in 9.10.0 [read: git log -S setParentField, git tag --contains, git show of both commits; exec: with the backport commit's core, codecs and test-framework sources shadowed over the 9.10.0 jars, both finder tests fail (-ea and -da) and the independent reproduction's repro fails 2 of 5, while at its parent 00e2fe6cacb the repro does not compile (no setParentField)]; not a regression: #12829 created the parent field and a document-path 'reserved field' check but no check in the doc-values update methods, whose only guard is the index-sort-field check from LUCENE-7581 (4efbde4e762, 2016); no later commit opened or closed the gap (2024-01-11 (#12829 merged into main as df6bd25ce44; the branch_9x backport 0aa88910ca9 was committed 2024-01-17, author date 2024-01-11))
- First affected version
- 9.10.0
- Affected versions
- all 19 releases from 9.10.0 through 10.5.2 (9.10.0, 9.11.0, 9.11.1, 9.12.0-9.12.3, 10.0.0, 10.1.0, 10.2.0-10.2.2, 10.3.0-10.3.2, 10.4.0, 10.5.0-10.5.2) and main ea57360912ed (unreleased 11.0.0) [exec: both finder tests fail on each, also with -da on 9.10.0, 9.12.3, 10.0.0, 10.5.2 and main; the independent reproduction's repro fails 2 of 5 on 9.10.0, 9.12.3, 10.0.0, 10.5.2 and main]; 10.6.0 (released 2026-10-29): the one-test minimal repro fails on its release jars (SHA-1 checked against Maven Central, -ea and -da, 2026-11-01) [exec]; 9.9.2 and earlier not affected (no IndexWriterConfig#setParentField; the repro does not compile on 9.9.2) [exec]; only a writer configured with a parent field can reach it; IndexWriter accepts one on an existing index only if the index has that field or no fields at all (#13016, #13341) [read]; not fixed upstream as of main ea57360912ed [read]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: Alloy, code reading
- Upstream and prior reports
- none found (2026-10-26: a scripted prior-report search with 20 GitHub issue/PR, 3 commit, 2 downstream-repo, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 6 web queries; 25 manual GraphQL issue/PR searches on apache/lucene and 5 on downstream repos; file lists of all 412 open PRs; the discussion threads of #12829, #13016, #13341, #15960, #16565 and #16581 read; earlier finder (25 queries) and the intended-behaviour check (16 mailing-list, 2 JIRA) searches); related prior items: #16809 (open: index-sort fields are updatable when the writer config has no sort; same update-guard methods and CheckIndex symptom, different field, trigger and fix) and its open fix PR #16810 (adds no parent-field check, so it would not fix this), #16565 and #8632 (LUCENE-7581) (precedents: the index-sort-field update guard), #12829 (introduced the parent field), #13016 (states why the field is guarded), #15960 (document-path reserved-field check)
- Lucene commit tested
- main
645d4fe14a20(2026-11-01): the minimal test fails through gradle in a scratch worktree with assertions on and off, and standalone on main (-ea,-da) and on the release jars of 9.10.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (10.6.0 also-da); it does not compile on 9.9.2. With the recommended fix it passes, together with 12 related core suites (383 tests, assertions on and off), the wholeorg.apache.lucene.indextest package, two parent-field suites in other modules and the core lint. Upstream main2f424fd428da(2026-10-30) has no change to the affected files since645d4fe14a20(read). Earlier, the hunt's and verifiers' tests ran on mainea57360912edand on all 19 releases from 9.10.0 to 10.5.2.
- Finding record
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1950-1962 (updateNumericDocValue javadoc: an existing, doc-values-only field), :1965, :2003, :2041 and :2098 (the only guard: config.getIndexSortFields().contains), :2081-2127 (buildDocValuesUpdate)lucene/core/src/java/org/apache/lucene/index/FieldInfos.java:391-398 (the soft-deletes field may not be the parent field), :562-647 (verifyOrCreateDvOnlyField accepts the doc-values-only NUMERIC parent field)lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:614-618 (a document that contains the parent field: 'is a reserved field and should not be added to any document')lucene/core/src/java/org/apache/lucene/index/MultiSorter.java:57-63 (block boundaries from the parent field's current values) and :166 (parents.nextSetBit(docID))lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:579-589 (setParentField: 'an internal field', 'the API guarantee that the document order of a block is not altered by the IndexWriter')lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1219-1301 (testSort; the 'segment has indexSort=... but docID=... sorts after docID=...' error at :1275)lucene/MIGRATE.md:875-881 ('The internally used parent field')
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:166-169, 604-609, 614-619, 645-651, 748-750a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1664-1675, 1721-1724, 1938-1948, 1950-1979, 1996-2017, 2037-2057, 2070-2079, 2081-2127a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/FieldInfos.java:562-647; CheckIndex.java:1219-1301; MultiSorter.java:52-63, 166a Lucene main checkout (645d4fe14a20) lucene/MIGRATE.md:875-881
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/MIGRATE.md:875-881
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:616-619; IndexWriterConfig.java:579-589; IndexWriter.java:1950-1971a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/index/TestIndexSorting.java:1987-2029 (testBadDVUpdate), 3243-3285 (testBlockContainsParentField); lucene/MIGRATE.md:875-881GitHub apache/lucene#12829 review comments r1403178782, r1423606084, r1424197106, r1439728693 (read 2026-11-01); commit 34c9495241d message (#15960)
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/FieldInfos.java:384-398, 562-647; IndexWriter.java:1964, 2010, 2048, 2097
- TestParentFieldDocValuesUpdate.java:
repro/LFV-095/src/org/apache/lucene/index/TestParentFieldDocValuesUpdate.java(61 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.
assets/LFV-095/bug.png(415 KB)
![Diagram of the bug: an index sorted on the long field sort with parent field _parent. One block, a child with sort 9 and its parent with sort 1; IndexWriter marks only the parent, so the block sorts by 1. A document that sets _parent is rejected as a reserved field, but updateNumericDocValue(id:child, _parent, -1) is accepted. After the commit the child carries a mark too, the segment reads as two blocks, [child] sort 9 then [parent] sort 1, and CheckIndex reports: segment has indexSort=<long: "sort"> but docID=0 sorts after docID=1. With the fix the update is rejected with IllegalArgumentException.](../assets/LFV-095/bug.png)