---
name: two-locks-one-env-var
description: "two independent mutexes over one process-global env var each serialize their own users flawlessly and guard NOTHING between them — the releases#111 root; the tell is a fixture path shape that the suspected helper could not have produced"
metadata: 
  node_type: memory
  type: project
  originSessionId: ad982400-9d45-45e9-8bd7-5bd9243cc1b3
  modified: 2026-08-02T09:48:52.839Z
---

**The class.** A process-global env var (`SPT_HOME`) guarded by *two* `static Mutex`es — one per
test-helper crate — is unguarded. Each helper serializes its own callers perfectly, which is what
makes it read as correct; nothing serializes helper A against helper B. Whoever writes last wins, and
since `spt_store::perch::spt_home()` is **not memoized** (re-reads env every call), a test saves a
fixture into home A and loads it back from home B. Symptom: absent store → empty access baseline
minted over it → an assertion fails on policy that was never loaded.

**Why:** the invariant is one lock per VARIABLE per BINARY, not per helper. A crate may keep its own
ergonomic wrapper; it must not keep its own mutex over a variable another helper in the same binary
also writes.

**How to apply:**
- **The tell is the path shape.** releases#111's panic printed `<tmp>\home\identity\trust\access.json`.
  The `home` *subdir* is `spt_test_support::TestHome`; `spt-daemon`'s `with_home` uses the tempdir
  **root**. A fixture path the suspected helper could not have produced names the real aggressor —
  faster than reading either helper.
- **Refute the "sibling races me" story first.** `with_home` holds its mutex across its whole body, so
  its callers were never racing each other. Read the helper's scope before accepting a filing's race.
- **Sweep for the class, not the instance.** Grep every crate declaring an `SPT_HOME` helper, then ask
  which of those binaries ALSO reach `spt_test_support` — that intersection is the whole population.
  Doing this found a **second live instance** (`crates/spt`: `testutil::LOCK` + `cli.rs` `TestHome`),
  latent and unreported. See [[sweep-dispatch-site-counts]].
- **Fix:** expose the shared lease (`spt_test_support::env_lock()`); crate helpers take *that* guard.
  NOT reentrant — a `TestHome` holds it for its lifetime, so never build one inside a `with_home` body.
- **The guard must discriminate:** hold the lease, have a second thread attempt a `TestHome`, assert it
  cannot enter and the env cannot move — *then* assert it proceeds after release, so a shared lock is
  proven to be a lock and not a deadlock. Run BOTH arms ([[instrument-soundness-guards]]).
- **Positive control matters:** a filtered run that reports `0 passed` proves nothing
  ([[zero-match-filter-reads-as-absent]]) — `spt --bins` 574 passed was the real check that sharing the
  lock did not deadlock the other helper's users.

Nextest hides it (per-test processes) and single-test runs hide it (no concurrent holder), so CI's
unit lane is **structurally blind**; it only burns whoever hand-runs the module.

Fixed @a6e04f0, `REQ-TEST-ENV-ONE-LOCK`. See [[stacked-defects-mask-each-other]],
[[unit-lane-inprocess-isolation]], [[isolated-home-isolates-broker-socket]].
