All findings · Lucene finding · LFV-500 · attributesource-removeallattributes-keeps-cached-state

AttributeSource.removeAllAttributes() leaves the cached State chain in place, so captureState(), copyTo() and hashCode() still see the removed attributes

After removeAllAttributes() on an AttributeSource that was already used, the source says it is empty, yet captureState(), copyTo(), hashCode(), toString() and clearAttributes() still act on the removed attributes: restoring its own captured state throws IllegalArgumentException. The method clears the two attribute maps but not the cached list those methods walk.

Summary

Priority
Low, rank 1050The removeAllAttributes() method leaves stale cached state that matters only to direct callers who reuse the source before adding attributes.
Impact
minor
Since
5.5.0 (SOLR-4619, 2016), when the method was addedintroduced 2016-01-21 · over 10 years
Affected
5.5.0 to 10.6.0 and main (13 releases and main run)
Status
documented
Found by
non-formal: code reading, then a randomized contract checker

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

How an AttributeSource keeps its attributes

  1. Every token stream is an AttributeSource. It holds attributes such as CharTermAttribute (the term text), and the tokenizer, its filters and the consumer share these objects.
  2. The source registers its attributes in two maps. hasAttribute(), getAttribute(), equals() and restoreState() look them up there.
  3. For speed it also caches the attribute objects as a linked list, built the first time a method walks them. clearAttributes(), captureState(), copyTo(), hashCode() and toString() walk this list; addAttribute() drops it when the set changes, so it is rebuilt.
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.
An 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.

The finding, step by step

  1. Create a new AttributeSource, add a CharTermAttribute, call clearAttributes() (as a tokenizer is documented to do for each token) and set the term to "foo".
  2. Call removeAllAttributes(). Now hasAttributes() is false and the source equals a new AttributeSource, so you expect it to behave like one: captureState() null, hashCode() 0, nothing printed.
  3. Instead restoreState(captureState()) throws IllegalArgumentException, reflectAsString(false) prints term=foo,bytes=[66 6f 6f], hashCode() is 110307 instead of 0, and copyTo(target) overwrites the target's term "keep" with "foo".
  4. 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.
  5. The next addAttribute() drops the stale list. Nothing in Lucene calls removeAllAttributes(), and there is no index or search effect; the fix is one line.

Who is affected

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.

The code (main 645d4fe14a20, AttributeSource.java)

  • :62-66: "These two maps must always be in sync!!!": attributes (interface to instance), attributeImpls (class to instance), and private 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() returns currentState[0] as is whenever it is non-null (:253-255); it rebuilds the chain from attributeImpls only when the cache is null and hasAttributes() is true.
  • :188-202: addAttributeImpl() sets this.currentState[0] = null for every new interface, with the comment "invalidate state to force recomputation in captureState()" (:196-197).
  • :288-292: removeAllAttributes() is attributes.clear(); attributeImpls.clear();. currentState[0] is kept.
  • Chain walkers, which see the removed instances: getAttributeImplsIterator() (:116), clearAttributes() (:272-276), endAttributes() (:282-286, called by TokenStream.end()), captureState() (:300-303), hashCode() (:335-341), reflectWith() (:418-422, behind reflectAsString() and toString()), copyTo() (:457-468).
  • Map-based methods, which are right: hasAttributes(), hasAttribute(), getAttribute(), addAttribute(); equals() checks hasAttributes() first (:350, :372); cloneAttributes() is guarded by hasAttributes() (:434).
  • restoreState(captureState()) throws because captureState() clones the stale chain and restoreState() looks each impl up in the now empty map (:322-327). restoreState() itself is fine.

The window

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 sourcenew AttributeSource()
hasAttributes(), equals(new AttributeSource())false, truefalse, true
getAttributeImplsIterator()1 impl0
reflectAsString(false)term=foo,bytes=[66 6f 6f]empty
hashCode()1103070
captureState()a State holding the removed implnull
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) before removeAllAttributes(), then any chain walker before the next addAttribute(). A filter built on the source with AttributeSource(input) shares the window.
  • Not on any Lucene path. The only references to removeAllAttributes() in Lucene's sources are its declaration and TestAttributeSource.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 IllegalArgumentException on 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; the restoreState() and copyTo() 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.

Minimal repro

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

TestRemoveAllAttributesCachedState.java (one test class)32 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.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());
  }
}
Command
cd $LUCENE && ./gradlew :lucene:core:test --tests TestRemoveAllAttributesCachedState -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
restoreState(captureState()) returns normally, as on a new AttributeSource.
Observed
IllegalArgumentException: State contains AttributeImpl of type ...CharTermAttributeImpl that is not in in this AttributeSource, with assertions on and off.

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

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.

Environment

lucenemain 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
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (the only platform run; plain Java, no native code)
Command (environment)
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

Observed output

# 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

Why it looks unintended

  • 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 new AttributeSource, but hashCode() 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 and toString() 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, yet copyTo(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.
  • Object contract: equal objects must have equal hash codes; at the same moment the source equals a new AttributeSource but 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 shared State[1] array and changed the reset to this.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), checks hasAttribute() and hasAttributes() 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.

Suggested fix

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.

Invalidate the cached chain in removeAllAttributes() (validated) (recommended)

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 next addAttribute() now sees an empty source. Not changed: an impls iterator obtained before the removal still walks the old chain (:117-120), and a State captured 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, including testRemoveAllAttributes) 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 patched AttributeSource.java is clean; only the repro test's long comment line is flagged, and ./gradlew tidy rewraps 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".

Make getCurrentState() check the maps (not run)

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.

Deprecate removeAllAttributes() (not run)

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.

Document the limitation (not recommended)

State that the source must not be used before the next addAttribute().

Trade-offs

Leaves the javadoc contracts and the Object contract broken.

Downstream note

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

How it was found

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 AttributeSource and noticed that removeAllAttributes() changes the attribute set without the currentState[0] = null that addAttributeImpl() 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, and equals() true while hashCode() 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()).

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene; lucene-solr, Solr, Elasticsearch, OpenSearch, Lucene.NET)2026-10-26119 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 PRsbackground only: #4115, #2871, #9656, #8044
Mailing lists, JIRA and the web2026-10-2697 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 queriesSOLR-4619 (the introducing change) and related items only
Upstream source and GitHub search2026-11-01last 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 AttributeSourcenot fixed (last change #15681); only #9656 and unrelated issues

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20; 10.6.0 affectedminimal repro fails with assertions on and off
main ea57360912ed (same AttributeSource.java) affectedprobe: 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 affectedversion-portable probe: the same 6 violations each
5.5.0 first releaseprobe: 6 violations; control (no chain built before removal) passes
5.4.1 no such methodprobe does not compile: removeAllAttributes() is absent (javap confirms)
fix (one line, shadowed on main) not affectedprobe 0 violations; minimal repro passes

Independent checks

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

Each independent check and its verdict
CheckVerdictWhat it found
Finder candidateRandomized 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), proceduralReading 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 realOwn 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, minorRe-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), minorRe-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 none6-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 confirmedRe-ran the finder's JUnit on main with fresh seeds: 4 of 4 fail with -ea and -da.
This doc real; fix validatedGradle in a scratch worktree of 645d4fe14a20; standalone runs on main and 10.6.0; the probe on main and with the fix.

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

Finding record

LFV-500, snapshot of 2026-11-06

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 main ea57360912ed, whose AttributeSource.java is identical, and the release jars 5.4.1 to 10.5.2.

Lucene and upstream sources

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-468
  • a 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

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

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.