All findings · Lucene finding · LFV-1191 · sleepinglockwrapper-zero-poll-interval-divide-by-zero

SleepingLockWrapper accepts pollInterval 0, then every obtainLock throws ArithmeticException: / by zero

SleepingLockWrapper's constructor accepts a pollInterval of 0 (it rejects only negative values), but obtainLock divides the timeout by it before its first lock attempt, so every obtainLock call, including the one in new IndexWriter(...), throws ArithmeticException: / by zero. Present since 5.3.0; checking <= 0 instead of < 0 fixes it.

Summary

Priority
Low, rank 957SleepingLockWrapper accepts a pollInterval of 0, and then every lock attempt, including the one in IndexWriter's constructor, throws ArithmeticException instead of the constructor rejecting the value, in every release since 5.3.0. It needs that explicit 0 on a class default configurations never use, and it fails at once with no index change.
Impact
minor
Since
5.3.0 (LUCENE-6508, 2015)introduced 2015-06-04 · over 11 years
Affected
5.3.0 to 10.6.0 and main; not fixed upstream
Status
documented
Found by
semi-formal: a proof-obligation audit of SleepingLockWrapper's contract, witnessed by an executable repro

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

SleepingLockWrapper wraps a Directory; when a lock is busy it retries every pollInterval ms until lockWaitTimeout runs out. Its constructor lets an interval of 0 through, and the retry loop divides by the interval before its first try.

new SleepingLockWrapper(dir, 1000, 0) passes the constructor check 'pollInterval < 0'; obtainLock("write.lock") computes lockWaitTimeout / pollInterval = 1000 / 0 and throws java.lang.ArithmeticException: / by zero at SleepingLockWrapper.java:81, before the wrapped directory is asked for the lock; with the fix 'pollInterval <= 0' the constructor throws IllegalArgumentException instead.
The repro's path: the constructor's check pollInterval < 0 lets 0 through; obtainLock first computes 1000 / 0 and throws ArithmeticException before the wrapped directory is asked. With <= 0 the constructor rejects 0 instead. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Wrap a directory so that locking is retried: new SleepingLockWrapper(new ByteBuffersDirectory(), 1000, 0), a 1,000 ms timeout and a poll interval of 0 ms. The constructor accepts it: it rejects only negative intervals ("pollInterval must be a non-negative number").
  2. Ask for a lock nobody holds: obtainLock("write.lock"). You expect the lock, or else an IllegalArgumentException from the constructor that names pollInterval.
  3. Instead it throws java.lang.ArithmeticException: / by zero, every time, with assertions on and off, and the wrapped directory is never asked. new IndexWriter(wrapper, config) fails the same way.
  4. Why: before its first lock attempt, obtainLock turns the timeout into a number of sleeps, lockWaitTimeout / pollInterval, here 1000 / 0 (SleepingLockWrapper.java:81). Every timeout fails: -1 (wait forever), 0 or 1000.

Who is affected

Only applications that build SleepingLockWrapper(dir, timeout, 0) themselves, for example to mean "retry without sleeping". Lucene never uses the class; positive intervals and the 2-argument constructor work. For those applications no lock can ever be taken: IndexWriter, CheckIndex and addIndexes fail at once. Nothing is written or damaged.

Mechanism, terms and numbers308 words

SleepingLockWrapper wraps a Directory and retries obtainLock, sleeping pollInterval milliseconds between attempts until lockWaitTimeout runs out. Its 3-argument constructor rejects only pollInterval < 0 ("pollInterval must be a non-negative number", SleepingLockWrapper.java:71-74 at 5adff78bb168), so 0 is accepted. Before its first lock attempt (:86), obtainLock computes long maxSleepCount = lockWaitTimeout / pollInterval; (:81). With 0, every obtainLock throws java.lang.ArithmeticException: / by zero, whatever the timeout and even when the lock is free, so new IndexWriter(wrapper, ...), new CheckIndex(wrapper) and addIndexes from such a directory all fail. It needs an explicit 0 on a class Lucene itself never uses, and it fails at once with no index change. Every release since 5.3.0 has it.

