All findings · Lucene finding · LFV-1135 · indexingchain-custom-termfreq-below-one-not-rejected

IndexWriter indexes a custom term frequency of 0 unchecked and commits a segment CheckIndex rejects

If your own TermFrequencyAttribute class reports a frequency of 0, IndexWriter stores it unchecked: with assertions off, the commit succeeds and CheckIndex rejects the segment, or, if that is the term's only document, the commit fails and closes the writer. Every release since 7.0.0 has it.

Summary

Priority
Low, rank 677A custom term frequency of 0 is indexed without a check, so the committed segment fails CheckIndex or the commit throws and closes the writer, in every release since 7.0.0. It needs the opt-in custom-frequency feature with an application-written attribute that skips the stock check, since every stock producer rejects 0, so it is contrived.
Impact
minor
Since
7.0.0 (2017, LUCENE-7854)introduced 2017-06-06 · about 9 years
Affected
every release from 7.0.0 to 10.6.0 (9 run) and main; not fixed upstream
Status
documented
Found by
semi-formal: a proof-obligation audit of FreqProxTermsWriterPerField's contract, witnessed by an executable repro

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

Background: custom term frequencies

  1. Normally a term's frequency in a document is how often it occurs there. Since 7.0.0 a TokenStream can set it directly with TermFrequencyAttribute, for example red|3 through DelimitedTermFrequencyTokenFilter, on a field indexed with DOCS_AND_FREQS.
  2. Lucene's own implementations of that attribute throw for values below 1. An application can register its own implementation with the Expert AttributeSource.addAttributeImpl or an AttributeFactory.
Diagram of the bug: two one-token documents on field f, foo with custom frequency 0 and foo with frequency 2, go through IndexingChain. The position and offset checks pass and the frequency is checked only for overflow, so the postings store doc 0 with freq 0 and doc 1 with freq 2; addDocument and commit succeed, and CheckIndex reports 'freq 0 is out of bounds' and 1 broken segment.
A custom frequency of 0 passes the indexing chain's per-token checks, which bound it only from above, and is stored as the posting's frequency; the committed segment then fails CheckIndex. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You index custom term frequencies on a DOCS_AND_FREQS field through your own TermFrequencyAttribute class, plugged in with the Expert addAttributeImpl, that does not copy the stock "1 or greater" check. Doc 0 has foo with frequency 0, doc 1 has foo with frequency 2.
  2. You expect addDocument to reject doc 0 with an IllegalArgumentException and drop only that document, as IndexWriter does for a negative position increment or offsets going backwards.
  3. Instead the indexing chain checks the custom frequency only for overflow and stores the 0 as the posting's frequency. With assertions off, both addDocument calls and commit return normally.
  4. CheckIndex then reports term [66 6f 6f]: doc 0: freq 0 is out of bounds and 1 broken segment containing 2 documents.
  5. If foo occurs only in the frequency-0 document, the commit itself throws cannot write negative vLong (got: -1) and closes the writer; a valid document added after the bad one is lost too.

Who is affected

Only applications that index custom term frequencies through their own TermFrequencyAttribute class that skips the stock "1 or greater" check, and emit 0 (for example a weight that rounds down to 0). Stock analysis, including DelimitedTermFrequencyTokenFilter and FeatureField, rejects 0 loudly. Default config and codec otherwise; two one-token documents suffice.

Mechanism, terms and numbers541 words

A field indexed with DOCS_AND_FREQS can take each token's frequency from TermFrequencyAttribute instead of counting occurrences. Lucene's two stock implementations of that attribute reject values below 1, but the interface states no range, and an application can plug in its own implementation through the Expert AttributeSource.addAttributeImpl or an AttributeFactory. IndexingChain.invertTokenStream checks every token's position increment and offsets before adding it, but checks the custom frequency only for overflow (IndexingChain.java:2053-2064 at 5adff78bb168); FreqProxTermsWriterPerField then stores the value as the posting's frequency (:125, :177, :196); on a DOCS_AND_FREQS field the only positivity guard is an assert (:143). With assertions off, a frequency of 0 is committed silently and CheckIndex rejects the segment (freq 0 is out of bounds). When a term's total frequency ends below its document count, the commit instead throws cannot write negative vLong (got: -1), which closes the writer and loses the uncommitted documents. Every release from 7.0.0 to 10.6.0 has it; stock analysis cannot trigger it.

The mechanism (main 5adff78bb168, under lucene/core/src/java/org/apache/lucene/)

  1. Only the stock setters check the range. TermFrequencyAttribute (analysis/tokenattributes/TermFrequencyAttribute.java:23-35) states no range. TermFrequencyAttributeImpl.setTermFrequency (:30-36) and PackedTokenAttributeImpl.setTermFrequency (:157-164) throw "Term frequency must be 1 or greater". A custom class plugged in with addAttributeImpl ("Expert", util/AttributeSource.java:176-188) bypasses both; it has to be registered before the default packed implementation claims the interface, as the repro does.
  2. The indexing chain checks the other token values, not this one. IndexingChain.invertTokenStream rejects a bad position increment or position overflow (index/IndexingChain.java:1996-2033) and bad offsets (:2035-2051) with non-aborting IllegalArgumentExceptions before the add. The custom frequency is only added to the field length with Math.addExact, "too many tokens" on overflow (:2053-2064): an upper bound, no lower bound. This is the fix site.
  3. It is stored as is. termsHashPerField.add (:2075, inside the aborting catch (Throwable) at :2098-2100) reaches FreqProxTermsWriterPerField.newTerm / addTerm, which store getTermFreq() as the posting's frequency (index/FreqProxTermsWriterPerField.java:125, :177, :196). getTermFreq() (:208-219) returns termFreqAtt.getTermFrequency() and rejects only freq != 1 when positions are indexed.
  4. On a DOCS_AND_FREQS field the only positivity guard is an assert. assert !hasFreq || postings.termFreqs[termID] > 0 (:143) runs only with -ea, and on the term's next occurrence, not on the document that carries the 0. Other index options do have checks, inside the add: with positions, getTermFreq() rejects any value other than 1 (:208-219); on a DOCS-only field addTerm rejects a custom frequency other than 1 (:147-152).
  5. What the flush writes. The postings get freq 0 (term foo: docFreq 2, totalTermFreq 2, doc 0 freq 0, doc 1 freq 2) and CheckIndex treats freq <= 0 as corruption (index/CheckIndex.java:1735-1739). When a term's total frequency ends below its document count (one document with frequency 0: totalTermFreq 0, docFreq 1), the terms dictionary writes ttf - df as a vLong (codecs/lucene103/blocktree/Lucene103BlockTreeTermsWriter.java:528), which throws at store/DataOutput.java:238 during the commit; the exception is tragic and closes the writer.

With assertions on, an internal assert fires instead: FreqProxTermsWriterPerField.addTerm:143 when the frequency-0 document comes first, Lucene104PostingsWriter.startDoc:290 during flush when it comes second (the devil's-advocate verifier's test).

Exposure and likelihood166 words
  • Who. Code that writes its own TermFrequencyAttribute implementation (an Expert hook) without the stock range check, on a field indexed with frequencies. No public example was found: a GitHub code search for implements TermFrequencyAttribute (top 30 hits) found only copies of Lucene's own class, so the likelihood is rated contrived.
  • Not hit. The two stock attributes throw for values below 1; DelimitedTermFrequencyTokenFilter calls the stock setter, so foo|0 is rejected; FeatureField rejects values below Float.MIN_NORMAL.
  • Effect. Silent in production mode (assertions off): the bad value is committed and only CheckIndex notices. In the single-document case the commit fails and closes the writer: in the probe, after one committed document, a frequency-0 document and a valid one were added; the commit threw, the writer closed, and the index kept only the first document. How search and merge behave on a segment with freq 0 was not run.
  • Default vs opt-in. Custom term frequencies are opt-in; nothing else is configured.
  • Scale. Two one-token documents; deterministic, single thread.

Minimal repro

One JUnit test method on the public API: a TermFrequencyAttribute class that stores what it is given, two one-token documents (foo with frequency 0, then 2), commit, CheckIndex. An IllegalArgumentException from addDocument counts as correct, so it doubles as the regression test. Deterministic, single thread.

TestCustomTermFreqBelowOne.java (lucene/core, package org.apache.lucene.index, 1 test)112 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.index;

import java.util.List;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.TermFrequencyAttribute;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
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;
import org.apache.lucene.util.AttributeImpl;
import org.apache.lucene.util.AttributeReflector;

public class TestCustomTermFreqBelowOne extends LuceneTestCase {

  public static final class UncheckedFreqAttributeImpl extends AttributeImpl
      implements TermFrequencyAttribute {
    private int freq = 1;

    @Override
    public void setTermFrequency(int termFrequency) {
      freq = termFrequency;
    }

    @Override
    public int getTermFrequency() {
      return freq;
    }

    @Override
    public void clear() {
      freq = 1;
    }

    @Override
    public void copyTo(AttributeImpl target) {
      ((TermFrequencyAttribute) target).setTermFrequency(freq);
    }

    @Override
    public void reflectWith(AttributeReflector reflector) {
      reflector.reflect(TermFrequencyAttribute.class, "termFrequency", freq);
    }
  }

  private static final class FooWithFreq extends TokenStream {
    private final int freq;
    private final TermFrequencyAttribute freqAtt;
    private final CharTermAttribute termAtt;
    private boolean done;

    FooWithFreq(int freq) {
      addAttributeImpl(new UncheckedFreqAttributeImpl());
      freqAtt = addAttribute(TermFrequencyAttribute.class);
      termAtt = addAttribute(CharTermAttribute.class);
      this.freq = freq;
    }

    @Override
    public boolean incrementToken() {
      if (done) {
        return false;
      }
      clearAttributes();
      termAtt.append("foo");
      freqAtt.setTermFrequency(freq);
      return done = true;
    }

    @Override
    public void reset() {
      done = false;
    }
  }

  public void testZeroCustomTermFrequency() throws Exception {
    FieldType type = new FieldType();
    type.setIndexOptions(IndexOptions.DOCS_AND_FREQS);
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w =
          new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
        // expected: addDocument rejects frequency 0 with IllegalArgumentException; actual: it is
        // indexed and the committed segment fails CheckIndex ("freq 0 is out of bounds")
        try {
          w.addDocument(List.of(new Field("f", new FooWithFreq(0), type)));
        } catch (IllegalArgumentException expected) {
        }
        w.addDocument(List.of(new Field("f", new FooWithFreq(2), type)));
        w.commit();
      }
      TestUtil.checkIndex(dir);
    }
  }
}
Command
cp $REPRO/TestCustomTermFreqBelowOne.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestCustomTermFreqBelowOne -Ptests.seed=DEADBEEF -Ptests.asserts=false
# without -Ptests.asserts=false: AssertionError at FreqProxTermsWriterPerField.addTerm:143
Expected
addDocument rejects the frequency-0 document with IllegalArgumentException, or the committed index passes CheckIndex.
Observed
RuntimeException: CheckIndex failed (term [66 6f 6f]: doc 0: freq 0 is out of bounds) with assertions off; AssertionError at FreqProxTermsWriterPerField.addTerm:143 with them on.

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

More files, environment, full output333 words

TestCustomTermFreqBelowOne (one method, extends LuceneTestCase, package org.apache.lucene.index of the lucene/core tests, Apache license header). It passes checkGoogleJavaFormat. The test method draws no random values, though the test framework still picks locale, time zone and the default similarity from the seed; this test was run with seed DEADBEEF, and the hunt's longer version of it also failed with four other seeds (a separate re-run and the audit). Run it with -Ptests.asserts=false for the production-mode failure (CheckIndex); with assertions on, an internal AssertionError fires on the second addDocument instead. With fix A it passes unchanged, assertions on and off. As written it accepts any IllegalArgumentException from the first addDocument; a stricter version would also assert the message and that the index then holds 1 document (not run).

Environment

lucenemain 5adff78bb168 (12.0.0-SNAPSHOT): fails; 7.0.0 to 10.6.0 (9 releases run): fail; 6.6.x: no API
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (only this platform was run)
Command (environment)
cp $REPRO/TestCustomTermFreqBelowOne.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestCustomTermFreqBelowOne -Ptests.seed=DEADBEEF -Ptests.asserts=false

Observed output

# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF -Ptests.asserts=false
TestCustomTermFreqBelowOne > testZeroCustomTermFrequency FAILED
    java.lang.RuntimeException: CheckIndex failed
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:BAD1EBD1119B1DB4]:0)
        at org.apache.lucene.tests.util.TestUtil.checkIndex(TestUtil.java:337)
        ...
  1>     test: terms, freq, prox...ERROR: org.apache.lucene.index.CheckIndex$CheckIndexException: term [66 6f 6f]: doc 0: freq 0 is out of bounds
  ...
  1> WARNING: 1 broken segments (containing 2 documents) detected

# gradle, same, assertions on
TestCustomTermFreqBelowOne > testZeroCustomTermFrequency FAILED
    java.lang.AssertionError
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:BAD1EBD1119B1DB4]:0)
        at org.apache.lucene.index.FreqProxTermsWriterPerField.addTerm(FreqProxTermsWriterPerField.java:143)
        at org.apache.lucene.index.TermsHashPerField.positionStreamSlice(TermsHashPerField.java:216)
        at org.apache.lucene.index.TermsHashPerField.add(TermsHashPerField.java:201)
        at org.apache.lucene.index.IndexingChain$PerField.invertTokenStream(IndexingChain.java:2075)
        ...

# with fix A: PASS with assertions on and off

# commit-failure probe (a separate main(), not the repro), assertions off, no fix:
# bar freq 1 committed; then foo freq 0 and baz freq 1 added; commit
second commit: java.lang.IllegalArgumentException: cannot write negative vLong (got: -1)
writer open: false
tragic exception: java.lang.IllegalArgumentException: cannot write negative vLong (got: -1)
documents in the index: 1
# with fix A: addDocument(foo, freq 0) throws IllegalArgumentException: term frequency must be 1 or greater (got 0) for field 'f'; commit ok; 2 documents

Why it looks unintended

  • Nothing documents 0 as valid: both stock implementations throw "Term frequency must be 1 or greater", and CheckIndex calls a 0 out of bounds.
  • IndexWriter already re-checks a negative position increment, which also only a custom attribute can produce, and this frequency's upper bound.
  • The >= 1 check went only into the setters when the feature was added (LUCENE-7854).
The full argument193 words
  • Contract. TermFrequencyAttribute states no range; both stock implementations reject values below 1 (TermFrequencyAttributeImpl.java:30-36, PackedTokenAttributeImpl.java:157-164); FreqProxTermsWriterPerField asserts termFreqs > 0 (:143); CheckIndex reports freq <= 0 as out of bounds (CheckIndex.java:1735-1739). Michael McCandless on dev@ (2020-11-06, "Payloads for each term"): "term frequency 0 is not allowed".
  • IndexWriter guards the index against bad token values. PositionIncrementAttributeImpl (:32-35) and PackedTokenAttributeImpl (:57-62) reject a negative increment, and IndexingChain still checks "position increment must be >= 0" (:2002-2008). Precedents: LUCENE-3874 (#4947, "don't let bogus positions corrumpt [sic] the index"), LUCENE-4127 ("don't let bogus offsets corrupt the index"), LUCENE-5977 (#7039, token stream safety checks in IndexWriter). The custom frequency's upper bound is checked too (6c3ece2b9f9, 2017).
  • Tests. TestCustomTermFreq (15 tests) covers positions, DOCS-only fields, overflow and term vectors, but not a value below 1, because its streams use the stock attribute.
  • History. LUCENE-7854 (d276acfbbcb, 2017) put the >= 1 check only into the setters; none of the 34 comments on #8905 discusses values below 1. The assert at :143 dates from 2012, when a frequency could only be counted up from 1.
The devil's-advocate check131 words

The strongest case for "not a bug": the attribute is an Expert hook, an implementation that accepts 0 is broken, and Robert Muir wrote on #11086 (2021): "If you use custom term frequencies (expert feature), then it is your responsibility to have reasonable values. Otherwise, things can easily overflow, break scoring, cause failures from checkindex."

Why it fails: the interface documents no range; IndexWriter already re-checks the negative position increment, which only the same kind of custom code can produce; and in the same thread (2021-08-13) they wrote "It isn't viable to have an option that says 'allow me to corrupt my index'". The fix is one comparison on a value the chain already reads, and it rejects only the bad document. The argument does bear on likelihood, hence priority Low.

Suggested fix

Recommended Reject a frequency below 1 in IndexingChain before the add (fix A)

Check the lower bound in IndexingChain.invertTokenStream next to the position and offset checks, before the aborting add: throw a non-aborting IllegalArgumentException("term frequency must be 1 or greater (got 0) for field 'f'") (+10 lines). Only the bad document is rejected. The repro passes with assertions on and off; 105 tests of the 11 closest suites pass.

          final int termFreq = invertState.termFreqAttribute.getTermFrequency();
          if (termFreq < 1) {
            throw new IllegalArgumentException(
                "term frequency must be 1 or greater (got "
                    + termFreq
                    + ") for field '"
                    + field.name()
                    + "'");
          }
All options and trade-offs4 options ยท 601 words

Option A was built and checked through gradle on 5adff78bb168; B to D were not run. A and B change behaviour only for a custom frequency below 1, which every stock producer already rejects; A applies to every inverted field, including DOCS_AND_CUSTOM_FREQS (see its trade-offs). If LFV-281 is fixed at the same time, B is the natural form.

Reject a frequency below 1 in IndexingChain before the add (fix A) (validated by the checks) (recommended)

Right after the offsets check in IndexingChain.invertTokenStream (IndexingChain.java:2051), read the custom frequency and throw an IllegalArgumentException when it is below 1. Like the position and offset checks above it, this runs before termsHashPerField.add, so it is non-aborting: the document is marked deleted, as for any analysis error, and the writer stays open. The message mirrors the stock setters.

          final int termFreq = invertState.termFreqAttribute.getTermFrequency();
          if (termFreq < 1) {
            throw new IllegalArgumentException(
                "term frequency must be 1 or greater (got "
                    + termFreq
                    + ") for field '"
                    + field.name()
                    + "'");
          }

Trade-offs

One more comparison per token on the indexing hot path (Lucene's SECURITY.md asks that robustness changes not slow hot paths). The value is read again a few lines below (IndexingChain.java:2059, on the branch for fields other than DOCS_AND_CUSTOM_FREQS, which adds it to the field length), so a final version can reuse the local there (not run). No benchmark was run. Scope: as written the check runs before the index-option branches, so it applies to every inverted field, not only DOCS_AND_FREQS. On DOCS-only and position-indexed fields the existing checks inside the aborting add (FreqProxTermsWriterPerField.java:147-152, :208-219) reject a frequency other than 1; the fix would reject a 0 before them, non-aborting. On DOCS_AND_CUSTOM_FREQS (10.5.0) custom frequencies are opaque scores (IndexOptions.java:52-55, FieldInfo.java:717-723), so decide whether 0 is a valid score there before rejecting it. With the fix only DOCS_AND_FREQS was run with a 0, and no repro covers a negative value, so this finding is about 0.

What the checks validated

  • Gradle in a scratch worktree at 5adff78bb168, seed DEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (on and off).
  • With the fix: the repro, TestCustomTermFreq (15), TestFieldInvertState (2), TestTermsHashPerField (3), TestIndexWriterExceptions (40, 3 @Nightly skipped), TestFeatureField (14), TestFeatureSort (6), TestFeatureDoubleValues (6), TestPackedTokenAttributeImpl (4), TestSimilarity (3), TestReqOptSumScorer (8) and analysis-common's TestDelimitedTermFrequencyTokenFilter (3): 105 tests, 0 failures, 3 skipped. :lucene:core:checkGoogleJavaFormat passes. The whole core module was not run.
  • The commit-failure probe with the fix: the frequency-0 document is rejected, the commit succeeds, the writer stays open and the index has 2 documents; CheckIndex is clean.

The same check inside LFV-281's validation step (not built)

LFV-281's proposed fix adds a validateCustomTermFrequency step before the add whenever the frequency is not 1. A < 1 check fits there as its first line.

Trade-offs

Same behaviour as A, and no extra work for the common frequency of 1; it depends on LFV-281's change landing first or together with it.

Turn the assert at FreqProxTermsWriterPerField:143 into a real check (not recommended)

Replace assert !hasFreq || postings.termFreqs[termID] > 0 with an exception.

Trade-offs

It runs inside the aborting termsHashPerField.add, so a bad document would close the writer and roll back everything since the last commit (the LFV-281 problem), and it fires on the term's next occurrence, not on the bad document.

Document the range on TermFrequencyAttribute only (not built)

State in the interface javadoc that the frequency must be 1 or greater.

Trade-offs

Cheapest, but IndexWriter would still commit a segment CheckIndex rejects. Worth adding together with A.

Downstream note

Downstream, a product that accepts raw per-token frequencies from users through its own non-checking attribute would let a frequency of 0 break a segment or close the writer; Lucene's stock analysis rejects such input.

How it was found

A proof-obligation audit of FreqProxTermsWriterPerField's contract: every posting frequency it stores must be at least 1 (its own assert, and CheckIndex). Tracing where the value comes from showed that only the stock attribute setters enforce it; a one-test public-API JUnit then witnessed a frequency of 0 in a committed segment.

The method in full131 words

Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).

  • The finder read FreqProxTermsWriterPerField's obligation that a stored frequency is above 0 (:143, and CheckIndex at CheckIndex.java:1735-1739) and traced each place the value is set (:125, :177, :196) back to TermFrequencyAttribute.getTermFrequency(). The indexing chain discharges the obligation for positions and offsets with explicit checks, but for the frequency it relies on the stock setters, which an application's own implementation does not run.
  • The finder then wrote a public-API JUnit test that fails on main with assertions on and off, and on 10.6.0 and 9.12.3.
  • An independent reproducer, two executed cross-model reviews, a devil's-advocate verifier and an adjudication reproduced it with their own tests ("Prior reports, history and checks").

Prior reports, history and checks

Prior reports

No prior report found in apache/lucene issues, PRs and commits (with the JIRA archive), the dev@, java-user@, issues@ and commits@ lists, downstream trackers or the web (searched 2026-11-02). Nearest items: #11086 (a maintainer's view that custom-frequency values are the user's responsibility, about huge values) and the 2020 dev@ remark that a frequency of 0 is not allowed.

Searches3 searches

No matching public report was found in the documented searches. Related, not reports: #11086 (LUCENE-10048), #8905 (LUCENE-7854, the origin; nothing on values below 1), the dev@ thread "Payloads for each term" (2020), #4947 and #7039 (precedents for IndexWriter checks), #15584 (DOCS_AND_CUSTOM_FREQS, nothing on values below 1). #13338 (open) shares only the low-level "cannot write negative vLong" symptom at merge, without custom frequencies, and is not linked. Limits: keyword search; GitHub search returns at most 50 items per query; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive)2026-11-02, re-checkedthree scripted prior-report searches (45 queries each: identifiers and error messages; trigger and symptom; generic wording), 20 own queries, issues and PRs updated since 2026-10-31no report; #11086, #8905, #15584 are the nearest items
Open PRs2026-11-02the changed files of all 408 open apache/lucene PRsfive touch IndexingChain.java or FreqProxTermsWriterPerField.java (#16269, #16116, #15330, #13525, #13342); none changes term-frequency handling
Mailing lists, JIRA, downstream, web2026-11-02dev@, java-user@, issues@ and commits@ (the threads found read in full), JIRA full-text queries, Solr, Elasticsearch, OpenSearch and Lucene.NET trackers, 9 web queriesnothing about this bug; the 2020 dev@ remark "term frequency 0 is not allowed"

History

Present since custom term frequencies were added (d276acfbbcb, LUCENE-7854, Mike McCandless, 2017-06-06; first release 7.0.0). Every release run, 7.0.0 to 10.6.0, and main fail; 6.6.x lacks the API. Not a regression and not fixed upstream.

Versions tested6 rows

Introduced by d276acfbbcb "LUCENE-7854: enable indexing custom term frequencies" (Mike McCandless, 2017-06-06; JIRA patch, issue #8905, no PR). It added TermFrequencyAttribute, put the >= 1 check only into the two stock setters, and made FreqProxTermsWriterPerField store whatever getTermFrequency() returns. Before it every occurrence counted 1, so a frequency of 0 could not occur. First release 7.0.0 (2017-09-20).

Later changes near the fix site kept the gap: 6c3ece2b9f9 ("too many tokens", 7.0.0), #12139 (null guard, 9.6.0) and #15584 (DOCS_AND_CUSTOM_FREQS, 10.5.0, which uses the same getTermFreq() path; not run). Not fixed upstream: upstream main 1c2072f02843 (2026-11-02) has no commit since 2026-10-25 on FreqProxTermsWriterPerField.java or TermFrequencyAttribute.java, and on IndexingChain.java only #16734 (columnar vectors). Releases before 9.1.0 ran a main() with the same two documents and CheckIndex, because they lack today's test framework.

Versions and branches executed, with results
Version or commitVerdictResult
main 5adff78bb168 affectedgradle: fails with assertions on and off; passes with fix A
10.6.0 affectedthe JUnit unchanged: CheckIndex freq 0 is out of bounds (-da); assert at addTerm:143 (-ea)
9.1.0, 9.12.3, 10.0.0 affectedthe JUnit unchanged (-da): the same CheckIndex error
10.5.2 reproducer's testthe independent reproducer's test (AttributeFactory route): the same failure
7.0.0, 7.7.3, 8.11.4, 9.0.0 main() formthe same two documents and check: the same CheckIndex error (7.0.0 also -ea: assert)
6.6.0, 6.6.6 no APITermFrequencyAttribute does not exist

Independent checks

A finder, an independent reproducer, two executed cross-model reviews (OpenAI GPT-5.6, intended and refute lenses), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced it and agree on the root cause. This doc's gradle runs reproduced it on main and validated the fix.

Checks and verdicts312 words

The cross-model reviews ran on main ea57360912ed, where FreqProxTermsWriterPerField.java and the attribute classes are identical to 5adff78bb168 and IndexingChain.java differs only by #16734 (columnar vectors); their fix checks did not compile there. The other checks on main ran on 5adff78bb168; the release runs used each release's own jars.

Each independent check and its verdict
CheckVerdictWhat it found
finder reproducedThe repro test fails on main (assertions on and off), 10.6.0 and 9.12.3; its fix passes the test and TestCustomTermFreq (15 of 15).
cross-model review, intended lens (OpenAI GPT-5.6, executed) real (medium)Own test: frequency 0 read back from the committed postings, then CheckIndex fails; the stock-attribute control passes; intended argument moderate.
independent reproducer realOwn test through AttributeFactory.getStaticImplementation (a second public route) fails on main, 10.6.0, 10.5.2 and 9.12.3; confirmed the single-document commit failure.
cross-model review, refute lens (OpenAI GPT-5.6, executed) real (medium)Own test fails (two seeds, assertions off; and on); a positive-frequency control passes; inputs in contract, root cause agrees.
devil's-advocate verifier real (medium)The misuse argument rated moderate and found not to hold; own test with the 0 in the second document fails (with assertions on, at Lucene104PostingsWriter.startDoc:290).
adjudication confirmed, Low to MediumRe-ran the reproducer's test on main with assertions off and a fresh seed: fail.
separate re-run, audit and final repro check confirmed (fail, fail)The re-run and the audit ran the hunt's test on main with assertions on and off: fail. The final repro check's runs of the one-test repro fail on main and pass with the fix; it set the impact class to minor.
this doc reproduced; fix A validatedGradle: fails without the fix; with it 105 tests of the closest suites run, 0 failures, 3 @Nightly skipped; the commit-failure probe confirms the tragic close and that the fix prevents it.

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 notes1,511 words

Finding record

LFV-1135, snapshot of 2026-11-06

Registry title
IndexWriter accepts a custom term frequency of 0 without any check: the frequency goes into the postings and the committed segment fails CheckIndex ('freq 0 is out of bounds'), or the commit throws and closes the writer
Area
index / IndexingChain inversion with custom term frequencies (TermFrequencyAttribute, FreqProxTermsWriterPerField)
Priority
Low, rank 677: A custom term frequency of 0 is indexed without a check, so the committed segment fails CheckIndex or the commit throws and closes the writer, in every release since 7.0.0. It needs the opt-in custom-frequency feature with an application-written attribute that skips the stock check, since every stock producer rejects 0, so it is contrived.
Impact class
minor
Impact note
Hits only applications that index custom term frequencies (an opt-in feature) through their own TermFrequencyAttribute implementation, plugged in with the Expert AttributeSource.addAttributeImpl or an AttributeFactory, that skips the stock '1 or greater' check and reports 0 on a field indexed with frequencies; otherwise default IndexWriterConfig and codec at ordinary scale: with -da nothing fails at index time and the committed segment fails CheckIndex, or, when a term's total frequency ends below its document count, the commit throws and closes the writer so uncommitted documents are lost; with -ea an internal assert fires instead; stock analysis (the stock attributes, DelimitedTermFrequencyTokenFilter, FeatureField) cannot reach it; every release from 7.0.0 to 10.6.0 and main. Minor rather than data corruption: the effect is a committed segment that CheckIndex rejects, but it is reachable only through an application-written TermFrequencyAttribute implementation that skips the stock '1 or greater' check, and a maintainer has called sane custom-frequency values the user's responsibility (#11086), so it is a Low missing-input-check bug.
Summary
You index a document through a TokenStream whose TermFrequencyAttribute is your own implementation (plugged in with AttributeSource.addAttributeImpl or an AttributeFactory) and that reports a frequency of 0 for a token, on a field indexed with DOCS_AND_FREQS. You expect IndexWriter to reject that document with an IllegalArgumentException, as it does for a negative position increment, backwards offsets or too many tokens, or at least never to write an index that its own CheckIndex calls broken. Instead the 0 goes into the postings unchecked: with assertions off addDocument and commit succeed and CheckIndex then reports 'term [66 6f 6f]: doc 0: freq 0 is out of bounds' and 1 broken segment, and if the term's only document has frequency 0, the commit instead throws 'cannot write negative vLong (got: -1)', which closes the writer and loses every uncommitted document.
Introduced
d276acfbbcb8ea6124fe486e35cea9eb928e4702 'LUCENE-7854: enable indexing custom term frequencies' by mikemccand (JIRA-patch workflow, issue migrated as #8905; no GitHub PR) [read: git blame on main, git log -G 'termFreqAttribute|getTermFrequency|TermFrequencyAttribute' over IndexingChain, DefaultIndexingChain, FreqProxTermsWriterPerField and FieldInvertState; exec: 7.0.0 fails, 6.6.0 and 6.6.6 lack the API]. It added TermFrequencyAttribute (its javadoc states no range), put the only lower-bound check into the two stock setters (TermFrequencyAttributeImpl and PackedTokenAttributeImpl: 'Term frequency must be 1 or greater') and made FreqProxTermsWriterPerField store whatever getTermFrequency() returns as the posting frequency; the indexing chain only adds the value to the field length (upper bound only; the follow-up 6c3ece2b9f9, 2017-06-07, turned the overflow into 'too many tokens'). Before it every occurrence counted 1, so a frequency of 0 could not occur. Not a regression: the -ea-only assert at FreqProxTermsWriterPerField:143 (c40b0375603b, rmuir, 2012) predates the feature, and #12139 (cce33b07e4f, 9.6.0, null guard) and #15584 (5158a52a082, 10.5.0, DOCS_AND_CUSTOM_FREQS length branch) changed the code nearby without adding a lower bound. (2017-06-06 (author and committer date of d276acfbbcb); first release 7.0.0 (tag 2017-09-08))
First affected version
7.0.0 [exec: the history variant fails with -da and -ea]; 6.6.6 and older lack the API [exec: the variant does not compile on 6.6.0 and 6.6.6, TermFrequencyAttribute missing; git tag --contains d276acfbbcb lists 7.0.0 first]
Affected versions
Every release from 7.0.0 (2017) through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT); never fixed. [exec, follow-up history runs, seed DEADBEEF, JDK 25] a core-only main() variant (same two documents, commit, CheckIndex) fails on 7.0.0 (-da and -ea), 7.7.3, 8.11.4 and 9.0.0 (-da), because releases before 9.1.0 lack today's test framework; the one-test JUnit unchanged fails on 9.1.0, 9.12.3 and 10.0.0 (-da) and on 10.6.0 and main (-ea and -da); the independent reproduction's own test fails on main, 10.6.0, 10.5.2 and 9.12.3 (-ea and -da); the intended-behaviour check's own test fails on main (-ea and -da) and 10.6.0 (-da); re-run on main 2026-11-02 fails with -ea and -da. Not new in 10.6 (10.5.2 and every earlier executed release fail). Not fixed upstream: upstream main 1c2072f02843 (2026-11-02) has no commit since 2026-10-25 on FreqProxTermsWriterPerField.java or TermFrequencyAttribute.java and only a9779ef3c19 (#16734, columnar vectors) on IndexingChain.java [read through the GitHub API, 2026-11-02]. DOCS_AND_CUSTOM_FREQS (10.5.0 and later) uses the same getTermFreq() path [read], not executed.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: proof-obligation audit
Upstream and prior reports
none found (2026-11-02, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's GitHub searches; rechecked the same day): three scripted prior-report searches of 45 queries each (identifiers and error messages; trigger and symptom; generic wording) over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, plus the changed-file lists of all 408 open apache/lucene PRs (five touch IndexingChain.java or FreqProxTermsWriterPerField.java: #16269, #16116, #15330, #13525, #13342; none changes term-frequency handling) and the threads found read in full: no report of IndexWriter accepting a custom term frequency below 1 (tracking issue #16937 does not mention it). Closest context, not reports: #11086 (LUCENE-10048, 2021: rmuir wrote that users of custom frequencies are responsible for 'reasonable values', and in the same thread that an option to 'allow me to corrupt my index' is not viable; about huge values, which IndexWriter rejects), #8905 (LUCENE-7854, origin; 34 comments, none about values below 1), a 2020-11-06 dev@ mail by Michael McCandless ('term frequency 0 is not allowed'), #15584 (DOCS_AND_CUSTOM_FREQS, nothing on values below 1), and the precedents #4947 (LUCENE-3874) and #7039 (LUCENE-5977) for IndexWriter re-validating TokenStream values. #13338 (open, 2024) shares only the low-level 'cannot write negative vLong' symptom at merge on a deployment without custom frequencies and is a different problem. Same-day recheck: GitHub issue search on apache/lucene for 'TermFrequencyAttribute', 'custom term frequency', 'freq 0 is out of bounds' and 'term frequency must be 1 or greater', and PR search for 'TermFrequencyAttribute', 'custom term frequency' and 'getTermFrequency', updated since 2026-10-31: 0; the 9 PRs created since 2026-10-31 (#16956-#16965) are unrelated; upstream commits since 2026-10-25: none on FreqProxTermsWriterPerField.java or TermFrequencyAttribute.java, only a9779ef3c19 (#16734, author date 2026-10-26, columnar vectors) on IndexingChain.java; upstream main head 1c2072f02843. Not fixed upstream.
Lucene commit tested
main 5adff78bb168 (2026-11-02) through gradle in a scratch worktree with assertions on and off, without and with fix A (the repro, 11 closest suites, checkGoogleJavaFormat), and a standalone commit-failure probe on the main jars (without and with fix A). The hunt ran the same two documents on 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (all fail); 6.6.0 and 6.6.6 lack the API. Only Linux aarch64 with OpenJDK 25.0.3 was run.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1996-2050 (at 5adff78bb168) (per-token checks before the add: position increment >= 0, position overflow, offsets, all non-aborting IllegalArgumentExceptions), :2052-2063 (custom frequency only added to invertState.length with Math.addExact; 'too many tokens' on overflow; no lower bound: fix site), :2068-2075 and :2098-2100 (termsHashPerField.add, the aborting step)
  • lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriterPerField.java:125, (at 5adff78bb168) :177, :196 (termFreqs set from or summed with getTermFreq()), :143 ('assert !hasFreq || postings.termFreqs[termID] > 0', -ea only), :208-219 (getTermFreq returns termFreqAtt.getTermFrequency(); only checks freq != 1 with positions)
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttribute.java:22-35 (at 5adff78bb168) (interface, no range stated), TermFrequencyAttributeImpl.java:30-36 and PackedTokenAttributeImpl.java:157-164 ('Term frequency must be 1 or greater'); lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1735-1739 (freq <= 0 reported as 'freq N is out of bounds')
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttribute.java:23-35, (at 5adff78bb168) TermFrequencyAttributeImpl.java:30-36, PackedTokenAttributeImpl.java:157-164
  • lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:176-188 (at 5adff78bb168) (addAttributeImpl, Expert)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1996-2033, (at 5adff78bb168) :2035-2051, :2053-2064, :2075, :2098-2100
  • lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriterPerField.java:125, (at 5adff78bb168) :143, :147-152, :177, :196, :208-219
  • lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1735-1739; (at 5adff78bb168) codecs/lucene103/blocktree/Lucene103BlockTreeTermsWriter.java:528; store/DataOutput.java:238
Who is affected
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttributeImpl.java:30-36, (at 5adff78bb168) PackedTokenAttributeImpl.java:157-164; document/FeatureField.java:162
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/miscellaneous/DelimitedTermFrequencyTokenFilter.java:67 (at 5adff78bb168)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/TermFrequencyAttribute.java:23-35, (at 5adff78bb168) TermFrequencyAttributeImpl.java:30-36, PackedTokenAttributeImpl.java:57-62, :157-164, PositionIncrementAttributeImpl.java:32-35
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2002-2008, (at 5adff78bb168) :2053-2064; index/FreqProxTermsWriterPerField.java:143; index/CheckIndex.java:1735-1739
  • lucene/core/src/test/org/apache/lucene/index/TestCustomTermFreq.java (at 5adff78bb168) (15 tests)
  • https://github.com/apache/lucene/issues/11086
  • https://lists.apache.org/thread/dcwm71rnjz7mz69ntwy04co3615zdl6w
Suggested fix
  • Lucene main 5adff78bb168 SECURITY.md:46 (robustness changes and hot paths)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2053-2064 (at 5adff78bb168) (length update, :2059 reads the frequency); index/FreqProxTermsWriterPerField.java:147-152, :208-219; index/IndexOptions.java:52-56; index/FieldInfo.java:717-723 (DOCS_AND_CUSTOM_FREQS)
History
  • Lucene main 5adff78bb168 dev-tools/doap/lucene.rdf (lucene-7.0.0 created 2017-09-20)
Diagrams
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1996-2075; (at 5adff78bb168) index/FreqProxTermsWriterPerField.java:125-219

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

Repro files

About the diagrams

The diagram illustrates the repro's two documents (main 5adff78bb168, IndexingChain.java:1996-2075, FreqProxTermsWriterPerField.java:125-219).

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.