For a field without frequencies, RAMFieldsConsumer.write sets freq = -1 and calls startDoc(docID, freq); startDoc creates new RAMDoc(docID, freq), whose constructor runs positions = new int[freq]. The same array doubles as the frequency: RAMDocsEnum.freq() returns positions.length. The -1 is the documented "frequencies omitted" marker of Lucene's push-style writers; RAMOnly's storage was never taught it.
- A postings format decides how a segment stores, for each term, the documents that contain it, plus frequencies and positions when the field indexes them. The test framework ships a few formats for tests;
RAMOnlykeeps everything in Java objects in memory. - A field's
IndexOptionssay what is stored.StringFieldandKeywordFielduseDOCS: documents only, no frequencies, no positions. Readers then report freq 1 for every document. - While writing, Lucene's push-style postings writers mark "frequencies omitted" by passing freq = -1 for each document.
StringField document goes through RAMOnly's writer: the field has no frequencies, so freq = -1, and the per-document record tries to allocate new int[-1]. Select the still to open it at full size. Generated with AI, checked against the code.- In a test, set the codec to
TestUtil.alwaysPostingsFormat(PostingsFormat.forName("RAMOnly")), add one document withnew StringField("id", "1", Field.Store.NO)and callIndexWriter.flush(). - You expect the flush to succeed, as with every other postings format, and the term
1to read back with docFreq 1 and freq 1 (a field without frequencies reports freq 1). - Instead
flush()throwsNegativeArraySizeException: -1and the writer closes itself, with assertions on or off, on main and on every release since 4.0.0-BETA (132 releases run). - Why: for a field without frequencies, RAMOnly's writer passes the marker freq = -1 to its own per-document record, which allocates its positions array as
new int[freq], that isnew int[-1].
Only tests that choose the RAMOnly test codec: Lucene's suite run with -Dtests.postingsformat=RAMOnly, or downstream tests that name it. The randomized test codec never picks it, and no Lucene test writes a field with it. Once chosen, any StringField or KeywordField crashes the first flush. It fails loudly; no production codec or user data is involved.
Mechanism, terms and numbers401 words
RAMOnlyPostingsFormat, the in-memory postings format of lucene-test-framework (SPI name RAMOnly), cannot write a field indexed with IndexOptions.DOCS, the index option of StringField and KeywordField. Its writer passes the "no frequencies" marker freq = -1 to its own startDoc, the same convention as Lucene's push-style postings writers, but its per-document record RAMDoc sizes its positions array with new int[freq]. So the first flush that contains such a field throws NegativeArraySizeException: -1, an aborting exception that closes the IndexWriter. One document with one StringField is enough, with assertions on or off. The array also serves as the format's frequency (freq() returns its length); the -1 marker arrived in 2012 (LUCENE-4248), after the array was written, and RAMDoc was never adapted to it (LUCENE-5268 later rewrote the writer and kept the -1). Only tests that pick this format are hit: Lucene's randomized codec never picks it, and no Lucene test actually writes a field with it.
The code (main 645d4fe14a20, lucene/test-framework/.../tests/codecs/ramonly/RAMOnlyPostingsFormat.java)
// :227-229, :270-278 RAMFieldsConsumer.write
boolean writeFreqs = indexOptions.subsumes(IndexOptions.DOCS_AND_FREQS); // false for DOCS
...
int freq;
if (writeFreqs) {
freq = postingsEnum.freq();
totalTermFreq += freq;
} else {
freq = -1; // "no frequencies" marker
}
postingsWriter.startDoc(docID, freq);
// :350-351 RAMPostingsWriterImpl.startDoc
current = new RAMDoc(docID, freq);
// :168-170 RAMDoc
positions = new int[freq]; // new int[-1] -> NegativeArraySizeException: -1
// :481-484 RAMDocsEnum.freq: the array doubles as the frequency
return current.positions.length;- Where the -1 comes from:
write()'s per-document loop (RAMOnlyPostingsFormat.java:262-297) matches, statement for statement, the one in the corePushPostingsWriterBase(PushPostingsWriterBase.java:130-164), includingfreq = -1when frequencies are omitted (PushPostingsWriterBase.java:142); that class'sstartDoccontract reads "freq will be -1 when term frequencies are omitted for the field" (:177-181). RAMOnly'sRAMPostingsWriterImpldoes not extend that class but follows the same convention; itsRAMDocdoes not. - Why the writer closes: the exception is thrown inside the DWPT flush, which calls
onAbortingException(t)(DocumentsWriterPerThread.java:585-587), so theIndexWriterhits a tragic event. This doc's probe seesisOpen() == falseafter the failed flush; the study verifier also checked thatgetTragicException()is the same exception. - Same root, sibling symptom (context, not part of the claim): a
DOCS_AND_FREQSfield allocatesnew int[freq]but never adds positions, so with-eafinishDoc'sassert posUpto == current.positions.length(:370-372) fails the flush; with-dait works. The recommended fix covers both.
Exposure and likelihood129 words
- Routes:
-Dtests.postingsformat=RAMOnlymakes the test framework usePostingsFormat.forName("RAMOnly")for every field (SetupAndRestoreStaticEnv.java:147-158), or a test sets it itself (for example withTestUtil.alwaysPostingsFormat). The package is exported and the format registered as aPostingsFormatprovider (module-info.java:45,:75;META-INF/services/org.apache.lucene.codecs.PostingsFormat:17), so downstream tests can use it. - Not default:
RandomCodecnever picks it (RandomCodec.java:236-259). Its only user in Lucene's sources,TestExternalCodecs, routes every field exceptfield1,field2andidto it, but its documents contain only those three fields (TestExternalCodecs.java:44-51,:72-83). NoBasePostingsFormatTestCasesubclass is for RAMOnly. - Scale: one document with one
StringField; deterministic; the first flush fails. - Effect: a
NegativeArraySizeExceptionand a closed writer in a test. No index damage, no wrong results, no production code path.
One JUnit test, public API only: the RAMOnly format for every field, one document with one StringField, then flush(). Compound files off and a plain ByteBuffersDirectory keep it to this defect. Deterministic, single thread, the same failure 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.tests.codecs.ramonly;
import org.apache.lucene.codecs.PostingsFormat;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
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 TestRAMOnlyPostingsFormat extends LuceneTestCase {
public void testDocsOnlyField() throws Exception {
IndexWriterConfig iwc =
new IndexWriterConfig()
.setCodec(TestUtil.alwaysPostingsFormat(PostingsFormat.forName("RAMOnly")))
.setUseCompoundFile(false);
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, iwc)) {
Document doc = new Document();
doc.add(new StringField("id", "1", Field.Store.NO));
w.addDocument(doc);
try {
w.flush();
} catch (NegativeArraySizeException e) {
// expected: a DOCS-only field flushes; actual: RAMDoc allocates new int[freq] with freq=-1
throw new AssertionError("expected flush of a DOCS-only field to succeed, got " + e, e);
}
}
}
}
mkdir -p $LUCENE/lucene/test-framework/src/test/org/apache/lucene/tests/codecs/ramonly
cp $REPRO/TestRAMOnlyPostingsFormat.java $LUCENE/lucene/test-framework/src/test/org/apache/lucene/tests/codecs/ramonly/
cd $LUCENE
./gradlew :lucene:test-framework:test --tests org.apache.lucene.tests.codecs.ramonly.TestRAMOnlyPostingsFormat -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
- Expected
flush()of the oneStringFielddocument succeeds, as with every other postings format.- Observed
AssertionError: expected flush of a DOCS-only field to succeed, got java.lang.NegativeArraySizeException: -1(fromRAMDoc.<init>), with assertions on and off.
More files, environment, full output288 words
- Smallest input: one document, one DOCS-only term. The failure is inside the postings write, before a compound file is built or a reader is opened;
setUseCompoundFile(false)and a plainByteBuffersDirectorykeep the fixed test from tripping the class's separate defects (compound-file header check, CheckIndex on close). - Assertions: pass
-Ptests.assertsexplicitly. Lucene's build enables assertions for about 75% of seeds, and with seedDEADBEEFits default is off (TestsAndRandomizationPlugin.java:240-249); a run without the flag printed-Ptests.asserts=falsein its reproduce line. - Releases: the same test fails on the 10.5.2 release jars with assertions on and off; it compiles from 9.1.0 on, and fails on all 34 targets 9.1.0 to 10.6.0 and main. A portable probe with the same input fails on every release from 4.0.0-BETA.
- As the regression test: this same method, in a new
lucene/test-framework/src/test/org/apache/lucene/tests/codecs/ramonly/(next to the existingcodecs/asserting/tests). With the fix,:lucene:test-framework:check -x testpasses with the test as is.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also 10.5.2 (release jars); 4.0.0-BETA to 10.6.0 by a portable probe |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
mkdir -p $LUCENE/lucene/test-framework/src/test/org/apache/lucene/tests/codecs/ramonly
cp $REPRO/TestRAMOnlyPostingsFormat.java $LUCENE/lucene/test-framework/src/test/org/apache/lucene/tests/codecs/ramonly/
cd $LUCENE
./gradlew :lucene:test-framework:test --tests org.apache.lucene.tests.codecs.ramonly.TestRAMOnlyPostingsFormat -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true
TestRAMOnlyPostingsFormat > testDocsOnlyField FAILED
java.lang.AssertionError: expected flush of a DOCS-only field to succeed, got java.lang.NegativeArraySizeException: -1
at __randomizedtesting.SeedInfo.seed([DEADBEEF:6082079233177FCB]:0)
at org.apache.lucene.tests.codecs.ramonly.TestRAMOnlyPostingsFormat.testDocsOnlyField(TestRAMOnlyPostingsFormat.java:46)
Caused by:
java.lang.NegativeArraySizeException: -1
at org.apache.lucene.tests.codecs.ramonly.RAMOnlyPostingsFormat$RAMDoc.<init>(RAMOnlyPostingsFormat.java:170)
at org.apache.lucene.tests.codecs.ramonly.RAMOnlyPostingsFormat$RAMPostingsWriterImpl.startDoc(RAMOnlyPostingsFormat.java:351)
at org.apache.lucene.tests.codecs.ramonly.RAMOnlyPostingsFormat$RAMFieldsConsumer.write(RAMOnlyPostingsFormat.java:278)
at org.apache.lucene.codecs.perfield.PerFieldPostingsFormat$FieldsWriter.write(PerFieldPostingsFormat.java:162)
at org.apache.lucene.index.FreqProxTermsWriter.flush(FreqProxTermsWriter.java:133)
...
at org.apache.lucene.index.IndexWriter.flush(IndexWriter.java:4357)
at org.apache.lucene.tests.codecs.ramonly.TestRAMOnlyPostingsFormat.testDocsOnlyField(TestRAMOnlyPostingsFormat.java:43)
1 test completed, 1 failed
BUILD FAILED
# -Ptests.asserts=false: the same failure and stack
# 10.5.2 release jars, assertions on and off: the same message, RAMDoc.<init>(RAMOnlyPostingsFormat.java:169)
# with the recommended fix: BUILD SUCCESSFUL, 1 test, 0 failures (assertions on and off)- Nothing documents a limit on index options; the one real limit, offsets, gets a clear
UnsupportedOperationException. - A DOCS field must report freq 1, and CheckIndex enforces it; the format's own writer makes the -1 its own storage cannot take.
- LUCENE-4248 (2012) changed the "no frequencies" marker from 0 to -1 and did not update RAMOnly.
The full argument209 words
- No documented limitation: the class javadoc says "Stores all postings data in RAM, but writes a small token (header + single int) ... NOTE: this codec sorts terms by reverse-unicode-order!" (
RAMOnlyPostingsFormat.java:56-61); the package javadoc says "Codec for testing that never writes to disk." (package-info.java:18). - The designed limitation is explicit:
throw new UnsupportedOperationException("this codec cannot index offsets")(:211-215). DOCS gets no such check; it fails on an array allocation. - The writer handles DOCS on purpose: it computes
writeFreqsand branches on it (:228,:234-235,:271-276); the crash is in the storage class it hands the marker to. - The read contract:
PostingsEnum#freq"Returns term frequency in the current document, or 1 if the field was indexed with IndexOptions#DOCS" (PostingsEnum.java:74-82). CheckIndex enforces it ("When a field didn't index freq, it must consistently 'lie' and pretend that freq was 1",CheckIndex.java:1765-1777) and requirestotalTermFreq == docFreqfor such fields (:1695-1705,:1935-1937).FieldsConsumer#writeputs no restriction on index options (FieldsConsumer.java:47-64). - History: LUCENE-4248 (2012) made the indexing chain pass -1 instead of 0 for omitted frequencies and documented it; it did not touch RAMOnly. No commit message, comment, CHANGES or MIGRATE entry mentions a RAMOnly index-options limitation.
The devil's-advocate check176 words
The strongest case for "intended": RAMOnly is a test-only codec (behind the @lucene.experimental PostingsFormat API) that nothing in Lucene's own suite exercises; the only in-tree route is an unusual system property; and the study verifier found other, separate breakages in the class (with compound files on, the default, any field fails because its 17-byte .id file has no index header or footer; positions without payloads are stored as -1; RAMDocsEnum.docID() throws before nextDoc()). So one could call it dead code that simply does not support DOCS. The cross-model refute review (OpenAI GPT-5.6) weighed exactly this status and still judged the bug real with high confidence.
Why it fails: the class is shipped, exported and SPI-registered in lucene-test-framework, so downstream tests can use it; nothing documents a DOCS restriction while the one real restriction, offsets, is enforced explicitly; and the crash comes from the format's own writer feeding its own storage the documented push-API marker. Low use lowers the severity, not the correctness. Fixing or removing the class is upstream's choice; either resolves the bug.
Recommended A. Store freq separately and size positions only when positions are indexed
Give RAMDoc its own freq, size its positions array only when positions are indexed, and write freq 1 (counted into totalTermFreq) for fields without frequencies; freq() returns the stored value. Validated: the test fails before and passes after, assertions on and off; the 643 test-framework tests (33 skipped, 0 failures) and TestExternalCodecs pass.
// RAMDoc
final int freq;
public RAMDoc(int docID, int freq, boolean hasPositions) {
this.docID = docID;
this.freq = freq;
positions = new int[hasPositions ? freq : 0];
}
// RAMFieldsConsumer.write, field without frequencies
freq = 1;
totalTermFreq += freq;
postingsWriter.startDoc(docID, freq, writePositions);
// RAMDocsEnum.freq()
return current.freq;All options and trade-offs3 options ยท 637 words
One fix was built and validated with gradle; the alternatives were not run.
A. Store freq separately and size positions only when positions are indexed (validated by the checks) (recommended)
+10/-7 lines in RAMOnlyPostingsFormat.java, no core change. The full diff (it applies to 645d4fe14a20):
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/RAMOnlyPostingsFormat.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/RAMOnlyPostingsFormat.java
@@ -162,12 +162,14 @@
static class RAMDoc implements Accountable {
final int docID;
+ final int freq;
final int[] positions;
byte[][] payloads;
- public RAMDoc(int docID, int freq) {
+ public RAMDoc(int docID, int freq, boolean hasPositions) {
this.docID = docID;
- positions = new int[freq];
+ this.freq = freq;
+ positions = new int[hasPositions ? freq : 0];
}
@Override
@@ -272,10 +274,11 @@
freq = postingsEnum.freq();
totalTermFreq += freq;
} else {
- freq = -1;
+ freq = 1;
+ totalTermFreq += freq;
}
- postingsWriter.startDoc(docID, freq);
+ postingsWriter.startDoc(docID, freq, writePositions);
if (writePositions) {
for (int i = 0; i < freq; i++) {
int pos = postingsEnum.nextPosition();
@@ -347,8 +350,8 @@
this.term = term;
}
- public void startDoc(int docID, int freq) {
- current = new RAMDoc(docID, freq);
+ public void startDoc(int docID, int freq, boolean hasPositions) {
+ current = new RAMDoc(docID, freq, hasPositions);
term.docs.add(current);
posUpto = 0;
}
@@ -480,7 +483,7 @@
@Override
public int freq() throws IOException {
- return current.positions.length;
+ return current.freq;
}
@Override// RAMDoc
final int freq;
public RAMDoc(int docID, int freq, boolean hasPositions) {
this.docID = docID;
this.freq = freq;
positions = new int[hasPositions ? freq : 0];
}
// RAMFieldsConsumer.write, field without frequencies
freq = 1;
totalTermFreq += freq;
postingsWriter.startDoc(docID, freq, writePositions);
// RAMDocsEnum.freq()
return current.freq;Trade-offs
- Separates the array's two jobs (frequency and positions storage), returns 1 for DOCS as
PostingsEnum#freqand CheckIndex require (CheckIndex.java:1765-1777), and keepstotalTermFreq == docFreqfor DOCS terms (:1695-1705,:1935-1937;TermsEnum#totalTermFreqis "the sum of the freq() for each doc"). - Also fixes the sibling
DOCS_AND_FREQSassertion infinishDoc. RAMDocgains one int per posting (test-only memory).- The class keeps its other, separate defects (compound files, positions without payloads,
docID()beforenextDoc()); this fix does not address them.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF: the minimal test fails before the change and passes after it, with assertions on and off. :lucene:test-framework:test(all of it, with the new test): 643 tests (Gradle's count), 33 skipped, 0 failures;:lucene:core:test --tests org.apache.lucene.TestExternalCodecs: 1 test, 0 failures (assertions on).:lucene:test-framework:check -x test(checkGoogleJavaFormat, ecjLint, forbiddenApis, checkLicenses): passes.- A probe with the fix reads back
docFreq=1 totalTermFreq=1 freq=1for aStringField, and freqs 2 and 1 for aDOCS_AND_FREQSfield "a a b" (this doc's run with assertions on; the independent fact check re-ran it with them on and off: same values); without it both flushes fail with-ea, and the DOCS one also with-da. The one-method test itself checks only that the flush no longer throws; the read-back values come from this probe. - Earlier and independently: a standalone fix check (pass with
-eaand-da), and the cross-model reviewer's own two tests andTestExternalCodecswith the same file.
positions = new int[Math.max(freq, 0)]; in RAMDoc stops the exception with one line. Not run.
Trade-offs
freq() then returns 0 for DOCS documents (CheckIndex requires 1), DOCS terms keep totalTermFreq = 0 (CheckIndex requires docFreq), and DOCS_AND_FREQS still trips the assertion. Not recommended.
Throw UnsupportedOperationException for fields without frequencies, as for offsets; or delete RAMOnlyPostingsFormat with its SPI and module entries. Not run.
Trade-offs
Rejecting gives a clear message but makes the most basic index option unsupported. Removal fits a class nothing in Lucene's suite exercises, but it is an API removal in the test framework and a maintainers' call.
Downstream test suites built on Lucene's test framework can choose RAMOnly by name the same way (a Solr test was run with it in 2015, SOLR-7631, and failed differently), and their DOCS-only fields would then crash at the first flush.
Non-formal. In the October 2026 study, a differential probe ran the same queries against all 15 registered postings formats; every format agreed except RAMOnly, which crashed at flush on DOCS-only fields. An independent verifier reproduced it with its own tests, and later checks cut it to one test.
The method in full121 words
- First signal: the study's fuzzing and property-based run (2026-10-22, main
7cecf79523) compared Lucene against independent oracles, including a second implementation inside Lucene: 15 postings formats. A probe that indexes the same documents and runs the same queries through every registered format found that all agreed except RAMOnly, whose flush threwNegativeArraySizeExceptionon aStringField. - Verification: the study verifier wrote its own 9-test class, a plain
main()run with-eaand-da, a run through-Dtests.postingsformat=RAMOnly, and an experimental patched copy that confirmed the root cause; it reproduced the crash on main and on the 10.5.2 jars. - Minimal repro (2026-10-29): one test method, one
StringField, public API only; fails on main645d4fe14a20with assertions on and off.
No prior report found (2026-11-01): GitHub issues and PRs incl. the JIRA archive, commits, five mailing lists, Solr and Lucene JIRA, downstream trackers, the web and all 410 open PRs' changed files. Nearest: SOLR-7631 (2015), a different RAMOnly failure at query time.
Searches5 searches
The identifiers are narrow and easy to search (RAMOnlyPostingsFormat, RAMDoc, NegativeArraySizeException: -1). Closest items, none this bug: SOLR-7631 (2015 comment: a Solr test run with -Dtests.postingsformat=RAMOnly failed with an NPE in RAMTermsEnum.docFreq at query time; different exception, method and root cause); open PR #15266 (adds a no-freqs check to BasePostingsFormatTestCase; does not touch RAMOnly); open PR #14716 (the only open PR touching the file; removes impacts()); LUCENE-5268 (#6332, wrote the current write()); the Lucene.NET port keeps new int[freq] (by reading; no issue there).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene incl. the migrated JIRA archive) | 2026-11-01 | a scripted prior-report search: 32 queries; 24 manual searches (e.g. "NegativeArraySizeException: -1", "new int[freq]", RAMTermsEnum, RAMOnlyPostingsFormat across GitHub, apache/lucenenet, apache/lucene-solr, apache/solr) | no report; #15266 and #14716 related only |
| Commits, JIRA (LUCENE and SOLR) | 2026-11-01 | 5 commit queries; 6 JIRA REST queries plus manual LUCENE and SOLR searches (RAMOnly*, RAMTermsEnum, tests.postingsformat AND RAMOnly, NegativeArraySizeException with startDoc or RAMDoc) | SOLR-7631: a different RAMOnly failure |
| Mailing lists (dev@, java-user@, issues@, commits@, builds@) | 2026-11-01 | 40 queries | only build logs listing the package; tests.postingsformat=RAMOnly has 0 hits |
| Downstream and web | 2026-11-01 | 4 queries over Elasticsearch, OpenSearch and Solr; manual searches in their repos; 7 web queries | no report |
| Open PRs | 2026-11-01 | the changed files of all 410 open apache/lucene PRs | only #14716 touches the file; not a fix |
Introduced in 4.0.0-BETA by LUCENE-4248 (2012), which made Lucene pass freq -1 instead of 0 for fields without frequencies; RAMOnly kept new int[freq]. Every release from 4.0.0-BETA to 10.6.0 (132) and main fails; 4.0.0-ALPHA passes; the commit is confirmed by revert and apply runs.
Versions tested7 rows
Introduced by LUCENE-4248 "Producers to the Codec API don't always follow the spec" (Robert Muir, 2012-07-23; migrated to apache/lucene#5318), commit 42bd0e24f87 on branch_4x and its trunk twin e0d137f8e2ad (same patch). Their message says "LUCENE-4828", very likely a typo: LUCENE-4828 is an unrelated 2013 issue, and Robert Muir's LUCENE-4248 comment 11 minutes before the trunk commit describes this patch. The commit made the indexing chain and PostingsConsumer.merge pass -1 instead of 0 for DOCS_ONLY fields and documented "freq will be -1 when term frequencies are omitted for the field"; RAMOnly was not changed.
Current form: LUCENE-5268 (78d52bef1f0f, 2013; first released in 6.0.0, backport 2be4061beea in 5.0.0) rewrote RAMOnly's writer and put freq = -1 into RAMOnly's own write(), the code on main today. The array line is older (blame: fa6500fa6caa, LUCENE-3490, 2011).
Not fixed upstream (a snapshot): no commit touches the file between 645d4fe14a20 and upstream main f815e679bf6c (2026-11-01).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails with assertions on and off (gradle and standalone) |
| 9.1.0 to 10.6.0 (33 releases) | affected | the unchanged minimal test fails, assertions on and off (10.5.2 also in an earlier re-verification) |
| 4.0.0-BETA to 9.0.0 | affected | the portable probe (same input, a commit as the flush) fails with NegativeArraySizeException: -1, assertions on and off |
| 4.0.0-ALPHA | not affected | passes, assertions on and off: the last unaffected release and the first with RAMOnly |
4.0.0-BETA with 42bd0e24f87's two indexing files (FreqProxTermsWriterPerField, PostingsConsumer) swapped back to the parent's versions; 4.0.0 with only the commit's four 0 -> -1 code hunks reverted in those files | bisect | both pass |
4.0.0-ALPHA with 42bd0e24f87's two files (and, as control, with its parent's) | bisect | fails with the commit's files; passes with the parent's |
| main with the recommended fix | not affected | passes, assertions on and off |
The study's verifier, re-runs on three later mains, the minimal repro, a re-verification on main and 10.5.2, a cross-model refute review (real, high confidence) and this doc's gradle runs all reproduce it; the fix passed every time it was tried.
Checks and verdicts206 words
All checks agree on the root cause; none found a documented limitation.
| Check | Verdict | What it found |
|---|---|---|
| study verifier | real | Own 9-test class, a plain main() with -ea and -da, the -Dtests.postingsformat=RAMOnly route and a patched copy; reproduced on main and 10.5.2; checked the tragic close; found the sibling DOCS_AND_FREQS assertion and three separate defects. |
| re-runs on later mains | still reproduces | A re-run on b83101cdd2 (2026-10-23) and the main refresh on a9779ef3c193 (2026-10-26). |
| minimal repro | fails on main, fix passes | One test method on main 645d4fe14a20, assertions on and off; 5 of 5 random seeds fail; the fix passes. |
| re-verification | real | Main 645d4fe14a20 and the 10.5.2 release jars, -ea and -da; fix check passes. |
| cross-model refute review | real, high confidence | OpenAI GPT-5.6, executed: its own test (also checking isOpen() false and the tragic exception) fails on main with -ea and -da, a positions-field control passes, and the fix passes its tests and TestExternalCodecs. |
| this doc's gradle runs | fails before, passes after the fix | Scratch worktree of 645d4fe14a20: the test before and after the fix with assertions on and off; 643 test-framework tests, TestExternalCodecs and the module's static checks with the fix; a freqs probe. |
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 notes522 words
- Registry title
- Test-framework RAMOnlyPostingsFormat cannot index DOCS-only fields
- Area
- test-framework / postings formats
- Priority
- Low, rank 1224: The test-framework RAMOnlyPostingsFormat crashes on DOCS-only fields, which affects only tests that explicitly select it.
- Impact class
- minor
- Impact note
- Only tests that pick the test framework's RAMOnlyPostingsFormat for a DOCS-only field; released in every version from 4.0.0-BETA to 10.6.0 (executed).
- Summary
- In a test, you index a field with IndexOptions.DOCS using the test framework's RAMOnlyPostingsFormat. You expect indexing to work as with other postings formats. Instead it throws NegativeArraySizeException and the writer closes tragically.
- Introduced
- 42bd0e24f87 on branch_4x and its trunk twin e0d137f8e2ad (same patch), LUCENE-4248 'Producers to the Codec API don't always follow the spec' (Robert Muir, 2012-07-23; the commit message says LUCENE-4828, very likely a typo): the indexing chain and PostingsConsumer.merge pass freq -1 instead of 0 for DOCS_ONLY fields, while RAMOnlyPostingsFormat's RAMDoc kept new int[freq] [exec: reverting only the commit's 0 -> -1 hunks makes 4.0.0-BETA and 4.0.0 pass; 4.0.0-ALPHA with the commit's two files fails, with its parent's files passes] (2012-07-23 (first released in 4.0.0-BETA))
- First affected version
- 4.0.0-BETA [exec] (first final release 4.0.0); 4.0.0-ALPHA passes [exec]
- Affected versions
- every release from 4.0.0-BETA to 10.6.0 (132 releases) and main [exec: a version-portable probe with -ea and -da on every release; the unchanged minimal test on 9.1.0 to 10.6.0 and main]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that agreed; not yet triaged by a person.
- Found by
- non-formal baseline: fuzzing/PBT
- Upstream and prior reports
- none found (study searches 2026-10-22; searched again 2026-11-01)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test through gradle with assertions on and off, before and after the recommended fix, in a scratch worktree, plus all:lucene:test-frameworktests,TestExternalCodecs, the module's static checks and a freqs probe with the fix (2026-11-01); the minimal test standalone on main and on the 10.5.2 release jars, assertions on and off (2026-11-01); a version-portable probe on 4.0.0-ALPHA through 10.6.0 and commit-level revert and apply runs on 4.0.0-ALPHA, 4.0.0-BETA and 4.0.0 (the version-history runs, 347 runs).
- The finding in 30 seconds
lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/RAMOnlyPostingsFormat.java:163-171, :227-229, :262-297, :350-354, :370-372, :481-484 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/codecs/PushPostingsWriterBase.java:130-164, :177-181 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:585-587 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/IndexOptions.java:31-36; lucene/core/src/java/org/apache/lucene/document/StringField.java:37, :42; KeywordField.java:56 (at 645d4fe14a20)
- Who is affected
lucene/test-framework/src/java/org/apache/lucene/tests/util/SetupAndRestoreStaticEnv.java:147-158 (at 645d4fe14a20)lucene/test-framework/src/java/org/apache/lucene/tests/index/RandomCodec.java:236-259 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/TestExternalCodecs.java:39-52, :72-83 (at 645d4fe14a20)lucene/test-framework/src/java/module-info.java:45, :75; lucene/test-framework/src/resources/META-INF/services/org.apache.lucene.codecs.PostingsFormat:17 (at 645d4fe14a20)
- Minimal repro
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-249 (at 645d4fe14a20)
- Why it looks unintended
lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/RAMOnlyPostingsFormat.java:56-61, :211-215, :228, :234-235, :271-276 (at 645d4fe14a20)lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/package-info.java:18 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/PostingsEnum.java:74-82 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1695-1705, :1765-1777, :1935-1937 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/codecs/FieldsConsumer.java:47-64 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1695-1705, :1765-1777, :1935-1937; TermsEnum.java:133-138 (at 645d4fe14a20)
- Diagrams
lucene/test-framework/src/java/org/apache/lucene/tests/codecs/ramonly/RAMOnlyPostingsFormat.java:168-170, :270-278, :350-351 (at 645d4fe14a20)
- TestRAMOnlyPostingsFormat.java:
repro/LFV-033/src/org/apache/lucene/tests/codecs/ramonly/TestRAMOnlyPostingsFormat.java(50 lines)
assets/LFV-033/bug.png(451 KB)
![The minimal repro's path: a document with StringField id=1 (IndexOptions.DOCS) is flushed through RAMOnlyPostingsFormat. RAMFieldsConsumer.write sees no frequencies and sets freq = -1, startDoc creates RAMDoc(docID, -1), and RAMDoc runs positions = new int[-1], which throws NegativeArraySizeException: -1; the IndexWriter closes. With the fix, freq is 1, the positions array is empty, and the term reads back with docFreq 1 and freq 1.](../assets/LFV-033/bug.png)