Skip to content

Store.correct() has a TOCTOU race between find and write #16

Description

@fayevans

Store.correct() has a TOCTOU race between find and write

Severity: High
File: packages/core/src/agent_memory/core/store.py:177

correct() reads the current record via self.find(name) (no lock), then acquires
store_lock only for the provenance append, then calls self.write() which acquires
a second, independent lock. Between the initial find() and the final write(),
a concurrent writer (another process or adapter hook) can modify the same record, and
correct() will silently overwrite those changes.

def correct(self, name, ...):
    current = self.find(name)          # no lock — reads stale snapshot
    ...
    with store_lock(self.layout):      # lock 1: provenance only
        for excerpt in provenance or []:
            ...
    return self.write(current)         # lock 2: writes the stale snapshot

Why it matters

Two concurrent agents calling correct on the same memory lose each other's edits.
The record_many batch path holds the lock for the entire operation (store.py:96), but
correct splits it into three non-atomic steps. A sleep cycle running _normalise_dates
or _settle_weights concurrently with a user-initiated correct will produce the same
lost-update pattern.

Activity

  1. Apageoflove commented on Oct 3, 2026

    @Apageoflove

    I think this one can also be closed. It was fixed by b49f1e6 ("fix: serialize corrections through shared store write path", 2026-09-17), which like #15 didn't reference the issue, so it stayed open.

    Scope note: everything below is code reading on current main plus the commit diff — 仅代码阅读层面的个人判断. The tests themselves I did not run(未验证,我没实测过:Windows 机器,store lock 用 fcntl);them passing is CI's word.

    What I checked:

    correct() now runs the whole read-modify-write under one store_lock: find, validate, mutate, then a new _write_locked helper that writes without re-acquiring the lock (store.py, the correct() around line 355).

    The same commit added tests/unit/test_correction_concurrency.py, which drives the exact race from the report: one process pauses inside its find() while a second process corrects the same record, then asserts both updates survive and the search index stays consistent. There is also a delete-racing-corrections case where retained provenance must equal successful corrections.

    delete, merge, feedback and record_many hold the lock across their find+write in the same shape, again per code reading.

    On that reading, the three-step window described in the report no longer exists on main — 需要确认的话以 CI 和维护者复验为准。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions