A BreakIterator hands out boundaries: offsets where a sentence starts or the text ends. This one gets its sentences from OpenNLP's detector, which leaves out leading whitespace.
setText(text)splits the text. The boundaries are the start of the text, the start of each later sentence, and the end of the text: 0, 33 and 60 for the two-sentence example below.first()returns the start of the text andnext()the next boundary;last()returns the end andprevious()the boundary before, orDONEonce it is back at the first boundary.following(offset)returns the first boundary after any offset from the start to the end of the text;isBoundary(offset)is built on it.
- You split
" Sentence number 1 has 6 words. Sentence number 2, 5 words."(two leading spaces, 60 characters) withOpenNLPSentenceBreakIterator. Walking forward withfirst()andnext()gives the boundaries 0, 33, 60. - You walk backward with
last()andprevious(), the reverse loop from theBreakIteratorjavadoc, and expect 60, 33, 0, thenDONE. - You get 60, 33, 2 (not a boundary), then
ArrayIndexOutOfBoundsException: Index -1. On the one-sentence text" Sentence number 1 has 6 words.",following(0)recurses untilStackOverflowError. Without the leading spaces both calls work. - Why: OpenNLP's sentence detector trims whitespace, so it reports the first sentence as starting at 2.
setTextstores 2, whilefirst()andnext()start at 0.previous()stops only at 0, so after 2 it reads the start before the first one (index -1);following(0)searches for the sentence that contains offset 0 and never finds one.
Code that uses this public BreakIterator directly: a backward walk, or following(), isBoundary() or next(-n) at an offset inside leading whitespace. There, any text that starts with whitespace followed by a sentence fails, every time. Indexing and query analysis through OpenNLPTokenizer are not affected; the Unified and FastVector highlighters did not fail in 41 executed calls on ordinary fields.
Mechanism, terms and numbers706 words
OpenNLPSentenceBreakIterator, the public java.text.BreakIterator of the OpenNLP analysis module, stores the sentence starts that OpenNLP's detector reports, and the detector trims whitespace. For a text that starts with whitespace the first stored start (2 for two leading spaces) therefore lies after the beginning of the text, which first() and next() treat as the first boundary. Every method that uses the stored first start then goes wrong: a backward walk returns the non-boundary 2 and then throws ArrayIndexOutOfBoundsException, following() (and the JDK's default isBoundary()) at an offset inside the whitespace recurses until StackOverflowError on a one-sentence text or, with two or more sentences, fails an internal assertion when assertions are enabled (with them off the result is right), and preceding() and next(-n) return 2. One leading space, newline, tab or no-break space is enough. OpenNLPTokenizer, the class's only user in Lucene, only walks forward and is not affected. Every release from 7.3.0 to 10.6.0 (10.6.0 by reading) and main is affected; a one-statement fix in setText was validated with the module's tests.
OpenNLPSentenceBreakIterator: apublic finaljava.text.BreakIteratorin the exported packageorg.apache.lucene.analysis.opennlp(OpenNLPSentenceBreakIterator.java:26-27). It keeps an arraysentenceStartswith the start offset of each detected sentence and an indexcurrentSentence.- The detector:
NLPSentenceDetectorOp.splitSentencescalls OpenNLP'sSentenceDetectorME.sentPosDetect(NLPSentenceDetectorOp.java:39-47), which trims whitespace from each sentence span. For" Sentence number 1 has 6 words. Sentence number 2, 5 words."it returns the spans[2..32)and[33..60). Every opennlp-tools version Lucene has bundled (1.8.3 to 2.5.12) trims. A single leading space, newline, tab, no-break space (U+00A0) or em space (U+2003) is trimmed the same way.
setTextstores the spans' starts unchanged (:195-205, the store at:203):sentenceStarts = [2, 33].first()returns the begin index, 0 (:44-48), andnext()moves tosentenceStarts[1]= 33 and then to the end, 60 (:63-72). The forward walk counts the two spaces as part of sentence 0, so 2 is never returned forward.previous()returnsDONEonly when the current index equals the begin index (:119). Fromlast()(60) it moves tosentenceStarts[1]= 33 (:126-127), then tosentenceStarts[0]= 2 (:129). 2 is not the begin index, so the next call readssentenceStarts[-1]:ArrayIndexOutOfBoundsException: Index -1. A one-sentence text fails the same way after32, 2.following(pos)accepts any offset from begin to end (:76-77), returnsDONEin the last sentence (:81-87), and otherwise runs a binary search commented "there are at least two sentences" (:88-90). An offset inside the leading whitespace is below every stored start. With one sentence (sentenceStarts = [2]) the search ismoveToSentenceAt(0, 0, -1), which calls itself with the same arguments forever (:97-102):StackOverflowError. With two sentences the assertionpos >= sentenceStarts[currentSentence]fails (:110) when assertions are on; with them off it returns 33, the right value, by accident.- For a non-zero offset the JDK's default
isBoundary(offset)callsfollowing(offset - 1)(offset 0 returnstruedirectly), soisBoundary(1)on a one-sentence text with one leading newline overflows the stack too; on a two-sentence text it fails the same assertion with-eaand returnsfalsewith-da.preceding(38)andnext(-2)afterlast()returnsentenceStarts[0]= 2 (:157,:175): no crash, but not a boundary. - Trailing whitespace is harmless:
last()and the end checks use the end index (:54,:64,:85), never a span end. Andpreceding()already handles an offset below the first stored start (:143-149, present since the class was added);previous(),following()andnext(int)do not.
On main 645d4fe14a20 with the module's test sentence model: forward walk [0, 33, 60]; backward walk 60, 33, 2, then ArrayIndexOutOfBoundsException; following(0) and following(1) on the one-sentence text: StackOverflowError; following(0) on the two-sentence text: AssertionError with -ea, 33 with -da; preceding(38) = 2 and next(-2) = 2. The same text without the leading spaces walks 58, 31, 0, DONE; with three trailing spaces 61, 31, 0, DONE.
Exposure and likelihood326 words
- Who is exposed. Users of the opt-in
lucene-analysis-opennlpmodule who navigateOpenNLPSentenceBreakIteratorthemselves, as a generaljava.text.BreakIterator. Not default; Lucene itself never does this. - Lucene's own paths are safe.
OpenNLPTokenizer(OpenNLPTokenizer.java:52), the only in-tree user, drives the iterator throughSegmentingTokenizerBase, which calls onlysetText,current()andnext()(SegmentingTokenizerBase.java:114,:167,:194,:199); the failing texts tokenize and index normally (executed).UnifiedHighlighterandFastVectorHighlighterconfigured with this iterator made 32 and 9 calls on ordinarily tokenized fields without an exception:FieldHighlighterasks for boundaries only at offsets inside a match (FieldHighlighter.java:182,:186), which never lie in the leading whitespace, andBreakIteratorBoundaryScannercallslast()then oneprevious()(BreakIteratorBoundaryScanner.java:40-41), which stops before the failing step. Only a contrived field whose tokens cover the leading whitespace (EdgeNGramTokenizer(1, 3), query gram" S") madeUnifiedHighlighter.highlightthrowStackOverflowError. - What it takes. One whitespace character (space, newline, tab, no-break space, em space) before the first sentence, with OpenNLP's model-backed detector: the trim is in its code, not in the model, though every run used the module's test models. The no-model
NLPSentenceDetectorOp()returns one untrimmed span (NLPSentenceDetectorOp.java:43-45) and is not affected, and whitespace-only text has no sentence and walks correctly. Deterministic: the backward walk and the one-sentencefollowing()fail with assertions on and off, the two-sentencefollowing()only with them on. - Effect. One failed call.
ArrayIndexOutOfBoundsExceptionis aRuntimeException;StackOverflowErroris anError, socatch (Exception e)does not stop it. It comes within milliseconds (at most 9 ms over 20 runs), nothing is written to an index, and the iterator works again after the nextsetText. - Versions. Every release from 7.3.0 (2018) to 10.6.0 (10.6.0 by reading), and main.
- How likely. Rare: a public code search (2026-10-26) found no use of the class outside its tokenizer in Solr, Elasticsearch or OpenSearch, and the recorded prior-art search found no public report.
The test shows that OpenNLPSentenceBreakIterator.following(0) on a one-sentence text that starts with two spaces recurses until StackOverflowError.
/*
* 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.opennlp;
import java.text.BreakIterator;
import org.apache.lucene.analysis.opennlp.tools.OpenNLPOpsFactory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.ClasspathResourceLoader;
public class TestOpenNLPSentenceBreakIteratorLeadingSpace extends LuceneTestCase {
public void testFollowingInLeadingSpace() throws Exception {
OpenNLPOpsFactory.getSentenceModel("en-test-sent.bin", new ClasspathResourceLoader(getClass()));
BreakIterator bi =
new OpenNLPSentenceBreakIterator(OpenNLPOpsFactory.getSentenceDetector("en-test-sent.bin"));
String text = " Sentence number 1 has 6 words.";
bi.setText(text);
// expected: text.length() or DONE (as without the leading spaces); actual: StackOverflowError
int next = bi.following(0);
assertTrue("following(0) = " + next, next == text.length() || next == BreakIterator.DONE);
}
}
# in a copy of this finding's repro folder (repro/LFV-623/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/analysis/opennlp/build/libs/lucene-analysis-opennlp-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.analysis.opennlp.TestOpenNLPSentenceBreakIteratorLeadingSpace
# the module(s) analysis-opennlp also need their third-party runtime jars
- Expected vs actual
- Expected: text.length() or DONE (DONE is what the class returns without the leading spaces); actual on main 645d4fe: StackOverflowError in moveToSentenceAt (with -ea and -da).
More files, environment, full output36 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.StackOverflowError
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
org.apache.lucene.analysis.opennlp.OpenNLPSentenceBreakIterator.moveToSentenceAt(OpenNLPSentenceBreakIterator.java:102)
[...]previous()must returnDONEat the first boundary, andfollowing()lists onlyIllegalArgumentException; the class's own check accepts offset 0.- The class itself treats 0 as the first boundary:
first()returns it andprevious()stops there. - Upstream tests assert
previous(),following(),preceding()andnext(n); an earlierpreceding()crash (#12210) was fixed as a bug.
The full argument308 words
- Contract (JDK 25 javadoc).
previous(): "Returns the boundary preceding the current boundary. If the current boundary is the first text boundary, it returnsBreakIterator.DONE".following(int offset): "Returns the first boundary following the specified character offset"; the only listed exception isIllegalArgumentExceptionfor an offset "less than the first text boundary or greater than the last text boundary", and the class's own check accepts 0 (OpenNLPSentenceBreakIterator.java:76-77). TheBreakIteratorclass javadoc's example "print each sentence in reverse order" is thelast()/previous()loop that crashes. - The class's own boundaries.
first()returns the begin index (:44-48),previous()stops there (:119), and the upstream tests assertfirst()equals the begin index (TestOpenNLPSentenceBreakIterator.java:127-128,:219). The stored first start is the only place that disagrees. - Public, tested as a general iterator.
public final classin an exported package (module-info.java:25), no@lucene.internalor@lucene.experimentaltag, javadoc "A BreakIterator that splits sentences using an OpenNLP sentence chunking model". The upstream test assertsprevious(),following(),preceding()andnext(n)(TestOpenNLPSentenceBreakIterator.java:84-115,:126-149). - Precedent. #12210, another contributor's report of an
ArrayIndexOutOfBoundsExceptionin this class'spreceding(), was fixed as a bug by #13170 (first released in 10.0.0). - A coverage gap, not a decision. The upstream tests cover whitespace-only text and padding before a slice's begin index (
testWhitespaceOnly,testSliceStart), never an iterated range that starts with whitespace followed by a sentence. With the fix all 33 upstream tests of the module still pass. - Anticipated in one method.
preceding()has handled an offset below the first stored start since the class was added (:143-149);previous()andfollowing()never did. - Reference behaviour. The JDK's sentence iterator walks the same text with a leading newline correctly (0, 32, 59 forward and backward), and this class handles trailing whitespace correctly.
The devil's-advocate check162 words
The strongest case for "intended or misuse": the class was added as a "shim class" so that OpenNLPTokenizer can extend SegmentingTokenizerBase (LUCENE-2899 patch comment), and the tokenizer only walks forward; the class already documents a deviation from the BreakIterator javadoc (the JDK-8015110 comments: DONE in the last sentence for following(), before the first start for preceding()); and callers could trim their text.
Why it fails: nothing marks the class internal or forward-only, its tests assert backward and random-access navigation, and #12210 came from a direct preceding() user. JDK-8015110 ("following(int) changes the current position when it returns DONE") justifies returning DONE, not crashes or a wrong first start, and the fix leaves that convention unchanged. No javadoc asks for trimmed text, setText accepts any text, and one newline triggers it. Verdict: not intended, a defect. The cross-model reviews (refute and works-as-designed) rated the "intended" case weak; the devil's advocate found that no argument survives, and the adjudication confirmed the bug.
Recommended A. Store the begin index as the first sentence start
In setText, after storing the detector's starts, set sentenceStarts[0] to the text's begin index: the first sentence starts where first() already says it does. One statement. Validated: the 3 new tests, the upstream iterator suite and all 36 module tests pass with assertions on and off, and check passes.
sentenceStarts[i] = spans[i].getStart() + text.getBeginIndex();
}
+ if (sentenceStarts.length > 0) {
+ // the first sentence begins at the start of the text: first() and next() already treat any
+ // whitespace before the detected (trimmed) sentence start as part of the first sentence
+ sentenceStarts[0] = text.getBeginIndex();
+ }
}All options and trade-offs4 options · 803 words
Option A was applied in a scratch worktree of main 645d4fe14a20 and run through gradle with assertions on and off; options B and C are described only. The workaround needs no Lucene change and was run on unfixed main.
In setText (OpenNLPSentenceBreakIterator.java:195-205), after the loop that stores spans[i].getStart() + text.getBeginIndex(), set sentenceStarts[0] = text.getBeginIndex() when there is at least one sentence. This is the finder's fix.
What it changes. Only texts that start with whitespace (otherwise sentenceStarts[0] already equals the begin index). Backward walks end ..., 0, DONE; following(0) on two sentences returns 33 with assertions on too; preceding(38) and next(-2) return 0 instead of 2; following() inside the whitespace of a one-sentence text returns DONE, the class's existing last-sentence convention. The detector's spans are untouched.
sentenceStarts[i] = spans[i].getStart() + text.getBeginIndex();
}
+ if (sentenceStarts.length > 0) {
+ // the first sentence begins at the start of the text: first() and next() already treat any
+ // whitespace before the detected (trimmed) sentence start as part of the first sentence
+ sentenceStarts[0] = text.getBeginIndex();
+ }
}Trade-offs
- One statement at the root cause; every navigation method inherits the corrected start. No performance effect (one store per
setText). - With the fix, the JDK's default
isBoundary(1)on a one-sentence text with one leading character throwsIllegalArgumentExceptioninstead ofStackOverflowError:following(0)now returns the class's usualDONE, andisBoundaryturnsDONEinto that exception. A one-sentence text without leading whitespace gets the same exception today. That last-sentence convention is upstream-asserted and separate from this bug. preceding()'s branch for an offset below the first start (:143-149) becomes unreachable; it can stay or go.- A caller that used
preceding()ornext(-n)as a snippet start in the second sentence now gets offset 0, the leading whitespace included, instead of 2 (read, not run through a highlighter).
What the checks validated
- The 3 new tests pass with assertions on and off (stock: 3 of 3 and 2 of 3 fail).
- The upstream
TestOpenNLPSentenceBreakIterator(9 tests) and all of:lucene:analysis:opennlp:test(36 tests: 33 upstream + 3 new) pass with assertions on and off. :lucene:analysis:opennlp:check -x testpasses (google-java-format, ecjLint, forbiddenApis, licenses), with the new test in place.- A plain-Java probe of every symptom: backward walks
60, 33, 0, DONEand32, 0, DONE;following(0)33 (two sentences) andDONE(one sentence);preceding(38)0;next(-2)0. - Earlier, in the hunt: the finder's and the reproducer's tests pass with it with assertions on and off (the adjudication's re-run with them on), the history probe passes with it on 7.3.0 and main, and the walk oracle's backward-walk and
next(-k)checks report 0 failures and 0 crashes (its remainingnext(k)failures are the separate LFV-622 bug).
Keep the trimmed starts and treat sentence 0's boundary as the begin index wherever a start is returned or compared: previous() (:127, :129), following() and moveToSentenceAt() for offsets below the first start (:88-112), preceding() (:157) and next(int) (:175).
Trade-offs
Four methods must agree, and fixing only previous() and following() removes the crashes but leaves preceding() and next(-n) returning the non-boundary 2. Same end result as A with more code. Not validated.
Make NLPSentenceDetectorOp.splitSentences (NLPSentenceDetectorOp.java:39-47) start the first span at 0.
Trade-offs
Changes the output of a public method in the exported tools package, whose callers may rely on the trimmed spans; the iterator is its only in-tree caller (OpenNLPSentenceBreakIterator.java:199). Not recommended, not validated.
Give the iterator a range that starts at the first non-whitespace character; offsets stay in the original text's coordinates because the class adds getBeginIndex() (:203):
int begin = 0;
while (begin < text.length()
&& (Character.isWhitespace(text.charAt(begin)) || Character.isSpaceChar(text.charAt(begin)))) {
begin++;
}
bi.setText(new StringCharacterIterator(text, begin, text.length(), begin));Trade-offs
The whitespace test must be at least as wide as OpenNLP's: the detector also trims U+00A0, for which Character.isWhitespace is false, hence the isSpaceChar term. The snippet is a method fragment (bi is the iterator, text the String) and needs import java.text.StringCharacterIterator. Offsets inside the skipped whitespace are then outside the range, so following() and preceding() there throw the documented IllegalArgumentException (:76-77, :137-138).
What the checks validated
On unfixed main this gives forward [2, 33, 60], backward [60, 33, 2] then DONE, and following(2) = 33, with no crash. The snippet exactly as printed was run with assertions on and off for the leading characters two spaces, newline plus tab, U+00A0, U+2003 and none: no crash, and every backward walk ends in DONE.
An application that walks this iterator over user-supplied text can be made to fail that call by any text that starts with whitespace; the StackOverflowError is an Error, so catch (Exception) does not stop it.
A bounded-exhaustive check ran the real iterator with a stub sentence detector, calling following() and preceding() at every offset of 84 sentence layouts; every crash sat in a leading gap. A second oracle, "walking backward must mirror walking forward", failed on every layout with a leading gap and on none without.
The method in full164 words
- Method (semi-formal). In the hunt's unit on the OpenNLP filters against the OpenNLP library, the finder ran the real
OpenNLPSentenceBreakIteratoragainst a stub sentence detector (the class only consumes the detector's spans): 84 configurations (1 to 14 sentences, start gaps 1 to 3, the first sentence at offset 0 or 2),following()andpreceding()at every offset andnext(k)fromfirst(), 3,192 checks. The first signal: 84 crashes, all at offsets inside the leading gap, none without one. - Walk oracle. An internal-consistency check that does not depend on the class's documented JDK-8015110 deviation: the backward walk must equal the reversed forward walk, and
next(±k)must equal k single steps. Its backward-walk check crashed on all 42 configurations with a leading gap and on none without; with the fix, 0 failures and 0 crashes (itsnext(k)failures are the separate LFV-622 bug, which needs its own fix). - Then a public-API JUnit with the real detector and the module's test model, and the checks below.
No prior report found (searched 2026-10-26: 290 queries over GitHub issues, PRs, commits and code, all 407 open PRs, JIRA, the Lucene lists and the web). Closest: #12210, a different crash in the same class's preceding(), fixed by #13170.
Searches7 searches
18 items were read; none describes this bug. The upstream file is unchanged through main 645d4fe14a2 (2026-10-28); a read-only GitHub refresh on 2026-10-29 (issue searches for the class name and five symptom phrasings) returned only #12210, #13170 and #3973; the 2026-10-26 search is the latest full one. Keyword search only; mailing-list hits read by subject and snippet; code search shows only its first 100 matches; private reports cannot be seen.
| Where | When | How | Found |
|---|---|---|---|
| scripted prior-report search | 2026-10-26 | two runs of 76 queries each plus the finder's earlier 51: GitHub issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, JIRA, the web | no report; high hits only #12210 and #13170 |
| GitHub (manual) | 2026-10-26 | 38 GraphQL and 9 gh searches; GitHub-wide issue and code search; the file lists of all 407 open apache/lucene PRs | no report; open PRs touching the file: #12295 (stale instanceof hunk) and #16911 (deletes branch_10x) |
| JIRA (LUCENE, SOLR, OPENNLP) | 2026-10-26 | 16 REST searches; all comments of LUCENE-2899 | nothing about leading whitespace, previous() or following(); LUCENE-2899 calls the class a "shim class" |
| Mailing lists | 2026-10-26 | 15 manual archive queries plus the tool's list queries | only the 2022 filter-side OpenNLP sentence threads (other code) |
| Downstream and OpenNLP | 2026-10-26 | Solr, Elasticsearch, OpenSearch, Lucene.NET and OpenNLP repositories | no report; the Lucene.NET port has the same code (read, not run); OpenNLP's span refactor keeps the trimming |
| Closest items (read) | 2026-10-26 | full threads | #12210 / #13170 (preceding() search start, fixed in 10.0.0), #3973 (LUCENE-2899, added the class) |
| GitHub refresh (read-only) | 2026-10-29 | issue searches for the class name and five symptom phrasings; upstream main and the file's commit history | only #12210, #13170 and #3973; upstream main still 645d4fe14a20, the file's last commit still #14216 |
In the class since it was added by LUCENE-2899 (3e2f9e62d77 / branch_7x b720e1ee3a5, 2017-12-15), first released in 7.3.0. Fails on all 12 executed releases from 7.3.0 to 10.5.2 and on main; the failing methods are unchanged at all 60 release tags read (8.8.2 to 8.11.3 inferred). Not fixed upstream.
Versions tested7 rows
Introduced with the class by LUCENE-2899, "Add OpenNLP Analysis capabilities as a module" (committed by Steve Rowe, 2017-12-15): b720e1ee3a5 on branch_7x (first release 7.3.0) and 3e2f9e62d77 on master (first release 8.0.0), the same file blob. A patch-era JIRA issue, migrated as apache/lucene#3973; no GitHub PR. git blame -w attributes every code line of setText, previous(), following() and moveToSentenceAt() to 3e2f9e62d77. The later commits are a formatting-only reformat (LUCENE-9570: a blank import line, the class javadoc, the two JDK-8015110 comments and the new String(...) call), a change of preceding()'s search start (#13170, 10.0.0) and an instanceof pattern (#14216). None changes the code of the failing methods.
What was executed. A plain main() probe using only API unchanged since 7.3.0, each release on its own jars and its own test sentence model, every run on OpenJDK 25.0.3, Linux aarch64; with every bundled opennlp-tools version from 1.8.3 to 2.5.12 the detector returns the spans [2,32) [33,60). The finder's JUnit also fails on 9.1.0, 9.4.2, 9.12.3, 10.0.0, 10.5.2 and main. At all 60 release tags from 7.3.0 to 10.5.2 in the local history clone, the methods involved match main once comments and whitespace are removed (read); 8.8.2 to 8.11.3 are inferred (tags not in the clone).
Not fixed upstream. Upstream main 645d4fe14a2 (2026-10-28), branch_10x, branch_10_6 and 10.5.2 carry the same file blob (read, not executed), so 10.6.0 (released 2026-10-29) and the unreleased lines are affected too.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | new test 3 of 3 fail with assertions on, 2 of 3 off (gradle); probe: backward walk 60, 33, 2 then AIOOBE, following(0) StackOverflowError |
main a9779ef3c193 | affected | history probe, the finder's and the reproducer's JUnit fail (same file blob) |
| 10.0.0 and 10.5.2 | affected | history probe fails (10.5.2 with -ea and -da); the finder's and the reproducer's JUnit fail on 10.5.2 |
| 9.0.0, 9.1.0, 9.4.2 and 9.12.3 | affected | history probe fails on 9.0.0 and 9.12.3; the finder's JUnit fails on 9.1.0, 9.4.2 and 9.12.3 |
| 7.3.0 to 8.11.4 (6 releases) | affected | history probe fails on 7.3.0 (-ea and -da), 7.4.0, 7.5.0, 7.7.3, 8.0.0 and 8.11.4 |
| 7.2.1 | no class | the class does not exist yet |
| fix A on 7.3.0 and on main | not affected | history probe passes: [60, 33, 0], DONE, 33; on 645d4fe14a20 the 3 new tests and all 36 module tests pass |
An independent reproducer (own test, different trigger), an intended-behaviour devil's advocate, a works-as-designed review by a reviewer from another model family and an adjudication all judged it real and re-ran it. A read-only refute review said "not real" only because it could not run code.
Checks and verdicts394 words
The hunt's re-runs used main a9779ef3c193, whose iterator file is byte-identical to 645d4fe14a20; the cross-model reviews could only read code. This doc's own fix validation ran on 645d4fe14a20 through gradle.
| Check | Verdict | What it found |
|---|---|---|
| Cross-model refute review | not real (low), procedural | An independent reviewer from another model family (OpenAI GPT-5.6), reading only: it could run no command, and its notes say the verdict is "procedural ... only because the required independent test could not be created or run". It agreed with the root cause and found the inputs within the contract (8 evidence items for real, 2 against: the missing run, and the tokenizer's forward-only use, which bears on reach). |
| Works-as-designed review | real (low) | The same reviewer family with an intended-behaviour lens, reading only: the "intended" argument is weak. The step re-ran the finder's JUnit on main: 3 of 3 fail with assertions on, 2 of 3 off. |
| Independent reproducer | real | Own public-API test with a newline plus tab trigger and a check that the detector returns span starts [2, 33] with the prefix and [0, 31] without: 5 of 6 fail on main with assertions on, 4 of 6 off, the control passes; 10.5.2 and 9.12.3 fail; 6 of 6 pass with the fix. Also found preceding(38) = 2 and next(-2) = 2. |
| Intended-behaviour devil's advocate | real | No "intended" or "misuse" argument survives (shim class, JDK-8015110 deviation, trim-your-text, trimmed start as a boundary, reachability, upstream-asserted behaviour). A single leading newline or tab suffices; the JDK sentence iterator and a trailing-whitespace control walk correctly; the 9 upstream iterator tests pass with the fix. |
| Adjudication | confirmed (high) | Re-ran the reproducer's test (5 of 6 fail with assertions on, 4 of 6 off; 6 of 6 pass with the fix) and the finder's (3 of 3 fail). |
| Follow-up probes and audit | affected 7.3.0 to 10.5.2; not security-sensitive; keep | History probe on 10 releases and main; security probe (6 tests) showing indexing and ordinary highlighting unaffected; a credibility audit that re-ran the finder's test (keep). |
| This doc's fix validation | fix A validated | New test, the upstream suite and all 36 module tests through gradle on 645d4fe14a20 with assertions on and off, stock and with fix A; check -x test with fix A; plain-Java probes of every symptom and of the workaround. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 430 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,554 words
- Registry title
- OpenNLPSentenceBreakIterator on text that starts with whitespace: a backward walk with previous() returns a non-boundary and then throws ArrayIndexOutOfBoundsException, and following() at an offset inside the leading whitespace throws StackOverflowError, because setText keeps the sentence detector's whitespace-trimmed first sentence start while first() and next() use the text's begin index
- Area
- analysis-opennlp / OpenNLPSentenceBreakIterator (public java.text.BreakIterator over OpenNLP sentence spans: setText, previous, following, moveToSentenceAt)
- Priority
- Low, rank 559: A backward walk or following() at an offset inside leading whitespace throws on a rarely used, direct-only iterator API, a loud crash rather than a silent wrong value.
- Impact class
- crash
- Impact note
- Hits only code that uses the opt-in analysis-opennlp module's public OpenNLPSentenceBreakIterator as a general java.text.BreakIterator, walking backward with previous() or calling following() or isBoundary() at an offset inside leading whitespace; for such code any text that starts with whitespace (one space, newline or tab is enough) makes a legal call throw ArrayIndexOutOfBoundsException (backward walk) or StackOverflowError (following(0) on a one-sentence text; an Error, so catch(Exception) does not stop it), deterministically, with -ea and -da, at any size; with two or more sentences following() in the whitespace only trips assertions under -ea and returns the right value under -da, and preceding() and next(-n) silently return the trimmed start, which is not a boundary; OpenNLPTokenizer (indexing and query analysis) never calls these methods and is not affected, and UnifiedHighlighter and FastVectorHighlighter with this iterator did not reach it on ordinarily tokenized fields (41 executed calls); every release from 7.3.0 to 10.6.0 (10.6.0 by reading) and main.
- Summary
- You create an OpenNLPSentenceBreakIterator, give it a text that starts with whitespace, for example ' Sentence number 1 has 6 words. Sentence number 2, 5 words.', and walk it backward with last()/previous() (the reverse loop shown in the BreakIterator javadoc), expecting the forward boundaries [0, 33, 60] in reverse order and then DONE. Instead previous() returns 2, which is not a boundary, and the next previous() throws ArrayIndexOutOfBoundsException (Index -1); following(0) on a one-sentence text with leading whitespace recurses until StackOverflowError. The OpenNLP sentence detector trims whitespace from its spans and setText stores those starts unchanged, so the first stored sentence start lies after the begin index that first() and next() treat as the first boundary.
- Introduced
- b720e1ee3a524034fb8a8a6188b0b23bf17ff1cb on branch_7x (first release 7.3.0) and 3e2f9e62d772218bf1fcae6d58542fad3ec43742 on master (first release 8.0.0), both 'LUCENE-2899: Add OpenNLP Analysis capabilities as a module' (committed by sarowe; patch-era JIRA issue LUCENE-2899, migrated as apache/lucene#3973, no GitHub PR), with the same file blob f69fbc6b: the class was added with this defect. [read: git blame -w attributes every code line of setText, previous(), following() and moveToSentenceAt() to 3e2f9e62d77, only two comment lines were re-wrapped by the LUCENE-9570 reformat 7f8b7ffbcad; the later commits 44fa35b65ff (#13170, preceding() start index) and b0ebaaed565 (#14216, instanceof pattern) do not touch the failing paths. exec: the history probe fails on 7.3.0, the first release with the class, and passes there with the one-line fix; no commit-level build was needed because 7.3.0 ships the introducing commit's blob] (2017-12-15 (both commits); first release 7.3.0, tagged 2018-04-04)
- First affected version
- 7.3.0 (tagged 2018-04-04), the first release with the analysis/opennlp module [exec: the history probe fails with -ea and -da]. No release is unaffected: 7.2.1 and older do not have the class (API absent; executed as a setup error, no module artifact)
- Affected versions
- Every release from 7.3.0 through 10.6.0 (69 releases through 10.5.2, and 10.6.0 by reading) and main a9779ef3c193; never fixed. [exec] the follow-up's history probe (backward walk and following(0) on one and two sentences after two leading spaces, each release with its own test sentence model; the controls without leading whitespace pass) fails on 7.3.0 (-ea and -da), 7.4.0, 7.5.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da), with every bundled opennlp-tools version from 1.8.3 to 2.5.12 (the detector trims the leading whitespace in all of them); the finder's JUnit fails on 9.1.0, 9.4.2, 9.12.3, 10.0.0, 10.5.2 and main; the independent reproduction's JUnit fails on main (-ea and -da), 10.5.2 and 9.12.3 (on 9.12.3 its preceding() checks hit the separate #12210 crash first, which #13170 fixed in 10.0.0; the other parts reproduce). [read] at all 60 release tags from 7.3.0 to 10.5.2 in the local history clone, first, next, last, following, moveToSentenceAt, previous and setText match main once comments and whitespace are removed (only preceding() differs before 10.0.0, not on a failing path); 8.8.2-8.11.3 are inferred (tags not in the clone; bracketed by 8.8.1, read, and 8.11.4, executed); branch_10_5, branch_10_6, branch_10x and upstream main a95c754ee00 (2026-10-26) carry the same file blob af1ada42 (not executed); the 7 commits ea57360912ed..a9779ef3c193 do not touch lucene/analysis/opennlp. Not executed: x86 (pure Java logic). JDK 25.0.3, aarch64
- 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 low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (2026-10-26, read-only): 290 logged queries (GitHub 110, commits 10, downstream 23, lists 84, JIRA 33, web 30; 18 items read, none the same bug): a scripted search over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web; manual GitHub GraphQL (38), gh search (9), JIRA REST (16) and mailing-list archive (15) queries; the file lists of all 407 open apache/lucene PRs; the LUCENE-2899 comments; the Solr, Elasticsearch, OpenSearch, Lucene.NET and OpenNLP repositories; GitHub-wide issue and code search. Closest items, not this bug: #12210 (another contributor's report of an ArrayIndexOutOfBoundsException in this class's preceding(), whose binary search started at sentenceStarts.length / 2) and its fix #13170 (merged 2024-03-11, first released in 10.0.0, changes only that line); #3973 / LUCENE-2899 (added the class and calls it a 'shim class' for OpenNLPTokenizer). Not fixed upstream: the file has the same blob (af1ada42) on a9779ef3c193, upstream main a95c754ee003 (2026-10-26), branch_10x and branch_10_6, and no open PR changes setText, previous() or following() (#12295 has only a stale instanceof hunk, #16911 deletes branch_10x). Update 2026-11-02: #16911 (open when searched) merged 2026-10-29 as 48cf3f515d6, which emptied branch_10x to a README tombstone (10.6 is the last 10.x minor; 10.x fixes go to branch_10_6); it fixes nothing here.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28) through gradle in a scratch worktree: the new 3-test class (stock: 3 of 3 fail with assertions on, 2 of 3 off; with fix A: 0), the upstreamTestOpenNLPSentenceBreakIterator(9 tests) and all 36 tests of:lucene:analysis:opennlp:testwith assertions on and off (stock: only the new tests fail; fix A: 0), and:lucene:analysis:opennlp:check -x testwith fix A; plain-Java probes of the same calls on stock and with fix A. The worktree was removed afterwards. The hunt's runs used maina9779ef3c193, whoseOpenNLPSentenceBreakIterator.javais byte-identical (blobaf1ada42), plus 12 releases from 7.3.0 to 10.5.2 (10 with the history probe, 9.1.0 and 9.4.2 with the finder's JUnit), all on OpenJDK 25.0.3, Linux aarch64, with the module's test sentence models.
- Finding record
lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java:26-27 (at a9779ef3c193) (javadoc 'A {@link BreakIterator} that splits sentences using an OpenNLP sentence chunking model.'; public final class), :44-48 (first() returns the begin index), :62-72 (next() goes from sentence 0 to sentenceStarts[1]), :74-94 (following(): range check :76-77 accepts the begin index; the last-sentence DONE branch :81-87 citing JDK-8015110; the 'at least two sentences' binary-search branch :88-93), :96-115 (moveToSentenceAt: recursion :102, assertions :109-112), :117-133 (previous(): stops only at the begin index :119, reads sentenceStarts[--currentSentence] :129), :135-161 (preceding(): pos < sentenceStarts[0] branch :143-149), :194-205 (setText stores the span starts unchanged, :203)lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java:39-46 (at a9779ef3c193) (splitSentences calls SentenceDetectorME.sentPosDetect, which trims whitespace from the spans; the no-model detector returns one span 0..length)lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizer.java:52 (at a9779ef3c193) (the only in-tree user of the class) and lucene/analysis/common/src/java/org/apache/lucene/analysis/util/SegmentingTokenizerBase.java:114, :167, :194, :199 (setText, current() and next() only)lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:182, (at a9779ef3c193) :186 (preceding()/following() at offsets inside a match) and lucene/highlighter/src/java/org/apache/lucene/search/vectorhighlight/BreakIteratorBoundaryScanner.java:40-41 (last() then one previous())lucene/analysis/opennlp/src/test/org/apache/lucene/analysis/opennlp/TestOpenNLPSentenceBreakIterator.java:160-166 (at a9779ef3c193) (testSliceStart: the padding lies before the slice's begin index), :190 (testWhitespaceOnly), :206 (testPrecedingWithTwoSentences, from #13170); no upstream test iterates a range that starts with whitespace followed by a sentence
- The finding in 30 seconds
Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java:26-27, 44-48, 54, 63-72, 75-94, 97-115, 118-133, 143-149, 150-160, 164-187, 195-205Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java:39-47JDK 25 java.text.BreakIterator javadoc and source (first(), previous(), following(int), isBoundary(int) default implementation, BreakIterator.java:379-395)
- Who is affected
Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPTokenizer.java:52Lucene main 645d4fe14a20 lucene/analysis/common/src/java/org/apache/lucene/analysis/util/SegmentingTokenizerBase.java:114, 167, 194, 199Lucene main 645d4fe14a20 lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:182, 186; lucene/highlighter/src/java/org/apache/lucene/search/vectorhighlight/BreakIteratorBoundaryScanner.java:40-41
- Why it looks unintended
JDK 25 java.text.BreakIterator javadoc (class example printEachBackward; previous(); following(int))Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java:26-27, 44-48, 76-77, 82-84, 119, 143-149Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/module-info.java:25Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/test/org/apache/lucene/analysis/opennlp/TestOpenNLPSentenceBreakIterator.java:84-115, 126-149, 160-167, 190-196, 217-228
- Suggested fix
Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/OpenNLPSentenceBreakIterator.java:76-77, 88-112, 127, 129, 137-138, 143-149, 157, 175, 195-205Lucene main 645d4fe14a20 lucene/analysis/opennlp/src/java/org/apache/lucene/analysis/opennlp/tools/NLPSentenceDetectorOp.java:39-47; lucene/analysis/opennlp/src/java/module-info.java:26
- Prior reports
a Lucene clone git ls-tree origin/main (645d4fe14a2, 2026-10-28), origin/branch_10x, origin/branch_10_6, releases/lucene/10.5.2: OpenNLPSentenceBreakIterator.java blob af1ada42 (read 2026-10-29)
- TestOpenNLPSentenceBreakIteratorLeadingSpace.java (JUnit test):
repro/LFV-623/src/org/apache/lucene/analysis/opennlp/TestOpenNLPSentenceBreakIteratorLeadingSpace.java(36 lines)
assets/LFV-623/bug.png(430 KB)
