Skip to content

UpdateManagerImpl.run() iterates updaters without the read lock #32

Description

@egg82

UpdateManagerImpl.run() iterates the updaters list without taking the read lock, while register() and unregister() mutate it under the write lock.

UpdateManagerImpl.java:128-131:

private void run() {
    pool.schedule(this::run, 5, TimeUnit.SECONDS);

    for (Updater u : updaters) {

That's the raw field, unsynchronized. Every other access is guarded — updaters() takes the read lock (:60), register() (:78) and unregister() (:99) take the write lock. updaters is a plain ArrayList, so a concurrent register() can throw ConcurrentModificationException in the scheduler thread or expose a partially-updated list.

The exposure is wider than startup registration, though. run() schedules its own next invocation at :129 before doing any work, and the pool is Executors.newScheduledThreadPool(Math.max(4, cores/2)) (FetcharrAPIImpl.java:22). Since u.run() blocks on HTTP to each *arr, any cycle taking longer than 5 seconds overlaps the next — so concurrent run() bodies are the normal case on a real install, not a rare race.

That matters for the fix: swapping in the existing updaters() snapshot closes the CME but not the overlap. If overlapping cycles aren't intended, :129 is the thing to look at.

Read from source; not run.


Found with Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions