Skip to content

Task 9: atomic bundle publication - #9

Merged
roachitect-aman merged 1 commit into
mainfrom
task-9-bundle-publication
Aug 25, 2026
Merged

Task 9: atomic bundle publication#9
roachitect-aman merged 1 commit into
mainfrom
task-9-bundle-publication

Conversation

@roachitect-aman

Copy link
Copy Markdown
Contributor

Adds the bundle publication section of dbprofiler.py: payload shaping, the manifest, and write_bundle(). This is the only code in the tool that writes to disk, so both of its properties are enforced rather than trusted.

Nothing raw escapes

Serialization is by an allowlist of contract types, not by dataclass structure. The obvious implementation is dataclasses.asdict(), and it would have been a hole: the collector records holding raw most-common values (ColumnStatistics) and raw query text (StatementActivity) are dataclasses too, so a structural walker serializes them happily. to_jsonable() checks membership in CONTRACT_TYPES and raises otherwise, so making a record serializable is a visible edit in one place.

Every CSV is shaped from already-tokenized records. Query text is tokenized whole: pg_stat_statements normalizes literals to $1 for most statements but not for utility statements or parser-folded constants, so there is no subset of the text that is safe by construction. That costs the text's analytical value and leaves queryid as the correlation handle — a partial redaction would be a guess dressed up as a guarantee.

Publication is all-or-nothing

The archive is built in a temporary file beside the destination, fsynced, and moved into place with os.replace(), with manifest.json serialized last after the payloads it hashes. Any failure — including KeyboardInterrupt, hence except BaseException — removes the temporary file and leaves the previous bundle untouched.

Entry paths are validated against one regex that rejects absolute paths, parent traversal, empty and dot segments, backslashes, drive letters, whitespace and control characters by construction, then checked against the allowlist of the nine legal payload paths. Symlink defence is applied at both ends: every entry carries an explicit S_IFREG | 0644 mode (zipfile's default is zero, and some extractors then fall back to the umask), and a symlink destination is refused rather than resolved.

Archives are byte-reproducible — fixed entry timestamps, sorted order, manifest last — so a bundle can be diffed.

Degradation

Omission is keyed off the collector's warning code, not off an empty record set. Otherwise a database that genuinely has no user tables is indistinguishable from one whose role could not read the view; the first should ship an empty CSV, the second should ship nothing and say why.

The negative assertions

They search decompressed members as well as the stored bytes. The archive is DEFLATE-compressed, so searching only the raw file would let compression hide the very literal the test is looking for. The temporary file is captured by wrapping os.replace, so the pre-publication artifact is searched too. Three literals are planted — an unnormalized query literal, a most-common value, and a histogram bound — plus the tokenization key and every connection detail.

These properties were mutation-tested: publishing raw query text, swapping the type allowlist for a structural dataclass check, replacing the rename with a non-atomic copy, and dropping the temp-file cleanup were each applied in turn, and every one is caught by the suite. A negative assertion that cannot fail is decoration.

Smaller notes

  • pg_stat_indexes.csv gains is_unique and is_primary from CatalogIndex, which task 5 collected and nothing had consumed. idx_scan == 0 marks a drop candidate, but whether it can be dropped depends on whether it backs a constraint.
  • wal_bytes is not in pg_stat_statements.csv: the plan named it, but SQL_STATEMENTS never selected it. The CSV carries what was collected, including stddev_exec_time and the temp-block counters the plan's list omitted.
  • CSV formula injection is noted in the plan doc and deliberately not addressed here — prefixing =-leading identifiers would desynchronize the CSVs from profile.json, which carries the same identifiers unquoted.

Verification

  • 317 tests pass
  • python3 dbprofiler.py --check-safety — OK, 14 SQL constants (no new SQL in this change)
  • ruff check — clean
  • source parses under Python 3.9

Adds the bundle publication section: payload shaping, the manifest, and
write_bundle().

Two properties are enforced rather than trusted.

Nothing raw escapes. Serialization is by an allowlist of contract types, not
by dataclass structure -- the collector records holding raw most-common values
and raw query text are dataclasses too, so a structural walker would have
written them straight to disk. Every CSV is shaped from already-tokenized
records, and query text is replaced by an HMAC token because
pg_stat_statements normalizes literals for most statements but not for utility
statements or parser-folded constants.

Publication is all-or-nothing. The archive is built in a temporary file beside
the destination, fsynced, and moved into place with os.replace(), with the
manifest serialized last after the payloads it hashes. Any failure removes the
temporary file and leaves the previous bundle untouched.

Entry paths are validated against one regex that rejects absolute paths, parent
traversal, empty and dot segments, backslashes, drive letters, whitespace and
control characters by construction, then checked against the allowlist of the
nine legal payload paths. Every entry carries an explicit regular-file mode so
no extractor sees a symlink bit, and a symlink destination is refused outright.

Sections omitted by degradation are keyed off the collector's warning code
rather than off an empty record set, so "the role could not read this view"
stays distinguishable from "this database has no user tables".

pg_stat_indexes.csv joins is_unique and is_primary from the catalog: idx_scan
== 0 marks a drop candidate, but whether it can be dropped depends on whether
it backs a constraint, and both facts were already collected.

The negative assertions search decompressed members as well as the stored
bytes -- searching only the compressed file would have let DEFLATE hide the
literal the test is looking for -- and the temporary file is captured before
the rename so the pre-publication artifact is searched too. Three literals are
planted, plus the tokenization key and every connection detail. The properties
were mutation-tested: publishing raw query text, using a structural serializer,
replacing the rename with a copy, and dropping the temp-file cleanup are each
caught.

317 tests pass; --check-safety and ruff are clean; the source parses under
Python 3.9.

Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
@roachitect-aman
roachitect-aman merged commit bbadf52 into main Aug 25, 2026
7 checks passed
@roachitect-aman
roachitect-aman deleted the task-9-bundle-publication branch August 25, 2026 03:19
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