Skip to content

Concurrent aps set profile + aps set profileName silently loses one update (no cross-process lock on FileState/Slice RMW) #99

Description

@0xLeif

Found during an adversarial dogfooding pass on main @ 6a40258 (release binary, Linux).

Repro

d=$(mktemp -d)
APS_HOME=$d aps set profile '{"name":"base","version":1}' >/dev/null
( APS_HOME=$d aps set profile '{"name":"parent","version":99}' >/dev/null ) &
( APS_HOME=$d aps set profileName "slice-name" >/dev/null ) &
wait
APS_HOME=$d aps get profile

Observed over 12 trials:

  • 7 ended {"name":"slice-name","version":1}: the parent write of version:99 was silently lost
  • 2 ended {"name":"parent","version":99}: the slice write was lost
  • 3 landed both updates

Both writers exit 0 and report success in every trial, so callers cannot detect the loss.

Why

A Slice set is a read-modify-write over the whole parent document: StateStore.set(.profileName) refreshes FileState from disk, mutates name, and rewrites profile.json; DynamicKeyStorage.sliceSet does the same for user-defined slices. #91 fixed the stale in-process cache, but there is no cross-process serialization on the document file (the SchemaFileLock only covers schema.json), so a parent writer and a slice writer interleave read/write and one update is dropped.

Two concurrent aps set profile calls are plain last-writer-wins on the whole document, which is defensible. The slice case is worse: partial updates amplify the loss, and the version-preservation guard added in #91 (expectedVersion) only compares against the post-refresh read, so it cannot see a peer that committed in between.

Suggestions

  • Take a per-file lock (reuse the SchemaFileLock pattern) around FileState/Slice read-modify-write, or
  • Detect the interleaving (compare the on-disk document again at write time) and fail with a conflict error instead of silently dropping the peer update, and/or
  • Document the semantics in specs/state-store if silent last-writer-wins is the intended contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghardeningCorrectness, isolation, or cleanup work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions