All findings · Lucene finding · LFV-1007 · ngram-tokenizer-maxgram-int-max-buffer-wrap

NGramTokenizer and EdgeNGramTokenizer accept maxGram = Integer.MAX_VALUE, but their buffer sizing overflows, so the first text of 17 or more chars throws

new EdgeNGramTokenizer(1, Integer.MAX_VALUE) is accepted and works on texts of up to 16 chars, but the first 17-char text throws ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16: 2 * maxGram overflows int, so the 16-char term buffer is never grown. Affects 4.4.0 to 10.6.0 and main.

Summary

Priority
Low, rank 642NGramTokenizer and EdgeNGramTokenizer are widely used but maxGram = Integer.MAX_VALUE is a rare setting and fails loudly without writing anything wrong.
Impact
crash
Since
4.4.0 (2013, LUCENE-5042)introduced 2013-06-12 · over 13 years
Affected
every release from 4.4.0 to 10.6.0, and main
Status
documented
Found by
a numeric-limits probe of analysis size parameters, then a z3 model of the int arithmetic

AI-assisted page: this finding was first signalled by code reading or testing 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

An n-gram tokenizer copies each gram into the term buffer, which it grows once, in its constructor, to 2 * maxGram chars. In int, 2 * 2,147,483,647 is -2, so the buffer is never grown and keeps its default 16 chars; the read buffer, 2 * maxGram + 1024 chars, gets 1022.

Background: n-gram tokenizers

  1. EdgeNGramTokenizer(minGram, maxGram) emits the prefixes of the text from minGram to maxGram code points: (1, 17) turns abc into a, ab, abc. NGramTokenizer also emits the grams that start later (b, bc, c). Typical use: prefix or substring autocomplete.
  2. Each token is copied into the term attribute's char buffer, which starts at 16 chars (on a 64-bit JVM). These tokenizers grow it once, in the constructor, to 2 * maxGram chars (a code point can take 2 chars), and then copy every gram straight into it.
Summary of the bug. The user builds new EdgeNGramTokenizer(1, Integer.MAX_VALUE) and tokenizes the 17-char text abcdefghijklmnopq; expected: the 17 prefixes a to abcdefghijklmnopq, or an IllegalArgumentException from the constructor. Inside, init computes 2 * maxGram + 1024 and 2 * maxGram in int: 2 * 2147483647 wraps to -2, so the read buffer is 1022 chars and termAtt.resizeBuffer(-2) does nothing, leaving the 16-char default term buffer. The first 16 prefixes fit; the 17th throws ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16. For text without supplementary characters, texts of 1 to 16 chars work, 17 to 1021 chars throw that exception, and 1022 chars or more throw IllegalArgumentException from CharacterUtils.fill. The fix rejects maxGram above 715,827,540 at construction.
EdgeNGramTokenizer(1, Integer.MAX_VALUE) on a 17-char text: the constructor accepts the value, 2 * maxGram wraps to -2, so the read buffer gets 1022 chars and the term buffer stays at 16; the 17th prefix does not fit and incrementToken throws. The fix rejects the value when the tokenizer is created. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You build new EdgeNGramTokenizer(1, Integer.MAX_VALUE) to mean "all prefixes, no upper limit" and tokenize the 17-char text abcdefghijklmnopq. The constructor checks maxGram and accepts it.
  2. You expect the 17 prefixes a, ab, ..., abcdefghijklmnopq: what maxGram = 17 gives, and what EdgeNGramTokenFilter gives for the same Integer.MAX_VALUE. Or else an IllegalArgumentException from the constructor.
  3. You get 16 prefixes, then ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16. Texts of up to 16 chars work; every longer text throws.
  4. Why: the constructor sizes its buffers in int. 2 * 2,147,483,647 wraps to -2, so termAtt.resizeBuffer(-2) does nothing and the term buffer keeps its default 16 chars: the 17-char prefix does not fit.
  5. So addDocument throws for each document with such a value (the index stays clean), and building a query from such text (QueryBuilder) throws too. NGramTokenizer and the nGram / edgeNGram factories fail the same way.

Who is affected

Users of the two tokenizers with maxGram = Integer.MAX_VALUE as "no upper limit", directly, via CustomAnalyzer or with maxGramSize=2147483647 in a factory configuration. Opt-in and rare. Short test strings pass, then every document or query text of 17 or more chars fails; nothing is corrupted. Workaround: a finite maxGram, or the n-gram token filters.

Mechanism, terms and numbers710 words

NGramTokenizer and EdgeNGramTokenizer (its subclass) accept maxGram = Integer.MAX_VALUE: init checks only minGram >= 1 and minGram <= maxGram. It then sizes the read buffer with CharacterUtils.newCharacterBuffer(2 * maxGram + 1024) and calls termAtt.resizeBuffer(2 * maxGram), both in int. For Integer.MAX_VALUE, 2 * maxGram wraps to -2: the read buffer gets 1022 chars, and resizeBuffer(-2) is a no-op, so the term buffer keeps its default size, 16 chars on a 64-bit JVM. With minGram = 1, texts of up to 16 chars tokenize correctly; the first text of 17 chars throws ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16 from incrementToken, and from 1022 chars (text without supplementary characters) an IllegalArgumentException from CharacterUtils.fill. The constructors, CustomAnalyzer and the nGram / edgeNGram factories (maxGramSize=2147483647) all reach it, at index time and at query time, while the n-gram token filters accept the same value and work. Introduced in 4.4.0 by LUCENE-5042 (2013); every release from 4.4.0 to 10.6.0 and main is affected. Opt-in and rare; documents are rejected one by one and nothing is corrupted.

The code (main 645d4fe14a20, lucene/analysis/common/.../analysis/ngram/)

  1. Accepted. NGramTokenizer.init throws only for minGram < 1 and minGram > maxGram (NGramTokenizer.java:118-124). EdgeNGramTokenizer extends NGramTokenizer (EdgeNGramTokenizer.java:30) and passes maxGram through; both factories read maxGramSize with getInt and pass it on unchecked (EdgeNGramTokenizerFactory.java:48-49, NGramTokenizerFactory.java:50-51).
  2. Read buffer wraps. charBuffer = CharacterUtils.newCharacterBuffer(2 * maxGram + 1024) with the comment "2 * maxGram in case all code points require 2 chars and + 1024 for buffering to not keep polling the Reader" (:128-132). 2 * 2147483647 is -2 in int, so the size is 1022, which passes the only check, bufferSize < 2 (CharacterUtils.java:39-43). The code-point buffer gets the same length (:133).
  3. Term buffer never grows. "// Make the term att large enough", termAtt.resizeBuffer(2 * maxGram) (:134-135), i.e. resizeBuffer(-2). CharTermAttributeImpl.resizeBuffer grows only if (termBuffer.length < newSize) (CharTermAttributeImpl.java:57-58), so the buffer keeps its initial ArrayUtil.oversize(10, Character.BYTES) chars (:30-32): 16 on a 64-bit JVM (read back after construction in an executed probe), 14 on a 32-bit one (ArrayUtil.java:174-203).
  4. Overflow. gramSize > maxGram (:162) is never true, so grams grow with the text. CharacterUtils.toChars(buffer, bufferStart, gramSize, termAtt.buffer(), 0) (:184) writes the 17th char of the 17-char prefix to index 16 of the 16-char buffer.
  5. From 1022 chars (text without supplementary characters). The compaction test at :146 is always true, so every call refills. After a first fill of 1022 one-char code points, buffer.length - bufferEnd is 0, and CharacterUtils.fill (:154) rejects it: "numChars must be >= 2 and <= the buffer size" (CharacterUtils.java:177-178).

Which values of maxGram (z3 model of the int arithmetic, confirmed on the jar)

maxGramwhat happens
up to 1,073,741,311no wrap; about 16.5 bytes of buffers per unit of maxGram, so 10^6 works and 10^8 runs out of memory at construction with a 512 MB heap
1,073,741,312 to 2,147,483,1362 * maxGram + 1024 is below 2: the constructor throws IllegalArgumentException: buffersize must be >= 2
2,147,483,137 to 2,147,483,647 (511 values, Integer.MAX_VALUE among them)accepted with a 2..1022-char read buffer and the default term buffer

Integer.MAX_VALUE by text length (minGram = 1, text without supplementary characters)

text lengthresult
1 to 16 charscorrect grams
17 to 1021 charsArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16
1022 chars or moreIllegalArgumentException: numChars must be >= 2 and <= the buffer size after the first gram

Text of supplementary characters fails as soon as a gram passes 16 chars (Index 17 out of bounds for length 16, for example 9 emoji = 18 chars), also from 1022 chars, because its first fill holds only 511 code points.

Not part of the claim (contrived): with a large minGram (1023 or more, or 512 or more for text of supplementary characters) on a text longer than the read buffer, incrementToken reaches assert exhausted; return false (:163-165) before any token, so the stream is silently empty with assertions off (NGramTokenizer(1023, Integer.MAX_VALUE) on 1100 a: 0 tokens instead of 3081) and throws AssertionError with them on. The fix below rejects these settings too.

Exposure and likelihood283 words
  • Who is hit. Users of the two tokenizers with maxGram = Integer.MAX_VALUE (or one of the 510 values just below it), for example "all prefixes" with EdgeNGramTokenizer. All entry points reach it: the constructors, CustomAnalyzer with edgeNGram / nGram, the factories' create(), and a schema with maxGramSize=2147483647.
  • Default vs opt-in. Opt-in: the n-gram tokenizers are optional analysis-common components with defaults maxGram = 2 (NGramTokenizer) and 1 (EdgeNGramTokenizer).
  • What goes wrong. The failure is deferred and depends on the data: a quick test with short strings passes, then every text of 17 chars or more throws from incrementToken. At index time addDocument throws for that document, the writer stays open, later documents index and CheckIndex is clean; at query time QueryBuilder throws the same exception. With minGram = 1 no output is silently wrong; only a contrived large-minGram setting ends a stream early (see the full explanation above).
  • Harder to trace in a long-running JVM. With default JVM flags, after repeated failures the JVM's fast-throw optimisation drops the message and stack: later failures show as ArrayIndexOutOfBoundsException: null with no Lucene frame.
  • Scale. None: 17 chars. Deterministic, single-threaded, assertions on or off.
  • Workaround. A finite maxGram (the longest text you expect), or EdgeNGramTokenFilter / NGramTokenFilter, which accept Integer.MAX_VALUE and work.
  • How likely. Rare. The idiom exists: an old Solr geohash plugin builds EdgeNGramTokenizer(input, Side.FRONT, 1, Integer.MAX_VALUE) on the pre-4.4 API, and its geohashes are 22 chars by default, so on 4.4.0 or later the same idiom would fail on every value. But the failure is loud on the first long value and the documented public searches found no report of it since 4.4.0 (2013).

Minimal repro

One JUnit method on the public API, in the n-gram package like the upstream tests: EdgeNGramTokenizer(1, Integer.MAX_VALUE) on the 17-char text abcdefghijklmnopq. It passes if the constructor rejects the value or the 17 prefixes come out, so it accepts either fix. It fails with assertions on and off.

TestEdgeNGramTokenizerMaxGramIntegerMaxValue.java (lucene/analysis/common, package org.apache.lucene.analysis.ngram, 1 test)45 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.analysis.ngram;

import java.io.StringReader;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestEdgeNGramTokenizerMaxGramIntegerMaxValue extends BaseTokenStreamTestCase {

  public void testSeventeenCharText() throws Exception {
    // expected: the 17 prefixes or an IllegalArgumentException; actual: ArrayIndexOutOfBounds
    String text = "abcdefghijklmnopq";
    String[] prefixes = new String[text.length()];
    for (int i = 0; i < prefixes.length; i++) {
      prefixes[i] = text.substring(0, i + 1);
    }
    EdgeNGramTokenizer tokenizer;
    try {
      tokenizer = new EdgeNGramTokenizer(1, Integer.MAX_VALUE);
    } catch (IllegalArgumentException acceptable) {
      return;
    }
    tokenizer.setReader(new StringReader(text));
    try {
      assertTokenStreamContents(tokenizer, prefixes);
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new AssertionError(
          "expected the 17 prefixes of " + text + " or an IllegalArgumentException, got " + e, e);
    }
  }
}
Command
cp $REPRO/TestEdgeNGramTokenizerMaxGramIntegerMaxValue.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/ngram/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.ngram.TestEdgeNGramTokenizerMaxGramIntegerMaxValue -Ptests.seed=DEADBEEF
Expected
The 17 prefixes a ... abcdefghijklmnopq, or an IllegalArgumentException from the constructor.
Observed
16 correct prefixes, then ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16 from NGramTokenizer.incrementToken.

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

More files, environment, full output258 words

Copy the test into lucene/analysis/common/src/test/org/apache/lucene/analysis/ngram/ of a Lucene checkout and run it with gradle (command below). 17 chars is the smallest failing input: 16 chars still work. With the fix it passes unchanged, so it doubles as the regression test.

The test logic has no random branch; it failed under every recorded seed, with assertions on and off. NGramTokenizer(1, Integer.MAX_VALUE), both factories, CustomAnalyzer, IndexWriter.addDocument and QueryBuilder fail the same way (executed with independent main() probes); they are left out of the test on purpose.

Executed: gradle on a clean 645d4fe14a20 (fails); the standalone runner on the stock main jars with -ea and -da and several seeds, and on the 10.6.0 and 10.5.2 release jars with -ea and -da (fails).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.6.0 and 10.5.2 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS), 64-bit
osLinux aarch64 (nothing platform-specific: it is int arithmetic)
Command (environment)
cp $REPRO/TestEdgeNGramTokenizerMaxGramIntegerMaxValue.java $LUCENE/lucene/analysis/common/src/test/org/apache/lucene/analysis/ngram/
cd $LUCENE && ./gradlew :lucene:analysis:common:test --tests org.apache.lucene.analysis.ngram.TestEdgeNGramTokenizerMaxGramIntegerMaxValue -Ptests.seed=DEADBEEF

Observed output

# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF (module prefixes and 5 test-framework frames trimmed)
TestEdgeNGramTokenizerMaxGramIntegerMaxValue > testSeventeenCharText FAILED
    java.lang.AssertionError: expected the 17 prefixes of abcdefghijklmnopq or an IllegalArgumentException, got java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16
        at org.apache.lucene.analysis.ngram.TestEdgeNGramTokenizerMaxGramIntegerMaxValue.testSeventeenCharText(TestEdgeNGramTokenizerMaxGramIntegerMaxValue.java:41)

        Caused by:
        java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 16
            at java.base/java.lang.Character.toChars(Character.java:9873)
            at org.apache.lucene.analysis.CharacterUtils.toChars(CharacterUtils.java:145)
            at org.apache.lucene.analysis.ngram.NGramTokenizer.incrementToken(NGramTokenizer.java:184)
            ...
            at org.apache.lucene.analysis.ngram.TestEdgeNGramTokenizerMaxGramIntegerMaxValue.testSeventeenCharText(TestEdgeNGramTokenizerMaxGramIntegerMaxValue.java:39)

# standalone runner
main 645d4fe14a20  -ea / -da   tests_run=1 failures=1   same ArrayIndexOutOfBoundsException at NGramTokenizer.java:184
10.6.0             -ea / -da   tests_run=1 failures=1   same
10.5.2             -ea / -da   tests_run=1 failures=1   same (CharacterUtils.toChars is line 111 there)

Why it looks unintended

  • The javadoc says only "the largest n-gram to generate"; the constructor validates maxGram and accepts this value.
  • The code sizes the buffers "in case all code points require 2 chars"; the n-gram token filters work with the same value.
  • Robert Muir called Integer.MAX_VALUE size parameters "a trap" (#8756), which led to construction-time checks in other tokenizers.
The full argument335 words
  • Contract. @param maxGram the largest n-gram to generate (NGramTokenizer.java:91, :107; EdgeNGramTokenizer.java:38, :49). No upper bound and no note that large values fail, there, in the factories or in package-info.
  • Intent in the code. "2 * maxGram in case all code points require 2 chars" (NGramTokenizer.java:130-132) and "Make the term att large enough" (:134): the wrap defeats both.
  • CHANGES. 4.4.0, LUCENE-3907: "EdgeNGramTokenizer now supports maxGramSize > 1024" (lucene/CHANGES.txt:13581), with no upper limit.
  • Siblings and history. EdgeNGramTokenFilter and NGramTokenFilter accept Integer.MAX_VALUE and give the right grams (executed). On 4.3.1 both tokenizers give the right grams for the same 16- and 17-char texts (executed; that version read at most 1024 chars).
  • Nearby tokenizers. Several analysis tokenizers reject oversized length settings up front with an IllegalArgumentException: KeywordTokenizer and CharTokenizer in their constructors (KeywordTokenizer.java:41-48, CharTokenizer.java:71) and StandardTokenizer.setMaxTokenLength (StandardTokenizer.java:95-99), against MAX_TOKEN_LENGTH_LIMIT = 1024 * 1024 (StandardTokenizer.java:80).
  • Maintainers (credit Robert Muir). On #8756 (LUCENE-7705), about size parameters of CharTokenizer and KeywordTokenizer: "I think Integer.MAX_VALUE is a trap for a number of reasons: enormous values will likely only blow up anyway (limited to 32766 in the index), if they don't they may create hellacious threadlocal memory usage in buffers, bad/malicious input could take out JVMs, etc.", and "Maybe follow the example of StandardTokenizer here", which led to MAX_TOKEN_LENGTH_LIMIT. On #6621 (LUCENE-5559, argument validation), about whether factories need their own checks: "if the factory would crash in some way that is confusing on certain inputs, then it would need a check too"; here neither the factories nor the tokenizer check. On #9395 (LUCENE-8348, open), a cost remark that proposes documentation, not a check: "it seems to surprise people that min=small, max=huge eats up a ton of resources".
  • Tests. No upstream test uses a huge maxGram (TestNGramTokenizer and TestEdgeNGramTokenizer go up to 1300). TestRandomChains.java:176 keeps random int arguments in -50..50 because the full range "could cause huge ram usage": about allocation, not a wrap.
The devil's-advocate check149 words

The strongest "works as designed" case (built by the intended-behaviour verifier): the buffers are sized from maxGram on purpose; the 4.4-4.10 EdgeNGramTokenizer javadoc warned that a maxGram larger than 1024 "will result in increased memory usage" (removed in 5.0); #9395 calls a huge max expensive; TestRandomChains avoids huge ints; configuration values are trusted (SECURITY.md:42). So Integer.MAX_VALUE can never be honoured as truly unbounded, and it fails loudly.

Why it fails: all of that is about cost, and the smaller huge values do fail fast at construction (OutOfMemoryError, or IllegalArgumentException: buffersize must be >= 2). As the adjudication put it: "That covers allocation cost or an OutOfMemoryError. It does not cover an accepted value wrapping into a tiny buffer and failing later with an internal-error exception." Nothing documents a limit. The adjudication kept the priority low and the confidence medium because the value is extreme.

Suggested fix

Recommended Reject maxGram above an overflow-safe bound

Reject maxGram above (Integer.MAX_VALUE - 1025) / 3 = 715,827,540 in NGramTokenizer.init with an IllegalArgumentException (+10 lines; both tokenizers and both factories inherit it). The bound keeps the tokenizer's buffer index arithmetic within int. The minimal test, the format check and all 1910 analysis-common tests pass.

/**
 * The largest accepted {@code maxGram}. The tokenizer buffers {@code 2 * maxGram + 1024} chars
 * and indexes that buffer with int arithmetic; this bound keeps those indexes in int range.
 */
public static final int MAX_NGRAM_SIZE_LIMIT = (Integer.MAX_VALUE - 1025) / 3;

// in init(), after the minGram > maxGram check
if (maxGram > MAX_NGRAM_SIZE_LIMIT) {
  throw new IllegalArgumentException(
      "maxGram must not be greater than " + MAX_NGRAM_SIZE_LIMIT + ", got: " + maxGram);
}
All options and trade-offs4 options · 714 words

Four options. Option A was validated with gradle in a scratch worktree at 645d4fe14a20; A', B and C were not executed. A rejecting fix (A, A', B) turns today's accidental success on texts of at most 16 chars into an error when the tokenizer is created; only C makes Integer.MAX_VALUE behave like maxGram = text length. A and A' also reject the contrived large-minGram settings that end a stream early.

Reject maxGram above an overflow-safe bound (validated by the checks) (recommended)

Add a constant MAX_NGRAM_SIZE_LIMIT = (Integer.MAX_VALUE - 1025) / 3 (715,827,540) and, in init after the minGram > maxGram check, throw IllegalArgumentException("maxGram must not be greater than " + MAX_NGRAM_SIZE_LIMIT + ", got: " + maxGram). Why this bound: it keeps every buffer index sum within int even under a loose bound on the loop state (bufferStart <= bufferEnd <= 2 * maxGram + 1024, minGram <= maxGram). The largest such sum is bufferStart + 1 + minGram (NGramTokenizer.java:163), at most 3 * maxGram + 1025. The hunt's z3 model proves that the modelled sums (:130, :146, :162, :163, :195) cannot wrap up to this bound. It is a conservative choice, not a proven maximum, and it does not cover the stream char offsets (:188, :209), which depend on the stream length, not on maxGram.

/**
 * The largest accepted {@code maxGram}. The tokenizer buffers {@code 2 * maxGram + 1024} chars
 * and indexes that buffer with int arithmetic; this bound keeps those indexes in int range.
 */
public static final int MAX_NGRAM_SIZE_LIMIT = (Integer.MAX_VALUE - 1025) / 3;

// in init(), after the minGram > maxGram check
if (maxGram > MAX_NGRAM_SIZE_LIMIT) {
  throw new IllegalArgumentException(
      "maxGram must not be greater than " + MAX_NGRAM_SIZE_LIMIT + ", got: " + maxGram);
}

Trade-offs

  • Integer.MAX_VALUE now fails when the tokenizer is created (for an Analyzer, when it first builds its components) instead of on the first long text; texts of at most 16 chars, which work by accident today, fail too.
  • Values just below the bound are still accepted and need about 12 GB of buffers per tokenizer (about 16.5 bytes per unit of maxGram), so they fail with OutOfMemoryError unless the heap holds that, as today; the tests check the rejection, not that values near the bound are practical.
  • Adds one public constant (it could be private); no other behaviour changes.

What the checks validated

  • Gradle, scratch worktree at 645d4fe14a20, OpenJDK 25.0.3, -Ptests.seed=DEADBEEF: without the fix the minimal test fails; with it :lucene:analysis:common:checkGoogleJavaFormat and the whole :lucene:analysis:common:test task pass (340 suites, 1910 tests, 0 failures, 18 skipped), including the minimal test, TestNGramTokenizer 14, TestEdgeNGramTokenizer 15, TestNGramFilters 13, TestNGramTokenFilter 17, TestEdgeNGramTokenFilter 17 and TestConditionalTokenFilter 12.
  • Shadowed onto the main jars: the minimal test passes with -ea and -da; the query probe and an independent probe get maxGram must not be greater than 715827540, got: 2147483647 for every Integer.MAX_VALUE case (both tokenizers, CustomAnalyzer, the IndexWriter and query paths) and for every boundary value from 1,073,741,312 up, while maxGram = 17, 1,000,000 and the token-filter control are unchanged.

The same check with a smaller, documented cap

Use StandardTokenizer.MAX_TOKEN_LENGTH_LIMIT (1024 * 1024) as the bound, as KeywordTokenizer and CharTokenizer do, and document it on the constructors.

Trade-offs

Matches those tokenizers and also removes most of the out-of-memory regime, but rejects values from 1,048,577 to 715,827,540, which work today when the heap is large enough. A maintainer's call. Not executed (the minimal test accepts it unchanged).

Overflow-checked sizing

Compute the sizes with Math.multiplyExact / Math.addExact in init and rethrow an ArithmeticException as IllegalArgumentException.

Trade-offs

Rejects exactly the values whose sizes wrap (from 1,073,741,312); 715,827,541 to 1,073,741,311 stay accepted (they need more than 11 GB of buffers) and are outside the model's index-overflow proof. Not executed.

Make a huge maxGram work as "no upper limit"

Size the buffers from a small value and grow the term buffer before the copy at NGramTokenizer.java:184 (and the read buffer when a gram needs more).

