Skip to content

Read-modify-write spans the metadata seam, so a concurrent field update is lost #400

Description

@blooop

Finding

MetadataStorage owns its lock but not its transaction, so a concurrent update to a different field of the same record is lost.

exclusive (domain/metadata.rs:550) takes the flock and calls self.load() under it — correct, and the module doc at :20-24 says why. But the interface offers get_* and add_* and no update, so every caller does read-modify-write across the seam: the record handed to add_worktree was cloned before the lock existed, and add_worktree (:504) inserts it wholesale over the freshly reloaded map.

Call sites:

  • flows/repo_manager.rs:1429-1436 — clone, set last_fetched, add
  • flows/workspace_clone.rs:960-961 — clone, set devpod_workspace_id, add
  • flows/lifecycle.rs:3068-3069 — clone, set devpod_workspace_id, add
  • flows/migration.rs:306 — clone, set local_path, add

The reload does protect other records, and a_mutation_reloads_under_the_lock_rather_than_rewriting_a_stale_copy (:2443) pins exactly that. What it does not protect is a second field of the record being written.

How real is it

Not demonstrated, and worth measuring before acting — hence not filed as a bug. Losing data needs two dl runs writing the same worktree key concurrently, and serialize_launch (flows/launch.rs:904) holds a per-workspace flock across part of that window. The clone-preparation writes (workspace_clone.rs:960) look to fall outside it. Someone should establish whether the window is reachable before paying for the fix.

Shape

update_worktree(key, |w| ...) / update_repository(key, |r| ...), running the closure on the record loaded under the lock. The four call sites become one line each, and the record structs (domain/model.rs:199-223) can stop exposing their fields to the flows layer.

Proving it

Two processes, one worktree key, one field each, asserted afterwards — which is only writable once the update crosses the seam as a closure.

Found by an architecture review.

Activity

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions