4 min readRACE-CONDITIONS · WEB · TIMING

The single-packet attack and the races click-twice-fast misses

How HTTP/2 last-byte synchronisation removes network jitter from race testing, and why the real TOCTOU lives at the database isolation level.

LAST BYTE WITHHELD req 1 req 2 req 3 req 4 req n final byte SENT AT ONCE server ALL AT ONCE TOCTOU WINDOW JITTER REMOVED LANDS TOGETHER
Last-byte synchronisation lands every request together

Click twice fast is theatre

The naive race test is two requests sent in sequence, maybe from two browser tabs. The problem is the requests do not arrive together. Each one rides its own TCP path, gets its own scheduling, and lands whenever the network feels like delivering it. The spread between them is tens of milliseconds. The window you are trying to hit, the gap between a server reading a value and writing it back, is often under a millisecond. You are throwing two darts a metre apart at a coin a centimetre wide. You will miss, conclude there is no race, and move on. The bug was there.

What the single-packet attack actually does

Over HTTP/1.1 you reduce jitter with last-byte synchronisation: you send all of each request except its final byte, wait, then release the last bytes together. HTTP/2 makes it cleaner. You put every request you want to race into a single TCP packet using multiple streams, and the server receives them in one read. They are processed as close to simultaneously as the machine allows, with the network removed from the equation. This is the single-packet technique, and the practical effect is that a race which fired one time in a thousand over the open internet now fires reliably.

The real bug lives at the isolation level

The synchronisation is just delivery. The vulnerability is what the database does when two transactions read the same row before either writes. Picture a coupon redemption:

sql
-- transaction reads current state
SELECT used FROM coupons WHERE code = 'WELCOME50';   -- used = false
-- application decides: not used, proceed
UPDATE coupons SET used = true WHERE code = 'WELCOME50';
INSERT INTO credits (account, amount) VALUES ('attacker', 5000);

That SELECT then UPDATE is the time-of-check-to-time-of-use gap. Under the default READ COMMITTED isolation level, two concurrent transactions both run the SELECT, both see used = false, both decide to proceed, and both insert the credit. The single UPDATE is idempotent and hides the problem, but the INSERT ran twice. The coupon paid out twice from one code.

The fix is the same thing whose absence is the bug: an atomic guarded write (UPDATE ... WHERE used = false and check the affected-row count), a row lock (SELECT ... FOR UPDATE), a unique constraint, or SERIALIZABLE isolation. If the developer split check and use into two statements without a lock between them, the window is real and the single-packet attack walks straight through it.

How I test it cleanly

I send a burst of identical one-time requests in a single packet and then count outcomes, not responses. Five HTTP 200s mean nothing on their own; what matters is the side effect. So I check the durable state afterward:

http
POST /api/coupon/redeem HTTP/2
Host: example.com

{"code":"WELCOME50"}

Fire 30 of these synchronised, then read the balance. If a single-use coupon credited the account more than once, the check-then-use gap is unguarded. Same shape for an invite that should bind one account but binds several, a withdrawal that clears against a balance read before it was debited, or a referral bonus claimed in parallel before the counter increments.

Why this is worth the trouble

The findings here are quiet and expensive. They do not throw errors, they do not reflect anything, and they pass every functional test because functional tests run one request at a time. The only way they surface is to remove the jitter, widen the window deliberately, and measure the side effect rather than the status code. Once you stop thinking of a race as “two clicks” and start thinking of it as “two transactions reading the same row before either commits,” you know exactly where to look and exactly what to send.