Trade-offs

The only option that makes Integer.MAX_VALUE behave like maxGram = text length, as the token filters do, but it changes the streaming loop and its memory profile: out of proportion for a rare setting. Not executed.

Downstream note

A product that lets its users define analyzers should cap maxGram itself: with this bug, one such setting makes every document or query text of 17 or more chars fail.

How it was found

A probe of numeric limits ran analysis components with size parameters far above the input length and compared them with the size set to the input length: the two n-gram tokenizers failed at Integer.MAX_VALUE, while the n-gram token filters passed. A z3 model of the int arithmetic then gave the exact range of affected values.

The method in full219 words

Method: executed differential testing, then a formal model.

  • First signal. A planning probe in a hunt round on numeric limits of public API arguments ran NGramTokenizer(1, n) and EdgeNGramTokenizer(1, n) on a 28-char text against maxGram = text length, for boundary values of n: ArrayIndexOutOfBoundsException for 2^31-1 and 2^31-2, IllegalArgumentException for 2^30, OutOfMemoryError for 1,073,741,224 with a 1 GiB heap. The n-gram token filters, ShingleFilter and LimitTokenCountFilter passed as controls.
  • Model. The finder modelled init's sizing and the index arithmetic of incrementToken in z3 with exact 32-bit wrap (21 queries): the exact partition of all maxGram values, and a bound (715,827,540) under which none of the modelled index sums can wrap.
  • Translation validation. A bounded-exhaustive comparison of the real jar with an exact reference n-gram semantics (8.7 million tokenizations, minGram 1 to 3 and near maxGram). For Integer.MAX_VALUE with minGram 1 to 3 it found no silently wrong output. Its silent results for Integer.MAX_VALUE all have minGram = Integer.MAX_VALUE - 1 on a text longer than the 1022-char read buffer: no tokens, as expected, but a final offset of 1022 instead of the text length (an AssertionError with assertions on). That is the contrived large-minGram corner described above, not claimed.
  • Reduction. A public-API JUnit test, later reduced to the single test shown here.

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-29 and twice on 2026-10-30) in GitHub issues, PRs and commits including the JIRA archive, the mailing lists, downstream trackers or the web. Related only: #9395 (a huge max is expensive) and #8756 (the Integer.MAX_VALUE "trap" in other tokenizers).

Searches4 searches

Related, not this bug: #9395 (LUCENE-8348, open: "min=small, max=huge eats up a ton of resources"), #8756 (LUCENE-7705: Integer.MAX_VALUE size parameters called a trap, which led to MAX_TOKEN_LENGTH_LIMIT in CharTokenizer and KeywordTokenizer), #13802 and the stale open PR #13814 (EdgeNGramTokenizer's default max gram size), #6106 (LUCENE-5042, which introduced the sizing and never discussed large values), #6621 (LUCENE-5559: lower-bound checks only), #5942 (LUCENE-4877: factories reject unknown parameters, not out-of-range values), #9009, #4980, #2304, #1834 (filter options, earlier rewrites and the old 1024-char clipping); downstream es#25841 (an ArrayIndexOutOfBoundsException for max_gram 6000 in TermsHashPerField, a different root cause), es#146497 and OpenSearch #23035 (gram-range limits and parsing). No open PR changes the sizing. The tracking issue and mailing-list thread do not mention n-grams, so there is no maintainer triage. Coverage limits: GitHub search is token-based, so exact-phrase matches on the error strings are approximate; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
Search tool (GitHub issues/PRs/commits incl. the JIRA archive, mailing lists, JIRA, downstream, web)2026-10-29; 2026-10-30 (twice)51 to 61 queries per run: class and parameter names, both error texts, overflow and Integer.MAX_VALUE phrasingsno hit about this bug (latest run: 469 hits, 0 high)
GitHub apache/lucene (manual)2026-10-29 and 2026-10-3036 queries: class names with maxGram, MAX_VALUE, 2147483647, overflow, out of bounds, both error strings, newCharacterBuffer, MAX_TOKEN_LENGTH_LIMIT, items since 2026-10-29; 18 related issues and 2 JIRA tickets read in full#9395 and #8756 remarks only
Open PRs2026-10-29 and 2026-10-30changed files of all open apache/lucene PRs (408 on 2026-10-30)only #13814 touches the n-gram tokenizers (default constant); #13390 edits an n-gram filter test and #14389 CharacterUtils case folding; none changes the sizing
Downstream, JIRA, mailing lists, web2026-10-3018 queries over Elasticsearch, OpenSearch, Solr, the archived lucene-solr repo and Lucene.NET; 13 JIRA queries; 42 list queries; 11 web queries; 9 Stack Overflow searchesresource-limit and parsing items only

History

Introduced in 4.4.0 by LUCENE-5042 (2013), which doubled the sizing to 2 * maxGram + 1024, added resizeBuffer(2 * maxGram) and made EdgeNGramTokenizer a subclass of NGramTokenizer. 4.3.1 passes; 13 releases from 4.4.0 to 10.6.0 and main fail when run, and every release tag from 4.4.0 to 10.6.0 has the sizing.

Versions tested7 rows

Introducing commits: e021451d6c0 (trunk, svn r1492185) and the branch_4x backport e6ce05111fe (svn r1492231), 2013-06-12, Adrien Grand, "LUCENE-5042: Fix the n-gram tokenizers and filters." (JIRA LUCENE-5042, now #6106; SVN era, no PR). The backport shadowed over the 4.4.0 jars reproduces the bug for both tokenizers. At its parent 0b198a431f4, NGramTokenizer throws NegativeArraySizeException in its constructor (from the earlier, never separately released new char[maxGram + 1024], LUCENE-4955), and EdgeNGramTokenizer, then a separate class with its own buffer, gives the right grams. In 4.4.0-5.5.5 only the current-version path is affected: factories with luceneMatchVersion < 4.4 built the unaffected Lucene43* tokenizers, removed in 6.0. The sizing is in all 125 upstream release tags from 4.4.0 to 10.6.0 (the 115 in the history clone, the nine 8.8.2-8.11.4 tags it lacks, read through the GitHub API, where NGramTokenizer.java is byte-identical in all nine, and 10.6.0), and in none of 2.1.0 to 4.3.1. No later change touched it; the last commit on the two tokenizers is 8ef6a0da568 (2020-12-28, reformatting). Old releases ran on JDK 25.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (gradle; standalone -ea and -da)
10.6.0, 10.5.2 affectedthe minimal test fails (-ea, -da); probe: 16 chars correct, 17 chars ArrayIndexOutOfBoundsException
4.4.0 to 10.0.0 11 versionsa portable probe fails on 4.4.0, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3 and 10.0.0: 16 chars correct, 17 chars ArrayIndexOutOfBoundsException
e6ce05111fe (introducing backport) affectedreproduces over the 4.4.0 jars, both tokenizers
0b198a431f4 (its parent) fails loudlyNegativeArraySizeException in the NGramTokenizer constructor; EdgeNGramTokenizer correct
4.3.1 not affectedboth tokenizers give the right grams for 16 and 17 chars (-ea, -da)
fix A (scratch worktree at 645d4fe14a20) not affectedthe minimal test and all 1910 analysis-common tests pass

Independent checks

An independent reproducer, a root-cause review by a model from another family, an intended-behaviour devil's advocate, an adjudication, a credibility audit, a current-main reproducer, a tie-breaker and two rounds of fact checks all confirm it; this doc re-ran the test with gradle, ran the query path and validated the fix on the whole analysis-common test task.

Checks and verdicts818 words

All cross-model reviews ran reading only and could not run Java. The refute lens returned a procedural "not real" ("a procedural verdict, not a static refutation") while agreeing on the root cause and that the inputs are in contract; the tie-breaker then ran what it asked for and confirmed the bug. The fact check of the previous doc found seven text errors (the large-minGram corner, the fix-bound rationale, the plugin's 22-char values, an unfounded judge attribution, the 1022-char threshold's qualifier, the open-PR result and a misattributed cross-model review check); the check from another model family added the 64-bit qualifier of the 16-char buffer and the parent-commit wording, among other caveats. A second round of both checks found that the doc misstated the scope of the finder's translation validation and listed the wrong earlier errors, and asked for the supplementary-character qualifier on the 1022-char threshold everywhere and narrower wording on novelty, nearby tokenizers and the query path. All are addressed here. No executing cross-model review exists.

Each independent check and its verdict
CheckVerdictWhat it found
finder (z3 model and translation validation) candidatez3 partition of all maxGram values (21 queries); 8.7 million tokenizations compared with a reference; JUnit fails 4 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; with its fix 4 of 4 pass and 42 upstream n-gram tests pass.
cross-model reviews, refute and intended lenses (OpenAI GPT-5.6, reading only) procedural "not real" / uncertainCould not run Java; agreed on the root cause and that the inputs are in contract; called the intended-behaviour case weak.
independent reproducer realOwn 6-test JUnit: its 3 Integer.MAX_VALUE tests fail and its 3 controls pass on main (-ea, -da), 10.5.2 and 9.12.3; a boundary probe confirms the partition.
root-cause review (OpenAI GPT-5.6, reading only) and the verifier's own check real (high)The review: both wraps (1022 and -2), the fix fixes it, inputs in contract. Separately, the verifier's own 32-bit and public-API check on the stock jars (not part of the review) matches.
intended-behaviour devil's advocate realBuilt the works-as-designed case (memory warnings, #9395, TestRandomChains) and rejected it; regime probe: 10^6 works, 10^8 out of memory, 2^30 rejected, Integer.MAX_VALUE accepted then fails at 17 chars.
adjudication confirmed (medium)Re-ran both JUnits (-ea, -da, a new seed); medium because of the no-noise risk; restricted the claim to Integer.MAX_VALUE.
Registration check and credibility audit keepSingle-test re-run fails on main and passes with the fix; the audit re-ran the finder's JUnit with a new seed (-da): 4 of 4 fail.
minimal-repro audit verifiedThe one test fails on main (-ea, -da) and passes with the fix.
current-main reproducer (two runs) realThe one test fails on main and 10.5.2 (-ea, -da) and passes with the fix; not fixed upstream.
tie-breaker (two runs) real (high)The one test on main and 10.5.2 (-ea, -da); an independent main() probe reading the 16-char term buffer after construction, with controls, NGramTokenizer, CustomAnalyzer, the 1022-char case, the construction boundary, the IndexWriter path and default JVM flags.
history check since 4.4.04.3.1 passes; 4.4.0, 6.0.0, 10.6.0 and main fail; the introducing backport reproduces, its parent does not.
first fact check (previous doc) bug real; 7 text errors (fixed)Re-ran the gradle repro, the fix on the whole module, probes on main, 10.6.0, 10.5.2 and 4.10.4 to 10.0.0, the factories and the large-minGram and supplementary cases.
first fact check of the brief (OpenAI GPT-5.6, reading only) bug real; caveats (addressed)Asked to qualify the 16-char buffer (64-bit JVM), minGram = 1, the parent-commit wording and the query path, which this doc now executes.
second fact check (this doc's previous revision) bug real; 2 text errors (fixed here)Re-ran the gradle repro (assertions on and off), fix A on the whole module, repro.sh with a new seed on main, 10.6.0 and 10.5.2, and its own probes on main, 4.3.1, 4.4.0, 8.11.4 and 9.12.3. Found the misstated translation-validation scope and the wrong list of earlier errors.
second fact check of the brief (OpenAI GPT-5.6, reading only) bug real; 1 error, wording (fixed here)Asked for the supplementary-character qualifier on the 1022-char threshold in the brief's impact section, and narrower wording on the upstream-status citation, release coverage, novelty, nearby tokenizers and the query path.
this doc gradle repro, query path, fixThe minimal test with gradle on a clean 645d4fe14a20 (fails) and with fix A (passes; format check and the whole analysis-common test task); the query path on main (fails) and with the fix (rejected at construction). This revision re-ran the finder's translation validation for Integer.MAX_VALUE only (unchanged harness, -ea and -da) and read the 8.8.2-8.11.4 release tags, which the history clone lacks, through the GitHub API.

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,589 words

Finding record

LFV-1007, snapshot of 2026-11-06

Registry title
NGramTokenizer and EdgeNGramTokenizer accept maxGram = Integer.MAX_VALUE, but their int buffer sizing wraps to a 1022-char read buffer and leaves the 16-char default term buffer, so the first text of 17 or more chars throws (ArrayIndexOutOfBoundsException up to 1021 chars, an unrelated IllegalArgumentException from 1022)
Area
analysis-common / n-gram tokenizers (NGramTokenizer.init buffer sizing, shared by EdgeNGramTokenizer; NGramTokenizerFactory and EdgeNGramTokenizerFactory pass maxGramSize through unchecked)
Priority
Low, rank 642: NGramTokenizer and EdgeNGramTokenizer are widely used but maxGram = Integer.MAX_VALUE is a rare setting and fails loudly without writing anything wrong.
Impact class
crash
Impact note
Applications on any release from 4.4.0 to 10.6.0 or main that build an NGramTokenizer or EdgeNGramTokenizer (directly, through CustomAnalyzer, or through NGramTokenizerFactory/EdgeNGramTokenizerFactory with maxGramSize=2147483647 in a schema) with maxGram = Integer.MAX_VALUE as 'no upper limit' get a tokenizer that the constructor accepts and that handles texts of up to 16 chars correctly, but every document or query text of 17 or more chars then throws from incrementToken (ArrayIndexOutOfBoundsException 'Index 16 out of bounds for length 16' for 17 to 1021 chars, IllegalArgumentException 'numChars must be >= 2 and <= the buffer size' from 1022 chars), so such documents are rejected at index time (a non-aborting per-document failure) and such queries fail; the index is not damaged, and maxGram values from 1,073,741,312 to 2,147,483,136 already fail at construction.
Summary
You build EdgeNGramTokenizer(1, Integer.MAX_VALUE) (or NGramTokenizer, or a factory with maxGramSize=2147483647) to mean 'all n-grams, no upper limit', and you expect the same tokens as with maxGram = text length, or else a clear IllegalArgumentException from the constructor, which validates maxGram and accepts it. Instead init computes the buffer sizes 2 * maxGram + 1024 and 2 * maxGram in int, which wrap to 1022 and -2, so the tokenizer gets a 1022-char read buffer and keeps its 16-char default term buffer. Texts of up to 16 chars come out right, but the first text of 17 or more chars throws ArrayIndexOutOfBoundsException 'Index 16 out of bounds for length 16' from incrementToken (from 1022 chars, an IllegalArgumentException about a buffer size the caller never passed).
Introduced
e021451d6c08433d2e442456b3b5925f4cdb7129 (trunk) / e6ce05111fe7c22b871fb87901d23970bc6b10b3 (branch_4x backport, the released code): LUCENE-5042 'Fix the n-gram tokenizers and filters' (Adrien Grand; JIRA issue now apache/lucene#6106; SVN era, no PR) [read: git log -S and git blame -w on the three sizing expressions in the full-history clone] [exec: e6ce05111fe's NGramTokenizer, EdgeNGramTokenizer and CharacterUtils compiled over the 4.4.0 jars reproduce the bug; its parent 0b198a431f4 does not: there NGramTokenizer throws NegativeArraySizeException at construction from LUCENE-4955's 'new char[maxGram + 1024]' (a03e38d5d05, never released on its own) and EdgeNGramTokenizer had its own growing buffer]. LUCENE-5042 doubled the factor to 2 * maxGram + 1024, so the top 511 values wrap back to a small positive size, made termAtt.resizeBuffer(2 * maxGram) a no-op for maxGram >= 2^30, and made EdgeNGramTokenizer a subclass of NGramTokenizer. Not a regression of a later change. (2013-06-12)
First affected version
4.4.0 [exec: fails with -ea and -da]; 4.3.1, the last release before it, passes [exec: -ea and -da; the old algorithm read at most 1024 chars and had no maxGram-sized buffer]
Affected versions
every release from 4.4.0 to 10.6.0 (the 10.6 RC, branch_10_6 4ae946e5a9b) and main 645d4fe14a20 [exec: 4.4.0, 4.10.4, 5.0.0, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, the 10.6 RC (core and analysis-common jars) and main all fail with the same ArrayIndexOutOfBoundsException 'Index 16 out of bounds for length 16' for both tokenizers (follow-up: a release-portable main() per API era, -da everywhere, -ea also on 4.4.0, 9.12.3 and 10.5.2); the finder's JUnit fails 4 of 4 on main (-ea, -da), 10.5.2 and 9.12.3; the independent reproduction's fails its 3 Integer.MAX_VALUE tests and passes its 3 controls on main, 10.5.2 and 9.12.3 (-ea, -da)] [read: the sizing expressions are in NGramTokenizer.java of all 115 release tags 4.4.0..10.5.2 and absent in 4.0.0..4.3.1]; in 4.4.0-5.5.5 only on the current-version path (factories with luceneMatchVersion < 4.4 used the unaffected Lucene43NGramTokenizer/Lucene43EdgeNGramTokenizer, removed in 6.0) [read]; not affected: 2.1.0-4.3.1 [read; 4.3.1 exec passes]; not fixed upstream (NGramTokenizer.java and EdgeNGramTokenizer.java last changed by 8ef6a0da568, author date 2020-12-28, reformatting only; upstream main head is 645d4fe14a20, author date 2026-10-28; no open PR changes the sizing). JDK 25.0.3 for all versions.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its “not real” is procedural), an adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal; also reached by Z3, exhaustive check
Upstream and prior reports
none found (2026-10-29, read-only; the prior-report record, verdict new, plus the finder's runs and the independent reproduction's, the intended-behaviour check's and the adjudication's searches): the scripted prior-report search (51 queries: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive and 6 web; 104 distinct hits, 0 high-overlap; the finder's earlier run with 51 other queries: 1205 hits, 0 high), 16 manual apache/lucene GitHub searches, every apache/lucene PR created since 2026-10-26 and issue created since 2026-10-25, the changed files of all 415 open PRs (none changes the sizing; only #13814, #13390, #14389 and #16911 touch ngram/ or CharacterUtils at all), the Solr, Elasticsearch and other downstream trackers, the LUCENE and SOLR JIRA archive and the Solr user lists. Recheck (2026-10-29): no apache/lucene item mentioning NGramTokenizer, EdgeNGramTokenizer, maxGram or ngram was updated since 2026-10-27; none of the 13 PRs and 10 issues created since 2026-10-27 (up to #16938) concerns n-grams; the newest commit on NGramTokenizer.java and EdgeNGramTokenizer.java is still 8ef6a0da568 (2020-12-28); upstream main head is 645d4fe14a20. Closest items, not reports: #9395 (LUCENE-8348, open: a maintainer notes that min=small, max=huge 'eats up a ton of resources'; a cost remark), #8756 (LUCENE-7705: a maintainer called Integer.MAX_VALUE size parameters a trap, which led to the CharTokenizer/KeywordTokenizer MAX_TOKEN_LENGTH_LIMIT; other classes), #13802 and the stale open PR #13814 (EdgeNGramTokenizer's default max gram size of 1), #6621 (LUCENE-5559, validation of negative values), #6106 (LUCENE-5042, which introduced the sizing and never discussed large values) and #4980 (LUCENE-3907); downstream elastic/elasticsearch#25841, #25887 and #146497 (n-gram resource limits, not this wrap).
Lucene commit tested
main 645d4fe14a20 (11.0.0-SNAPSHOT): the minimal test with gradle in a clean scratch worktree (fails) and with fix A (passes, with the format check and the whole analysis-common test task: 340 suites, 1910 tests, 0 failures); the standalone runner with -ea and -da (fails; passes with the fix); the 10.6.0 and 10.5.2 release jars (fails). A version-portable probe on 16- and 17-char texts: 4.3.1 passes; 13 releases from 4.4.0 to 10.6.0 and main fail. Upstream main 2f424fd428d (2026-10-30) has not changed the n-gram package, CharacterUtils or CharTermAttributeImpl.

Lucene and upstream sources

Finding record
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/NGramTokenizer.java:87-112 (at 645d4fe14a20) (constructor javadoc '@param maxGram the largest n-gram to generate', lines 91 and 107), :118-124 (init validates only minGram >= 1 and minGram <= maxGram), :128-133 (CharacterUtils.newCharacterBuffer(2 * maxGram + 1024) in int, with the comment on surrogate pairs and buffering, and the int buffer of the same length), :135 (termAtt.resizeBuffer(2 * maxGram)), :163-164 ('assert exhausted; return false'), :184 (CharacterUtils.toChars into termAtt.buffer(): the ArrayIndexOutOfBoundsException site)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/EdgeNGramTokenizer.java:30 (at 645d4fe14a20) ('extends NGramTokenizer'), :34-53 (both constructors pass maxGram to NGramTokenizer with edgesOnly = true; javadoc 'the largest n-gram to generate' at 38 and 49)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/EdgeNGramTokenizerFactory.java:45-53 (at 645d4fe14a20) and NGramTokenizerFactory.java:47-55 (maxGramSize read with getInt and passed through unchecked)
  • lucene/core/src/java/org/apache/lucene/analysis/CharacterUtils.java:36-44 (at 645d4fe14a20) (newCharacterBuffer: 'buffersize must be >= 2'), :174-179 (fill: 'numChars must be >= 2 and <= the buffer size')
  • lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/CharTermAttributeImpl.java:30-32 (at 645d4fe14a20) (initial term buffer oversize(10, 2) = 16 chars), :57-64 (resizeBuffer grows only when length < newSize, so -2 is a no-op)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/core/KeywordTokenizer.java:41-47 (at 645d4fe14a20) (the module's convention: an oversized size is rejected with IllegalArgumentException at construction)
  • lucene/CHANGES.txt:13581 (at 645d4fe14a20) (4.4.0, LUCENE-3907: 'EdgeNGramTokenizer now supports maxGramSize > 1024', no limit stated)
  • lucene/analysis.tests/src/test/org/apache/lucene/analysis/tests/TestRandomChains.java:176 (at 645d4fe14a20) (TODO: full int range 'could cause huge ram usage'; about allocation, not a wrap)
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1987-1994 (at 645d4fe14a20) (an analyzer exception is non-aborting; the document is marked deleted)
  • SECURITY.md:28, :38, :42 ('configuration values are trusted as given'), :48-50 (in-scope reports)
The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/NGramTokenizer.java:62-64, :91, :107, :118-136, :146-154, :162-165, :184
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/EdgeNGramTokenizer.java:30-31, :38, :40-42, :49, :51-53; EdgeNGramTokenizerFactory.java:48-49, :62; NGramTokenizerFactory.java:50-51, :68
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/analysis/CharacterUtils.java:39-43, :139-147, :174-178
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/analysis/tokenattributes/CharTermAttributeImpl.java:30-32, :57-66; lucene/core/src/java/org/apache/lucene/util/ArrayUtil.java:174-203 (64-bit vs 32-bit rounding)
Who is affected
  • GitHub dsmiley/SOLR-2155 src/main/java/solr2155/solr/schema/GeoHashField.java:69, :81 and src/main/java/solr2155/lucene/spatial/geohash/GridNode.java:51 (read-only GitHub API, 2026-10-30: the idiom, default length 22)
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/NGramTokenizer.java:63-64; EdgeNGramTokenizer.java:31-32 (defaults)
Why it looks unintended
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/NGramTokenizer.java:91, :107, :118-124, :128-135; EdgeNGramTokenizer.java:38, :49
  • a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:13581
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/core/KeywordTokenizer.java:41-48; lucene/analysis/common/src/java/org/apache/lucene/analysis/util/CharTokenizer.java:71; lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizer.java:80, :95-99
  • a Lucene main checkout (645d4fe14a20) lucene/analysis.tests/src/test/org/apache/lucene/analysis/tests/TestRandomChains.java:176
  • GitHub apache/lucene #8756 (LUCENE-7705), comment by Robert Muir (read with gh issue view, 2026-10-30)
Suggested fix
  • a Lucene main checkout (645d4fe14a20) lucene/analysis/common/src/java/org/apache/lucene/analysis/ngram/NGramTokenizer.java:146, :162-163, :175, :184, :188, :195, :209
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/analysis/standard/StandardTokenizer.java:80

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20, the minimal repro (the 17-char text) and an executed probe on the stock main jars (assertions on and off) and with the fix.

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.