Skip to content

fix(hosted): compare token and approval expiries after the advisory lock wait - #917

Merged
nedtwigg merged 3 commits into
mainfrom
fix/issue-911
Oct 2, 2026
Merged

nedtwigg merged 3 commits into
mainfrom
fix/issue-911

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

locked opens its transaction with BEGIN and then waits on pg_advisory_xact_lock, but every liveness check inside it compared against now(), which Postgres fixes at the transaction's start, before the wait. A setup token whose expiry passed while restoreSetupToken waited on the Burrow's lock was therefore reinserted, and at MAX_TOKENS_PER_BURROW the trimmed CTE evicted a live token to make room for it. The enrollment poll's redeem had the same flaw.

Those comparisons now read LOCKED_NOW (statement_timestamp(), exported beside locked in hosted/server/relay-auth.ts). Each statement inside the action starts after the lock is held, and unlike clock_timestamp() it stays the same across a statement's CTEs, so pruned, trimmed, and the insert guard agree on one instant. This covers admit's three comparisons and the poll's redeem UPDATE plus its follow-up SELECT. admit's trimmed CTE is also gated on the row's own liveness: it ran whether or not the insert guard admitted the row, so even a correctly refused restore evicted a live token. after() keeps clock_timestamp() for TTL stamps. hosted.md gains the rule, and its word budget is ratcheted by 50.

The new test in hosted/server/tests/relay.test.ts fills a Burrow to its token cap, holds that Burrow's advisory lock from a second connection across a restored token's expiry, and asserts the restore inserts nothing and evicts nothing. The test failed in CI on the first commit with 7 of 8 tokens left, which exposed the trim; the second commit gates it. Against a local Postgres 16, the same lock-wait pattern inserted the expired row with WHERE expiry > now(), and inserted nothing with clock_timestamp() or statement_timestamp(), and the gated statement left a full Burrow's 8 tokens intact for an expired restore.

Closes #911 — automated triage

… transactions

`now()` is the transaction's start, read at BEGIN before
pg_advisory_xact_lock waits, so a setup token that expired while its
restore waited was reinserted and could evict a live one. Compare
against statement_timestamp() (LOCKED_NOW) instead.

Closes #911
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0a37ad8
Status:⚡️  Build in progress...

View logs

The trimmed CTE ran whether or not the insert's guard admitted the
row, so a restore refused as expired still evicted the Burrow's
oldest live token at the cap.
@nedtwigg
nedtwigg merged commit f719f39 into main Oct 2, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expiry checks inside locked transactions read the transaction's start time, before the lock wait

2 participants