BUG-HUNT: [concurrency] Thread-unsafe Settings singleton in get_settings() allows duplicate initialization #7318

Open
opened 2026-04-10 16:46:47 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Concurrency — Thread-Unsafe Settings Singleton

Severity Assessment

  • Impact: Two threads could independently initialize Settings() and one thread's reference could be discarded, leading to inconsistent configuration state or wasted initialization work
  • Likelihood: Medium — any multi-threaded startup path (e.g. uvicorn with multiple workers or tests running in parallel) can trigger this
  • Priority: Medium

Location

  • File: src/cleveragents/config/settings.py
  • Function/Class: Settings.get_settings()
  • Lines: 716–720

Description

The Settings.get_settings() class method implements a singleton pattern using an unguarded class variable _instance. There is no lock protecting the check-and-set sequence, creating a classic TOCTOU race condition: two threads can simultaneously observe cls._instance is None, both create a new Settings() instance, and one discards the other. While Settings.__init__ calls _apply_external_env_overrides() and primes LangSmith state, the discarded instance means side effects have been run twice and the final singleton may not be the one returned to one of the callers.

Evidence

# src/cleveragents/config/settings.py lines 716-720
@classmethod
def get_settings(cls: type[Settings]) -> Settings:
    """Return the cached singleton instance (creating it if needed)."""
    if cls._instance is None:          # ← Thread A and B both see None
        cls._instance = cls()          # ← Both create a new Settings()
    return cls._instance               # ← Each returns their own copy

The class-level _instance variable is declared on line 33 as:

_instance: ClassVar[Settings | None] = None

No threading.Lock guards the check-and-assign.

Expected Behavior

get_settings() should use a lock (or threading.local + module-level guard) to ensure exactly one Settings instance is created, even under concurrent access.

Actual Behavior

Under concurrent access, multiple Settings() objects may be created; race determines which one becomes the singleton.

Suggested Fix

_instance: ClassVar[Settings | None] = None
_lock: ClassVar[threading.Lock] = threading.Lock()

@classmethod
def get_settings(cls: type[Settings]) -> Settings:
    if cls._instance is None:
        with cls._lock:
            if cls._instance is None:  # double-checked locking
                cls._instance = cls()
    return cls._instance

Category

concurrency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor

## Bug Report: Concurrency — Thread-Unsafe Settings Singleton ### Severity Assessment - **Impact**: Two threads could independently initialize `Settings()` and one thread's reference could be discarded, leading to inconsistent configuration state or wasted initialization work - **Likelihood**: Medium — any multi-threaded startup path (e.g. uvicorn with multiple workers or tests running in parallel) can trigger this - **Priority**: Medium ### Location - **File**: `src/cleveragents/config/settings.py` - **Function/Class**: `Settings.get_settings()` - **Lines**: 716–720 ### Description The `Settings.get_settings()` class method implements a singleton pattern using an unguarded class variable `_instance`. There is no lock protecting the check-and-set sequence, creating a classic TOCTOU race condition: two threads can simultaneously observe `cls._instance is None`, both create a new `Settings()` instance, and one discards the other. While `Settings.__init__` calls `_apply_external_env_overrides()` and primes LangSmith state, the discarded instance means side effects have been run twice and the final singleton may not be the one returned to one of the callers. ### Evidence ```python # src/cleveragents/config/settings.py lines 716-720 @classmethod def get_settings(cls: type[Settings]) -> Settings: """Return the cached singleton instance (creating it if needed).""" if cls._instance is None: # ← Thread A and B both see None cls._instance = cls() # ← Both create a new Settings() return cls._instance # ← Each returns their own copy ``` The class-level `_instance` variable is declared on line 33 as: ```python _instance: ClassVar[Settings | None] = None ``` No `threading.Lock` guards the check-and-assign. ### Expected Behavior `get_settings()` should use a lock (or `threading.local` + module-level guard) to ensure exactly one `Settings` instance is created, even under concurrent access. ### Actual Behavior Under concurrent access, multiple `Settings()` objects may be created; race determines which one becomes the singleton. ### Suggested Fix ```python _instance: ClassVar[Settings | None] = None _lock: ClassVar[threading.Lock] = threading.Lock() @classmethod def get_settings(cls: type[Settings]) -> Settings: if cls._instance is None: with cls._lock: if cls._instance is None: # double-checked locking cls._instance = cls() return cls._instance ``` ### Category concurrency ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor
HAL9000 added this to the v3.5.0 milestone 2026-04-10 19:06:07 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7318
No description provided.