BUG-HUNT: [concurrency] TOCTOU race condition on message sequence number in PersistentSessionService.append_message() #7326

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

Bug Report: Concurrency — TOCTOU Race on Message Sequence Number

Severity Assessment

  • Impact: Two concurrent callers appending messages to the same session will both read the same count, both create messages with sequence=count, and both insert them — causing duplicate sequence numbers and corrupting message order
  • Likelihood: Low-Medium — requires concurrent appends to the same session, possible in multi-threaded server mode
  • Priority: Medium

Location

  • File: src/cleveragents/application/services/session_service.py
  • Function/Class: PersistentSessionService.append_message()
  • Lines: 184–195

Description

append_message() determines the next sequence number by calling count_for_session() outside the transaction, then creates the message with that count as sequence. The count read and the insert are two separate operations — not atomic. Two concurrent callers for the same session will both see the same count and both assign the same sequence number.

Evidence

# session_service.py lines 184-195
count = self._message_repo.count_for_session(session_id)  # ← read sequence

message = SessionMessage(
    message_id=str(ULID()),
    role=role,
    content=content,
    sequence=count,    # ← both threads use same count!
    timestamp=datetime.now(),
    metadata=metadata or {},
)

self._message_repo.append(session_id, message)  # ← both insert with same sequence

Race scenario:

  1. Thread A: count = count_for_session(session_id) → 5
  2. Thread B: count = count_for_session(session_id) → 5 (B also sees 5)
  3. Thread A: inserts message with sequence=5
  4. Thread B: inserts message with sequence=5 — DUPLICATE!

Expected Behavior

The sequence number should be assigned atomically within the insert transaction, using a database-level mechanism (e.g. MAX(sequence) + 1 in a serialized transaction) or by acquiring a session-level lock before reading the count.

Actual Behavior

Under concurrent access, two messages can be assigned the same sequence number, corrupting conversation order.

Suggested Fix

Use an atomic insert with sequence computed from the database in a single transaction:

# Option 1: DB-level MAX + 1 in a single transaction
# Option 2: Acquire session-level lock before count_for_session
# Option 3: Use ULID/timestamp ordering instead of integer sequence

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 — TOCTOU Race on Message Sequence Number ### Severity Assessment - **Impact**: Two concurrent callers appending messages to the same session will both read the same `count`, both create messages with `sequence=count`, and both insert them — causing duplicate sequence numbers and corrupting message order - **Likelihood**: Low-Medium — requires concurrent appends to the same session, possible in multi-threaded server mode - **Priority**: Medium ### Location - **File**: `src/cleveragents/application/services/session_service.py` - **Function/Class**: `PersistentSessionService.append_message()` - **Lines**: 184–195 ### Description `append_message()` determines the next sequence number by calling `count_for_session()` outside the transaction, then creates the message with that count as `sequence`. The count read and the insert are two separate operations — not atomic. Two concurrent callers for the same session will both see the same count and both assign the same sequence number. ### Evidence ```python # session_service.py lines 184-195 count = self._message_repo.count_for_session(session_id) # ← read sequence message = SessionMessage( message_id=str(ULID()), role=role, content=content, sequence=count, # ← both threads use same count! timestamp=datetime.now(), metadata=metadata or {}, ) self._message_repo.append(session_id, message) # ← both insert with same sequence ``` Race scenario: 1. Thread A: `count = count_for_session(session_id)` → 5 2. Thread B: `count = count_for_session(session_id)` → 5 (B also sees 5) 3. Thread A: inserts message with `sequence=5` 4. Thread B: inserts message with `sequence=5` — DUPLICATE! ### Expected Behavior The sequence number should be assigned atomically within the insert transaction, using a database-level mechanism (e.g. `MAX(sequence) + 1` in a serialized transaction) or by acquiring a session-level lock before reading the count. ### Actual Behavior Under concurrent access, two messages can be assigned the same sequence number, corrupting conversation order. ### Suggested Fix Use an atomic insert with sequence computed from the database in a single transaction: ```python # Option 1: DB-level MAX + 1 in a single transaction # Option 2: Acquire session-level lock before count_for_session # Option 3: Use ULID/timestamp ordering instead of integer sequence ``` ### 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.2.0 milestone 2026-04-10 19:05:20 +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#7326
No description provided.