Repository navigation
fix(persistence): reject unsafe persistent session_replication_role login defaults #588
Description
Finding
TEPP now fails closed on session-local trigger suppression through direct SET, PostgreSQL set_config, and writable pg_settings, including the bounded Unicode-identifier cases tracked through #587. One PostgreSQL configuration surface remains outside that execution-context authority: persistent login defaults.
PostgreSQL 18 defines these as first-class configuration surfaces:
ALTER ROLE { role | ALL } [ IN DATABASE database ] SET parameter = valuestores a role/session default;SET ... FROM CURRENTstores the current session value. The value is applied when a subsequent session logs in.ALTER DATABASE ... SET parameter = valuesupplies a database default for subsequent sessions.ALTER SYSTEM SET parameter = valuewrites the cluster-wide default topostgresql.auto.conf; it becomes effective after configuration reload/restart as applicable and cannot be rolled back.
session_replication_role=replica disables ordinary/default triggers and rules. A migration that leaves tepp_app_runtime, every role (ALL), the target database, or the cluster with a persistent replica default can therefore make a later TEPP application session bypass append-only/retention trigger enforcement even though the migration's own session ends in ordinary execution mode and role attributes remain NOSUPERUSER/NOBYPASSRLS.
Primary PostgreSQL 18 authority:
- https://www.postgresql.org/docs/18/sql-alterrole.html
- https://www.postgresql.org/docs/18/config-setting.html
- https://www.postgresql.org/docs/18/sql-altersystem.html
- https://www.postgresql.org/docs/18/runtime-config-client.html
Required ordinary-forward RED → repair
Add public validate_migration_catalog() contracts before changing production code.
Unsafe persistent defaults must fail closed:
ALTER ROLE tepp_app_runtime SET session_replication_role = replica;ALTER ROLE tepp_app_runtime IN DATABASE <db> SET session_replication_role TO replica;ALTER ROLE ALL SET session_replication_role = replicaand the database-scopedALLequivalent;ALTER ROLE tepp_app_runtime SET session_replication_role FROM CURRENTbecause the persisted value cannot be proven ordinary-trigger-safe from syntax alone;ALTER DATABASE <db> SET session_replication_role = replica;ALTER SYSTEM SET session_replication_role = replica.
Controls:
- direct
originandlocaldefaults preserve ordinary trigger execution; - unrelated parameters remain unrelated;
- role/database mutations inside an explicit transaction that rolls back are non-durable and must not poison final state;
ALTER SYSTEMis not given a rollback control because PostgreSQL forbids it inside transaction blocks;- strings/comments/dollar bodies containing marker text remain opaque;
- identifier-prefix/lookalike objects do not impersonate the protected role/parameter/database surfaces.
Repair boundary
Do not add another raw-SQL lexer or one-off string-search authority. Consume the existing normalized + committed transaction projection and extend the first-class execution-context/default-state model. Preserve #576–#587 semantics for direct SET/set_config/pg_settings, Unicode fail-closed behavior, role final-state logic, and current rollback projection.
Long-term aggregate should distinguish at least:
- session-current execution mode;
- role-specific and role+database login defaults;
- database/global defaults and precedence;
FROM CURRENT,DEFAULT/RESET, and statement ordering/final state;- reload/restart activation for system defaults;
- transaction durability;
- protected trigger/rule dependency state.
A conservative interim fail-closed boundary is acceptable only if it is explicit about unsupported final-state transitions; do not silently certify a historical unsafe default merely because a later unrelated statement exists.
Keep this issue open until the surviving post-#538 non-force-restacked exact head has hosted Rust/Live PostgreSQL, current-topology rustdoc 100%, authored line/branch/edge acceptance, security/CodeQL, qualifying independent review, and immutable release evidence. No force push, destructive rebase, self-approval, skip/xfail, coverage-denominator manipulation, or gate weakening.
Activity
seonghobae commentedon Sep 18, 2026
Ordinary-forward implementation progress on #521:
9fdd078f7459a3d1e468f50846d44c43038f4c6a— public exported RED for persistentALTER ROLE,ALTER DATABASE, andALTER SYSTEMsession_replication_roledefaults, includingFROM CURRENT, safeorigin/local, unrelated target/parameter, rollback, and opaque-marker controls.7cf0f42eda53d0bc6452bafc6fc69f4b8afadbfb— first bounded causal repair. Added documentedstatement_sets_unsafe_persistent_replication_role_default()after shared lexical normalization + committed-state projection; no raw-SQL lexer or executable migration rewrite. RED→repair production delta is one file, +127/-10.ALTER USERis explicitly an alias forALTER ROLEand exposes the same{role_specification | ALL} [IN DATABASE ...] SET/RESETgrammar. That sibling was not covered by the first repair, sod1088e7ed81595665c296c4151c3800e1d42162cadded public RED/controls forALTER USER,ALL,IN DATABASE,FROM CURRENT, safe values, unrelated users,USER MAPPING, and rollback.8c2d868031801874a0fb09108417ba7502138e3dfoldsALTER USERonto the same role-default authority while preservingALTER USER MAPPINGas unrelated.d1088e7... → 8c2d868...is one production file, +7/-4. It also restores the source file trailing newline.The current bounded model intentionally fails closed on
RESET/DEFAULT/FROM CURRENTrather than pretending to own precedence/final-default state. That conservative behavior is still a known limitation: a first-class default-state aggregate should eventually fold role, role+database, database, and system defaults in statement order and distinguish a genuine final safe reset from an unresolved inherited unsafe value.Exact-head Rust Foundation execution remains non-authoritative because #521 is Draft; hosted Rust/Live PostgreSQL jobs are lifecycle-skipped. A scoped CodeRabbit review has been requested on exact
8c2d868...; no GREEN or qualifying approval is claimed yet.