Skip to content

wt config state marker under concurrency can rename the shared parent directory of many repos (CWD-recovery false-match / TOCTOU) #3552

Description

@4i3n6

Summary

Under a storm of concurrent wt config state marker calls, worktrunk renamed the
shared parent directory of ~180 sibling repos. The parent got renamed to a truncated
form of a child repo's name (a 9-char repo name; the parent ended up named after its
first 7 chars). Every repo under the parent became inaccessible and running agents
broke with NotFound. It was a single rename(2) — inode identity and mtime
preserved, only ctime changed — so nothing was deleted, but it is a data-loss-class
failure.

I never invoked wt switch/merge/remove. The only wt calls in play were
wt config state marker set, emitted automatically by the OpenCode activity-marker
plugin.

Environment

  • worktrunk 0.68.0
  • Harness: OpenCode with the bundled worktrunk activity-marker plugin
  • Layout: ~180 git repos as siblings under a single parent directory ($PARENT);
    one of them has 3 worktrees, also siblings under $PARENT.

Trigger

The plugin shells out to wt on every session event:

// session.status  -> wt config state marker set <marker>
// session.idle    -> wt config state marker set <marker>
// session.deleted -> wt config state marker clear

With N parallel agents, every status/idle transition spawns a wt process.
Process-launch logs show ~465 wt spawns in ~29s, all contending on worktrunk state
and CWD/worktree resolution.

Timeline (relative)

  • T-13s: agent healthy, cwd = $PARENT/<repo>
  • T+0s: burst of wt spawns begins (~465 over ~29s)
  • T+5s: $PARENT inode ctime changes -> rename($PARENT -> sibling <repo-truncated>);
    mtime unchanged, so a pure rename, not copy/delete
  • T+29s: harness reports NotFound: FileSystem.access ($PARENT/<repo>); git worktree ops fail

Suspected root cause

The CWD-recovery path (hardened in #1193, "prevent false matches when multiple repos
share a parent directory") still appears to mis-resolve under concurrency. When a wt
observes a transiently-missing worktree while hundreds of siblings share $PARENT,
recovery/relocate picks the wrong target and renames the parent. Looks TOCTOU,
consistent with the other rename/TOCTOU fixes in the changelog (#2870, #2849, #2808).

Impact

A shipped, automatic agent plugin (wt config state marker) — a command a user would
reasonably assume is read-mostly/telemetry — can rename the parent of every repo in a
workspace, with no explicit mutating subcommand from the user.

Asks

  1. wt config state marker (and any read-mostly/telemetry command) must never
    relocate/rename a directory. Gate relocation behind explicit mutating commands.
  2. Serialize/lock state mutations per repo so the marker storm cannot race CWD-recovery.
  3. Hard-refuse any recovery/relocate whose target is an ancestor of the repo or contains
    other repos.

I can attach the relevant process-launch and OpenCode logs on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions