All findings · Lucene finding · LFV-033 · ramonly-postings-docs-only-fields

The test framework's RAMOnlyPostingsFormat cannot flush a DOCS-only field: NegativeArraySizeException: -1

A test that uses the test framework's RAMOnly postings format crashes on its first flush as soon as a document has a StringField: NegativeArraySizeException: -1, and the IndexWriter closes. The format stores "no frequencies" as freq = -1 and then allocates an array of that length.

Summary

Priority
Low, rank 1224The test-framework RAMOnlyPostingsFormat crashes on DOCS-only fields, which affects only tests that explicitly select it.
Impact
minor
Since
4.0.0-BETA (LUCENE-4248, 2012)introduced 2012-07-23 · over 14 years
Affected
4.0.0-BETA to 10.6.0 (132 releases) and main
Status
documented
Found by
non-formal baseline: a differential probe across all registered postings formats

AI-assisted page: this finding was first signalled by a testing baseline (randomized, fuzz, property-based or stress testing, or static analysis) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background: postings formats and DOCS-only fields

  1. 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; RAMOnly keeps everything in Java objects in memory.
  2. A field's IndexOptions say what is stored. StringField and KeywordField use DOCS: documents only, no frequencies, no positions. Readers then report freq 1 for every document.
  3. While writing, Lucene's push-style postings writers mark "frequencies omitted" by passing freq = -1 for each document.
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.
One 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.

The finding, step by step

  1. In a test, set the codec to TestUtil.alwaysPostingsFormat(PostingsFormat.forName("RAMOnly")), add one document with new StringField("id", "1", Field.Store.NO) and call IndexWriter.flush().
  2. You expect the flush to succeed, as with every other postings format, and the term 1 to read back with docFreq 1 and freq 1 (a field without frequencies reports freq 1).
  3. Instead flush() throws NegativeArraySizeException: -1 and the writer closes itself, with assertions on or off, on main and on every release since 4.0.0-BETA (132 releases run).
  4. 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 is new int[-1].

Who is affected

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 core PushPostingsWriterBase (PushPostingsWriterBase.java:130-164), including freq = -1 when frequencies are omitted (PushPostingsWriterBase.java:142); that class's startDoc contract reads "freq will be -1 when term frequencies are omitted for the field" (:177-181). RAMOnly's RAMPostingsWriterImpl does not extend that class but follows the same convention; its RAMDoc does not.
  • Why the writer closes: the exception is thrown inside the DWPT flush, which calls onAbortingException(t) (DocumentsWriterPerThread.java:585-587), so the IndexWriter hits a tragic event. This doc's probe sees isOpen() == false after the failed flush; the study verifier also checked that getTragicException() is the same exception.
  • Same root, sibling symptom (context, not part of the claim): a DOCS_AND_FREQS field allocates new int[freq] but never adds positions, so with -ea finishDoc's assert posUpto == current.positions.length (:370-372) fails the flush; with -da it works. The recommended fix covers both.
Exposure and likelihood129 words
  • Routes: -Dtests.postingsformat=RAMOnly makes the test framework use PostingsFormat.forName("RAMOnly") for every field (SetupAndRestoreStaticEnv.java:147-158), or a test sets it itself (for example with TestUtil.alwaysPostingsFormat). The package is exported and the format registered as a PostingsFormat provider (module-info.java:45, :75; META-INF/services/org.apache.lucene.codecs.PostingsFormat:17), so downstream tests can use it.
  • Not default: RandomCodec never picks it (RandomCodec.java:236-259). Its only user in Lucene's sources, TestExternalCodecs, routes every field except field1, field2 and id to it, but its documents contain only those three fields (TestExternalCodecs.java:44-51, :72-83). No BasePostingsFormatTestCase subclass is for RAMOnly.
  • Scale: one document with one StringField; deterministic; the first flush fails.
  • Effect: a NegativeArraySizeException and a closed writer in a test. No index damage, no wrong results, no production code path.

Minimal repro

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.

TestRAMOnlyPostingsFormat.java50 lines
/*
 * 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);
      }
    }
  }
}
Command
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 one StringField document succeeds, as with every other postings format.
Observed
AssertionError: expected flush of a DOCS-only field to succeed, got java.lang.NegativeArraySizeException: -1 (from RAMDoc.<init>), with assertions on and off.

All files of this repro: repro/LFV-033/

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 plain ByteBuffersDirectory keep the fixed test from tripping the class's separate defects (compound-file header check, CheckIndex on close).
  • Assertions: pass -Ptests.asserts explicitly. Lucene's build enables assertions for about 75% of seeds, and with seed DEADBEEF its default is off (TestsAndRandomizationPlugin.java:240-249); a run without the flag printed -Ptests.asserts=false in 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 existing codecs/asserting/ tests). With the fix, :lucene:test-framework:check -x test passes with the test as is.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also 10.5.2 (release jars); 4.0.0-BETA to 10.6.0 by a portable probe
jdkOpenJDK 25.0.3
osLinux aarch64
Command (environment)
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)

Observed output

# 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)

Why it looks unintended

  • 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 writeFreqs and 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 requires totalTermFreq == docFreq for such fields (:1695-1705, :1935-1937). FieldsConsumer#write puts 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.

Suggested fix

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#freq and CheckIndex require (CheckIndex.java:1765-1777), and keeps totalTermFreq == docFreq for DOCS terms (:1695-1705, :1935-1937; TermsEnum#totalTermFreq is "the sum of the freq() for each doc").
  • Also fixes the sibling DOCS_AND_FREQS assertion in finishDoc.
  • RAMDoc gains one int per posting (test-only memory).
  • The class keeps its other, separate defects (compound files, positions without payloads, docID() before nextDoc()); 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=1 for a StringField, and freqs 2 and 1 for a DOCS_AND_FREQS field "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 -ea and -da), and the cross-model reviewer's own two tests and TestExternalCodecs with the same file.

B. Clamp the array size

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.

C. Reject DOCS explicitly, or remove the class

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 note

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.

How it was found

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 threw NegativeArraySizeException on a StringField.
  • Verification: the study verifier wrote its own 9-test class, a plain main() run with -ea and -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 main 645d4fe14a20 with assertions on and off.

Prior reports, history and checks

Prior reports

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).

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene incl. the migrated JIRA archive)2026-11-01a 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-015 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-0140 queriesonly build logs listing the package; tests.postingsformat=RAMOnly has 0 hits
Downstream and web2026-11-014 queries over Elasticsearch, OpenSearch and Solr; manual searches in their repos; 7 web queriesno report
Open PRs2026-11-01the changed files of all 410 open apache/lucene PRsonly #14716 touches the file; not a fix

History

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).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails with assertions on and off (gradle and standalone)
9.1.0 to 10.6.0 (33 releases) affectedthe 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 affectedthe portable probe (same input, a commit as the flush) fails with NegativeArraySizeException: -1, assertions on and off
4.0.0-ALPHA not affectedpasses, 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 bisectboth pass
4.0.0-ALPHA with 42bd0e24f87's two files (and, as control, with its parent's) bisectfails with the commit's files; passes with the parent's
main with the recommended fix not affectedpasses, assertions on and off

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
study verifier realOwn 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 reproducesA re-run on b83101cdd2 (2026-10-23) and the main refresh on a9779ef3c193 (2026-10-26).
minimal repro fails on main, fix passesOne test method on main 645d4fe14a20, assertions on and off; 5 of 5 random seeds fail; the fix passes.
re-verification realMain 645d4fe14a20 and the 10.5.2 release jars, -ea and -da; fix check passes.
cross-model refute review real, high confidenceOpenAI 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 fixScratch 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.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes522 words

Finding record

LFV-033, snapshot of 2026-11-06

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-framework tests, 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).

Lucene and upstream sources

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)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.