Skip to content
6 changes: 6 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
"description": "Protobuf skills for designing messages and fields and for evolving schemas without breaking consumers",
"source": "./plugins/trogonstack-protobuf",
"category": "development"
},
{
"name": "trogonstack-concurrency",
"description": "Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores",
"source": "./plugins/trogonstack-concurrency",
"category": "development"
}
]
}
5 changes: 5 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
"name": "trogonstack-protobuf",
"source": "plugins/trogonstack-protobuf",
"description": "Protobuf skills for designing messages and fields and for evolving schemas without breaking consumers"
},
{
"name": "trogonstack-concurrency",
"source": "plugins/trogonstack-concurrency",
"description": "Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores"
}
]
}
3 changes: 3 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
},
"plugins/trogonstack-protobuf": {
"component": "trogonstack-protobuf"
},
"plugins/trogonstack-concurrency": {
"component": "trogonstack-concurrency"
}
},
"plugins": [
Expand Down
3 changes: 2 additions & 1 deletion .github/release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"plugins/trogonstack-eda": "0.2.0",
"plugins/trogonstack-product-requirements": "0.2.0",
"plugins/trogonstack-elixir": "0.2.0",
"plugins/trogonstack-protobuf": "0.1.1"
"plugins/trogonstack-protobuf": "0.1.1",
"plugins/trogonstack-concurrency": "0.0.1"
}
9 changes: 9 additions & 0 deletions plugins/trogonstack-concurrency/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "trogonstack-concurrency",
"description": "Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores",
"version": "0.0.1",
"author": {
"name": "TrogonStack",
"url": "https://github.com/TrogonStack"
}
}
25 changes: 25 additions & 0 deletions plugins/trogonstack-concurrency/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "trogonstack-concurrency",
"displayName": "TrogonStack Concurrency",
"description": "Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores",
"version": "0.0.1",
"author": {
"name": "TrogonStack"
},
"homepage": "https://github.com/TrogonStack/agentskills/tree/main/plugins/trogonstack-concurrency",
"repository": "https://github.com/TrogonStack/agentskills",
"license": "MIT",
"keywords": [
"trogonstack",
"concurrency",
"toctou",
"race-condition"
],
"category": "developer-tools",
"tags": [
"concurrency",
"toctou",
"race-condition"
],
"skills": "./skills/"
}
7 changes: 7 additions & 0 deletions plugins/trogonstack-concurrency/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# trogonstack-concurrency

Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores.

```bash
claude plugin install trogonstack-concurrency@trogonstack
```
19 changes: 19 additions & 0 deletions plugins/trogonstack-concurrency/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "trogonstack-concurrency",
"version": "0.0.1",
"description": "Concurrency skills for finding and fixing time-of-check to time-of-use races in files, databases, caches, authorization, and distributed stores",
"author": {
"name": "TrogonStack",
"url": "https://github.com/TrogonStack"
},
"homepage": "https://github.com/TrogonStack/agentskills/tree/main/plugins/trogonstack-concurrency",
"repository": "https://github.com/TrogonStack/agentskills",
"license": "MIT",
"keywords": [
"trogonstack",
"concurrency",
"toctou",
"race-condition"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: concurrency-find-toctou
description: "Find, prove, and fix time-of-check to time-of-use (TOCTOU) races: code that checks a condition, then acts as if nothing changed in between. Covers check-then-act in files (exists then open), databases (SELECT then INSERT, find-or-create, read-modify-write), authorization (permission check, later effect), caches (get-or-compute), distributed stores (NATS KV revision, JetStream expected sequence, event-store expected version), and async code (check, await, write). Each finding names the check, the act, the window, who can interleave, the consequence, and the fix. Use when reviewing code for race conditions, duplicate inserts, lost updates, double charges, stale permission checks, flaky tests under parallel load, intermittent 'already exists' errors, when asked 'is this atomic', 'can two requests collide', or 'what if this runs twice', and before adding a mutex, lock, or retry loop. Do not use for: deadlocks or lock ordering, performance tuning, memory-model data races, event model or NATS subject design."
allowed-tools:
- AskUserQuestion
- Read
- Write
- Shell
---

# Find TOCTOU Races

A check is true only at the instant it is made. Correctness has to come from an atomic primitive at the point of effect, never from the gap between the check and the act being short.

## Core Principle

Every `if exists / if allowed / if enough / if not yet` followed by a write is a bet that nothing else touched the same state in between. The bet is lost by another request, another process, another node, a retry, a redelivered message, or the same user clicking twice. A short window does not shrink the bug, it only lowers how often the test suite sees it.

The fix is never "check faster" or "check again". The fix is to make the store that owns the state reject or absorb the second actor. Everything below follows from that.

## Principles

Apply them in order. A pair that fails an earlier principle is a finding; the later principles decide the fix and what the finding has to cite. Each one names the references that show it in practice; read them when the situation matches, not up front.

### 1. A Read Is A Fact About The Past

The moment a read returns, the value it reported is history. A condition evaluated on that value is a statement about a moment that has already passed, and nothing in the language makes the next line run in the same moment. Any check-then-act pair is therefore stale by construction unless something else holds the world still.

Read [references/principles/read-is-the-past.md](references/principles/read-is-the-past.md) when deciding whether a pair is a candidate at all, or when someone argues the window is too small to matter.

### 2. Only The Owner Of The State Can Decide

The store that holds the state is the only actor that sees every writer. A decision made anywhere else is made on a copy: an application variable, a cache, a projection, a session. The atomic primitive has to come from the owner (a constraint, a conditional write, an exclusive create, a revision), and the caller's job is to ask the owner to decide.

Read [references/principles/owner-decides.md](references/principles/owner-decides.md) when picking which layer the fix belongs in, or when a proposed fix adds a lock in the caller.

### 3. The Write Must Carry Its Condition

When the condition travels inside the write, the store evaluates it at the instant it applies the write, and the earlier check becomes a fast path or disappears. `WHERE status = 'pending'`, `ON CONFLICT`, `O_EXCL`, an expected revision, and an expected version are all the same move: the condition and the effect in one indivisible operation whose result tells the caller whether it won.

Read [references/principles/write-carries-condition.md](references/principles/write-carries-condition.md) when shaping the fix, and the surface reference for how the store under review spells it.

### 4. Serialization Is Proven By Naming The Serializer

A pair is safe only when something already makes the actors take turns, and that something has a name: one writer by construction, a lock every writer takes, a synchronous section with no yield. "Unlikely", "rarely called", and "never seen it" are not names. A safe verdict without a named serializer is an unreviewed pair.

Read [references/principles/serializer-is-named.md](references/principles/serializer-is-named.md) before marking any pair safe.

### 5. A Lock Only Helps When The Read Happens Under It

A lock serializes the actors that take it, for the span they hold it. A read outside the lock, a writer that skips the lock, or a lease that can expire mid-act all leave the race in place and add the cost of the lock on top. Reach for a lock only when the decision genuinely needs application code between the read and the write, and only when every writer can be made to take it.

Read [references/principles/lock-moves-the-race.md](references/principles/lock-moves-the-race.md) when a lock or a lease is proposed or already present.

### 6. Retry Belongs Around The Conditional Write

A conditional write reports a conflict; a retry around it reloads, re-decides, and tries again against fresh state. A retry around a check-then-act repeats the race with the same stale read. Serializable isolation, optimistic versions, and expected revisions all require this loop or they turn a silent race into a user-facing error.

Read [references/principles/retry-wraps-the-write.md](references/principles/retry-wraps-the-write.md) when a retry loop exists or the fix introduces one.

### 7. Idempotence Absorbs What Ordering Cannot Prevent

Redelivery, client retries, and double submits run the same act twice no matter how well the single run is guarded. When the second run produces the same end state and no second side effect, the race is tolerable even though it exists. Idempotence is a property of the effect, keyed by something deterministic, and the finding has to say which side effects it covers.

Read [references/principles/idempotence-absorbs.md](references/principles/idempotence-absorbs.md) when the interleaver is a retry or a redelivery, or when the effect cannot be made conditional.

## Procedure

1. Locate every check-then-act pair: a read whose result decides a write, including pairs split across functions, an `await`, a queue hop, or two requests.
2. Describe the pair: the check, the act, the window, who can interleave, and the consequence.
3. Name the serializer, or name the principle the pair violates. A pair without a named serializer is a finding.
4. Pick the primitive from the owner of the state, with the write carrying its condition. Read [references/review/fix-catalog.md](references/review/fix-catalog.md) to choose, then the surface and library references for the spelling.
5. Write the finding in the format below, one per pair. Pairs judged safe are listed separately with their serializer.

## Finding Format

```
### <short title>

- Location: <file:line> (check) and <file:line> (act)
- Check: <what is read and the condition on it>
- Act: <what is written>
- Window: <what runs between them: an await, a network call, a queue hop, a second request>
- Interleaver: <who else can run between the check and the act, and how>
- Consequence: <the concrete wrong outcome: duplicate row, negative balance, write by a revoked user>
- Principle: <the principle the pair violates, by name>
- Fix: <the atomic primitive, and the code shape that uses it>
- Residual: <what the fix still does not cover, or "none">
```

Read [references/review/finding-template.md](references/review/finding-template.md) for a fully worked example.

## Surfaces

Each surface file names the principles it instantiates, shows the smells in SQL or pseudocode, and points at the library files that spell the primitive.

Read [references/surfaces/filesystem.md](references/surfaces/filesystem.md) when the check is `exists`, `stat`, `access`, or a path lookup, and the act opens, creates, renames, or deletes.

Read [references/surfaces/database.md](references/surfaces/database.md) when the check is a `SELECT` and the act is an `INSERT`, `UPDATE`, or `DELETE`, including find-or-create, counters, balances, and status transitions.

Read [references/surfaces/authorization.md](references/surfaces/authorization.md) when the check is a permission, role, ownership, or quota decision and the effect happens later, in another request, or in a job.

Read [references/surfaces/cache.md](references/surfaces/cache.md) when the check is a cache miss and the act fills the cache or recomputes a value.

Read [references/surfaces/distributed.md](references/surfaces/distributed.md) when the state lives in NATS KV, a JetStream stream, an event store, or behind an at-least-once consumer.

Read [references/surfaces/in-process.md](references/surfaces/in-process.md) when the check and the act are in the same process but separated by an `await`, a message to another process, or a second call into the same server.

## Systems

Each system file states what the store itself guarantees, independent of the client library: its isolation or execution model, the writes that carry a condition, the locks it offers, and what does not serialize despite looking like it does.

Read [references/systems/postgres.md](references/systems/postgres.md) when the state lives in PostgreSQL: isolation levels, `ON CONFLICT` arbiters, `FOR UPDATE` and `FOR NO KEY UPDATE`, advisory locks, `40001`.

Read [references/systems/mysql.md](references/systems/mysql.md) when the state lives in MySQL or InnoDB: snapshot versus locking reads, gap locks on missing rows, `ON DUPLICATE KEY UPDATE`, affected rows versus found rows, `GET_LOCK`.

Read [references/systems/sqlite.md](references/systems/sqlite.md) when the state lives in SQLite: one writer, `BEGIN IMMEDIATE` versus deferred, `SQLITE_BUSY` after a read, WAL mode, `:memory:` per connection.

Read [references/systems/redis.md](references/systems/redis.md) when the state lives in Redis: single-command atomicity, `SET NX PX`, `WATCH` and `EXEC`, scripts, leases versus locks, failover, cluster slots.

Read [references/systems/nats.md](references/systems/nats.md) when the state lives in NATS: expected sequence headers, `Nats-Msg-Id`, KV revisions, streams as claims, at-least-once consumers and their configuration.

## Libraries

After the surface and system references, read the file for the library under review when one exists. Each covers one library, organized by the surfaces it touches. Redis and NATS clients have no separate file; their spellings live at the end of the system file.

- TypeScript: [node-fs](references/typescript/node-fs.md), [node-postgres](references/typescript/node-postgres.md), [prisma](references/typescript/prisma.md), [drizzle](references/typescript/drizzle.md), [event-loop](references/typescript/event-loop.md).
- Rust: [std-fs](references/rust/std-fs.md), [sqlx](references/rust/sqlx.md), [tokio](references/rust/tokio.md), [moka-dashmap](references/rust/moka-dashmap.md), [atomics](references/rust/atomics.md).
- Elixir: [file](references/elixir/file.md), [ecto](references/elixir/ecto.md), [ets](references/elixir/ets.md), [genserver](references/elixir/genserver.md), [registry](references/elixir/registry.md).
- Go: [os](references/go/os.md), [database-sql](references/go/database-sql.md), [sync](references/go/sync.md), [singleflight](references/go/singleflight.md).
- Python: [os](references/python/os.md), [psycopg](references/python/psycopg.md), [sqlalchemy](references/python/sqlalchemy.md), [asyncio](references/python/asyncio.md).
- C: [posix-fs](references/c/posix-fs.md), [atomics](references/c/atomics.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# C Atomics And pthreads

Spells scalar compare-and-swap and the mutex discipline from [in-process.md](../surfaces/in-process.md).

## Compare-And-Swap Loop

```c
#include <stdatomic.h>

_Atomic uint64_t counter;

uint64_t cur = atomic_load(&counter);
for (;;) {
if (cur >= limit) return -1;
if (atomic_compare_exchange_weak(&counter, &cur, cur + 1)) break;
}
```

`atomic_compare_exchange_weak` reloads `cur` with the observed value on failure, which is what the loop needs. `atomic_load` then `atomic_store` is the race; `atomic_fetch_add` alone cannot enforce the limit because it has no condition.

GCC and Clang builtins: `__atomic_compare_exchange_n(&counter, &cur, cur + 1, true, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)`.

## Mutex Discipline

A `pthread_mutex_t` held across both the check and the write is correct when every writer to that state takes the same mutex. A check outside the mutex followed by a locked write is the race, and so is a double-checked read of a non-atomic field without a fence.

## Where Atomics Stop

An atomic protects one word. A decision that reads two atomics and writes one has the same gap as any two reads; use a mutex over the pair or pack the state into one word.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# POSIX Filesystem In C

Spells the filesystem primitives from [filesystem.md](../surfaces/filesystem.md).

## Exclusive Create

```c
int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd < 0) {
if (errno == EEXIST) { /* someone else created it */ }
else return -1;
}
```

`mkdir` fails with `EEXIST` the same way; `access` or `stat` before either is the race.

## Open Then Verify

Verify the descriptor you hold, instead of `stat` then `open`.

```c
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;
struct stat st;
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_uid != getuid()) {
close(fd);
return -1;
}
```

## Untrusted Trees

Open the directory once and use `openat(dirfd, "child", O_NOFOLLOW | O_CLOEXEC)` for each child, so a swapped parent component cannot redirect the walk. On Linux, `openat2` with `RESOLVE_BENEATH` refuses every escape in one call.

## Temp File And Atomic Replace

`int fd = mkstemp(template);` where `template` ends in `XXXXXX` and lives in the target's directory. Write to that descriptor, `fsync(fd)`, `close(fd)`, then `rename(tmp, target)`. Building a name and calling `access` first is the race.

## Idempotent Delete

Call `unlink` and accept `ENOENT`.
Loading
Loading