Code review · P3 Task 3, the spool reader¶
review-code on autopilot/P3-t03, base a8d0c5b, 2026-10-30. The step is P1 Task 3. It adds:
spectio.runner.spool.read_spool, which reads one launch's spool past a cursor, with caps and confinement;spectio.runner.types:SpoolBatch,REFUSEDandEXCESS;- in
spectio_sdk.events:MAX_SPOOL_FILES,MAX_EVENT_NODES,MAX_EVENT_DEPTHand the file-name grammarSPOOL_FILE_NAME; - a CI step in the
productjob that checks the import contracts in a fresh environment (SEC-P3T1-06); packages/spectio/tests/test_spool_reader.py, 61 tests.
The reviewers were code-reviewer and security-reviewer, independent and in parallel. Format: README.md. What the reader does is owned by api §14.1.
| Round | Head | code-reviewer | security-reviewer |
|---|---|---|---|
| 1 | 5f7f55d (the step: reader and CI step) |
changes (0 blockers, 1 major, 5 minors) | changes (0 blockers, 2 majors, 5 minors) |
| 2 | cd78f18 (the round-1 fixes) |
approve (0 blockers, 0 majors, 6 minors) | approve (0 blockers, 0 majors, 3 minors) |
| 3 | 8058fb0 (the round-2 fixes) |
approve (0 blockers, 0 majors, 2 minors) | approve (0 blockers, 0 majors, 1 minor) |
| after 3 | a7a4610 (the round-3 fixes) |
not re-reviewed; the orchestrator read the diff and re-ran the tests (152 passed) | not re-reviewed; same check |
Rounds. Round 1 asked for changes (two security majors); round 2 approved, with minors. Round 3 was an extra review of the worker's fixes for those minors, made after approval (8058fb0); it approved, and its three minors were fixed in a7a4610, which the orchestrator checked without a further review. Round 3 is outside the two-round rule of review-code step 4: the exception of inbox #75 covers only rounds while a security blocker or major is open, and none was. Recorded in inbox #76.
Test-first. The worker's record: the first run failed at collection (ModuleNotFoundError, no reader yet); the round-1 tests failed 19 before the fix, the round-2 tests 17, the round-3 tests 3; all pass after each fix. Test command, passing at a7a4610 (61 tests in this file, 152 in the workspace):
uv run pytest packages/spectio/tests/test_spool_reader.py -v
CODE step=P3-t03 review=approve b/M/m=0/1/13 security=approve b/M/m=0/2/9 rounds=3 simplify=changed -2/+1 tests=pass
The counts are per reviewer over the three rounds: code 1 major (CR-P3T3-01) and 13 minors (CR-02 to CR-14); security 2 majors (SEC-P3T3-01, -02) and 9 minors (SEC-03 to SEC-11). code-simplifier ran after round 3 (commit bd206f6, −2/+1): it removed the unreachable ENOTDIR refusal (CR-P3T3-12). The tests pass after it.
Measured by security-reviewer at the end: the worst read took 0.86 s with a traced peak of 11.1 MiB. On the round-1 head, 500 lines took 788 MiB.
Findings¶
The finding texts are summarised from the fix commits' messages and the round summaries. A pair such as CR-P3T3-01 / SEC-P3T3-03 is one problem found by both reviewers.
| Id | Severity | Finding | Status |
|---|---|---|---|
| SEC-P3T3-01 | major | Memory and time of one read were unbounded: only lines were capped, not the bytes of a read or the size of an event's data; on the round-1 head, 500 lines took 788 MiB. |
fixed in 35fb19e: a read takes at most max_bytes (MAX_READ_BYTES, 2 MiB) from the files, skipped bytes included; data may hold at most MAX_EVENT_NODES (4096) values and MAX_EVENT_DEPTH (32) levels (test_req6_read_stops_at_max_bytes, test_req6_memory_bounded_on_nested_lines, test_req6_event_node_cap, test_req6_event_depth_cap) |
| SEC-P3T3-02 | major | No owner or mode check: .spectio, events or a spool file of another user, or one writable by others, was read. |
fixed in 35fb19e: .spectio and events must be private directories of the user (fstat after the O_NOFOLLOW open), else PermissionError; a file of another owner, or writable by group or others, is refused (foreign_owner, writable_by_others; the test_req12_* tests) |
| CR-P3T3-01 / SEC-P3T3-03 | major / minor | An OSError on one file stopped the whole read. |
fixed in 35fb19e: ELOOP, EACCES and ENXIO refuse the file (symlink, unreadable); any other error is counted read_error and the file is retried (test_unreadable_file_refused_and_the_next_one_read, test_read_error_retried_next_read) |
| CR-P3T3-02 / SEC-P3T3-04 | minor | The directory listing was unbounded. | fixed in 35fb19e: at most MAX_DIR_ENTRIES (16 384) entries per read (test_directory_listing_cap) |
| CR-P3T3-03 / SEC-P3T3-05 | minor | Junk names could take the cursor's file slots and grow the cursor. | fixed in 35fb19e: slots go to this launch's spool files only; a name outside the grammar is kept as a short hash, MAX_BAD_NAMES (64) at most; the rest is counted through a high-water mark, #excess (test_junk_names_leave_the_slots_to_spool_files, test_spool_file_cap) |
| CR-P3T3-04 / SEC-P3T3-06 | minor | The fresh-environment import check resolved its dependencies anew, unlocked. | fixed in cd78f18: each environment installs uv export --frozen --no-dev --no-emit-workspace --package <pkg> with --require-hashes, then the workspace packages with --no-deps |
| CR-P3T3-05 / SEC-P3T3-07 | minor | api §14.1 did not state the reader's rules: the caps, the grammar, the drop reasons, the cursor. | fixed by the docs of this step: api §14.1, "Dropped and counted, never executed", and its bounds table |
| CR-P3T3-06 | minor | A file of exactly MAX_FILE_BYTES that grew was never counted file_too_large. |
fixed in 35fb19e (test_req6_file_of_exactly_max_bytes_counted_when_it_grows) |
| CR-P3T3-07 | minor | The files read came from the listing, so a listing cut at MAX_DIR_ENTRIES could skip a file admitted earlier. |
fixed in 8058fb0: the files read come from the cursor, in name order (test_admitted_files_read_past_the_listing_cap) |
| CR-P3T3-08 | minor | Unended lines in earlier files can use up the byte budget of a read and starve later files. It needs a buggy or hostile writer. | open, follow-up (Progress page) |
| CR-P3T3-09 / SEC-P3T3-10 | minor | An event over the node or depth cap was counted invalid, and was fully parsed before the check. |
fixed in 8058fb0: it is counted too_complex; a line with more than 2 × (4096 + 16) of [, { and , is dropped before it is parsed (test_too_complex_line_not_parsed) |
| CR-P3T3-10 | minor | A file gone between the listing and the open was counted read_error. |
fixed in 8058fb0: it is skipped (test_file_gone_between_listing_and_open) |
| CR-P3T3-11 | minor | uv export ran without --quiet in the CI step. |
fixed in 8058fb0: --quiet |
| CR-P3T3-12 | minor | simplify: the ENOTDIR refusal is unreachable: a single name opened relative to an O_DIRECTORY descriptor cannot fail with it. |
fixed by code-simplifier in bd206f6 (−2/+1) |
| SEC-P3T3-08 | minor | Cursor values were not checked on entry. | fixed in 8058fb0: every value is checked on entry (an int, not a bool; REFUSED or 0 to MAX_FILE_BYTES for a file; REFUSED for a name hash; a count for #excess); anything else is reset and counted bad_cursor; too small max_lines or max_bytes raise ValueError (test_bad_cursor_values_refused, test_budgets_too_small_refused) |
| SEC-P3T3-09 | minor | The reader trusts launch_dir and the path to it, and nothing named who checks them. |
partly fixed in 8058fb0: the docstring names the callers, P1 Tasks 9 and 11. Remainder, follow-up: P1 Task 9 must create .spectio and .spectio/events at 0700 and walk the path per component as req. 10 says, else every read raises PermissionError |
| CR-P3T3-13 / SEC-P3T3-11 | minor | A cursor key that is not a spool file name of this launch took a file slot, and another launch's file named in the cursor was opened. | fixed in a7a4610: such a key is dropped and counted bad_cursor once, and the file is never opened (test_foreign_cursor_key_dropped_once, test_other_launch_cursor_key_dropped_and_never_opened); verified by the orchestrator, not re-reviewed |
| CR-P3T3-14 | minor | The worst-read timing test allowed 1 s, which a loaded CI runner could miss. | fixed in a7a4610: 5 s; test_too_complex_line_not_parsed proves the mechanism; verified by the orchestrator, not re-reviewed |
Closed from Task 1's review (record): SEC-P3T1-04, the reader's rules, in 48ddf5b; SEC-P3T1-06, the fresh-environment import check, in 5f7f55d and cd78f18.