SELECT pg_catalog.set_config('search_path', '', false) sent as a literal is correctly
intercepted by the query parser (#1055 / #1072) and tracked as session state.
However, the same call sent over the extended protocol with bound parameters:
SELECT pg_catalog.set_config($1, $2, false)
-- Bind: $1 = 'search_path', $2 = ''is not intercepted: parse_args() only handles A_Const nodes, so with ParamRef
arguments it returns None and the statement is passed through as a plain Command::Query
without any session-state tracking. The server connection is left with an empty
search_path, returns to the pool, and every subsequent client that gets this connection
fails with 42P01 relation "..." does not exist on unqualified table names — until the
pooler is restarted.
Relevant code: frontend/router/parser/query/set_config.rs (parse_args → None →
"warn and pass through" branch).
- pgdog v0.1.51 (latest release),
query_parser = "on" - transaction pooling (default), plain PostgreSQL 18 backend
default_pool_size = 1in the repro only to make the reuse deterministic; with a larger pool the poisoned connection simply bites intermittently
# pgdog.toml
[general]
query_parser = "on"
default_pool_size = 1
min_pool_size = 1
[[databases]]
name = "pgdog"
host = "postgres"
port = 5432# 1. baseline — fresh client through pgdog
psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c 'SHOW search_path'
# "$user", public
# 2. poison: set_config with bound parameters (extended protocol), then disconnect
psql -h <pgdog> -p 6432 -U pgdog -d pgdog <<'SQL'
SELECT pg_catalog.set_config($1, $2, false) \bind search_path "" \g
SQL
# 3. fresh clients now inherit the poisoned server connection
for i in 1 2 3 4 5; do
psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c 'SHOW search_path'
done
# "" ×5 ← empty search_path leaked across clientsControl cases where this particular path is handled correctly (no leak observed):
- literal
SELECT pg_catalog.set_config('search_path', '', false)— simple protocol - the same literal over the extended protocol (
\bind \gwith no params) SET search_path = ''on its own
Note: this is not the only way a pooled connection can end up with an empty
search_path. A separate issue exists where the following sequence poisons the
connection with no set_config() involved at all (reproduced on the same v0.1.51):
SET search_path TO '';
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY;
RESET search_path;
ROLLBACK;That sequence is emitted by pg_dump -t <table>, so a pg_dump through the pooler can
still poison a connection regardless of the fix for the case described above.
When set_config() arguments cannot be resolved to constants, passing the statement
through silently leaves the pooler blind to a session-state change. Suggested options:
- mark the server connection as tainted and issue
RESET ALL(orDISCARD ALL) before returning it to the pool, or - resolve the bound values from the
Bindmessage where available.
For the literal set_config over the extended protocol, pgdog's synthesized response
is slightly malformed — libpq reports
server sent data ("D" message) without prior row description ("T" message).
The value is applied and tracked correctly; only the wire response is off.