- Every token stream is an
AttributeSource. It holds attributes such asCharTermAttribute(the term text), and the tokenizer, its filters and the consumer share these objects. - The source registers its attributes in two maps.
hasAttribute(),getAttribute(),equals()andrestoreState()look them up there. - For speed it also caches the attribute objects as a linked list, built the first time a method walks them.
clearAttributes(),captureState(),copyTo(),hashCode()andtoString()walk this list;addAttribute()drops it when the set changes, so it is rebuilt.
AttributeSource keeps its attributes in two maps and a cached list. removeAllAttributes() empties the maps, so hasAttributes() is false, but keeps the list, which captureState(), copyTo(), hashCode() and toString() still walk until the next addAttribute(). Select the still to open it at full size. Generated with AI, checked against the code.- Create a new
AttributeSource, add aCharTermAttribute, callclearAttributes()(as a tokenizer is documented to do for each token) and set the term to "foo". - Call
removeAllAttributes(). NowhasAttributes()is false and the source equals a newAttributeSource, so you expect it to behave like one:captureState()null,hashCode()0, nothing printed. - Instead
restoreState(captureState())throwsIllegalArgumentException,reflectAsString(false)printsterm=foo,bytes=[66 6f 6f],hashCode()is 110307 instead of 0, andcopyTo(target)overwrites the target's term "keep" with "foo". - Why: the source keeps its attributes in two maps and also caches a linked list of them, built the first time something walks them.
addAttribute()drops that list whenever the set changes;removeAllAttributes()clears the maps but keeps the list. - The next
addAttribute()drops the stale list. Nothing in Lucene callsremoveAllAttributes(), and there is no index or search effect; the fix is one line.
Only code that calls removeAllAttributes() on a source that was already used, then captures, copies, hashes, prints, iterates or clears it before adding an attribute again. Nothing in Lucene calls the method, and at its only known caller every consumer checked adds an attribute before it looks. No setting and no scale needed; no index or search effect. Rare.
Mechanism, terms and numbers582 words
AttributeSource, the base class of every TokenStream, keeps its attributes in two maps and caches the attribute instances as a linked State chain. addAttributeImpl() drops the chain whenever the attribute set changes. removeAllAttributes() clears both maps but not the chain, so once something has walked the chain (for example clearAttributes(), which tokenizers are documented to call per token), the methods that walk it keep seeing the removed instances until the next addAttribute().
With one CharTermAttribute set to "foo": restoreState(captureState()) on the same source throws IllegalArgumentException, copyTo(new AttributeSource()) throws, copyTo(target) overwrites the target's term "keep" with "foo", reflectAsString(false) prints term=foo,bytes=[66 6f 6f], and hashCode() is 110307 although equals() says the source equals a new AttributeSource (hash 0). Nothing in Lucene calls the method; at the only known caller, every consumer checked adds an attribute before it looks, so the impact is small. Present since the method was added in 5.5.0; the fix is one line, currentState[0] = null, the same invalidation addAttributeImpl() does.
:62-66: "These two maps must always be in sync!!!":attributes(interface to instance),attributeImpls(class to instance), andprivate final State[] currentState, the cached chain.AttributeSource(AttributeSource input)shares all three with its input (:79-85), so a filter and its tokenizer see the same chain.:252-266:getCurrentState()returnscurrentState[0]as is whenever it is non-null (:253-255); it rebuilds the chain fromattributeImplsonly when the cache is null andhasAttributes()is true.:188-202:addAttributeImpl()setsthis.currentState[0] = nullfor every new interface, with the comment "invalidate state to force recomputation in captureState()" (:196-197).:288-292:removeAllAttributes()isattributes.clear(); attributeImpls.clear();.currentState[0]is kept.- Chain walkers, which see the removed instances:
getAttributeImplsIterator()(:116),clearAttributes()(:272-276),endAttributes()(:282-286, called byTokenStream.end()),captureState()(:300-303),hashCode()(:335-341),reflectWith()(:418-422, behindreflectAsString()andtoString()),copyTo()(:457-468). - Map-based methods, which are right:
hasAttributes(),hasAttribute(),getAttribute(),addAttribute();equals()checkshasAttributes()first (:350,:372);cloneAttributes()is guarded byhasAttributes()(:434). restoreState(captureState())throws becausecaptureState()clones the stale chain andrestoreState()looks each impl up in the now empty map (:322-327).restoreState()itself is fine.
It opens when any chain walker ran before removeAllAttributes(), and closes at the next addAttribute(), or at an addAttributeImpl() that registers at least one Attribute interface: after the removal every interface is new, so the first one added nulls the chain. An impl that implements no Attribute sub-interface registers nothing and leaves the stale chain (:149-169, :193-200). If nothing walked the chain before the removal, the source behaves like an empty one.
The numbers (one CharTermAttribute, clearAttributes(), term "foo", then removeAllAttributes(); assertions on and off identical)
call after removeAllAttributes() | this source | new AttributeSource() |
|---|---|---|
hasAttributes(), equals(new AttributeSource()) | false, true | false, true |
getAttributeImplsIterator() | 1 impl | 0 |
reflectAsString(false) | term=foo,bytes=[66 6f 6f] | empty |
hashCode() | 110307 | 0 |
captureState() | a State holding the removed impl | null |
restoreState(captureState()) | IllegalArgumentException "State contains AttributeImpl of type ...CharTermAttributeImpl that is not in in this AttributeSource" | no error |
copyTo(new AttributeSource()) | IllegalArgumentException "...that is not in the target" | no error |
copyTo(target whose term is "keep") | target term becomes "foo" | target unchanged |
clearAttributes() | clears the removed instance (a held reference with "bar" becomes empty) | touches nothing |
With the one-line fix every row gives the right-hand value.
Exposure and likelihood253 words
- Trigger. Any chain walker (
clearAttributes(),endAttributes(),captureState(),hashCode(),toString()/reflection, the impls iterator) beforeremoveAllAttributes(), then any chain walker before the nextaddAttribute(). A filter built on the source withAttributeSource(input)shares the window. - Not on any Lucene path. The only references to
removeAllAttributes()in Lucene's sources are its declaration andTestAttributeSource.testRemoveAllAttributes. A read-only code search found no caller in Elasticsearch or OpenSearch. The only known caller is the pre-analyzed field tokenizer of Solr 9.x. Its parsers add attributes again for any value that has tokens (a value with no token list adds none), and every consumer that was checked adds an attribute before it observes the stream, which closes the window; Solr main removed that field type (SOLR-17839). - Default versus opt-in. No setting is involved: an explicit call to a rarely used method plus a legal call order.
- Scale. None. One attribute and one
clearAttributes()before the removal are enough. - Effect. Wrong return values and an
IllegalArgumentExceptionon that in-memory source;copyTo()can write removed values into another source. No index, query or search effect: indexing runs gave the same outcome with and without the fix, and CheckIndex was clean. - The
hashCode()symptom needs non-default values. If the attributes are cleared right before the removal, the stale impls hash to 0 and the mismatch hides; therestoreState()andcopyTo()failures do not depend on the values. - Likelihood: rare. The usual remove-then-re-add pattern closes the window at once, and nobody has reported it since 2016.
One JUnit class, one test method, one attribute, public API only, core module only. It adds a CharTermAttribute, calls clearAttributes() (which builds the cached list), removeAllAttributes(), then restoreState(captureState()).
/*
* 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.util;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRemoveAllAttributesCachedState extends LuceneTestCase {
public void testRestoreOwnStateAfterRemoveAllAttributes() {
AttributeSource source = new AttributeSource();
source.addAttribute(CharTermAttribute.class);
source.clearAttributes();
source.removeAllAttributes();
// expected: restoring the source's own captured state succeeds; actual: IllegalArgumentException (state holds the removed CharTermAttributeImpl)
source.restoreState(source.captureState());
}
}
cd $LUCENE && ./gradlew :lucene:core:test --tests TestRemoveAllAttributesCachedState -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
restoreState(captureState())returns normally, as on a newAttributeSource.- Observed
IllegalArgumentException: State contains AttributeImpl of type ...CharTermAttributeImpl that is not in in this AttributeSource, with assertions on and off.
More files, environment, full output224 words
new AttributeSource() gets a CharTermAttribute; clearAttributes() builds the cached chain, as a tokenizer does for every token; removeAllAttributes() empties the maps; restoreState(captureState()) then fails because the captured state still holds the removed CharTermAttributeImpl. Deterministic, single thread; LuceneTestCase is only the base class, and the package matches TestAttributeSource without using package-private API. It fails the same way with assertions on and off.
Regression test (for the recommended fix): this same class, or its one method added to TestAttributeSource next to testRemoveAllAttributes; it passes with the fix. The comment line is longer than 100 columns, so ./gradlew tidy rewraps it.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; release 10.6.0: fails; 5.5.0 to 10.5.2: a version-portable probe fails; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (the only platform run; plain Java, no native code) |
cp $REPRO/TestRemoveAllAttributesCachedState.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE && ./gradlew :lucene:core:test --tests TestRemoveAllAttributesCachedState -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (environment lines trimmed; -Ptests.asserts=false identical)
> java.lang.IllegalArgumentException: State contains AttributeImpl of type org.apache.lucene.analysis.tokenattributes.CharTermAttributeImpl that is not in in this AttributeSource
> at __randomizedtesting.SeedInfo.seed([DEADBEEF:2B2D639CF8B7BF22]:0)
> at org.apache.lucene.util.AttributeSource.restoreState(AttributeSource.java:326)
> at org.apache.lucene.util.TestRemoveAllAttributesCachedState.testRestoreOwnStateAfterRemoveAllAttributes(TestRemoveAllAttributesCachedState.java:30)
:lucene:core:test (FAILURE): 1 test, 1 failure
# standalone, seed DEADBEEF: main 645d4fe14a20 and 10.6.0, -ea and -da, fail with the same exception; with the fix: pass- The javadoc says it removes "all attributes and their implementations", and
captureState()'s result can restore "this ... AttributeSource"; here that throws. addAttributeImpl()drops the cache whenever the set changes;removeAllAttributes()is the one method that skips it.equals()matches a newAttributeSource, buthashCode()is 110307, not 0. LUCENE-3042 fixed a missed reset of this cache in 2011.
The full argument288 words
- The method's own javadoc: "Removes all attributes and their implementations from this AttributeSource" (
AttributeSource.java:288). Afterwards the impls iterator, reflection andtoString()still list the removed implementation. captureState(): "The return value can be passed to restoreState to restore the state of this or another AttributeSource" (:294-297). On the same emptied source that throws.copyTo(): "The given instance has to provide all Attributes this instance contains" (:450-452). The source contains none, yetcopyTo(new AttributeSource())throws and a matching target receives the removed values.clearAttributes(): "Resets all Attributes in this AttributeSource" (:268-271); it resets instances that are no longer in it.Objectcontract: equal objects must have equal hash codes; at the same moment the source equals a newAttributeSourcebut hashes to 110307, not 0.- The cache is meant to follow the attribute set:
addAttributeImpl()invalidates it (:196-197). It has done so since LUCENE-1693 (31a5f0edcc4, Michael Busch, 2009; the comment at:196); LUCENE-3042 (49ab81b316c, Uwe Schindler, 2011) made the cache a sharedState[1]array and changed the reset tothis.currentState[0] = null(:197). - Precedent: LUCENE-3042 (#4115, fixed 2011) was this same cache missing an invalidation on another path (attributes added to an already consumed chain). Its CHANGES entry calls it "This bug", and it was backported to 2.9, 3.0 and 3.1.
- Intent of the method: SOLR-4619 added it "to support reuse of token streams with variable attributes, like PreAnalyzedTokenizer", listed under New Features for 5.5.0. Neither the discussion nor the patch mentions the cache; the only test,
TestAttributeSource.testRemoveAllAttributes(TestAttributeSource.java:214-264), checkshasAttribute()andhasAttributes()only. - Nothing says otherwise: no javadoc, test, CHANGES or MIGRATE entry accepts the stale state.
The devil's-advocate check206 words
The strongest case for "intended": the org.apache.lucene.util package-info says "APIs in this package are primarily internal and may change between releases"; the TokenStream javadoc says attributes "must be added during instantiation", and committers called a tokenizer with no attributes before reset() "a bug really" (Robert Muir, SOLR-4619) and adding an attribute in incrementToken() "completely wrong" (Alan Woodward, LUCENE-8610 / #9656); nothing in Lucene calls the method; and the intended use, remove then re-add, never sees the stale chain.
Why it fails: the package-info sentence was added in 2026 by a javadoc-only change (#15719), long after the method shipped as a public, final, unmarked New Feature; the repro uses a plain AttributeSource, which the TokenStream rule does not cover, and no AttributeSource javadoc forbids using the source after the removal; the ordinary reading of the method's one-sentence javadoc and of captureState()'s strongly implies that the cache follows the attribute set, and the Object contract is explicit (the defect is that equal objects have unequal hash codes at the same moment, not that a mutable object's hash changes). These points argue for low impact, which the rating already reflects, not for intended behaviour. Every verifier, the cross-model lenses and the adjudication rated the intended argument weak.
Recommended Invalidate the cached chain in removeAllAttributes() (validated)
Add this.currentState[0] = null; to removeAllAttributes(), the same invalidation addAttributeImpl() does; sources built with AttributeSource(input) share the array, so they are fixed too. Validated on main: the repro passes with assertions on and off, as do TestAttributeSource and the 21 core analysis test classes (175 tests, two seeds).
public final void removeAllAttributes() {
attributes.clear();
attributeImpls.clear();
+ // invalidate the cached state chain, as addAttributeImpl() does when the attribute set changes
+ this.currentState[0] = null;
}All options and trade-offs4 options · 471 words
One fix was validated in a scratch worktree of 645d4fe14a20; the alternatives are described but not run.
One statement at the end of removeAllAttributes(), the same one addAttributeImpl() uses. The finder wrote it, and an independent reproducer wrote the same line on its own.
public final void removeAllAttributes() {
attributes.clear();
attributeImpls.clear();
+ // invalidate the cached state chain, as addAttributeImpl() does when the attribute set changes
+ this.currentState[0] = null;
}Trade-offs
- None found. Sources built with
AttributeSource(input)share the array, so they are fixed too. No cost on Lucene's in-tree paths (the method has no Lucene caller); one array write per explicit removal. Code that observes the source after the removal and before the nextaddAttribute()now sees an empty source. Not changed: an impls iterator obtained before the removal still walks the old chain (:117-120), and aStatecaptured before the removal keeps the old values, because it is a deep copy by design (:298-302); neither is this bug. Add a CHANGES entry.
What the checks validated
Validated on 2026-11-01 with gradle (--no-daemon) in a scratch worktree of 645d4fe14a20, OpenJDK 25.0.3:
- stock: the repro fails with assertions on and off;
- with the fix (+2 lines in
AttributeSource.java): the repro passes with assertions on and off;TestAttributeSource(7 tests, includingtestRemoveAllAttributes) and every core analysis test class (org.apache.lucene.analysis.*, 21 classes) pass with seeds DEADBEEF and CAFEBABE: 22 classes, 175 tests, 0 failures; checkGoogleJavaFormat: the patchedAttributeSource.javais clean; only the repro test's long comment line is flagged, and./gradlew tidyrewraps it into two lines;- standalone, with the fix shadowed: the repro passes with assertions on and off, and the probe gives the empty source's value in every row of the table above.
Earlier hunt runs with the same line: the finder's 4 tests and upstream TestAttributeSource (7 of 7) pass; an independent reproducer's 8-test JUnit passes 8 of 8; the copyTo() probe keeps the target's "keep".
Return null from getCurrentState() when !hasAttributes(), or rebuild a stale chain.
Trade-offs
Adds a check to a method that clearAttributes() calls for every token, to guard against the one method that forgot the invalidation; the recommended fix changes only that method.
The method has no Lucene caller, and its only known caller was removed from Solr main; upstream may prefer to deprecate it in 10.x and remove it on main with a MIGRATE note.
Trade-offs
Does not fix released branches unless combined with the recommended one-line fix, which could ship first.
State that the source must not be used before the next addAttribute().
Trade-offs
Leaves the javadoc contracts and the Object contract broken.
At the only known caller, the pre-analyzed field tokenizer of Solr 9.x (removed from Solr main), every consumer that was checked adds an attribute before it looks at the stream, so no effect was found there (read and emulated, not run).
Code reading of AttributeSource, in a hunt over attribute and char-array contracts, spotted that removeAllAttributes() skips the cache reset that addAttributeImpl() does. A randomized contract checker (3,000 random attribute sets, 134,973 checks) then reported violations of only this root cause, and a public-API test confirmed it.
The method in full122 words
Method: non-formal (code reading, then a randomized contract checker and executed tests).
- Finder. The hunt unit for attribute and char-array contracts read
AttributeSourceand noticed thatremoveAllAttributes()changes the attribute set without thecurrentState[0] = nullthataddAttributeImpl()performs. - Contract checker. The unit's randomized property-based checker built 3,000 random attribute sets over 3 attribute factories and ran 134,973 contract checks. It reported 4 distinct violations, all after
removeAllAttributes()and all this root cause:captureState()non-null, the impls iterator non-empty, reflection non-empty, andequals()true whilehashCode()differs. - Repro. A public-API JUnit test then failed on main with assertions on and off and on 10.5.2, and passed with the one-line fix; the later minimal repro keeps one of its four checks,
restoreState(captureState()).
No prior report: 266 logged queries on 2026-10-26 over GitHub (Lucene and downstream), the mailing lists, JIRA and the web, re-checked on 2026-11-01. The nearest item, LUCENE-3042 (#4115, fixed 2011), is the same cache missing an invalidation on another path; a precedent.
Searches3 searches
Verdict new: in the recorded searches, no issue, PR, JIRA entry, list thread, commit or downstream report says that removeAllAttributes() keeps the cached State chain. Closest, none of them this bug: #4115 / LUCENE-3042 (fixed 2011, the same cache missing an invalidation when attributes are added to an already consumed chain; the precedent), #2871 / LUCENE-1796 (2009 design note that adding attributes invalidates the cached state), SOLR-4619 (added the method; none of its 26 comments mentions the cache), #9656 / LUCENE-8610 (closed, Not A Bug; names removeAllAttributes() only in a test stream), SOLR-13077 (open; consumer-side attribute caching), #8044 / LUCENE-6987 (open; TokenStream workflow documentation), SOLR-17839 (Solr main removed the pre-analyzed field, the only known caller). Limits: word-based search; private channels cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene; lucene-solr, Solr, Elasticsearch, OpenSearch, Lucene.NET) | 2026-10-26 | 119 issue/PR queries (a scripted prior-report search and 77 GraphQL searches; the body and comments of all 143 apache/lucene hits read), 7 commit and 6 downstream-repository queries, the review comments of the 4 PRs that changed AttributeSource.java, the changed files of all 408 open apache/lucene PRs | background only: #4115, #2871, #9656, #8044 |
| Mailing lists, JIRA and the web | 2026-10-26 | 97 list queries (dev@, java-user@, issues@, commits@ and solr-user@ lucene.apache.org; the Solr lists), 25 JIRA queries with full reads of SOLR-4619, SOLR-13077, SOLR-11764, SOLR-17839, LUCENE-3042 and LUCENE-6987, 10 web queries | SOLR-4619 (the introducing change) and related items only |
| Upstream source and GitHub search | 2026-11-01 | last commits to AttributeSource.java on upstream main; gh search issues for removeAllAttributes, "AttributeSource captureState", "AttributeSource currentState" and the exception text; gh search prs removeAllAttributes and open PRs mentioning AttributeSource | not fixed (last change #15681); only #9656 and unrelated issues |
Present since the method was added: SOLR-4619 (eb47a8b965f0, 2016-01-21) added removeAllAttributes() without the cache reset, first released in 5.5.0 and 6.0.0. The body never changed; 13 releases from 5.5.0 to 10.6.0 and main fail when run, and 5.4.1 has no such method.
Versions tested6 rows
Introduced by eb47a8b965f0 "SOLR-4619: Improve PreAnalyzedField query analysis" (Steven Rowe, 2016-01-21, svn era, a JIRA patch; branch_5x backport 6fa0606cc8f3 the same day with an identical AttributeSource diff). It added removeAllAttributes() with only the two clear() calls; git blame gives the method body to that commit, and no later commit touches it.
Not a regression. The cache it skips is older: the State chain and its reset in addAttributeImpl() are from LUCENE-1693 (31a5f0edcc4, Michael Busch, 2009; the comment at AttributeSource.java:196); LUCENE-3042 (49ab81b316c, Uwe Schindler, 2011) made the cache a shared State[1] array and changed the reset to this.currentState[0] = null (:197), as git blame -L 195,199 shows.
Releases in between those that were run were not run; the git history shows the same method body in all of them. The local history has 8.x tags only up to 8.8.1: 8.11.4 ran from its release jars, and 8.8.2 to 8.11.3 are affected by inference.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0 | affected | minimal repro fails with assertions on and off |
main ea57360912ed (same AttributeSource.java) | affected | probe: 6 violations (-ea and -da); the finder's and the independent reproduction's JUnit tests fail |
| 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4, 8.0.0, 7.7.3, 7.0.0, 6.6.6, 6.0.0, 5.5.5 | affected | version-portable probe: the same 6 violations each |
| 5.5.0 | first release | probe: 6 violations; control (no chain built before removal) passes |
| 5.4.1 | no such method | probe does not compile: removeAllAttributes() is absent (javap confirms) |
| fix (one line, shadowed on main) | not affected | probe 0 violations; minimal repro passes |
An independent reproducer, an intended-behaviour devil's advocate, an adjudication and a credibility audit each re-ran it and confirmed it; a cross-model intended-lens review judged it real. The one dissent, a read-only refute review, said "not real" only because it could not run code, and agreed on the root cause. This doc's gradle runs validated the fix.
Checks and verdicts324 words
Every verdict below re-ran a repro on stock main jars or release jars, except the reading-only cross-model reviews, for which separate runs re-ran the repro (fails).
| Check | Verdict | What it found |
|---|---|---|
| Finder | candidate | Randomized contract checker: 134,973 checks, 4 distinct violations, all this root cause; public-API JUnit fails 4 of 4 on main (-ea, -da) and 10.5.2; probe fails on 9.12.3, 8.11.4, 7.7.3, 6.6.6 and 5.5.5; with the fix 4 of 4 pass and upstream TestAttributeSource 7 of 7. |
| Cross-model intended-lens review | real (high) | An independent reviewer from another model family (OpenAI GPT-5.6), reading only: intended argument weak, the fix fixes it. |
| Cross-model refute review | not real (low), procedural | Reading only, could not run code, so its verdict is procedural; it agreed on the root cause and the legal inputs and rated the intended argument weak. A separate re-run fails. |
| Independent reproducer | real | Own 8-test JUnit: 6 fail and 2 controls pass on main (-ea, -da), 10.5.2 and 9.12.3; probe fails on 8.11.4 and 5.5.0; 5.4.1 lacks the method; its own one-line fix passes 8 of 8. |
| Intended-behaviour devil's advocate | real, minor | Re-ran the finder's JUnit on main and 10.5.2; executed the silent copyTo() overwrite; found nothing upstream that accepts the stale state. |
| Adjudication | confirmed (high), minor | Re-ran the independent reproduction's JUnit with -ea and -da (6 of 8 fail), the finder's (4 of 4), and the independent reproduction's with the fix (8 of 8 pass). |
| Security assessment | none | 6-test probe: containment tests pass, the effect test fails on main and 10.5.2 and passes with the fix; no index, query or denial-of-service effect. |
| Credibility audit | confirmed | Re-ran the finder's JUnit on main with fresh seeds: 4 of 4 fail with -ea and -da. |
| This doc | real; fix validated | Gradle in a scratch worktree of 645d4fe14a20; standalone runs on main and 10.6.0; the probe on main and with the fix. |
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 509 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,336 words
- Registry title
- AttributeSource.removeAllAttributes() leaves the cached State chain in place, so until the next addAttribute() captureState/restoreState, copyTo, hashCode, the impls iterator, reflection and clearAttributes/endAttributes still act on the removed attributes
- Area
- core / util AttributeSource (removeAllAttributes and the cached State chain, getCurrentState / currentState[0])
- Priority
- Low, rank 1050: The removeAllAttributes() method leaves stale cached state that matters only to direct callers who reuse the source before adding attributes.
- Impact class
- minor
- Impact note
- Only code that calls the public AttributeSource.removeAllAttributes() (no caller in Lucene; the only known caller is a downstream pre-analyzed field tokenizer that adds attributes again straight away) and then uses the same source before its next addAttribute() is hit: captureState() hands back the removed attributes, so restoreState(captureState()) and copyTo(an empty source) throw IllegalArgumentException, copyTo(a source with matching attributes) silently copies the removed values into it, hashCode() disagrees with equals(), and the impls iterator, reflectAsString()/toString() and clearAttributes()/endAttributes() still see the removed instances; with -ea and -da, no scale needed; no index, query or search effect.
- Summary
- You call removeAllAttributes() on an AttributeSource (for example a token stream you reuse with a different set of attributes) after it has been used, for example by clearAttributes(), which every Tokenizer calls for each token. You expect it to behave like a new, empty source until you add attributes again, as hasAttributes() (false) and equals() (equal to a new AttributeSource) already say. Instead the method clears its two maps but not the cached chain of attribute instances that captureState(), copyTo(), hashCode(), the impls iterator, reflection and clearAttributes()/endAttributes() walk, so until the next addAttribute() they still see the removed attributes: restoreState(captureState()) on the same source throws IllegalArgumentException, hashCode() differs from that of an equal empty source, and reflectAsString() still prints 'term=foo,...'.
- Introduced
- eb47a8b965f0da9109fc72c7fe545dd6e5fff523 (SOLR-4619 'Improve PreAnalyzedField query analysis', svn trunk r1725869; branch_5x backport 6fa0606cc8f3, r1725871, identical AttributeSource diff; svn era, no PR): it added removeAllAttributes() as 'attributes.clear(); attributeImpls.clear();' without resetting currentState[0] [read: git log -S removeAllAttributes on main and on the 5.5.0 release line returns only these commits, and git blame of AttributeSource.java:289-292 gives all four lines to eb47a8b965f; the body is unchanged since] [exec: 5.4.1, the last release before it, has no such method (compile error, javap); 5.5.0, the first release with it, fails the probe]. Not a regression: present since the method was added. The cache it skips predates it (LUCENE-1693, 31a5f0edcc4, 2009), as does the invalidation in addAttributeImpl() (LUCENE-3042, 49ab81b316c, 2011) (2016-01-21)
- First affected version
- 5.5.0 [exec] (6.0.0 on the trunk line [exec]; 5.4.1 has no removeAllAttributes() [exec: compile error])
- Affected versions
- 5.5.0-5.5.5, 6.0.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.6.0 and main ea57360912ed and 645d4fe14a20. [exec] the minimal repro fails 1 of 1 on main 645d4fe14a20 and on the 10.6.0 release jars (SHA-1 checked against Maven Central), -ea and -da (2026-11-01); the follow-up's main() probe (public API, core jar only) reports the same 6 violations on 5.5.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da), and its control (chain never built before removal) passes on 5.5.0, 9.12.3, 10.5.2 and main; the finder's JUnit fails 4 of 4 on main (-ea, -da), 10.5.2 and (follow-up run) 9.12.3; the independent reproduction's JUnit fails 6 of 8 on main and 10.5.2 (-ea and -da) and 9.12.3; the finder's main() probe fails on 9.12.3, 8.11.4, 7.7.3, 6.6.6 and 5.5.5; the independent reproduction's main() probe on 8.11.4 and 5.5.0. [read] 8.8.2-8.11.3 have no tags in the local history (released from the lucene-solr branch_8x line); they are affected by inference (method unchanged, 8.11.4 executed). Not fixed upstream: upstream main a9779ef3c193 (2026-10-26), branch_10x, branch_10_6 and branch_10_5 have the same method body
- 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
- non-formal: code reading; also reached by fuzzing/PBT
- Upstream and prior reports
- none found (2026-10-26, read-only; with the finder's and the intended-behaviour check's searches): 266 logged queries, namely a scripted prior-report search over apache/lucene GitHub issues, PRs and commits, downstream repositories (Elasticsearch, OpenSearch, Solr), the dev@, java-user@, issues@, commits@ and solr-user@ lists, the JIRA archive and the web; 77 GitHub GraphQL searches over apache/lucene, lucene-solr, solr, elasticsearch, OpenSearch and lucenenet, with the body and comments of all 143 apache/lucene hits read; the review comments of the 4 PRs that changed AttributeSource.java since the move to GitHub; JIRA across all projects; the Solr lists; the changed files of all 408 open apache/lucene PRs; the file's history on main, branch_10x, branch_10_6 and branch_10_5. Related, not this bug: #4115/LUCENE-3042 (closed, fixed 2011: the precedent, the same cache missing an invalidation on a different path, attributes added to an already consumed filter chain, five years before removeAllAttributes() existed), #2871/LUCENE-1796 (2009 design note that adding attributes invalidates the cached state), SOLR-4619 (added the method; no comment on the cache), #9656/LUCENE-8610 (closed, Not A Bug: lazily added attributes and a null attribute cached by the indexing chain; names removeAllAttributes() only in a test stream), SOLR-13077 (open: consumer-side attribute caching with a reused pre-analyzed tokenizer), #8044/LUCENE-6987 (open: TokenStream workflow documentation), SOLR-17839 (Solr main removed the pre-analyzed field, the only known caller). No open PR touches AttributeSource.java.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal repro (gradle in a scratch worktree and standalone, assertions on and off), the probe behind the numbers, and the recommended fix; release 10.6.0: the minimal repro. The hunt's runs used mainea57360912ed, whoseAttributeSource.javais identical, and the release jars 5.4.1 to 10.5.2.
- Finding record
lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:195-199 (at ea57360912ed) (addAttributeImpl: 'invalidate state to force recomputation in captureState()', this.currentState[0] = null when a new interface is added), :252-266 (getCurrentState returns the cached chain whenever it is non-null; rebuilds it only if hasAttributes()), :116 (getAttributeImplsIterator walks the chain), :268-286 (clearAttributes and endAttributes walk the chain), :288-292 (removeAllAttributes: javadoc 'Removes all attributes and their implementations from this AttributeSource'; body clears the two maps only), :294-303 (captureState javadoc: the return value 'can be passed to restoreState to restore the state of this or another AttributeSource'; returns null when there is no chain), :318-332 (restoreState looks impls up in the map), :334-341 (hashCode walks the chain), :343-377 (equals checks hasAttributes() first), :457-463 (copyTo walks the chain; 'has to provide all Attributes this instance contains'), :418 and :477 (reflectWith and toString walk the chain)lucene/core/src/test/org/apache/lucene/util/TestAttributeSource.java:235, (at ea57360912ed) :256 (testRemoveAllAttributes, the only in-tree use of removeAllAttributes(); checks hasAttribute()/hasAttributes() only); grep of lucene/**/*.java finds no other caller
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:62-66, :79-85, :116-117, :188-202, :252-266, :272-292, :300-303, :318-341, :350, :372, :418-422, :434, :457-468a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/analysis/TokenStream.java:161 (end() calls endAttributes())
- Who is affected
a Lucene main checkout (645d4fe14a20) grep of lucene/**/*.java for removeAllAttributes: AttributeSource.java:289 and TestAttributeSource.java:235, :256 only (2026-11-01)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:188-202, :268-271, :288-303, :334-377, :450-457 (javadocs and code)a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/util/TestAttributeSource.java:214-264 (testRemoveAllAttributes: hasAttribute()/hasAttributes() only)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/analysis/TokenStream.java:134-136 (attributes must be added during instantiation)
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:79-85 (AttributeSource(input) shares currentState), :252-276 (getCurrentState is called by clearAttributes per token)
- Prior reports
GitHub (read-only, 2026-11-01): commits?path=lucene/core/src/java/org/apache/lucene/util/AttributeSource.java (448c7d8e2414, #15681); main at 2f424fd428da; gh search issues/prs as listed
- Diagrams
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/AttributeSource.java:62-66, :252-266, :288-292
- TestRemoveAllAttributesCachedState.java (one test class):
repro/LFV-500/src/org/apache/lucene/util/TestRemoveAllAttributesCachedState.java(32 lines)
Made in parallel with this doc, from the code at 645d4fe14a20 and the probe this doc's numbers come from (the maps and the cached chain before and after removeAllAttributes()). The caption and alt text describe what the code and the probe support.
assets/LFV-500/bug.png(509 KB)
![An AttributeSource with one CharTermAttribute holding "foo": its two maps (interface to instance, class to instance) and the cached State list that points at the same CharTermAttributeImpl. After removeAllAttributes() the two maps are empty and hasAttributes() is false, but the cached list still points at the removed instance, so captureState() returns it, restoreState(captureState()) throws IllegalArgumentException, reflectAsString(false) prints term=foo,bytes=[66 6f 6f] and hashCode() is 110307 instead of 0. The next addAttribute() drops the list. Executed on main 645d4fe14a20 with assertions on and off.](../assets/LFV-500/bug.png)