the world between processes is unreliable
File & Network Access
TOCTOU file races, partial writes, dropped packets, and retries without idempotency — the failures the Fallacies of Distributed Computing warn about.
Disk and network calls fail partway, arrive twice, or never arrive at all.
01in the wild
In the wild
TOCTOU File Race
Checking then using a path leaves a gap where the file can change underneath you.
example.py
# SMELL: check-then-use -- the file can change in the gap (TOCTOU)
if os.path.exists(path): # an attacker swaps the file here...
with open(path) as f: # ...so you open something else
return f.read()
# RIGHT: just try to open it; let one syscall decide atomically
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
return NoneBetween exists() and open() the path can be replaced (a symlink swap). Opening directly collapses check and use into one atomic operation.
// observed
check-then-open: races, symlink swaps, surprises try/except: one atomic syscall, no gap
example.sh
# SMELL: test then act -- classic TOCTOU window
if [ -f "$f" ]; then
rm "$f" # $f may have changed between test and rm
fi
# RIGHT: act and tolerate the race
rm -f "$f" # idempotent: no error if it is already goneThe [ -f ] test and the rm are two steps with a gap between them. rm -f is a single idempotent action that doesn't care about the race.
// observed
[ -f ] then rm: window for a swap rm -f: atomic, idempotent
Retry & Timeout
A timed-out call may have already succeeded; a call with no deadline can hang forever.
example.py
# SMELL: blind retry of a non-idempotent call -> double charge
for _ in range(3):
try:
return charge(card, amount) # a timeout is not a failure!
except Timeout:
continue # the first call may have won
# RIGHT: an idempotency key makes retries safe to repeat
key = idempotency_key(order)
for _ in range(3):
try:
return charge(card, amount, idempotency_key=key)
except Timeout:
continue # the server dedupes by key -> at most one chargeA timeout means 'unknown', not 'failed'. Retrying a non-idempotent call can double it; an idempotency key lets the server collapse retries to one effect.
// observed
blind retry: a timed-out success is charged again idem key: server collapses retries to one charge
example.go
// SMELL: no timeout -- a stalled peer hangs the caller forever
resp, err := http.Get(url) // can block indefinitely
// RIGHT: bound every network call with a context deadline
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)Without a deadline, one slow peer ties up a connection (and a goroutine) forever. A context timeout fails fast and frees the resource.
// observed
no timeout: one slow peer exhausts the pool with ctx: fails fast at 2s, resources freed
02cross-pollination
Where this compounds
Nondeterminism
- Temp-File Check-Then-Use Race × Time, Money & Entropy
Data Corruption
- Untrusted Deserialization Across a Boundary × Insecure Deserialization
- Schema Drift / Mixed-Version Write × Unconstrained Inputs
- Stored / Second-Order Injection × Lack of Input Validation
- Encoding / Charset Corruption (Mojibake) × Version & Library Mismanagement
- Unchecked Sentinel Corrupts a Shared Record × Missing & Undefined Returns
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.