Skip to content

Instantly share code, notes, and snippets.

@IgorOhrimenko
Created August 1, 2026 15:03
Show Gist options
  • Select an option

  • Save IgorOhrimenko/d24c20e109a3c615b9682a0a9723fdeb to your computer and use it in GitHub Desktop.

Select an option

Save IgorOhrimenko/d24c20e109a3c615b9682a0a9723fdeb to your computer and use it in GitHub Desktop.
PgDog: a RESET inside a transaction leaves poisoned connections in the pool (search_path leak via pg_dump -t)

PgDog: a RESET inside a transaction leaves poisoned connections in the pool

When a client runs RESET <param>, PgDog clears its record of the server's tracked parameters, assuming the session is back to its defaults and will be re-synced on the next checkout (backend/server.rs, CommandComplete handling: "RESET" => self.client_params.clear()).

That assumption breaks inside a transaction. A ROLLBACK undoes the RESET, reviving the parameter values that were in effect before it — but PgDog has just stopped tracking them. The connection returns to the pool carrying session state the pooler doesn't know about, so no RESET is generated for the next client, and that client inherits the state.

With search_path this is user-visible as 42P01 relation "..." does not exist on unqualified table names, for every client that gets the connection, until it is recycled (server_lifetime) or the pooler is restarted.

pg_dump -t <table> emits exactly this sequence, so dumping a single table through the pooler poisons a connection.

Environment

  • pgdog v0.1.51 (latest release), query_parser = "on"
  • transaction pooling (default), plain PostgreSQL 18 backend
  • default_pool_size = 1 in 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

Reproduction

# 1. baseline — fresh client through pgdog
psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c "SELECT current_setting('search_path')"
#  "$user", public

# 2. poison: RESET inside a transaction, then ROLLBACK
psql -h <pgdog> -p 6432 -U pgdog -d pgdog <<'SQL'
SET search_path TO '';
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY;
RESET search_path;
ROLLBACK;
SQL

# 3. fresh clients inherit the poisoned connection
for i in $(seq 1 5); do
  psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c "SELECT current_setting('search_path')"
done
#  ""  ×5

# ... and unqualified names break
psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c 'SELECT count(*) FROM some_table'
#  ERROR:  relation "some_table" does not exist

The same happens with a real dump, no hand-written SQL involved:

pg_dump -h <pgdog> -p 6432 -U pgdog -d pgdog -t some_table > /dev/null
psql -h <pgdog> -p 6432 -U pgdog -d pgdog -tA -c "SELECT current_setting('search_path')"
#  ""

Notes:

  • Verify with SELECT current_setting('search_path') rather than SHOW search_path: SHOW can be answered by the pooler itself, current_setting() goes to the server. The unqualified-SELECT failure confirms it independently.
  • Removing any single statement from the sequence makes it go away — including SET TRANSACTION ISOLATION LEVEL, which is what forces the client to be paired with a server connection before the RESET runs.
  • A full pg_dump (no -t) does not leave the connection poisoned; the single-table form does.

Expected behavior

A connection whose session state may have been revived by ROLLBACK should not be handed to another client as-is. Marking it dirty so the existing checkin cleanup (RESET ALL, pg_advisory_unlock_all(), DISCARD TEMP) runs is enough; plain RESET outside a transaction needs no extra work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment