Skip to content

Resolve set_config() arguments from the Bind message - #1298

Open
IgorOhrimenko wants to merge 3 commits into
pgdogdev:mainfrom
IgorOhrimenko:fix-setconfig-bound-params
Open

Resolve set_config() arguments from the Bind message#1298
IgorOhrimenko wants to merge 3 commits into
pgdogdev:mainfrom
IgorOhrimenko:fix-setconfig-bound-params

Conversation

@IgorOhrimenko

@IgorOhrimenko IgorOhrimenko commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

set_config() interception (#1055, #1072) only handles constant arguments. Over the extended protocol with bound parameters:

SELECT pg_catalog.set_config($1, $2, false)
-- Bind: $1 = 'search_path', $2 = ''

parse_args() returns None (arguments aren't A_Const) and the statement is passed through as a plain Command::Query with no session-state tracking. The server connection is left with an empty search_path, goes back into the pool, and every subsequent client that gets it fails with 42P01 relation "..." does not exist on unqualified table names — until the connection is recycled or the pooler is restarted.

Reproduction (verified on v0.1.51 with query_parser = "on", transaction pooling): https://gist.github.com/IgorOhrimenko/703ef2ab3541f8448372ba83f7c78fb2

Fix

Resolve $n arguments from the Bind message, so a parameterized set_config() is tracked exactly like a constant one. The boolean third argument is decoded from either wire format.

Command::DirtyQuery remains as a fallback for arguments that still cannot be resolved — no Bind message at all, a missing or non-text parameter, or an expression such as set_config('search_path', current_setting('search_path'), false). Such a statement executes normally, but the server connection is marked dirty at pairing time so the existing checkin cleanup (RESET ALL, pg_advisory_unlock_all(), DISCARD TEMP) runs before it is handed to another client. Marking has to happen when the client is paired with the server: checkin and cleanup happen inside execute(), so marking afterwards is too late.

The second commit also fixes the synthesized response, which this change made reachable for real clients: describing a portal that returns a row answered with NoData, so libpq rejected the reply with D message without prior T. A statement that returns no rows now correctly gets NoData instead of an empty row description.

Scope

This fixes the set_config path only. A pooled connection can still end up with an empty search_path through an unrelated sequence involving no set_config() at all, which pg_dump -t <table> emits — fixed separately in #1299.

Testing

  • Parser: parameterized set_config() resolves to Command::Set with the bound values; a NULL parameter becomes a reset; $1 with no Bind and an expression argument both fall back to Command::DirtyQuery on a write route.
  • Protocol: the synthesized reply is a well-formed extended-protocol exchange for both statement and portal describes. Both tests fail without the fake.rs change.
  • Integration (integration/python/test_set_config_leak.py): a client poisons the pool via bound parameters, a later client must see a clean search_path. Runs against a dedicated database with a single server connection so the reuse is deterministic. Verified to fail on main and pass with this branch.

set_config() called with bound parameters can't be intercepted by the
query parser (arguments aren't A_Const), so the session state change
went untracked and the server connection poisoned subsequent clients
(e.g. search_path='' -> 42P01 on unqualified names).

Instead of passing the statement through silently, route it as
Command::DirtyQuery: it executes like a regular query, but the server
connection is marked dirty at pairing time, so its session state is
reset (RESET ALL) before the connection is returned to the pool.
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.13527% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...dog/src/frontend/router/parser/query/set_config.rs 87.75% 6 Missing ⚠️
.../src/frontend/router/parser/query/test/test_set.rs 96.55% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@IgorOhrimenko
IgorOhrimenko marked this pull request as draft August 1, 2026 11:36
@levkk

levkk commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

When set_config() arguments can't be resolved to constants

This is a bug in the parser. It should be able to extract Bind message arguments.

@IgorOhrimenko
IgorOhrimenko marked this pull request as ready for review August 1, 2026 20:29
Extend the parser to read $n arguments from the Bind message instead of
only handling constants, so a parameterized set_config() is tracked like
any other SET. DirtyQuery is kept for arguments we still cannot resolve:
no Bind message, a missing or non-text parameter, or an expression.

Describing a portal that returns a row now answers with the row
description instead of NoData, which made libpq reject the synthesized
response with 'D message without prior T'. A statement that returns no
rows now correctly gets NoData instead of an empty row description.
Runs against a dedicated database with a single server connection, so a
poisoned connection is always handed to the next client.
@IgorOhrimenko

Copy link
Copy Markdown
Contributor Author

Thanks — done. The parser now pulls $n from the Bind message, so a parameterized set_config() is tracked like a constant one (the is_local argument is decoded from either wire format).

DirtyQuery is kept as a fallback for the cases the parser still can't resolve: no Bind message, a missing or non-text parameter, or an expression argument such as set_config('search_path', current_setting('search_path'), false).

Making those calls take the Command::Set path surfaced a second problem, fixed in the same commit: describing a portal that returns a row answered with NoData, so libpq rejected the synthesized reply with D message without prior T. Statements that return no rows now get NoData instead of an empty row description.

Tests added at three levels: parser (bound values, NULL, both fallbacks), protocol (the full extended-protocol exchange — fails without the fake.rs change), and an integration test that poisons the pool from one client and checks another, against a database with a single server connection so the reuse is deterministic.

@IgorOhrimenko IgorOhrimenko changed the title Mark server connection dirty on set_config() with non-constant args Resolve set_config() arguments from the Bind message Aug 2, 2026
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.

2 participants