Skip to content

Add safe subprocess helpers and the full --check-safety audit - #3

Merged
roachitect-aman merged 1 commit into
mainfrom
task-3-subprocess-and-safety
Aug 25, 2026
Merged

Add safe subprocess helpers and the full --check-safety audit#3
roachitect-aman merged 1 commit into
mainfrom
task-3-subprocess-and-safety

Conversation

@roachitect-aman

Copy link
Copy Markdown
Contributor

EOF2
Task 3 of the MVP plan. Credentials now flow to psql and pg_dump through the
environment only, and --check-safety grew from a token scan into a real audit of
this file's own SQL and child-process usage.

Subprocess helpers

A single run_command() is the only subprocess.run in the file. That is the point:
one call site is what makes the credential handling reviewable at a glance, and
--check-safety now enforces it mechanically.

safe_env() starts from our own environment, so an operator's PGSSLMODE and
PGSSLROOTCERT keep working, drops every DBPROFILER_* variable so no child ever
sees the raw URL or the token key, overlays the parsed connection settings, and pins
LC_ALL=C so error text does not depend on the operator's locale.

On top of it: run_psql, run_psql_scalar, run_pg_dump, and
probe_server_version — which finally gives the version gate from task 2 a caller.

Redaction

redact_error() scrubs connection URLs, password= / PGPASSWORD= assignments, and
libpq env values (longest first) out of child stderr before it is surfaced. Assume
every error path is printed.

PGPASSWORD is replaced however short. Other libpq values are replaced only at
length >= 4, because blind-replacing a two-character PGUSER shreds unrelated words
and destroys the diagnostic. PGPORT is never replaced — a bare port number matches
far too much text. There is a test for each of those three behaviours.

--check-safety now audits the source, not just the SQL

It parses this file with ast rather than grepping it, and requires exactly one
subprocess call site, an explicit env= on it, no shell=, and no credentialed
connection URL in any string literal. A bare postgres://... placeholder in a
docstring stays legal; one with an @ does not.

Deviations from the plan

The allowlist has no pg_catalog wildcard. The plan called for one. It should
not have: pg_catalog is not a safety boundary. It holds role password hashes
(pg_authid, pg_shadow), actual user data (pg_largeobject), raw statistic values
without the per-permission filtering the pg_stats view applies (pg_statistic),
and two relations whose option fields carry passwords (pg_subscription,
pg_user_mapping). Relations are now enumerated one by one, with a
DENIED_RELATIONS map recording a reason per trap, and a schema qualifier other than
pg_catalog is itself a violation — so evil.pg_class is rejected even though
pg_class is allowed. Set-returning functions get their own small allowlist, so
FROM unnest(...) passes and FROM pg_read_file(...) does not.

Dropped -A and -F, from the psql arguments. --csv is its own output format,
and -A placed after it overrides it. Keeping both would have made the parse
order-dependent.

Added -w to both commands, and ON_ERROR_STOP=1. Without -w a missing
password blocks the child on a terminal read, and the run fails by timeout instead of
immediately. ON_ERROR_STOP turns a SQL error into a nonzero exit rather than a
silently empty result set.

pg_inherits is deliberately still absent. Task 5 reads it, and the house rule is
that a relation is allowlisted in the same change as its collector.

Verification

  • 92 tests pass (45 new).
  • python3 dbprofiler.py --check-safety exits 0.
  • ruff check clean. S608 added to the test per-file ignores: the safety tests
    plant deliberately unsafe SQL to prove the guard rejects it.
  • Source re-verified as 3.9-parseable.
  • Adversarially probed: planting a second, shell-using call site into the real source
    produces all three expected violations.

Credentials reach psql and pg_dump through the environment only. A single
run_command() is the one child-process call site in the file, so the
credential handling can be reviewed at a glance. safe_env() inherits the
operator's environment, so their PGSSLMODE and PGSSLROOTCERT keep working,
strips every DBPROFILER_* variable so no child sees the raw URL or the
token key, overlays the parsed connection settings, and pins LC_ALL.

redact_error() scrubs connection URLs, password assignments, and libpq env
values from child stderr before it is surfaced. PGPASSWORD is replaced
however short; other values only at length >= 4, because blind-replacing a
two-character PGUSER shreds unrelated words and destroys the diagnostic.
PGPORT is never replaced -- a bare port number matches too much text.

--check-safety now audits the source, not just the SQL. It parses this file
with ast and requires exactly one subprocess call site, an explicit env= on
it, no shell=, and no credentialed connection URL in any string literal.

Three deviations from the plan:

The allowlist has no pg_catalog wildcard. That schema is not a safety
boundary: it holds role password hashes (pg_authid, pg_shadow), user data
(pg_largeobject), raw statistic values without the permission filtering the
pg_stats view applies (pg_statistic), and two relations whose option fields
carry passwords (pg_subscription, pg_user_mapping). Relations are
enumerated one by one, with a DENIED_RELATIONS map recording a reason per
trap, and a schema qualifier other than pg_catalog is itself a violation.

The psql arguments drop -A and -F,. --csv is its own output format and -A
placed after it overrides it, which would have made the parse
order-dependent.

Both commands gain -w. Without it a missing password blocks the child on a
terminal read, and the run fails by timeout instead of immediately.
ON_ERROR_STOP turns a SQL error into a nonzero exit rather than a silently
empty result.

pg_inherits is deliberately absent: task 5 reads it, and a relation is
allowlisted in the same change as its collector.

92 tests pass.

Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
@roachitect-aman
roachitect-aman merged commit 3d2a0e9 into main Aug 25, 2026
7 checks passed
@roachitect-aman
roachitect-aman deleted the task-3-subprocess-and-safety branch August 25, 2026 00:12
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