The bug, step by step

  1. Constructor (SleepingLockWrapper.java:61-75 at 5adff78bb168): lockWaitTimeout must be LOCK_OBTAIN_WAIT_FOREVER (-1) or >= 0 (:65-70); pollInterval is checked with if (pollInterval < 0) (:71-74). 0 passes and is stored.
  2. obtainLock (:79-102) counts sleeps, not elapsed time: it first computes long maxSleepCount = lockWaitTimeout / pollInterval; (:81), then tries in.obtainLock(lockName) (:86), sleeps pollInterval ms (:93) and loops while sleepCount++ < maxSleepCount or the timeout is -1 (:97).
  3. With pollInterval 0 the long division at :81 throws ArithmeticException for every timeout: -1/0, 0/0 and 1000/0 all throw. Directory.obtainLock documents only LockObtainFailedException and IOException (Directory.java:159-167); this exception is unchecked and names no argument.
  4. Callers that take a lock through the wrapper fail the same way (read at 5adff78bb168): new IndexWriter(dir, conf) (IndexWriter.java:954), IndexWriter.addIndexes(Directory...) on such a source directory (acquireWriteLocks, :3063), new CheckIndex(dir) (CheckIndex.java:472) and the replicator's ReplicaNode (ReplicaNode.java:103). Opening a reader takes no lock and is unaffected. An independent check executed the IndexWriter path: the exception surfaces from IndexWriter.<init>(IndexWriter.java:954).
Exposure and likelihood201 words
  • Opt-in only. Lucene's own code never uses the class; its only in-tree user is TestSleepingLockWrapper. The 2-argument constructor uses DEFAULT_POLL_INTERVAL (1000 ms) and works.
  • Reach. The class is public, final and not deprecated, and carries no @lucene.internal tag of its own; its superclass FilterDirectory and the Lock it returns are marked @lucene.internal (FilterDirectory.java:31, Lock.java:34), which a maintainer may weigh. LUCENE-6525 named it as the replacement for IndexWriterConfig's write-lock timeout (5.3.0 IndexWriterConfig.java:91-92: "@deprecated Use {@link SleepingLockWrapper} if you want sleeping."). The devil's-advocate verifier's GitHub code search for new SleepingLockWrapper recorded two downstream callers, neither passing 0: Hibernate Search (new SleepingLockWrapper( directory, 2000, 20 )) and an older Elasticsearch shared-filesystem path (the 2-argument constructor, timeout 5000); not re-checked for this doc. No public report of a zero interval was found since 2015.
  • Before 10.2.0 DEFAULT_POLL_INTERVAL was a mutable public static (10.1.0 SleepingLockWrapper.java:36), so setting it to 0 reached the same division through the 2-argument constructor (read, not executed). #14221 made it final.
  • Effect. 100% of lock attempts on such a wrapper fail, deterministically, with an unchecked exception that callers catching IOException or LockObtainFailedException do not expect. No hang, no lock taken, no index change.

Minimal repro

One JUnit test method on the public API: wrap a ByteBuffersDirectory with pollInterval 0 and ask for a free lock. Deterministic, single-threaded, under a second; it fails with assertions on and off. It asserts the recommended fix (the constructor rejects 0).

TestSleepingLockWrapperZeroPoll.java (one test, public API)31 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.store;

import org.apache.lucene.tests.util.LuceneTestCase;

public class TestSleepingLockWrapperZeroPoll extends LuceneTestCase {

  public void testZeroPollInterval() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      // the constructor should reject 0; today obtainLock throws ArithmeticException "/ by zero"
      expectThrows(
          IllegalArgumentException.class,
          () -> new SleepingLockWrapper(dir, 1000, 0).obtainLock("write.lock").close());
    }
  }
}
Command
cp $REPRO/TestSleepingLockWrapperZeroPoll.java $LUCENE/lucene/core/src/test/org/apache/lucene/store/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.store.TestSleepingLockWrapperZeroPoll -Ptests.seed=5EED1191 -Ptests.asserts=true
# and with -Ptests.asserts=false (assertions off)
Expected
The constructor rejects pollInterval 0 with an IllegalArgumentException (the recommended fix), or else the free lock is returned.
Observed
expected IllegalArgumentException but got java.lang.ArithmeticException: / by zero at SleepingLockWrapper.java:81, with assertions on and off.

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

More files, environment, full output185 words

The test expects an IllegalArgumentException, the outcome of the recommended fix. If maintainers prefer to define 0 as "retry without sleeping" (option B), the assertion becomes "the lock is obtained"; the test fails today either way, because obtainLock throws ArithmeticException.

Pass -Ptests.asserts explicitly: without it, Lucene's build chooses assertions on or off from the seed (about 75% on; seed 5EED1191 alone gives off). As a regression test, the same method can go into the existing TestSleepingLockWrapper.

Environment

lucenemain 5adff78bb168 (12.0.0-SNAPSHOT): this test fails; an earlier variant of it (same trigger) also fails on 10.6.0 and 9.12.3, and release-adapted copies of that variant fail on 5.3.0 to 8.11.4
jdkOpenJDK 25.0.3
osLinux aarch64 (nothing platform-specific)

Observed output

TestSleepingLockWrapperZeroPoll > testZeroPollInterval FAILED
    junit.framework.AssertionFailedError: Unexpected exception type, expected IllegalArgumentException but got java.lang.ArithmeticException: / by zero
        at org.apache.lucene.store.TestSleepingLockWrapperZeroPoll.testZeroPollInterval(TestSleepingLockWrapperZeroPoll.java:26)
        Caused by:
        java.lang.ArithmeticException: / by zero
            at org.apache.lucene.store.SleepingLockWrapper.obtainLock(SleepingLockWrapper.java:81)

# main 5adff78bb168, gradle, seed 5EED1191: the same failure with -Ptests.asserts=true and =false (stack frames shown without gradle's app// prefix)
# with the fix (pollInterval <= 0 rejected): BUILD SUCCESSFUL, 1 test, 0 failures, assertions on and off

Why it looks unintended

  • The constructor's own check and message say "pollInterval must be a non-negative number"; 0 is inside that range.
  • No javadoc, test or discussion says 0 is invalid; the class's own test only draws 2-10 ms.
  • Even if 0 is misuse, it should be rejected up front, not fail on every later lock with / by zero.
The full argument101 words
  • Contract. @param pollInterval poll once per this interval in milliseconds until {@code lockWaitTimeout} is exceeded. (SleepingLockWrapper.java:58-59); the constructor rejects only negative values (:71-74).
  • Tests. TestSleepingLockWrapper draws pollInterval from TestUtil.nextLong(random(), 2, 10) (TestSleepingLockWrapper.java:30), so 0 was never exercised. No CHANGES or MIGRATE entry mentions a zero interval.
  • History. The < 0 check and the division were written together in fe6c3dc9397 (LUCENE-6508, 2015). Both expressions are unchanged since; the only later edit beside them is the 2020 reformat 2d6ad2fee6d (LUCENE-9570), which rewrapped the message line; none of the 58 comments on #7567 discusses a zero poll interval.
The devil's-advocate check122 words

The strongest case for "intended": a zero interval is a busy retry loop; the class javadoc says "This is not a good idea." (:26); and the pollInterval < 0 check looks copied from the lockWaitTimeout check beside it, where 0 does mean something (no waiting). So the author probably meant "positive", and 0 is misuse.

Why it fails: that argues for rejecting 0 in the constructor with an IllegalArgumentException, which is the suggested fix. It does not justify accepting 0 and then failing on every lock attempt with an exception that names no argument. "Not a good idea" discourages the class, but it stays public and was the named replacement API. Verdict: a validation off-by-one, a real Low bug.

Suggested fix

Recommended A. Reject 0 in the constructor (validated)

In the constructor, reject pollInterval <= 0 instead of < 0, with "pollInterval must be a positive number". The repro passes with assertions on and off, and TestSleepingLockWrapper and the org.apache.lucene.store tests pass. Alternative: define 0 as "retry without sleeping", which needs a time-based loop.

-    if (pollInterval < 0) {
+    if (pollInterval <= 0) {
       throw new IllegalArgumentException(
-          "pollInterval must be a non-negative number (got " + pollInterval + ")");
+          "pollInterval must be a positive number (got " + pollInterval + ")");
     }
All options and trade-offs2 options ยท 276 words

Option A was applied in scratch worktrees at 5adff78bb168 and checked with gradle; option B was not built.

A. Reject 0 in the constructor (validated) (recommended)

Change the check at SleepingLockWrapper.java:71-74 from < 0 to <= 0 and the message to "must be a positive number". Optionally say "must be positive" in the @param pollInterval javadoc too.

-    if (pollInterval < 0) {
+    if (pollInterval <= 0) {
       throw new IllegalArgumentException(
-          "pollInterval must be a non-negative number (got " + pollInterval + ")");
+          "pollInterval must be a positive number (got " + pollInterval + ")");
     }

Trade-offs

Code that passes 0 today cannot take any lock already, so no working lock path breaks; the error moves to construction and names the argument. One narrow behaviour change: a wrapper built with 0 and used only for reading (no lock taken) works today and would fail at construction.

What the checks validated

  • The repro passes with assertions on and off (gradle, seed 5EED1191).
  • With the fix, TestSleepingLockWrapper passes (6 tests, 1 skipped by a seed-dependent assumption) and org.apache.lucene.store.* passes (29 classes, 778 tests, 0 failures; the new test included), with assertions on and, in a separate run, off.
  • :lucene:core:checkGoogleJavaFormat passes with the test file added.

B. Keep 0 and define it as "retry without sleeping"

Special-case 0 in obtainLock: skip the division and the Thread.sleep.

Trade-offs

Needs a decision on what the timeout means with 0, because the loop counts sleeps, not elapsed time (:81, :97): it would need a time-based deadline, and a 0 interval spins a CPU while another process holds the lock. Not built or tested.

Downstream note

A product that lets users set this poll interval and passes 0 through would fail to open every index writer on that directory; keeping the value at 1 ms or more avoids it.

How it was found

A proof-obligation audit of SleepingLockWrapper's contract: the constructor's check admits every non-negative pollInterval, so obtainLock must work for each of them, and before its first lock attempt it divides by the interval, which fails for 0. A one-method JUnit test on the public API then witnessed it.

The method in full61 words

The obligation came from the class's own validation and message ("non-negative") and the @param pollInterval javadoc, checked against the only use of the field, the division and the sleep in obtainLock. The witness test (an earlier variant of the repro above, with the same trigger) fails on main with assertions on and off and on the 10.6.0 and 9.12.3 jars.

Prior reports, history and checks

Prior reports

No prior report found in apache/lucene issues, PRs and commits (incl. the JIRA archive), the mailing lists, downstream trackers, code search or the web (searched 2026-11-03; GitHub re-checked the same day). Nearest: #7567 (LUCENE-6508), where the check and the division were written.

Searches3 searches

Nearest items, none of them this bug: #7567 (LUCENE-6508, origin; its 58 comments never discuss a zero interval), #7583 (LUCENE-6525, made the class public), #2124 (LUCENE-1048, an int overflow of the result of the analogous division lockWaitTimeout / LOCK_POLL_INTERVAL, and of its counter, in the pre-5.3 Lock.obtain(long) for very large timeouts, fixed 2007), #1431 (LUCENE-353, a loop-counter bug in that older loop), #14221 (made DEFAULT_POLL_INTERVAL final); #12946 and #14286 mention the class in passing. No open PR touches SleepingLockWrapper.java.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive)2026-11-03; re-checked the same daymore than 200 queries in total with scripted prior-report searches and by hand, among them SleepingLockWrapper, pollInterval, 'poll interval', obtainLock ArithmeticException, '"by zero" lock', the exact constructor message, DEFAULT_POLL_INTERVAL, LOCK_POLL_INTERVAL; git log -S on both linesno report; #7567, #7583, #2124, #1431, #14221 are history
Open PRs2026-11-03the changed files of all 409 open apache/lucene PRs; the 28 updated since 2026-11-01 re-checkednone touches SleepingLockWrapper.java or its test
Mailing lists, downstream, code search, web2026-11-03dev@, java-user@, issues@, commits@; Solr, Elasticsearch, OpenSearch, Lucene.NET; GitHub code search; 8 web and 3 Q&A searchesnothing about a zero poll interval

History

Introduced by LUCENE-6508 (fe6c3dc9397, 2015) and made public by LUCENE-6525; first released in 5.3.0. 5.3.0, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.6.0 and main fail; never fixed upstream.

Versions tested4 rows

fe6c3dc9397 "LUCENE-6508: Simplify directory/lock API" (2015-06-04, svn era, no PR) wrote both the pollInterval < 0 check and the division, in a then package-private class in org.apache.lucene.index; 5438754ac1c "LUCENE-6525: Deprecate IndexWriterConfig's write lock timeout" (2015-06-14) moved it to org.apache.lucene.store and made it public. A precursor SleepingLockFactory with the same check and division existed briefly on the LUCENE-6508 development branch (de466b70d30, removed the same day by 6c6c5ec9131); it was never released and is not an ancestor of fe6c3dc9397. Later commits on the file (LUCENE-7013 license header, LUCENE-9570 reformat, LUCENE-9878 "Lock obtain timed out" message, #13001 @SuppressForbidden, #14221 final DEFAULT_POLL_INTERVAL) leave both the condition and the division unchanged. The release runs used an earlier variant of the repro (same trigger; it accepts the lock or an IllegalArgumentException from the constructor); before 9.1.0 only its LuceneTestCase package was changed (and RAMDirectory on 5.3.0 and 6.6.6).

Versions and branches executed, with results
Version or commitVerdictResult
main 5adff78bb168 affectedgradle: fails with assertions on and off at :81; passes with the fix
10.6.0, 10.5.2 affectedfails at :81
5.3.0, 6.6.6, 7.7.3, 8.11.4, 9.12.3 affectedfails at :79
5.2.1 no classSleepingLockWrapper does not exist; the test does not compile

Independent checks

An independent reproducer, two executed cross-model reviews (OpenAI GPT-5.6, real, high), a devil's-advocate verifier and an adjudication agree, and separate re-runs and a final repro check reproduced it. This doc's gradle runs reproduced it on main and validated the fix.

Checks and verdicts200 words

The cross-model reviews ran their Java on main ea57360912ed, whose SleepingLockWrapper.java is identical to 5adff78bb168's; one re-ran on 5adff78bb168 and 10.6.0.

Each independent check and its verdict
CheckVerdictWhat it found
independent reproducer realOwn test (lockWaitTimeout 0, pollInterval 0) fails on main, 10.6.0, 10.5.2 and 9.12.3; a check on 5.3.0 fails too.
cross-model reviews, refute and intended lenses (OpenAI GPT-5.6, executed) real (high)Own tests fail with the same exception; pollInterval 1 controls pass; timeouts -1, 0 and positive all throw without calling the wrapped directory; option A passes their tests and the existing suite.
devil's-advocate verifier real, LowOwn test fails 3 of 4, including new IndexWriter(wrapper, ...); the pollInterval 1 control passes; the misuse argument supports rejecting 0, not the current behaviour.
adjudication, separate re-runs and final repro check confirmed, LowRe-runs fail on main with assertions on and off and pass with option A; one minimal deterministic test.
this doc's fix check reproduced; option A validatedGradle in scratch worktrees at 5adff78bb168: the repro fails without and passes with option A; TestSleepingLockWrapper and the store tests (29 classes, 778 tests, the new test included) pass.

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

Finding record

LFV-1191, snapshot of 2026-11-06

Registry title
SleepingLockWrapper's constructor accepts pollInterval 0 (it rejects only negative values: 'pollInterval must be a non-negative number'), and then every obtainLock on the wrapper, including the write-lock acquisition in IndexWriter's constructor, throws ArithmeticException '/ by zero' from 'lockWaitTimeout / pollInterval' instead of the constructor rejecting 0 up front (since 5.3.0)
Area
core / store: SleepingLockWrapper (constructor pollInterval check, obtainLock)
Priority
Low, rank 957: SleepingLockWrapper accepts a pollInterval of 0, and then every lock attempt, including the one in IndexWriter's constructor, throws ArithmeticException instead of the constructor rejecting the value, in every release since 5.3.0. It needs that explicit 0 on a class default configurations never use, and it fails at once with no index change.
Impact class
minor
Impact note
Hits only applications that build a SleepingLockWrapper with an explicit pollInterval of 0, which the constructor accepts: every obtainLock on it, including the write-lock acquisition in IndexWriter's constructor, then throws an unchecked ArithmeticException '/ by zero' at once, with -ea and -da [exec], instead of an IllegalArgumentException from the constructor (the likely fix) or a working lock; no lock is taken and no index state changes, default configurations never use the class, positive intervals and the 2-argument constructor are unaffected, and every release since 5.3.0 (2015) has it.
Summary
You wrap a directory in new SleepingLockWrapper(dir, 1000, 0), with a pollInterval the constructor accepts (it rejects only negative values, with the message 'pollInterval must be a non-negative number'), and ask it for a lock that nobody holds. You expect either that lock or an IllegalArgumentException from the constructor. Instead every obtainLock, including the one in IndexWriter's constructor, throws java.lang.ArithmeticException '/ by zero' from 'lockWaitTimeout / pollInterval' before the wrapped directory is even asked for the lock.
Introduced
fe6c3dc9397b0397aebf84fedb57875beb163d1d (LUCENE-6508 'Simplify directory/lock API', migrated issue apache/lucene#7567; svn-era commit r1683606, no pull request; same-day branch_5x commit dcd53bc7336de75e0ad8128e83789cb4854ffa81, r1683609): it wrote both the 'pollInterval < 0' check and the 'lockWaitTimeout / pollInterval' division, in a class that was then package-private in org.apache.lucene.index [read: git blame at 5adff78bb168 and git log -S on both lines find only this change]. 5438754ac1cdd3304dfc51e4c7e72fbd4ae916cd (LUCENE-6525 'Deprecate IndexWriterConfig's write lock timeout', #7583, 2015-06-14; branch_5x e8ea6fa26c1) moved it to org.apache.lucene.store and made it public, so applications can call the 3-argument constructor from then on. Both are first released in 5.3.0. (2015-06-04 (author date = commit date, 18:20 UTC; branch_5x 18:46 UTC); public since 2015-06-14 (LUCENE-6525); first release 5.3.0)
First affected version
5.3.0 [exec: a copy of the repro that uses RAMDirectory and the pre-9.1 LuceneTestCase package fails with ArithmeticException at SleepingLockWrapper.java:79 with -ea and -da]; 5.2.1, the last release before, is not affected [exec: the class does not exist there and the test does not compile]
Affected versions
Every release from 5.3.0 (2015) through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT); never fixed. [exec, JDK 25.0.3, Linux aarch64] history step: 5.3.0 (-ea, -da), 6.6.6, 7.7.3, 8.11.4, 9.12.3 and 10.6.0 (-da) and main (-ea, -da) fail with ArithmeticException '/ by zero' in obtainLock (at :79 on 5.3.0 to 9.12.3, at :81 on 10.6.0 and main); runs before 9.1.0 use copies that change only the LuceneTestCase package (and, for 5.3.0 and 6.6.6, use RAMDirectory instead of ByteBuffersDirectory); finder: main (-ea, -da), 10.6.0 and 9.12.3 (-da); the independent reproduction's own test: main (-ea, -da), 10.6.0, 10.5.2 and 9.12.3 (-da), plus a one-off check on the 5.3.0 core jar; the intended-behaviour check's own test: main (-ea, -da) and 10.6.0 (-da); judge and re-runs on main (-ea, -da). Not affected: 5.2.1 and earlier (no such class). Not new in 10.6 (10.5.2 and every executed older release fail). Not fixed upstream: upstream main still has 'if (pollInterval < 0)' at :71 and the division at :81 (contents API, 2026-11-03); the newest commit on the file is 33f314f3e95 on main and db775275305 on branch_10_6 (#14221, author date 2025-02-12, which made DEFAULT_POLL_INTERVAL final and touches neither line).
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: proof-obligation audit
Upstream and prior reports
none found (2026-11-03, on top of the finder's and the adjudication's searches; rechecked the same day): three scripted prior-report searches (identifiers and messages; symptoms and triggers; history and the pre-5.3 Lock.obtain(long) code; 153 queries over apache/lucene issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 1553, 1687 and 110 distinct hits, none a report) plus the finder's run (45 queries, 1560 distinct hits, 0 reports), 25 manual GitHub issue and PR searches, 4 commit searches, code searches (Solr, Elasticsearch, OpenSearch, Lucene.NET and all Java code; the only downstream use found is an Elasticsearch test asserting a directory is not a SleepingLockWrapper), the changed files of all 409 open apache/lucene PRs (none touches SleepingLockWrapper.java or its test), 8 web and 3 Stack Exchange searches, direct reads of the old lock-timeout list threads and JIRA LUCENE-1877, and the file history on main, branch_11x and branch_10_6 (author dates). The re-check found 'SleepingLockWrapper' still returning only #12946 and #14286, nothing for 'pollInterval', 'obtainLock ArithmeticException' or '"by zero" lock', none of the 28 open PRs updated since 2026-11-01 touching SleepingLockWrapper.java or the store Lock, LockFactory, FilterDirectory or Directory files, and upstream main still on 'pollInterval < 0' and the division. Related history, not reports: #7567 (LUCENE-6508, the introducing change; its 58 comments never discuss a zero poll interval), #7583 (LUCENE-6525, made the class public), #2124 (LUCENE-1048, an int overflow of the result of the analogous division, and of its counter, in the pre-5.3 Lock.obtain(long) for very large timeouts, fixed 2007), #1431 (LUCENE-353, a loop-counter off-by-one in that older loop, fixed 2006), #14221 (made DEFAULT_POLL_INTERVAL final), and #12946 and #14286, which mention the class in passing.
Lucene commit tested
main 5adff78bb168 (12.0.0-SNAPSHOT): gradle in two scratch worktrees for this doc (the repro fails with assertions on and off and passes with the fix; with the fix TestSleepingLockWrapper (6 tests, 1 skipped by a seed-dependent assumption) and the org.apache.lucene.store tests (29 classes, 778 tests, the new test included) pass; the formatter check passes). Earlier runs of an earlier variant of the test (same trigger; it accepts the lock or an IllegalArgumentException from the constructor) on the 10.6.0 and 9.12.3 jars, and of release-adapted copies of that variant on 5.3.0, 6.6.6, 7.7.3 and 8.11.4, all fail with the same ArithmeticException.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:23-28 (at 5adff78bb168) (class javadoc 'This is not a good idea.', public final class, no @lucene.internal), :52-60 (3-argument constructor javadoc: '@param pollInterval poll once per this interval in milliseconds until lockWaitTimeout is exceeded'), :61-75 (constructor; :71-74 'if (pollInterval < 0) throw new IllegalArgumentException("pollInterval must be a non-negative number ...")': fix site), :79-102 (obtainLock: ':81 long maxSleepCount = lockWaitTimeout / pollInterval;' before the first in.obtainLock at :86)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:954 (at 5adff78bb168) ('writeLock = d.obtainLock(WRITE_LOCK_NAME);' in the IndexWriter constructor)
  • lucene/core/src/test/org/apache/lucene/store/TestSleepingLockWrapper.java:29-30 (at 5adff78bb168) (pollInterval = TestUtil.nextLong(random(), 2, 10): 0 never exercised); grep of lucene/ at 5adff78bb168: no user of the class besides that test
The finding in 30 seconds
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:61-75 (3-argument constructor; :65-70 lockWaitTimeout check; :71-74 'if (pollInterval < 0)' with 'pollInterval must be a non-negative number (got ...)')
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:79-102 (obtainLock; :81 'long maxSleepCount = lockWaitTimeout / pollInterval;'; :86 in.obtainLock; :93 Thread.sleep(pollInterval); :97 loop condition)
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/Directory.java:159-167 (obtainLock javadoc: LockObtainFailedException, IOException)
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:954 ('writeLock = d.obtainLock(WRITE_LOCK_NAME);'), :3063 (acquireWriteLocks); CheckIndex.java:472; lucene/replicator/src/java/org/apache/lucene/replicator/nrt/ReplicaNode.java:103 (grep of '.obtainLock(' in lucene/*/src/java)
Who is affected
  • Lucene main 5adff78bb168: grep of 'SleepingLockWrapper' in lucene/ (only SleepingLockWrapper.java and TestSleepingLockWrapper.java)
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:23-36 (class javadoc 'This is not a good idea.', public final class, DEFAULT_POLL_INTERVAL = 1000), :48-50 (2-argument constructor)
  • a Lucene clone tag releases/lucene-solr/5.3.0: lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:91-92 ('@deprecated Use {@link SleepingLockWrapper} if you want sleeping.')
  • a Lucene clone tags releases/lucene/10.1.0 and 10.2.0: SleepingLockWrapper.java:36 ('public static long DEFAULT_POLL_INTERVAL' vs 'public static final long')
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/FilterDirectory.java:31 and Lock.java:34 ('@lucene.internal' in the class javadoc)
  • the devil's-advocate verifier's verdict record (its GitHub code search for 'new SleepingLockWrapper': Hibernate Search IndexAccessorImpl 'new SleepingLockWrapper( directory, 2000, 20 )'; an older Elasticsearch shared-filesystem path with the 2-argument constructor and 5000)
Why it looks unintended
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:26, :58-59, :65-74
  • Lucene main 5adff78bb168: lucene/core/src/test/org/apache/lucene/store/TestSleepingLockWrapper.java:29-30 (lockWaitTimeout 20-100, pollInterval 2-10)
Suggested fix
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:81, :97 (the loop counts sleeps)
History
  • a Lucene clone tags releases/lucene-solr/5.3.0 (:71 check, :79 division) and releases/lucene/10.6.0 (:71, :81)
Diagrams
  • Lucene main 5adff78bb168: lucene/core/src/java/org/apache/lucene/store/SleepingLockWrapper.java:71-74, :81, :86

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

Repro files

About the diagrams

One diagram of the call path in the repro, made separately.

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.