Skip to content

fix(audit): one-fwrite records + document db stmt-cache invariant (F6)#32

Merged
farkasmark merged 1 commit into
mainfrom
fix/audit-stream-lock-dbcache-doc
Jun 23, 2026
Merged

fix(audit): one-fwrite records + document db stmt-cache invariant (F6)#32
farkasmark merged 1 commit into
mainfrom
fix/audit-stream-lock-dbcache-doc

Conversation

@farkasmark

Copy link
Copy Markdown
Contributor

Audit finding F6 (last of the batch), two small parts:

  1. Audit JSONL interleave. With --audit, records emitted from compute.async / gpu.async worker threads share stderr with the event loop, and ShJsonWriter wrote token-by-token (many fwrites + a separate newline fputc), so two records could interleave byte-wise and corrupt the JSONL. Each record now accumulates into a thread-local buffer and flushes with a single fwrite, which the stdio stream lock makes atomic relative to other threads. Lock-free (records are built start-to-finish on one thread, no nesting), no log-lock coupling. Oversized records (>4 KiB, none in practice) truncate rather than interleave. Output format unchanged (test_audit green).
  2. db prepared-statement cache non-reentrancy (iterator-invalidation note). Documents the invariant at the row-callback call site + the LRU-evict site in cap/db.c: a row callback must not re-enter the statement cache (evicting the in-flight stmt would dangle it for the next sqlite3_step). Holds today (pure C result-marshallers); comment-only, to preserve it if a streaming per-row script callback API is ever added.

Validated: build clean, 50/50 unit suites (incl. test_audit), green under ASan. The concurrent-interleave fix is exercised by the TSan CI job.

Two audit findings, both small:

1. Audit JSONL interleave. With --audit, records emitted from
   compute.async / gpu.async worker threads share stderr with the event
   loop, and ShJsonWriter wrote token-by-token (many fwrites plus a separate
   fputc newline), so two records could interleave byte-wise and corrupt the
   JSONL. Accumulate each record into a thread-local buffer and flush it with
   a SINGLE fwrite, which the stdio stream lock makes atomic relative to
   other threads. Lock-free (a record is always built start-to-finish on one
   thread, no nesting), no coupling to the log lock. Oversized records
   (>4 KiB, none in practice) truncate rather than interleave. Output format
   is unchanged (test_audit still green).

2. db prepared-statement cache non-reentrancy (iterator-invalidation note).
   Document the invariant at the row-callback call site and the LRU-evict
   site in cap/db.c: a row callback must not re-enter the statement cache,
   because evicting the in-flight stmt (sqlite3_finalize) would dangle it for
   the next sqlite3_step. Holds today (all callers are pure C result-
   marshallers that hand the result set to script only after the loop ends);
   comment-only, to preserve the invariant if a streaming per-row script
   callback API is ever added.

Validated: build clean, 50/50 unit suites (incl. test_audit), green under
ASan. The concurrent-interleave fix is exercised by the TSan CI job.
@farkasmark farkasmark merged commit de24d33 into main Jun 23, 2026
20 of 21 checks passed
@farkasmark farkasmark deleted the fix/audit-stream-lock-dbcache-doc branch June 23, 2026 10:11
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.

1 participant