Skip to content

fix(api): test-suite deadlock + 3 CodeQL alerts on main - #71

Merged
agjs merged 2 commits into
mainfrom
fix/test-deadlock-and-codeql
May 31, 2026
Merged

agjs merged 2 commits into
mainfrom
fix/test-deadlock-and-codeql

Conversation

@agjs

@agjs agjs commented May 31, 2026

Copy link
Copy Markdown
Contributor

Three independent fixes batched into one PR because the test-suite fix is what unblocks merging to main without flaking, and the security alerts are the next things blocking a clean main.

Test deadlock — tests/helpers/db.ts

Symptom: random CI failures of apps-api-ci.yml. The integration tests share one postgres instance, and cleanDatabase() was a single "TRUNCATE ... RESTART IDENTITY CASCADE" statement. TRUNCATE acquires AccessExclusiveLock on every listed table; concurrent SELECTs from another test worker take AccessShareLock on those same tables. With the suite running files in parallel, the two lock orders collide and postgres aborts one side with deadlock_detected.

Fix: rewrite cleanDatabase() as a transaction that takes a pg_advisory_xact_lock (serialising cleanup calls across all workers) and DELETEs each table in child-first dependency order. DELETE takes RowExclusiveLock, which does NOT conflict with the SELECT's AccessShareLock — so a mid-cleanup worker no longer deadlocks with a mid-test worker. Trade-off is a few ms of extra wall-clock per cleanup, well under the budget for a CI suite that never spuriously fails.

CodeQL js/path-injection (x2) — templates/email/preview.ts

The dev-only email preview server reads req.url-derived paths via fs.existsSync and fs.readFileSync. The previous path.join + startsWith(PREVIEW_DIR) guard was correct in practice, but path.join doesn't canonicalise '..' segments and CodeQL can't see the prefix check as sufficient. Switch to path.resolve (which normalises traversals), strip leading slashes / query / fragment from the URL, and re-validate after the directory/extension fallback — mostly to make the contract explicit at every read site.

CodeQL js/redos — scripts/codegen/new-resource.ts

Codegen scaffolder's usersBlock regex used (?:[^}]\n)? — a quantifier-inside-a-quantifier shape that backtracks pathologically on input with many bare newlines. Replaced with [\s\S]*? (a single lazy quantifier = linear scan). Captures are renumbered accordingly.

Summary

Test plan

  • bun run check (or bun run check:full for the cross-app pass) from the repo root
  • Stack smoke if compose/infra touched: cd infra/compose/compose && ./dev.sh up

App merge bars

Area Command
API cd apps/api && bun run validate
UI cd apps/ui && bun run validate
Docs cd apps/docs && bun run build:ci
Repo drift bun run check (from repo root)

Conventions

  • No any, no blind as, no !
  • New env vars in schema + .env.example (+ SECURITY.md when relevant)
  • Tests updated for changed behavior

Screenshots

Three independent fixes batched into one PR because the test-suite fix
is what unblocks merging to main without flaking, and the security
alerts are the next things blocking a clean main.

Test deadlock — tests/helpers/db.ts

Symptom: random CI failures of apps-api-ci.yml. The integration tests
share one postgres instance, and cleanDatabase() was a single
"TRUNCATE ... RESTART IDENTITY CASCADE" statement. TRUNCATE acquires
AccessExclusiveLock on every listed table; concurrent SELECTs from
another test worker take AccessShareLock on those same tables. With
the suite running files in parallel, the two lock orders collide and
postgres aborts one side with deadlock_detected.

Fix: rewrite cleanDatabase() as a transaction that takes a
pg_advisory_xact_lock (serialising cleanup calls across all workers)
and DELETEs each table in child-first dependency order. DELETE takes
RowExclusiveLock, which does NOT conflict with the SELECT's
AccessShareLock — so a mid-cleanup worker no longer deadlocks with a
mid-test worker. Trade-off is a few ms of extra wall-clock per
cleanup, well under the budget for a CI suite that never spuriously
fails.

CodeQL js/path-injection (x2) — templates/email/preview.ts

The dev-only email preview server reads req.url-derived paths via
fs.existsSync and fs.readFileSync. The previous path.join +
startsWith(PREVIEW_DIR) guard was correct in practice, but path.join
doesn't canonicalise '..' segments and CodeQL can't see the prefix
check as sufficient. Switch to path.resolve (which normalises
traversals), strip leading slashes / query / fragment from the URL,
and re-validate after the directory/extension fallback — mostly to
make the contract explicit at every read site.

CodeQL js/redos — scripts/codegen/new-resource.ts

Codegen scaffolder's usersBlock regex used (?:[^}]*\n)*? — a
quantifier-inside-a-quantifier shape that backtracks pathologically
on input with many bare newlines. Replaced with [\s\S]*? (a single
lazy quantifier = linear scan). Captures are renumbered accordingly.
Comment thread apps/api/src/templates/email/preview.ts Fixed
…jection

Previous attempt (path.resolve + startsWith) was correct in practice
but the static analyzer doesn't trust prefix checks — every fs.* call
that took a string derived from req.url stayed flagged.

Switch to the pattern CodeQL recognises as safe: walk PREVIEW_DIR at
server start with fs.readdirSync, build a Map<urlPath, absolutePath>,
and look up each request against it. req.url is now an untrusted KEY
into the map; the value handed to fs.readFileSync is an absolute path
that originated from filesystem enumeration, not from user input.
There is no flow from req.url to readFileSync for the taint analyzer
to follow.

Functional behavior preserved: "/" still serves index.html, both
"/auth/login" and "/auth/login.html" still resolve to the same file
(map miss on the bare route falls through to "route.html").
@agjs
agjs merged commit 943f5f0 into main May 31, 2026
26 checks passed
@agjs
agjs deleted the fix/test-deadlock-and-codeql branch May 31, 2026 10:53
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.

2 participants