Skip to content

NegotiateProtocolVersion never reports unrecognized _pq_. protocol options (breaks PostgreSQL 19beta libpq connections) #455

Description

@SteveLauC

Environment

  • pgwire 0.40.7 (latest), server API
  • Client: psql built from PostgreSQL 19beta sources
  • Any pgwire-based server is affected; nothing specific to my setup.

What happens

libpq in 19beta defaults max_protocol_version to the reserved GREASE
version 3.9999 ("for 19beta only, will be reverted before RC1" —
fe-connect.c). So every connection from a 19beta libpq sends:

  • protocol version 3.9999 in the startup packet
  • a startup parameter _pq_.test_protocol_negotiation (empty value)

and requires the server to report that parameter as unsupported via a
NegotiateProtocolVersion message. pgwire-based servers reject the
connection instead:

psql: error: connection to server at "127.0.0.1", port 5433 failed:
server did not report the unsupported "_pq_.test_protocol_negotiation"
parameter in its protocol negotiation message
        This indicates a bug in either the server being contacted
        or a proxy handling the connection. Please consider
        reporting this to the maintainers of that software.
        For more information, including instructions on how to
        work around this issue for now, visit
                https://wiki.postgresql.org/wiki/Grease

Root cause

protocol_negotiation() in src/api/auth/mod.rs only looks at the
version numbers and never inspects startup parameters:

  • known version (3.0/3.2): sets the version and returns — no
    NegotiateProtocolVersion is sent even when the packet carries
    _pq_. options;
  • unknown version with the same major (e.g. 3.9999): sends
    NegotiateProtocolVersion::new(newest_server_version.into(), vec![])
    — with an empty unsupported-options list.

A PostgreSQL server collects every startup parameter whose name starts
with _pq_. and echoes the unrecognized ones in the
NegotiateProtocolVersion message (backend_startup.c,
ProcessStartupPacket):

else if (strncmp(nameptr, "_pq_.", 5) == 0)
{
    /*
     * Any option beginning with _pq_. is reserved for use as a
     * protocol-level option, but at present no such options are
     * defined.
     */
    unrecognized_protocol_options =
        lappend(unrecognized_protocol_options, pstrdup(nameptr));
}
...
if (PG_PROTOCOL_MINOR(proto) > PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST) ||
    unrecognized_protocol_options != NIL)
    SendNegotiateProtocolVersion(unrecognized_protocol_options);

libpq validates the reply strictly (fe-protocol3.c,
pqGetNegotiateProtocolVersion3): every reported option must have been
requested, and every requested _pq_. option must be reported — hence
the error above.

Note this is not a "new protocol version" feature: the _pq_. option
reporting has been part of the protocol since 2017
(postgres commit ae65f6066d, "Provide for forward compatibility with
future minor protocol versions"), and it was back-patched to all
supported releases at the time. It is deliberately orthogonal to the
version number — the commit message says the mechanism "makes it
possible for the client to request previously-unsupported features
without bumping the protocol version at all" — so even a 3.0-only
server must implement it. Every real PostgreSQL server since 2017
passes the GREASE test.

Suggested fix

Collect _pq_.-prefixed parameter names in protocol_negotiation()
and include them in the message in both paths (pgwire currently
supports no protocol options, so all of them are "unrecognized"):

let unsupported_options: Vec<String> = startup_message
    .parameters
    .keys()
    .filter(|name| name.starts_with("_pq_."))
    .cloned()
    .collect();
  • unknown version, same major: NegotiateProtocolVersion::new(newest.into(), unsupported_options)
  • known version: keep the version the client asked for, but still send
    NegotiateProtocolVersion::new(version.into(), unsupported_options)
    when the list is non-empty (a negotiation message that changes
    nothing is a protocol violation on the client side)

I have this implemented and verified against 19beta psql (connection
succeeds, \conninfo shows the negotiated 3.2) and 17.10 psql
(regression-clean). Happy to open a PR.

Workaround

Client side: pin the protocol version, e.g.
psql "host=... max_protocol_version=3.2".

Activity

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions