sql: search-path-correct pg_get_viewdef and upstream search_path pin#37422
sql: search-path-correct pg_get_viewdef and upstream search_path pin#37422def- wants to merge 2 commits into
Conversation
17fa03a to
5ec5549
Compare
pg_get_viewdef's text overloads matched the bare view name against mz_views across every schema of every database. That threw "more than one record produced in subquery" whenever two views shared a name anywhere in the environment, resolved views in other databases, and rejected schema-qualified names. Route the text overloads through the search-path-aware text -> regclass cast instead, matching PostgreSQL's regclass semantics (current database, search path, first match). Not-found now errors like PostgreSQL rather than returning NULL. Part of SQL-450. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Defense against upstream search_path hijacking. An upstream role default such as `ALTER ROLE mz_user SET search_path = attacker, pg_catalog` could otherwise shadow the unqualified pg_catalog relations and functions our source metadata queries rely on, letting a hostile upstream feed Materialize forged schema, publication, or RLS-policy rows. Setting search_path through the connection startup options carries GUC source PGC_S_CLIENT, which outranks the PGC_S_USER / PGC_S_DATABASE defaults set by ALTER ROLE / ALTER DATABASE. Applying it at the single connection chokepoint covers every upstream connection, normal and replication alike, and every current and future query, without qualifying each reference by hand. Part of SQL-450. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5ec5549 to
b1c1250
Compare
aljoscha
left a comment
There was a problem hiding this comment.
looks good, I had one inline question where I couldn't immediately see why a test change was needed, would be good to resolve that before merging
| SELECT pg_get_viewdef('next_arrivals'); | ||
| ---- | ||
| db error: ERROR: access to system object mz_views is restricted | ||
| db error: ERROR: access to system object mz_databases is restricted |
There was a problem hiding this comment.
Our error evaluation order is quite strange, so no idea!
There was a problem hiding this comment.
I guess it's just that the old code was only looking at mz_views, and now we also look at mz_databases as part of the lookup, so now either of these can trip the check.
|
I'm looking at this, will have some comments shortly. |
|
So, one issue is that now the text overloads of I'm leaning towards just accepting this risk, because it seems unlikely that anybody has an index or MV with |
|
Some more comments from Claude. About 3., we might want to ask @MaterializeInc/sources-sinks team whether a Postgres source connecting though something like pgbouncer is a thing that we want to support. If we don't want to support that, then 3. would be a non-issue. 2. Temporary views break, and can silently return the wrong definition
Temp views get rows in Consequences:
Caveat: the old behavior was only reliable when the name was globally unique 3. The search_path pin can be silently discarded by poolers, and is never verified
The pin exists only as the Notes on exposure:
Suggested hardening, at the same chokepoint in
Keeping the startup-parameter pin is still worthwhile (it covers walsender Btw. no ingestion doc states that Postgres sources need a direct (non-pooled)
4. Missing-name error carries SQLSTATE 22000, not PostgreSQL's 42P01
The error for an unresolvable name comes from Pre-existing for all 5. Materialized views return NULL where PostgreSQL returns the definition
The regclass alias covers The gap predates this PR (the OID overloads share it), but the new comment 6. Raw options append relies on an undocumented cross-crate invariant
PostgreSQL parses the 7. Cleanup: duplicated bodies and non-idiomatic cast syntax
|
Motivation
Part of SQL-450.
A review of the SQL builtin catalog and the PostgreSQL source utilities surfaced
two name-resolution hazards. This PR addresses two of them; a third (the
mz_connection_oid/mz_secret_oidhelpers) is left for a focused follow-up.Description
1. Search-path-correct
pg_get_viewdef(src/sql/src/func.rs).The text overloads matched the bare view name against
mz_viewsacross everyschema of every database. That threw
more than one record produced in subquerywhenever two views shared a name anywhere in the environment, resolvedviews in other databases, and rejected schema-qualified names. They now resolve
through the search-path-aware
text -> regclasscast, matching PostgreSQL'sregclass semantics (current database, search path, first match). One behavior
change:
pg_get_viewdef('missing')now errors like PostgreSQL instead ofreturning NULL. The OID overloads were already correct and are unchanged.
2. Pin
search_pathon upstream PostgreSQL connections (src/postgres-util/src/tunnel.rs).Our source-metadata queries reference
pg_catalogrelations and functions byunqualified name. An upstream role default such as
ALTER ROLE mz_user SET search_path = attacker, pg_catalogcould shadow themwith attacker-owned relations or functions, letting a hostile upstream feed
Materialize forged schema, publication, or RLS-policy rows. Rather than
qualifying every reference by hand (brittle, easy to miss one), pin
search_path=pg_catalogvia the connection startup options at the singleconnection chokepoint. The startup-options GUC source
PGC_S_CLIENToutranksthe
PGC_S_USERandPGC_S_DATABASEdefaults set byALTER ROLE/ALTER DATABASE, so it holds even against a hostile per-role default, and itcovers every upstream connection (normal and replication) and every current and
future query. Data queries already reference user tables by fully-qualified
name, so ingestion is unaffected.
Not in this PR
The
mz_connection_oid/mz_secret_oidhelpers (reached fromhas_connection_privilege/has_secret_privilege) have the same bare-nameglobal-resolution issue as
pg_get_viewdef, but a correct fix requires inliningcurrent-database + search-path resolution because those object types have no OID
alias to reuse the existing resolver. That is a larger, standalone change and
will land in a follow-up so it can be reviewed on its own.
Verification
bin/sqllogictest -- test/sqllogictest/pg_get_viewdef.sltpasses (27/27), withnew regression coverage for duplicate names, schema-qualified names, and
error-on-missing.
test/sqllogictest/rbac_mcp_agent.sltupdated for the restricted-role errormessage that shifts from
mz_viewstomz_databases, since regclassresolution reaches
mz_databasesfirst.cargo check -p mz-sql,cargo check -p mz-postgres-util, andbin/fmtclean.pg-cdcintegration suite in CI, so any regression in ingestion would fail there.
🤖 Generated with Claude Code