pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feed[PATCH] libpq: Add PQpassfileLookup()
8+ messages / 3 participants
[nested] [flat]
* [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-04 14:06 Diego <mrstephenamell@gmail.com>
0 siblings, 1 reply; 8+ messages in thread
From: Diego @ 2026-09-04 14:06 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org; +Cc: Denis Smirnov <darthunix@gmail.com>; Jacob Champion <jacob.champion@enterprisedb.com>
Hi hackers,
This grew out of the portaddr thread [1]. The problem: an application
that connects through an intermediary, such as a local SSH tunnel on a
dynamic port, connects to a host and port that no longer match the
password file entry written for the real server, so the .pgpass lookup
comes up empty — and the only way out today is to reimplement the
passfile parser on the client side.
After portaddr was withdrawn, Denis Smirnov suggested exposing the
existing lookup as a public API instead [2]:
> I think a better solution would be to expose the existing
> passwordFromFile() code through a new public libpq API, perhaps
> PQpassfileLookup().
>
> Then pgcli could look up the password using the original host and
> port, and connect to the local tunnel port with that password. This
> would avoid both a new connection parameter and a separate .pgpass
> parser in each client.
The attached patch does exactly that. The function is a thin wrapper
around the existing passwordFromFile():
char *PQpassfileLookup(const char *hostname, const char *port,
const char *dbname, const char *username,
const char *passfile);
The first four arguments correspond to the fields of a passfile line
and behave as they do during connection establishment: a NULL or empty
hostname is matched as "localhost" (including the default socket
directory rule), and a NULL or empty port becomes the compiled-in
default. dbname and username get no defaults — resolving those belongs
to the conninfo layer, and a caller of this function already knows
them. If passfile is NULL or empty, PGPASSFILE and then the default
location are used, as for a connection.
The return value is malloc'd (to be freed with PQfreemem), or NULL
when no password could be found. The permission check also behaves
exactly as during connection establishment: warn on stderr and ignore
the file.
For tests, I added a --passfile mode to libpq_testclient and a new TAP
test (t/007_passfile.pl) covering matching, wildcards, escaping and
de-escaping, the localhost rule, PGPASSFILE, and the permission check.
The lookup is purely client-side, so none of it needs a server.
To show it end to end, a small PoC is attached too
(poc-passfilelookup.c). The setup: a server on 127.0.0.1:5440, this
passfile line, written before any tunnel exists:
127.0.0.1:5440:postgres:tuser:sekret
and a "tunnel" (a TCP relay) that binds port 0, so the kernel picks
the local port at that moment — 41395 on this run. The connection
through the tunnel cannot use the passfile today, with or without this
patch applied (nothing changes in connection establishment):
$ psql -w "host=127.0.0.1 port=41395 user=tuser dbname=postgres"
psql: error: connection to server at "127.0.0.1", port 41395
failed: fe_sendauth: no password supplied
With the patch, the client makes the two calls — look up under the
real host and port, connect to the local one:
$ ./poc-passfilelookup 127.0.0.1 5440 postgres tuser 41395
PQpassfileLookup("127.0.0.1", "5440", "postgres", "tuser", NULL) =
"sekret"
connected to 127.0.0.1:41395 as tuser -> OK
Against an unpatched libpq the PoC does not even link (undefined
reference to `PQpassfileLookup'), which is a fair summary of the
status quo: the only way a client can do this today is by parsing the
passfile itself.
I'll register this in the next open CommitFest.
[1] https://postgr.es/m/70436540-9d77-4014-89af-462881ed18d9@gmail.com
[2] https://postgr.es/m/B2EDB5AE-27F7-4580-871E-1C2433BAEF18@gmail.com
Thank you,
BR,
Diego
Attachments:
[text/x-patch] v1-0001-libpq-Add-PQpassfileLookup.patch (14.5K, ../../b650e555-11c7-415d-8bda-c747c492ab4a@gmail.com/3-v1-0001-libpq-Add-PQpassfileLookup.patch)
download | inline diff:
From 119a0cd7dfa72fedce1beda4ea6172d5c935708c Mon Sep 17 00:00:00 2001
From: Diego <mrstephenamell@gmail.com>
Date: Mon, 31 Aug 2026 16:38:53 -0300
Subject: [PATCH v1] libpq: Add PQpassfileLookup()
An application that connects through an intermediary, such as a local
SSH tunnel, connects to a host and port that no longer match the
password file entry written for the real server, so libpq's password
file lookup comes up empty during connection establishment. Until now,
such an application had to reimplement the password file parser on its
side to keep .pgpass working.
Expose the existing lookup as a public function, PQpassfileLookup(), so
that a client can look up the password under the real server's host and
port and pass the result as the password connection parameter while
connecting to the intermediary's address. The function applies the
same rules as connection establishment: the same field matching and
de-escaping, the same localhost and default-port handling, the same
permission checks, and the same fallback to PGPASSFILE and the default
password file location when no file is given.
Also add a --passfile mode to libpq_testclient, and a TAP test
exercising the lookup; it needs no server.
Author: Diego <mrstephenamell@gmail.com>
Suggested-by: Denis Smirnov <darthunix@gmail.com>
Discussion: https://postgr.es/m/B2EDB5AE-27F7-4580-871E-1C2433BAEF18@gmail.com
---
doc/src/sgml/libpq.sgml | 59 +++++++++
src/interfaces/libpq/exports.txt | 1 +
src/interfaces/libpq/fe-connect.c | 50 ++++++++
src/interfaces/libpq/libpq-fe.h | 9 ++
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq/t/007_passfile.pl | 128 +++++++++++++++++++
src/interfaces/libpq/test/libpq_testclient.c | 36 +++++-
7 files changed, 283 insertions(+), 1 deletion(-)
create mode 100644 src/interfaces/libpq/t/007_passfile.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 68487a3954f..5a439d55f83 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -7976,6 +7976,59 @@ char *PQencryptPassword(const char *passwd, const char *user);
</listitem>
</varlistentry>
+ <varlistentry id="libpq-PQpassfileLookup">
+ <term><function>PQpassfileLookup</function><indexterm><primary>PQpassfileLookup</primary></indexterm></term>
+
+ <listitem>
+ <para>
+ Looks up a password in a password file
+ (see <xref linkend="libpq-pgpass"/>).
+<synopsis>
+char *PQpassfileLookup(const char *hostname, const char *port, const char *dbname, const char *username, const char *passfile);
+</synopsis>
+ </para>
+
+ <para>
+ This function performs the same password file lookup that connection
+ establishment performs when no password has been specified, and
+ returns the password from the first matching line. It is intended
+ for applications that connect through an intermediary, for example a
+ local SSH tunnel: such an application can look up the password under
+ the real server's host and port, and then pass the result as the
+ <xref linkend="libpq-connect-password"/> connection parameter while
+ connecting to the intermediary's address.
+ </para>
+
+ <para>
+ The <parameter>hostname</parameter>, <parameter>port</parameter>,
+ <parameter>dbname</parameter> and <parameter>username</parameter>
+ arguments correspond to the first four fields of a password file
+ line. If <parameter>hostname</parameter> is <symbol>NULL</symbol> or
+ empty, or matches <application>libpq</application>'s default socket
+ directory path, the host name <literal>localhost</literal> is
+ searched for; if <parameter>port</parameter> is <symbol>NULL</symbol>
+ or empty, the compiled-in default port is used. No defaults are
+ applied for <parameter>dbname</parameter> and
+ <parameter>username</parameter>; if either is <symbol>NULL</symbol>
+ or empty, no password is returned.
+ <parameter>passfile</parameter> is the password file to use; if it is
+ <symbol>NULL</symbol> or empty, the file named by the
+ <envar>PGPASSFILE</envar> environment variable is used if set, else
+ the default password file location.
+ </para>
+
+ <para>
+ The return value is a string allocated by <function>malloc</function>,
+ or <symbol>NULL</symbol> if no password could be found. Use
+ <xref linkend="libpq-PQfreemem"/> to free the result when done with
+ it. The password file permission requirements described in
+ <xref linkend="libpq-pgpass"/> apply, and, as during connection
+ establishment, a warning is written to <filename>stderr</filename>
+ if the file is ignored because of them.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-PQmakeEmptyPGresult">
<term><function>PQmakeEmptyPGresult</function><indexterm><primary>PQmakeEmptyPGresult</primary></indexterm></term>
@@ -9462,6 +9515,12 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
is assumed that the file is stored in a directory that is secure, so
no special permissions check is made.
</para>
+
+ <para>
+ An application can perform the same password file lookup that
+ connection establishment performs by calling
+ <xref linkend="libpq-PQpassfileLookup"/>.
+ </para>
</sect1>
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..def61d63724 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208
PQfullProtocolVersion 209
appendPQExpBufferVA 210
PQgetThreadLock 211
+PQpassfileLookup 212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index ec98c58a407..e6beda8c002 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8142,6 +8142,56 @@ passwordFromFile(const char *hostname, const char *port,
}
+/*
+ * PQpassfileLookup
+ *
+ * Look up a password in a password file, applying the same rules that
+ * connection establishment applies when no password has been specified.
+ * This lets applications that connect through an intermediary (for
+ * example, a local SSH tunnel) look up the password under the real
+ * server's host and port while connecting elsewhere.
+ *
+ * The first four arguments correspond to the fields of a password file
+ * line, and NULL or empty values are treated the same way as during
+ * connection establishment: hostname is matched as "localhost", port
+ * defaults to DEF_PGPORT_STR, while dbname and username must be
+ * supplied. If passfile is NULL or empty, PGPASSFILE or the default
+ * password file location is used.
+ *
+ * Returns a malloc'd string the caller must free with PQfreemem(), or
+ * NULL if no password could be found.
+ */
+char *
+PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile)
+{
+ char pgpassfile[MAXPGPATH];
+ const char *errmsg;
+
+ if (passfile == NULL || passfile[0] == '\0')
+ {
+ const char *pgpassenv = getenv("PGPASSFILE");
+
+ if (pgpassenv != NULL && pgpassenv[0] != '\0')
+ strlcpy(pgpassfile, pgpassenv, sizeof(pgpassfile));
+ else
+ {
+ char homedir[MAXPGPATH];
+
+ if (!pqGetHomeDirectory(homedir, sizeof(homedir)))
+ return NULL;
+ snprintf(pgpassfile, sizeof(pgpassfile), "%s/%s",
+ homedir, PGPASSFILE);
+ }
+ passfile = pgpassfile;
+ }
+
+ return passwordFromFile(hostname, port, dbname, username,
+ passfile, &errmsg);
+}
+
+
/*
* If the connection failed due to bad password, we should mention
* if we got the password from the pgpassfile.
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f51fd620b0a..b63489a3bfe 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -69,6 +69,10 @@ extern "C"
/* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */
#define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1
+/* Features added in PostgreSQL v20: */
+/* Indicates presence of PQpassfileLookup */
+#define LIBPQ_HAS_PASSFILE_LOOKUP 1
+
/*
* Option flags for PQcopyResult
*/
@@ -367,6 +371,11 @@ extern PQconninfoOption *PQconninfo(PGconn *conn);
/* free the data structure returned by PQconndefaults() or PQconninfoParse() */
extern void PQconninfoFree(PQconninfoOption *connOptions);
+/* look up a password in a password file */
+extern char *PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile);
+
/*
* close the current connection and reestablish a new one with the same
* parameters
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..b9f93ddb852 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -161,6 +161,7 @@ tests += {
't/004_load_balance_dns.pl',
't/005_negotiate_encryption.pl',
't/006_service.pl',
+ 't/007_passfile.pl',
],
'env': {
'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/007_passfile.pl b/src/interfaces/libpq/t/007_passfile.pl
new file mode 100644
index 00000000000..d5c839ff500
--- /dev/null
+++ b/src/interfaces/libpq/t/007_passfile.pl
@@ -0,0 +1,128 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use File::Copy;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test PQpassfileLookup(), via libpq_testclient --passfile. The lookup is
+# purely client-side, so no server is involved. An argument of "-" is
+# passed to the function as NULL.
+
+my $td = PostgreSQL::Test::Utils::tempdir;
+my $passfile = "$td/pgpass";
+
+delete $ENV{PGPASSFILE};
+
+append_to_file($passfile, <<'EOF');
+# a comment line
+server.example.com:5432:proddb:diego:secret1
+server.example.com:5433:*:diego:secret2
+localhost:*:mydb:me:localpw
+special.example.com:5432:db\:colon:us\\er:pa\\ss\:word
+*:*:*:fallback:fallpw:anything after an unescaped colon is ignored
+server.example.com:5432:proddb:diego:shadowed
+EOF
+chmod 0600, $passfile or die "chmod: $!";
+
+my ($out, $err);
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($out, 'secret1', 'exact match returns the first matching password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5433', 'anydb', 'diego'
+ ]);
+is($out, 'secret2', 'wildcard field matches any value');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '-', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'NULL hostname and port match a localhost entry');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'empty hostname matches a localhost entry');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'special.example.com', '5432', 'db:colon', 'us\\er'
+ ]);
+is($out, 'pa\\ss:word', 'escaped characters are matched and de-escaped');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'anyhost', '1234', 'anydb', 'fallback'
+ ]);
+is($out, 'fallpw', 'password field ends at the first unescaped colon');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'otherdb', 'diego'
+ ]);
+is($err, 'no password found', 'no matching line returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', "$td/does_not_exist",
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($err, 'no password found', 'missing password file returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', '-', 'diego'
+ ]);
+is($err, 'no password found', 'NULL dbname returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', '-'
+ ]);
+is($err, 'no password found', 'NULL username returns no password');
+
+# A NULL passfile falls back to the PGPASSFILE environment variable.
+{
+ local $ENV{PGPASSFILE} = $passfile;
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '-',
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ is($out, 'secret1', 'NULL passfile falls back to PGPASSFILE');
+}
+
+SKIP:
+{
+ skip 'password file permissions are not checked on Windows', 2
+ if $windows_os;
+
+ my $passfile_insecure = "$td/pgpass_insecure";
+ copy($passfile, $passfile_insecure)
+ or die "could not copy $passfile to $passfile_insecure: $!";
+ chmod 0644, $passfile_insecure or die "chmod: $!";
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile_insecure,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ like(
+ $err,
+ qr/has group or world access/,
+ 'insecure password file draws a warning');
+ like($err, qr/no password found/, 'insecure password file is ignored');
+}
+
+done_testing();
diff --git a/src/interfaces/libpq/test/libpq_testclient.c b/src/interfaces/libpq/test/libpq_testclient.c
index 20730709ee7..bfdce989e33 100644
--- a/src/interfaces/libpq/test/libpq_testclient.c
+++ b/src/interfaces/libpq/test/libpq_testclient.c
@@ -23,6 +23,38 @@ print_ssl_library(void)
printf("%s\n", lib);
}
+/*
+ * Look up a password with PQpassfileLookup(). The arguments are passfile,
+ * hostname, port, dbname and username; an argument of "-" is passed as NULL.
+ */
+static int
+test_passfile_lookup(int argc, char *argv[])
+{
+ const char *args[5];
+ char *password;
+
+ if (argc < 7)
+ {
+ fprintf(stderr, "usage: libpq_testclient --passfile PASSFILE HOSTNAME PORT DBNAME USERNAME\n");
+ return 1;
+ }
+
+ for (int i = 0; i < 5; i++)
+ args[i] = strcmp(argv[i + 2], "-") == 0 ? NULL : argv[i + 2];
+
+ password = PQpassfileLookup(args[1], args[2], args[3], args[4], args[0]);
+
+ if (!password)
+ {
+ fprintf(stderr, "no password found\n");
+ return 1;
+ }
+
+ printf("%s\n", password);
+ PQfreemem(password);
+ return 0;
+}
+
int
main(int argc, char *argv[])
{
@@ -31,7 +63,9 @@ main(int argc, char *argv[])
print_ssl_library();
return 0;
}
+ else if ((argc > 1) && !strcmp(argv[1], "--passfile"))
+ return test_passfile_lookup(argc, argv);
- printf("currently only --ssl is supported\n");
+ printf("currently only --ssl and --passfile are supported\n");
return 1;
}
base-commit: 6a857156827b6a938bd02b9bf7281e7b71dfd088
--
2.43.0
[text/x-csrc] poc-passfilelookup.c (1.9K, ../../b650e555-11c7-415d-8bda-c747c492ab4a@gmail.com/4-poc-passfilelookup.c)
download | inline:
/*
* poc-passfilelookup.c
*
* PoC for PQpassfileLookup(): a client that connects through a local
* tunnel looks up the password under the REAL server's host and port
* (the entry that already exists in the passfile), then connects to
* the tunnel's local port passing that password explicitly.
*
* Compile: cc poc-passfilelookup.c -o poc-passfilelookup -lpq
* Usage: poc-passfilelookup REALHOST REALPORT DBNAME USER LOCALPORT
*
* The passfile is not named here: passing NULL makes the lookup use
* PGPASSFILE or ~/.pgpass, exactly as a connection would.
*/
#include <stdio.h>
#include <stdlib.h>
#include "libpq-fe.h"
int
main(int argc, char *argv[])
{
char *password;
PGconn *conn;
PGresult *res;
if (argc != 6)
{
fprintf(stderr, "usage: %s REALHOST REALPORT DBNAME USER LOCALPORT\n",
argv[0]);
return 2;
}
/* Step 1: look up the password under the REAL host and port. */
password = PQpassfileLookup(argv[1], argv[2], argv[3], argv[4], NULL);
if (password == NULL)
{
fprintf(stderr, "no password found for %s:%s:%s:%s\n",
argv[1], argv[2], argv[3], argv[4]);
return 1;
}
printf("PQpassfileLookup(\"%s\", \"%s\", \"%s\", \"%s\", NULL) = \"%s\"\n",
argv[1], argv[2], argv[3], argv[4], password);
/* Step 2: connect to the LOCAL tunnel port with that password. */
{
const char *keywords[] = {"host", "port", "dbname", "user",
"password", NULL};
const char *values[] = {"127.0.0.1", argv[5], argv[3], argv[4],
password, NULL};
conn = PQconnectdbParams(keywords, values, 0);
}
PQfreemem(password);
if (PQstatus(conn) != CONNECTION_OK)
{
fprintf(stderr, "connection failed: %s", PQerrorMessage(conn));
PQfinish(conn);
return 1;
}
res = PQexec(conn, "select current_user");
if (PQresultStatus(res) == PGRES_TUPLES_OK)
printf("connected to 127.0.0.1:%s as %s -> OK\n",
argv[5], PQgetvalue(res, 0, 0));
PQclear(res);
PQfinish(conn);
return 0;
}
^ permalink raw reply [nested|flat] 8+ messages in thread
* RE: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-07 11:15 Grigorev Jurij <ju.grigorev@ftdata.ru>
parent: Diego <mrstephenamell@gmail.com>
0 siblings, 1 reply; 8+ messages in thread
From: Grigorev Jurij @ 2026-09-07 11:15 UTC (permalink / raw)
To: Diego <mrstephenamell@gmail.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>; +Cc: Denis Smirnov <darthunix@gmail.com>; Jacob Champion <jacob.champion@enterprisedb.com>
Hi Diego,
I reviewed v1 (CF 7261). Thanks for writing this, and thanks to Denis
for the original suggestion.
I think exposing the existing passfile lookup is a useful API for the
SSH-tunnel/dynamic-port case.
Just found a couple of issues and some smaller test/documentation points.
1. Error handling is lost by PQpassfileLookup()
The new function calls:
const char *errmsg;
return passwordFromFile(hostname, port, dbname, username,
passfile, &errmsg);
but never examines errmsg. passwordFromFile() uses it to distinguish
an allocation failure from an ordinary lookup miss. The connection
path turns that error into CONNECTION_BAD, whereas PQpassfileLookup()
returns NULL in both cases.
The documentation currently says:
NULL if no password could be found
which does not make the distinction clear.
I think the simplest option is to keep the proposed public signature
and document that NULL is returned if no matching password is found or
if the lookup cannot be completed, for example because of an allocation
failure. Alternatively, the public API would need an error output
parameter.
The local errmsg variable is still required by the current internal
passwordFromFile() signature unless that function is changed to permit
a NULL error pointer.
2. PGPASSFILE can be silently truncated
When passfile is NULL or empty, the wrapper copies PGPASSFILE into a
MAXPGPATH-sized buffer:
strlcpy(pgpassfile, pgpassenv, sizeof(pgpassfile));
A value longer than MAXPGPATH is therefore silently truncated. This
differs from normal connection option processing, where the value of
PGPASSFILE is dynamically allocated.
There is no need to copy the environment value here. passfile can
point directly to pgpassenv; the local buffer is needed only when
constructing the default path.
3. Test coverage for argument defaults
The TAP test covers matching, wildcards, first-match-wins, escaping,
PGPASSFILE, and the permissions check. A few behaviours documented by
the new API are not exercised:
* NULL and empty port should use DEF_PGPORT_STR. The current
localhost test uses a wildcard port, so it does not test that
default. The test should account for a non-standard compiled-in
port rather than assuming 5432.
* NULL dbname and username are tested, but their empty-string forms
are not.
* NULL passfile with PGPASSFILE is tested, but empty passfile and the
fallback to the default password file location are not. The test
client already preserves an empty argument, so the former only
needs another TAP case.
* The documented default-socket-directory-to-localhost rule is not
currently tested.
The last two may require some platform-specific handling, but the
default-port and empty-argument cases should be straightforward.
I am less sure about this test:
*:*:*:fallback:fallpw:anything after an unescaped colon is ignored
The documented format requires a colon in a field to be escaped.
Testing that trailing fields are silently ignored appears to preserve
an undocumented parser behaviour. Since that behaviour is inherited
from passwordFromFile() rather than introduced by this API, I would
prefer to leave it out of this patch.
A couple of smaller points:
* test_passfile_lookup() checks argc < 7 and silently accepts extra
arguments. argc != 7 would match the usage string more closely.
* The C comment describing hostname defaults does not mention the
default socket directory rule that is described in the SGML docs.
* The returned allocation contains a plaintext password.
PQfreemem() does not erase it, whereas libpq explicitly clears
passwords stored in PGconn before freeing them. It would be useful
for the documentation to mention that callers are responsible for
securely clearing the result where appropriate.
The rest looks good to me: reusing passwordFromFile(), the feature
macro, exports.txt entry, Meson integration, and the general test
structure all look appropriate. Connection establishment remains
unchanged, as intended.
I will mark the CF entry as Waiting on Author. Happy to review the
next version!
Best regards,
Yuriy Grigoryev
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-09 12:33 Diego <mrstephenamell@gmail.com>
parent: Grigorev Jurij <ju.grigorev@ftdata.ru>
0 siblings, 1 reply; 8+ messages in thread
From: Diego @ 2026-09-09 12:33 UTC (permalink / raw)
To: Grigorev Jurij <ju.grigorev@ftdata.ru>; +Cc: Denis Smirnov <darthunix@gmail.com>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Jurij,
Thanks for the great review! v2 attached: all your points addressed,
plus a rebase onto current master.
> I think the simplest option is to keep the proposed public signature
> and document that NULL is returned if no matching password is found or
> if the lookup cannot be completed, for example because of an allocation
> failure. Alternatively, the public API would need an error output
> parameter.
Done the simple way: the docs and the function's comment now say NULL
means no matching password was found or the lookup could not be
completed. I also took your other suggestion and made
passwordFromFile() accept a NULL error pointer, so the wrapper no
longer needs a dummy variable.
> There is no need to copy the environment value here. passfile can
> point directly to pgpassenv; the local buffer is needed only when
> constructing the default path.
Done exactly that way, the truncation is gone.
> * NULL and empty port should use DEF_PGPORT_STR. The current
> localhost test uses a wildcard port, so it does not test that
> default. The test should account for a non-standard compiled-in
> port rather than assuming 5432.
Added. The test client grew a --passfile-defaults mode that prints
DEF_PGPORT_STR and DEFAULT_PGSOCKET_DIR, and the TAP test writes its
passfile entry under whatever port that reports, so no 5432 assumption
anywhere. NULL and empty port are both covered. I tried it against a
build configured with -Dpgport=5499 and it passes there too.
> * NULL dbname and username are tested, but their empty-string forms
> are not.
Added both. Small deviation here: the test client maps an argument of
"=" to an empty string instead of taking a real empty argument. No
TAP test in the tree passes an empty argv element today, and I did not
want to find out on the Windows CI whether one survives process
spawning there.
> * NULL passfile with PGPASSFILE is tested, but empty passfile and the
> fallback to the default password file location are not.
Added both: an empty passfile falling back to PGPASSFILE, and a NULL
passfile with no PGPASSFILE falling back to ~/.pgpass, by pointing
HOME at a temporary directory (skipped on Windows, where the default
location does not come from the environment).
> * The documented default-socket-directory-to-localhost rule is not
> currently tested.
Added, using the socket directory reported by --passfile-defaults
(skipped if the platform reports none).
> Testing that trailing fields are silently ignored appears to preserve
> an undocumented parser behaviour. Since that behaviour is inherited
> from passwordFromFile() rather than introduced by this API, I would
> prefer to leave it out of this patch.
Agreed, I removed the test case and its passfile line.
> * test_passfile_lookup() checks argc < 7 and silently accepts extra
> arguments. argc != 7 would match the usage string more closely.
Changed.
> * The C comment describing hostname defaults does not mention the
> default socket directory rule that is described in the SGML docs.
Added.
> * The returned allocation contains a plaintext password.
> PQfreemem() does not erase it, whereas libpq explicitly clears
> passwords stored in PGconn before freeing them. It would be useful
> for the documentation to mention that callers are responsible for
> securely clearing the result where appropriate.
Added a paragraph to the docs: the result is a cleartext password,
PQfreemem() does not erase it, and callers that do not want it
lingering in memory should overwrite it before freeing.
v2 also wraps the doc synopsis like the neighboring entries do, and
now spells out that PGPASSFILE is the only environment variable the
function looks at (PGHOST and PGPORT are not applied to the lookup
keys).
I'll move the CF entry back to Needs review.
Thank you,
BR,
Diego
Attachments:
[text/x-patch] v2-0001-libpq-Add-PQpassfileLookup.patch (19.3K, ../../00e741a7-8dc1-4993-87f4-8976e9c3b6ba@gmail.com/2-v2-0001-libpq-Add-PQpassfileLookup.patch)
download | inline diff:
From 7f6c251fcb063fdd72533c723065ff0576dfe836 Mon Sep 17 00:00:00 2001
From: Diego <mrstephenamell@gmail.com>
Date: Mon, 31 Aug 2026 16:38:53 -0300
Subject: [PATCH v2] libpq: Add PQpassfileLookup()
An application that connects through an intermediary, such as a local
SSH tunnel, connects to a host and port that no longer match the
password file entry written for the real server, so libpq's password
file lookup comes up empty during connection establishment. Until now,
such an application had to reimplement the password file parser on its
side to keep .pgpass working.
Expose the existing lookup as a public function, PQpassfileLookup(), so
that a client can look up the password under the real server's host and
port and pass the result as the password connection parameter while
connecting to the intermediary's address. The function applies the
same rules as connection establishment: the same field matching and
de-escaping, the same localhost and default-port substitutions for
missing values, the same permission checks, and the same fallback to
PGPASSFILE and the default password file location when no file is
given. PGPASSFILE is the only environment variable consulted; the
lookup keys are used as given.
Also add --passfile and --passfile-defaults modes to libpq_testclient,
and a TAP test exercising the lookup; it needs no server.
Author: Diego <mrstephenamell@gmail.com>
Reviewed-by: Yuriy Grigoryev <ju.grigorev@ftdata.ru>
Suggested-by: Denis Smirnov <darthunix@gmail.com>
Discussion: https://postgr.es/m/B2EDB5AE-27F7-4580-871E-1C2433BAEF18@gmail.com
---
doc/src/sgml/libpq.sgml | 73 +++++++
src/interfaces/libpq/exports.txt | 1 +
src/interfaces/libpq/fe-connect.c | 63 +++++-
src/interfaces/libpq/libpq-fe.h | 9 +
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq/t/007_passfile.pl | 201 +++++++++++++++++++
src/interfaces/libpq/test/libpq_testclient.c | 60 +++++-
7 files changed, 403 insertions(+), 5 deletions(-)
create mode 100644 src/interfaces/libpq/t/007_passfile.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 68487a3954f..4b37faf7741 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -7976,6 +7976,73 @@ char *PQencryptPassword(const char *passwd, const char *user);
</listitem>
</varlistentry>
+ <varlistentry id="libpq-PQpassfileLookup">
+ <term><function>PQpassfileLookup</function><indexterm><primary>PQpassfileLookup</primary></indexterm></term>
+
+ <listitem>
+ <para>
+ Looks up a password in a password file
+ (see <xref linkend="libpq-pgpass"/>).
+<synopsis>
+char *PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile);
+</synopsis>
+ </para>
+
+ <para>
+ This function performs the same password file lookup that connection
+ establishment performs when no password has been specified, and
+ returns the password from the first matching line. It is intended
+ for applications that connect through an intermediary, for example a
+ local SSH tunnel: such an application can look up the password under
+ the real server's host and port, and then pass the result as the
+ <xref linkend="libpq-connect-password"/> connection parameter while
+ connecting to the intermediary's address.
+ </para>
+
+ <para>
+ The <parameter>hostname</parameter>, <parameter>port</parameter>,
+ <parameter>dbname</parameter> and <parameter>username</parameter>
+ arguments correspond to the first four fields of a password file
+ line. If <parameter>hostname</parameter> is <symbol>NULL</symbol> or
+ empty, or matches <application>libpq</application>'s default socket
+ directory path, the host name <literal>localhost</literal> is
+ searched for; if <parameter>port</parameter> is <symbol>NULL</symbol>
+ or empty, the compiled-in default port is used. No defaults are
+ applied for <parameter>dbname</parameter> and
+ <parameter>username</parameter>; if either is <symbol>NULL</symbol>
+ or empty, no password is returned.
+ <parameter>passfile</parameter> is the password file to use; if it is
+ <symbol>NULL</symbol> or empty, the file named by the
+ <envar>PGPASSFILE</envar> environment variable is used if set, else
+ the default password file location.
+ <envar>PGPASSFILE</envar> is the only environment variable this
+ function consults; in particular, <envar>PGHOST</envar> and
+ <envar>PGPORT</envar> are not applied to the lookup keys.
+ </para>
+
+ <para>
+ The return value is a string allocated by <function>malloc</function>,
+ or <symbol>NULL</symbol> if no matching password was found or the
+ lookup could not be completed, for example because of a memory
+ allocation failure. Use <xref linkend="libpq-PQfreemem"/> to free
+ the result when done with it.
+ </para>
+
+ <para>
+ Note that the result contains a cleartext password, and that
+ <xref linkend="libpq-PQfreemem"/> does not erase it. Callers that
+ do not want to keep the password around in memory are responsible
+ for overwriting the result before freeing it. The password file
+ permission requirements described in
+ <xref linkend="libpq-pgpass"/> apply, and, as during connection
+ establishment, a warning is written to <filename>stderr</filename>
+ if the file is ignored because of them.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-PQmakeEmptyPGresult">
<term><function>PQmakeEmptyPGresult</function><indexterm><primary>PQmakeEmptyPGresult</primary></indexterm></term>
@@ -9462,6 +9529,12 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
is assumed that the file is stored in a directory that is secure, so
no special permissions check is made.
</para>
+
+ <para>
+ An application can perform the same password file lookup that
+ connection establishment performs by calling
+ <xref linkend="libpq-PQpassfileLookup"/>.
+ </para>
</sect1>
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..def61d63724 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208
PQfullProtocolVersion 209
appendPQExpBufferVA 210
PQgetThreadLock 211
+PQpassfileLookup 212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2ce128da157..e16ff17cb46 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8001,7 +8001,8 @@ pwdfMatchesString(char *buf, const char *token)
* Get a password from the password file. Return value is malloc'd.
*
* On failure, *errmsg is set to an error to be returned. It is
- * left NULL on success, or if no password could be found.
+ * left NULL on success, or if no password could be found. Callers
+ * that do not care about the distinction can pass errmsg as NULL.
*/
static char *
passwordFromFile(const char *hostname, const char *port,
@@ -8014,7 +8015,8 @@ passwordFromFile(const char *hostname, const char *port,
#endif
PQExpBufferData buf;
- *errmsg = NULL;
+ if (errmsg)
+ *errmsg = NULL;
if (dbname == NULL || dbname[0] == '\0')
return NULL;
@@ -8083,7 +8085,8 @@ passwordFromFile(const char *hostname, const char *port,
/* Make sure there's a reasonable amount of room in the buffer */
if (!enlargePQExpBuffer(&buf, 128))
{
- *errmsg = libpq_gettext("out of memory");
+ if (errmsg)
+ *errmsg = libpq_gettext("out of memory");
break;
}
@@ -8124,7 +8127,8 @@ passwordFromFile(const char *hostname, const char *port,
if (!ret)
{
- *errmsg = libpq_gettext("out of memory");
+ if (errmsg)
+ *errmsg = libpq_gettext("out of memory");
return NULL;
}
@@ -8152,6 +8156,57 @@ passwordFromFile(const char *hostname, const char *port,
}
+/*
+ * PQpassfileLookup
+ *
+ * Look up a password in a password file, applying the same rules that
+ * connection establishment applies when no password has been specified.
+ * This lets applications that connect through an intermediary (for
+ * example, a local SSH tunnel) look up the password under the real
+ * server's host and port while connecting elsewhere.
+ *
+ * The first four arguments correspond to the fields of a password file
+ * line, and NULL or empty values are treated the same way as during
+ * connection establishment: hostname is matched as "localhost" (as is
+ * a hostname equal to the default Unix-socket directory), port
+ * defaults to DEF_PGPORT_STR, while dbname and username must be
+ * supplied. If passfile is NULL or empty, PGPASSFILE or the default
+ * password file location is used.
+ *
+ * Returns a malloc'd string the caller must free with PQfreemem(), or
+ * NULL if no matching password was found or the lookup could not be
+ * completed.
+ */
+char *
+PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile)
+{
+ char pgpassfile[MAXPGPATH];
+
+ if (passfile == NULL || passfile[0] == '\0')
+ {
+ const char *pgpassenv = getenv("PGPASSFILE");
+
+ if (pgpassenv != NULL && pgpassenv[0] != '\0')
+ passfile = pgpassenv;
+ else
+ {
+ char homedir[MAXPGPATH];
+
+ if (!pqGetHomeDirectory(homedir, sizeof(homedir)))
+ return NULL;
+ snprintf(pgpassfile, sizeof(pgpassfile), "%s/%s",
+ homedir, PGPASSFILE);
+ passfile = pgpassfile;
+ }
+ }
+
+ return passwordFromFile(hostname, port, dbname, username,
+ passfile, NULL);
+}
+
+
/*
* If the connection failed due to bad password, we should mention
* if we got the password from the pgpassfile.
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f51fd620b0a..b63489a3bfe 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -69,6 +69,10 @@ extern "C"
/* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */
#define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1
+/* Features added in PostgreSQL v20: */
+/* Indicates presence of PQpassfileLookup */
+#define LIBPQ_HAS_PASSFILE_LOOKUP 1
+
/*
* Option flags for PQcopyResult
*/
@@ -367,6 +371,11 @@ extern PQconninfoOption *PQconninfo(PGconn *conn);
/* free the data structure returned by PQconndefaults() or PQconninfoParse() */
extern void PQconninfoFree(PQconninfoOption *connOptions);
+/* look up a password in a password file */
+extern char *PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile);
+
/*
* close the current connection and reestablish a new one with the same
* parameters
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..b9f93ddb852 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -161,6 +161,7 @@ tests += {
't/004_load_balance_dns.pl',
't/005_negotiate_encryption.pl',
't/006_service.pl',
+ 't/007_passfile.pl',
],
'env': {
'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/007_passfile.pl b/src/interfaces/libpq/t/007_passfile.pl
new file mode 100644
index 00000000000..2bd165e4993
--- /dev/null
+++ b/src/interfaces/libpq/t/007_passfile.pl
@@ -0,0 +1,201 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use File::Copy;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test PQpassfileLookup(), via libpq_testclient --passfile. The lookup is
+# purely client-side, so no server is involved. An argument of "-" is
+# passed to the function as NULL, and an argument of "=" as an empty
+# string (an empty argv element is not portable).
+
+my $td = PostgreSQL::Test::Utils::tempdir;
+my $passfile = "$td/pgpass";
+
+delete $ENV{PGPASSFILE};
+
+# The compiled-in defaults that the lookup falls back to.
+my ($defaults) = run_command([ 'libpq_testclient', '--passfile-defaults' ]);
+$defaults =~ s/\r//g;
+my ($defport, $socketdir) = split /\n/, $defaults;
+
+append_to_file($passfile, <<'EOF');
+# a comment line
+server.example.com:5432:proddb:diego:secret1
+server.example.com:5433:*:diego:secret2
+localhost:*:mydb:me:localpw
+special.example.com:5432:db\:colon:us\\er:pa\\ss\:word
+server.example.com:5432:proddb:diego:shadowed
+EOF
+append_to_file($passfile,
+ "defport.example.com:$defport:defdb:defuser:defportpw\n");
+chmod 0600, $passfile or die "chmod: $!";
+
+my ($out, $err);
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($out, 'secret1', 'exact match returns the first matching password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5433', 'anydb', 'diego'
+ ]);
+is($out, 'secret2', 'wildcard field matches any value');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '-', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'NULL hostname and port match a localhost entry');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '=', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'empty hostname matches a localhost entry');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'defport.example.com', '-', 'defdb', 'defuser'
+ ]);
+is($out, 'defportpw', 'NULL port matches an entry for the default port');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'defport.example.com', '=', 'defdb', 'defuser'
+ ]);
+is($out, 'defportpw', 'empty port matches an entry for the default port');
+
+SKIP:
+{
+ skip 'no default Unix-socket directory on this platform', 1
+ unless defined $socketdir && $socketdir =~ m{^/};
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ $socketdir, '-', 'mydb', 'me'
+ ]);
+ is($out, 'localpw',
+ 'default socket directory matches a localhost entry');
+}
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'special.example.com', '5432', 'db:colon', 'us\\er'
+ ]);
+is($out, 'pa\\ss:word', 'escaped characters are matched and de-escaped');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'otherdb', 'diego'
+ ]);
+is($err, 'no password found', 'no matching line returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', "$td/does_not_exist",
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($err, 'no password found', 'missing password file returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', '-', 'diego'
+ ]);
+is($err, 'no password found', 'NULL dbname returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', '=', 'diego'
+ ]);
+is($err, 'no password found', 'empty dbname returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', '-'
+ ]);
+is($err, 'no password found', 'NULL username returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', '='
+ ]);
+is($err, 'no password found', 'empty username returns no password');
+
+# A NULL or empty passfile falls back to the PGPASSFILE environment
+# variable.
+{
+ local $ENV{PGPASSFILE} = $passfile;
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '-',
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ is($out, 'secret1', 'NULL passfile falls back to PGPASSFILE');
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '=',
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ is($out, 'secret1', 'empty passfile falls back to PGPASSFILE');
+}
+
+SKIP:
+{
+ skip 'default password file location cannot be redirected on Windows', 1
+ if $windows_os;
+
+ # Without PGPASSFILE, the lookup falls back to ~/.pgpass.
+ my $homedir = PostgreSQL::Test::Utils::tempdir;
+ my $homepassfile = "$homedir/.pgpass";
+
+ append_to_file($homepassfile,
+ "home.example.com:5432:homedb:homeuser:homepw\n");
+ chmod 0600, $homepassfile or die "chmod: $!";
+
+ local $ENV{HOME} = $homedir;
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '-',
+ 'home.example.com', '5432', 'homedb', 'homeuser'
+ ]);
+ is($out, 'homepw', 'NULL passfile falls back to ~/.pgpass');
+}
+
+SKIP:
+{
+ skip 'password file permissions are not checked on Windows', 2
+ if $windows_os;
+
+ my $passfile_insecure = "$td/pgpass_insecure";
+ copy($passfile, $passfile_insecure)
+ or die "could not copy $passfile to $passfile_insecure: $!";
+ chmod 0644, $passfile_insecure or die "chmod: $!";
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile_insecure,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ like(
+ $err,
+ qr/has group or world access/,
+ 'insecure password file draws a warning');
+ like($err, qr/no password found/, 'insecure password file is ignored');
+}
+
+done_testing();
diff --git a/src/interfaces/libpq/test/libpq_testclient.c b/src/interfaces/libpq/test/libpq_testclient.c
index 20730709ee7..87309837489 100644
--- a/src/interfaces/libpq/test/libpq_testclient.c
+++ b/src/interfaces/libpq/test/libpq_testclient.c
@@ -23,6 +23,57 @@ print_ssl_library(void)
printf("%s\n", lib);
}
+/*
+ * Print the compiled-in defaults that the passfile lookup falls back to,
+ * for use by the TAP test.
+ */
+static void
+print_passfile_defaults(void)
+{
+ printf("%s\n%s\n", DEF_PGPORT_STR, DEFAULT_PGSOCKET_DIR);
+}
+
+/*
+ * Look up a password with PQpassfileLookup(). The arguments are passfile,
+ * hostname, port, dbname and username; an argument of "-" is passed as
+ * NULL, and an argument of "=" as an empty string (an empty command-line
+ * argument cannot be relied on to survive process spawning everywhere).
+ */
+static int
+test_passfile_lookup(int argc, char *argv[])
+{
+ const char *args[5];
+ char *password;
+
+ if (argc != 7)
+ {
+ fprintf(stderr, "usage: libpq_testclient --passfile PASSFILE HOSTNAME PORT DBNAME USERNAME\n");
+ return 1;
+ }
+
+ for (int i = 0; i < 5; i++)
+ {
+ if (strcmp(argv[i + 2], "-") == 0)
+ args[i] = NULL;
+ else if (strcmp(argv[i + 2], "=") == 0)
+ args[i] = "";
+ else
+ args[i] = argv[i + 2];
+ }
+
+ password = PQpassfileLookup(args[1], args[2], args[3], args[4], args[0]);
+
+ if (!password)
+ {
+ fprintf(stderr, "no password found\n");
+ return 1;
+ }
+
+ printf("%s\n", password);
+ PQfreemem(password);
+ return 0;
+}
+
int
main(int argc, char *argv[])
{
@@ -31,7 +82,14 @@ main(int argc, char *argv[])
print_ssl_library();
return 0;
}
+ else if ((argc > 1) && !strcmp(argv[1], "--passfile"))
+ return test_passfile_lookup(argc, argv);
+ else if ((argc > 1) && !strcmp(argv[1], "--passfile-defaults"))
+ {
+ print_passfile_defaults();
+ return 0;
+ }
- printf("currently only --ssl is supported\n");
+ printf("currently only --ssl, --passfile and --passfile-defaults are supported\n");
return 1;
}
base-commit: 11fb89fdbd319a53e32dce77685118979cafac0e
--
2.43.0
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-12 13:26 Grigorev Jurij <ju.grigorev@ftdata.ru>
parent: Diego <mrstephenamell@gmail.com>
0 siblings, 1 reply; 8+ messages in thread
From: Grigorev Jurij @ 2026-09-12 13:26 UTC (permalink / raw)
To: Diego <mrstephenamell@gmail.com>; +Cc: Denis Smirnov <darthunix@gmail.com>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Diego,
Thanks for the updated patch. I went through v2 and confirmed that all
the points from my previous review have been addressed. CI is green,
the implementation, tests, and API boundary look good to me, and I
found no further issues.
Noticed one small wording issue in both the commit message and the
documentation. They say that PGPASSFILE is the only environment
variable consulted by PQpassfileLookup(). Strictly speaking, when the
default password file location is used, pqGetHomeDirectory() consults
HOME on Unix. The new TAP test relies on this behavior as well.
Perhaps this could instead say:
Other libpq connection-parameter environment variables are not
applied to the lookup keys; in particular, PGHOST and PGPORT are
ignored.
This is only a documentation wording issue; the implementation is
correct. With that adjusted, the patch looks ready for committer review
from my side. Since this introduces a new public libpq API, additional
opinions on the API are of course welcome.
Best regards,
Yuriy Grigoryev
________________________________________
От: Diego <mrstephenamell@gmail.com>
Отправлено: 9 сентября 2026 г. 19:33:16
Кому: Григорьев Юрий
Копия: Denis Smirnov; Jacob Champion; pgsql-hackers@lists.postgresql.org
Тема: Re: [PATCH] libpq: Add PQpassfileLookup()
Hi Jurij,
Thanks for the great review! v2 attached: all your points addressed,
plus a rebase onto current master.
> I think the simplest option is to keep the proposed public signature
> and document that NULL is returned if no matching password is found or
> if the lookup cannot be completed, for example because of an allocation
> failure. Alternatively, the public API would need an error output
> parameter.
Done the simple way: the docs and the function's comment now say NULL
means no matching password was found or the lookup could not be
completed. I also took your other suggestion and made
passwordFromFile() accept a NULL error pointer, so the wrapper no
longer needs a dummy variable.
> There is no need to copy the environment value here. passfile can
> point directly to pgpassenv; the local buffer is needed only when
> constructing the default path.
Done exactly that way, the truncation is gone.
> * NULL and empty port should use DEF_PGPORT_STR. The current
> localhost test uses a wildcard port, so it does not test that
> default. The test should account for a non-standard compiled-in
> port rather than assuming 5432.
Added. The test client grew a --passfile-defaults mode that prints
DEF_PGPORT_STR and DEFAULT_PGSOCKET_DIR, and the TAP test writes its
passfile entry under whatever port that reports, so no 5432 assumption
anywhere. NULL and empty port are both covered. I tried it against a
build configured with -Dpgport=5499 and it passes there too.
> * NULL dbname and username are tested, but their empty-string forms
> are not.
Added both. Small deviation here: the test client maps an argument of
"=" to an empty string instead of taking a real empty argument. No
TAP test in the tree passes an empty argv element today, and I did not
want to find out on the Windows CI whether one survives process
spawning there.
> * NULL passfile with PGPASSFILE is tested, but empty passfile and the
> fallback to the default password file location are not.
Added both: an empty passfile falling back to PGPASSFILE, and a NULL
passfile with no PGPASSFILE falling back to ~/.pgpass, by pointing
HOME at a temporary directory (skipped on Windows, where the default
location does not come from the environment).
> * The documented default-socket-directory-to-localhost rule is not
> currently tested.
Added, using the socket directory reported by --passfile-defaults
(skipped if the platform reports none).
> Testing that trailing fields are silently ignored appears to preserve
> an undocumented parser behaviour. Since that behaviour is inherited
> from passwordFromFile() rather than introduced by this API, I would
> prefer to leave it out of this patch.
Agreed, I removed the test case and its passfile line.
> * test_passfile_lookup() checks argc < 7 and silently accepts extra
> arguments. argc != 7 would match the usage string more closely.
Changed.
> * The C comment describing hostname defaults does not mention the
> default socket directory rule that is described in the SGML docs.
Added.
> * The returned allocation contains a plaintext password.
> PQfreemem() does not erase it, whereas libpq explicitly clears
> passwords stored in PGconn before freeing them. It would be useful
> for the documentation to mention that callers are responsible for
> securely clearing the result where appropriate.
Added a paragraph to the docs: the result is a cleartext password,
PQfreemem() does not erase it, and callers that do not want it
lingering in memory should overwrite it before freeing.
v2 also wraps the doc synopsis like the neighboring entries do, and
now spells out that PGPASSFILE is the only environment variable the
function looks at (PGHOST and PGPORT are not applied to the lookup
keys).
I'll move the CF entry back to Needs review.
Thank you,
BR,
Diego
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-13 05:20 Denis Smirnov <darthunix@gmail.com>
parent: Grigorev Jurij <ju.grigorev@ftdata.ru>
0 siblings, 1 reply; 8+ messages in thread
From: Denis Smirnov @ 2026-09-13 05:20 UTC (permalink / raw)
To: Grigorev Jurij <ju.grigorev@ftdata.ru>; +Cc: Diego <mrstephenamell@gmail.com>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Diego,
Thanks for v2!
> * The returned allocation contains a plaintext password.
> PQfreemem() does not erase it, whereas libpq explicitly clears
> passwords stored in PGconn before freeing them. It would be useful
> for the documentation to mention that callers are responsible for
> securely clearing the result where appropriate.
The new docs now say this, but passwordFromFile() leaves part of the
original password after removing escapes in place:
Password in .pgpass: pa\\ss\:word
Returned buffer: pa\ss:word\0d\0
A caller using explicit_bzero(password, strlen(password)) before
PQfreemem() leaves the final 'd' untouched. Could we zero this tail in
passwordFromFile() before returning? The caller does not know the
original allocation size.
Best regards,
Denis Smirnov
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-16 13:39 Diego <mrstephenamell@gmail.com>
parent: Denis Smirnov <darthunix@gmail.com>
0 siblings, 1 reply; 8+ messages in thread
From: Diego @ 2026-09-16 13:39 UTC (permalink / raw)
To: Denis Smirnov <darthunix@gmail.com>; Grigorev Jurij <ju.grigorev@ftdata.ru>; +Cc: Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Denis, hi Yuriy,
Thanks to both of you.v3 attached, now as a series of two patches:
- v3-0001 fixes the residue Denis found, in passwordFromFile() itself.
- v3-0002 is the API patch, rebased on 0001, with Yuriy's wording.
> The new docs now say this, but passwordFromFile() leaves part of the
> original password after removing escapes in place:
> Password in .pgpass: pa\\ss\:word
> Returned buffer:pa\ss:word\0d\0
> A caller using explicit_bzero(password, strlen(password)) before
> PQfreemem() leaves the final 'd' untouched. Could we zero this tail in
> passwordFromFile() before returning? The caller does not know the
> original allocation size.
Confirmed, and it is a bit wider than the last character: strdup(t)
copies the rest of the line, and the in-place loop only overwrites the
de-escaped length, so everything after that point -- the tail of the
escaped password and any further fields on the line -- survives past
the terminator.With a line such as
host:5432:db:user:pw:extra:fields:here
the allocation ends up as "pw\0extra:fields:here\0".
It is also not specific to the new function.Connection establishment
stores the same allocation in conn->connhost[i].password, and
pqReleaseConnHosts() clears it with explicit_bzero(p, strlen(p)), so the
same bytes have been left behind in libpq's own cleanup: they have been
there since the de-escaping was added in 8d15e3ec4fc (2011), and the
explicit_bzero() that fails to reach them dates from 74a308cf522 (2019).
That is why 0001 is a separate patch: it stands on its own against
master, touches only passwordFromFile(), and could be back-patched if a
committer thinks that is worth it -- I have no strong opinion either
way.For what it is worth, it cherry-picks cleanly onto REL_19_STABLE;
on REL_18_STABLE down to REL_14_STABLE the only conflict is the
function's header comment, which is a single line there, and the code
hunks apply.
Rather than zeroing the tail after the fact, 0001 moves the existing
de-escape loop above the strdup(), so it runs in place on the line
buffer -- which is already cleared with explicit_bzero(buf.data,
buf.maxlen) on every exit -- and strdup() then copies only the
de-escaped password.The loop body is unchanged, the returned string
is byte-identical, and the allocation is exactly strlen() + 1 by
construction, so there is nothing a caller needs to know.If you
would rather have a one-line explicit_bzero() of the tail instead,
that is easy to do, but it keeps the oversized copy around and the
documentation could not promise anything about strlen().
0002 depends on 0001: it does not apply on bare master, and the
sentence in its docs and commit message about overwriting strlen()
bytes is only true on top of it.Squashing the two is fine by me if a
committer prefers that; if 0001 is dropped, those sentences go with it.
How I checked it, on master @ bd124434333:
* the libpq TAP suite and authentication/001_password, on 0001 alone
and on the full series: all green (007_passfile now has 20
subtests), no new compiler warnings, pgindent clean.
* an out-of-tree corpus of 21 lookups against a password file that
covers the escaping corners (your example, a lone trailing
backslash, an escaped colon as the last character, an empty
password, fields after the password, a 10 kB password, 3000 escaped
colons, a CRLF line, a four-field line, a wildcard line) run
through v2 and v3: identical output and exit code for all 21.
* a small harness that searches for the expected leftover bytes past
the terminator, within malloc_usable_size() and under
MALLOC_PERTURB_ so untouched slack cannot be mistaken for data: on
v2 it finds them in 5 of the 21 lines ("d" in your example,
"extra:fields:here", 2999 bytes of the escaped-colon case); on v3
it finds them in none.
There is no in-tree test for this, because nothing public can observe
bytes past the terminator without undefined behaviour; the corpus and
the harness are outside the tree.
With that, the sentence in the docs about clearing the result became a
real contract: the string holds nothing but the password and libpq
writes nothing past its terminating zero byte, so overwriting strlen()
bytes before freeing it is sufficient.The comment above
PQpassfileLookup() says the same.
> Noticed one small wording issue in both the commit message and the
> documentation.They say that PGPASSFILE is the only environment
> variable consulted by PQpassfileLookup().Strictly speaking, when the
> default password file location is used, pqGetHomeDirectory() consults
> HOME on Unix.The new TAP test relies on this behavior as well.
> Perhaps this could instead say:
> Other libpq connection-parameter environment variables are not
> applied to the lookup keys; in particular, PGHOST and PGPORT are
> ignored.
Right -- fixed with your sentence, verbatim, in both the docs and the
commit message, and "the default password file location" in the docs
now points at the pgpass section, which already covers HOME (and
%APPDATA% on Windows).I also added a TAP case for an escaped colon
at the end of the password.I did not add one for fields after the
password, per your earlier point about not testing undocumented parser
behaviour; the out-of-tree corpus above includes that line.
One thing I expect to be asked, so let me say it up front: the new
function has no error channel.A lookup that finds nothing, a missing
or badly-permissioned file, no home directory, and an allocation
failure all come back as NULL.That mirrors what connection
establishment does when the password file yields nothing -- the connect
path only turns the out-of-memory case into a hard error -- and it
keeps the function a plain wrapper around the existing lookup.If an
error out-parameter is preferred I can add one; I did not want to
design more API than the use case needs.Relatedly, the default-file
fallback in PQpassfileLookup() repeats a few lines of the connect path;
I can factor a small static helper if that is wanted.
Both patches apply on master in order, most recently checked against
a4f18fd8f28.I'll leave the CF entry at Needs review.
Thanks,
Diego
Attachments:
[text/x-patch] v3-0002-libpq-Add-PQpassfileLookup.patch (20.2K, ../../5b8e0436-853d-4b5e-8546-1862faf84cc6@gmail.com/2-v3-0002-libpq-Add-PQpassfileLookup.patch)
download | inline diff:
From 28383a3d28b35faf5afe313a2ebe98f6a5168926 Mon Sep 17 00:00:00 2001
From: Diego <mrstephenamell@gmail.com>
Date: Mon, 31 Aug 2026 16:38:53 -0300
Subject: [PATCH v3 2/2] libpq: Add PQpassfileLookup()
An application that connects through an intermediary, such as a local
SSH tunnel, connects to a host and port that no longer match the
password file entry written for the real server, so libpq's password
file lookup comes up empty during connection establishment. Until now,
such an application had to reimplement the password file parser on its
side to keep .pgpass working.
Expose the existing lookup as a public function, PQpassfileLookup(), so
that a client can look up the password under the real server's host and
port and pass the result as the password connection parameter while
connecting to the intermediary's address. The function applies the
same rules as connection establishment: the same field matching and
de-escaping, the same localhost and default-port substitutions for
missing values, the same permission checks, and the same fallback to
PGPASSFILE and the default password file location when no file is
given. Other libpq connection-parameter environment variables are not
applied to the lookup keys; in particular, PGHOST and PGPORT are
ignored. The result holds nothing but the password, so callers can
clear it with explicit_bzero() over strlen() bytes before freeing it.
Also add --passfile and --passfile-defaults modes to libpq_testclient,
and a TAP test exercising the lookup; it needs no server.
Author: Diego <mrstephenamell@gmail.com>
Reviewed-by: Yuriy Grigoryev <ju.grigorev@ftdata.ru>
Reviewed-by: Denis Smirnov <darthunix@gmail.com>
Suggested-by: Denis Smirnov <darthunix@gmail.com>
Discussion: https://postgr.es/m/B2EDB5AE-27F7-4580-871E-1C2433BAEF18@gmail.com
Discussion: https://postgr.es/m/b650e555-11c7-415d-8bda-c747c492ab4a@gmail.com
---
doc/src/sgml/libpq.sgml | 77 +++++++
src/interfaces/libpq/exports.txt | 1 +
src/interfaces/libpq/fe-connect.c | 65 +++++-
src/interfaces/libpq/libpq-fe.h | 9 +
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq/t/007_passfile.pl | 209 +++++++++++++++++++
src/interfaces/libpq/test/libpq_testclient.c | 60 +++++-
7 files changed, 417 insertions(+), 5 deletions(-)
create mode 100644 src/interfaces/libpq/t/007_passfile.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 68487a3954f..2557abfab6d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -7976,6 +7976,77 @@ char *PQencryptPassword(const char *passwd, const char *user);
</listitem>
</varlistentry>
+ <varlistentry id="libpq-PQpassfileLookup">
+ <term><function>PQpassfileLookup</function><indexterm><primary>PQpassfileLookup</primary></indexterm></term>
+
+ <listitem>
+ <para>
+ Looks up a password in a password file
+ (see <xref linkend="libpq-pgpass"/>).
+<synopsis>
+char *PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile);
+</synopsis>
+ </para>
+
+ <para>
+ This function performs the same password file lookup that connection
+ establishment performs when no password has been specified, and
+ returns the password from the first matching line. It is intended
+ for applications that connect through an intermediary, for example a
+ local SSH tunnel: such an application can look up the password under
+ the real server's host and port, and then pass the result as the
+ <xref linkend="libpq-connect-password"/> connection parameter while
+ connecting to the intermediary's address.
+ </para>
+
+ <para>
+ The <parameter>hostname</parameter>, <parameter>port</parameter>,
+ <parameter>dbname</parameter> and <parameter>username</parameter>
+ arguments correspond to the first four fields of a password file
+ line. If <parameter>hostname</parameter> is <symbol>NULL</symbol> or
+ empty, or matches <application>libpq</application>'s default socket
+ directory path, the host name <literal>localhost</literal> is
+ searched for; if <parameter>port</parameter> is <symbol>NULL</symbol>
+ or empty, the compiled-in default port is used. No defaults are
+ applied for <parameter>dbname</parameter> and
+ <parameter>username</parameter>; if either is <symbol>NULL</symbol>
+ or empty, no password is returned.
+ <parameter>passfile</parameter> is the password file to use; if it is
+ <symbol>NULL</symbol> or empty, the file named by the
+ <envar>PGPASSFILE</envar> environment variable is used if set, else
+ the default password file location (see <xref linkend="libpq-pgpass"/>).
+ Other <application>libpq</application> connection-parameter
+ environment variables are not applied to the lookup keys; in
+ particular, <envar>PGHOST</envar> and <envar>PGPORT</envar> are
+ ignored.
+ </para>
+
+ <para>
+ The return value is a string allocated by <function>malloc</function>,
+ or <symbol>NULL</symbol> if no matching password was found or the
+ lookup could not be completed, for example because of a memory
+ allocation failure. Use <xref linkend="libpq-PQfreemem"/> to free
+ the result when done with it.
+ </para>
+
+ <para>
+ Note that the result contains a cleartext password, and that
+ <xref linkend="libpq-PQfreemem"/> does not erase it. The string
+ holds nothing but the password: <application>libpq</application>
+ writes nothing past its terminating zero byte, so a caller that
+ does not want the password to linger in memory can overwrite
+ <function>strlen</function>(<replaceable>result</replaceable>)
+ bytes before freeing it. The password file
+ permission requirements described in
+ <xref linkend="libpq-pgpass"/> apply, and, as during connection
+ establishment, a warning is written to <filename>stderr</filename>
+ if the file is ignored because of them.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-PQmakeEmptyPGresult">
<term><function>PQmakeEmptyPGresult</function><indexterm><primary>PQmakeEmptyPGresult</primary></indexterm></term>
@@ -9462,6 +9533,12 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
is assumed that the file is stored in a directory that is secure, so
no special permissions check is made.
</para>
+
+ <para>
+ An application can perform the same password file lookup that
+ connection establishment performs by calling
+ <xref linkend="libpq-PQpassfileLookup"/>.
+ </para>
</sect1>
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..def61d63724 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208
PQfullProtocolVersion 209
appendPQExpBufferVA 210
PQgetThreadLock 211
+PQpassfileLookup 212
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a86564e192a..f660e518cb6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8005,7 +8005,8 @@ pwdfMatchesString(char *buf, const char *token)
* explicit_bzero(ret, strlen(ret)); pqReleaseConnHosts() relies on this.
*
* On failure, *errmsg is set to an error to be returned. It is
- * left NULL on success, or if no password could be found.
+ * left NULL on success, or if no password could be found. Callers
+ * that do not care about the distinction can pass errmsg as NULL.
*/
static char *
passwordFromFile(const char *hostname, const char *port,
@@ -8018,7 +8019,8 @@ passwordFromFile(const char *hostname, const char *port,
#endif
PQExpBufferData buf;
- *errmsg = NULL;
+ if (errmsg)
+ *errmsg = NULL;
if (dbname == NULL || dbname[0] == '\0')
return NULL;
@@ -8087,7 +8089,8 @@ passwordFromFile(const char *hostname, const char *port,
/* Make sure there's a reasonable amount of room in the buffer */
if (!enlargePQExpBuffer(&buf, 128))
{
- *errmsg = libpq_gettext("out of memory");
+ if (errmsg)
+ *errmsg = libpq_gettext("out of memory");
break;
}
@@ -8145,7 +8148,8 @@ passwordFromFile(const char *hostname, const char *port,
if (!ret)
{
- *errmsg = libpq_gettext("out of memory");
+ if (errmsg)
+ *errmsg = libpq_gettext("out of memory");
return NULL;
}
@@ -8164,6 +8168,59 @@ passwordFromFile(const char *hostname, const char *port,
}
+/*
+ * PQpassfileLookup
+ *
+ * Look up a password in a password file, applying the same rules that
+ * connection establishment applies when no password has been specified.
+ * This lets applications that connect through an intermediary (for
+ * example, a local SSH tunnel) look up the password under the real
+ * server's host and port while connecting elsewhere.
+ *
+ * The first four arguments correspond to the fields of a password file
+ * line, and NULL or empty values are treated the same way as during
+ * connection establishment: hostname is matched as "localhost" (as is
+ * a hostname equal to the default Unix-socket directory), port
+ * defaults to DEF_PGPORT_STR, while dbname and username must be
+ * supplied. If passfile is NULL or empty, PGPASSFILE or the default
+ * password file location is used.
+ *
+ * Returns a malloc'd string the caller must free with PQfreemem(), or
+ * NULL if no matching password was found or the lookup could not be
+ * completed. The string holds nothing but the password (see
+ * passwordFromFile()), so a caller that wants it gone from memory can
+ * overwrite strlen() bytes before freeing it.
+ */
+char *
+PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile)
+{
+ char pgpassfile[MAXPGPATH];
+
+ if (passfile == NULL || passfile[0] == '\0')
+ {
+ const char *pgpassenv = getenv("PGPASSFILE");
+
+ if (pgpassenv != NULL && pgpassenv[0] != '\0')
+ passfile = pgpassenv;
+ else
+ {
+ char homedir[MAXPGPATH];
+
+ if (!pqGetHomeDirectory(homedir, sizeof(homedir)))
+ return NULL;
+ snprintf(pgpassfile, sizeof(pgpassfile), "%s/%s",
+ homedir, PGPASSFILE);
+ passfile = pgpassfile;
+ }
+ }
+
+ return passwordFromFile(hostname, port, dbname, username,
+ passfile, NULL);
+}
+
+
/*
* If the connection failed due to bad password, we should mention
* if we got the password from the pgpassfile.
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f51fd620b0a..b63489a3bfe 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -69,6 +69,10 @@ extern "C"
/* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */
#define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1
+/* Features added in PostgreSQL v20: */
+/* Indicates presence of PQpassfileLookup */
+#define LIBPQ_HAS_PASSFILE_LOOKUP 1
+
/*
* Option flags for PQcopyResult
*/
@@ -367,6 +371,11 @@ extern PQconninfoOption *PQconninfo(PGconn *conn);
/* free the data structure returned by PQconndefaults() or PQconninfoParse() */
extern void PQconninfoFree(PQconninfoOption *connOptions);
+/* look up a password in a password file */
+extern char *PQpassfileLookup(const char *hostname, const char *port,
+ const char *dbname, const char *username,
+ const char *passfile);
+
/*
* close the current connection and reestablish a new one with the same
* parameters
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..b9f93ddb852 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -161,6 +161,7 @@ tests += {
't/004_load_balance_dns.pl',
't/005_negotiate_encryption.pl',
't/006_service.pl',
+ 't/007_passfile.pl',
],
'env': {
'with_ssl': ssl_library,
diff --git a/src/interfaces/libpq/t/007_passfile.pl b/src/interfaces/libpq/t/007_passfile.pl
new file mode 100644
index 00000000000..1a61c2c48c5
--- /dev/null
+++ b/src/interfaces/libpq/t/007_passfile.pl
@@ -0,0 +1,209 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+use strict;
+use warnings FATAL => 'all';
+use File::Copy;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test PQpassfileLookup(), via libpq_testclient --passfile. The lookup is
+# purely client-side, so no server is involved. An argument of "-" is
+# passed to the function as NULL, and an argument of "=" as an empty
+# string (an empty argv element is not portable).
+
+my $td = PostgreSQL::Test::Utils::tempdir;
+my $passfile = "$td/pgpass";
+
+delete $ENV{PGPASSFILE};
+
+# The compiled-in defaults that the lookup falls back to.
+my ($defaults) = run_command([ 'libpq_testclient', '--passfile-defaults' ]);
+$defaults =~ s/\r//g;
+my ($defport, $socketdir) = split /\n/, $defaults;
+
+append_to_file($passfile, <<'EOF');
+# a comment line
+server.example.com:5432:proddb:diego:secret1
+server.example.com:5433:*:diego:secret2
+localhost:*:mydb:me:localpw
+special.example.com:5432:db\:colon:us\\er:pa\\ss\:word
+endcolon.example.com:5432:enddb:enduser:end\:
+server.example.com:5432:proddb:diego:shadowed
+EOF
+append_to_file($passfile,
+ "defport.example.com:$defport:defdb:defuser:defportpw\n");
+chmod 0600, $passfile or die "chmod: $!";
+
+my ($out, $err);
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($out, 'secret1', 'exact match returns the first matching password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5433', 'anydb', 'diego'
+ ]);
+is($out, 'secret2', 'wildcard field matches any value');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '-', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'NULL hostname and port match a localhost entry');
+
+($out, $err) = run_command(
+ [ 'libpq_testclient', '--passfile', $passfile, '=', '-', 'mydb', 'me' ]);
+is($out, 'localpw', 'empty hostname matches a localhost entry');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'defport.example.com', '-', 'defdb', 'defuser'
+ ]);
+is($out, 'defportpw', 'NULL port matches an entry for the default port');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'defport.example.com', '=', 'defdb', 'defuser'
+ ]);
+is($out, 'defportpw', 'empty port matches an entry for the default port');
+
+SKIP:
+{
+ skip 'no default Unix-socket directory on this platform', 1
+ unless defined $socketdir && $socketdir =~ m{^/};
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ $socketdir, '-', 'mydb', 'me'
+ ]);
+ is($out, 'localpw',
+ 'default socket directory matches a localhost entry');
+}
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'special.example.com', '5432', 'db:colon', 'us\\er'
+ ]);
+is($out, 'pa\\ss:word', 'escaped characters are matched and de-escaped');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'endcolon.example.com', '5432', 'enddb', 'enduser'
+ ]);
+is($out, 'end:', 'escaped colon at the end of the password is de-escaped');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'otherdb', 'diego'
+ ]);
+is($err, 'no password found', 'no matching line returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', "$td/does_not_exist",
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+is($err, 'no password found', 'missing password file returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', '-', 'diego'
+ ]);
+is($err, 'no password found', 'NULL dbname returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', '=', 'diego'
+ ]);
+is($err, 'no password found', 'empty dbname returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', '-'
+ ]);
+is($err, 'no password found', 'NULL username returns no password');
+
+($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile,
+ 'server.example.com', '5432', 'proddb', '='
+ ]);
+is($err, 'no password found', 'empty username returns no password');
+
+# A NULL or empty passfile falls back to the PGPASSFILE environment
+# variable.
+{
+ local $ENV{PGPASSFILE} = $passfile;
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '-',
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ is($out, 'secret1', 'NULL passfile falls back to PGPASSFILE');
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '=',
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ is($out, 'secret1', 'empty passfile falls back to PGPASSFILE');
+}
+
+SKIP:
+{
+ skip 'default password file location cannot be redirected on Windows', 1
+ if $windows_os;
+
+ # Without PGPASSFILE, the lookup falls back to ~/.pgpass.
+ my $homedir = PostgreSQL::Test::Utils::tempdir;
+ my $homepassfile = "$homedir/.pgpass";
+
+ append_to_file($homepassfile,
+ "home.example.com:5432:homedb:homeuser:homepw\n");
+ chmod 0600, $homepassfile or die "chmod: $!";
+
+ local $ENV{HOME} = $homedir;
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', '-',
+ 'home.example.com', '5432', 'homedb', 'homeuser'
+ ]);
+ is($out, 'homepw', 'NULL passfile falls back to ~/.pgpass');
+}
+
+SKIP:
+{
+ skip 'password file permissions are not checked on Windows', 2
+ if $windows_os;
+
+ my $passfile_insecure = "$td/pgpass_insecure";
+ copy($passfile, $passfile_insecure)
+ or die "could not copy $passfile to $passfile_insecure: $!";
+ chmod 0644, $passfile_insecure or die "chmod: $!";
+
+ ($out, $err) = run_command(
+ [
+ 'libpq_testclient', '--passfile', $passfile_insecure,
+ 'server.example.com', '5432', 'proddb', 'diego'
+ ]);
+ like(
+ $err,
+ qr/has group or world access/,
+ 'insecure password file draws a warning');
+ like($err, qr/no password found/, 'insecure password file is ignored');
+}
+
+done_testing();
diff --git a/src/interfaces/libpq/test/libpq_testclient.c b/src/interfaces/libpq/test/libpq_testclient.c
index 20730709ee7..87309837489 100644
--- a/src/interfaces/libpq/test/libpq_testclient.c
+++ b/src/interfaces/libpq/test/libpq_testclient.c
@@ -23,6 +23,57 @@ print_ssl_library(void)
printf("%s\n", lib);
}
+/*
+ * Print the compiled-in defaults that the passfile lookup falls back to,
+ * for use by the TAP test.
+ */
+static void
+print_passfile_defaults(void)
+{
+ printf("%s\n%s\n", DEF_PGPORT_STR, DEFAULT_PGSOCKET_DIR);
+}
+
+/*
+ * Look up a password with PQpassfileLookup(). The arguments are passfile,
+ * hostname, port, dbname and username; an argument of "-" is passed as
+ * NULL, and an argument of "=" as an empty string (an empty command-line
+ * argument cannot be relied on to survive process spawning everywhere).
+ */
+static int
+test_passfile_lookup(int argc, char *argv[])
+{
+ const char *args[5];
+ char *password;
+
+ if (argc != 7)
+ {
+ fprintf(stderr, "usage: libpq_testclient --passfile PASSFILE HOSTNAME PORT DBNAME USERNAME\n");
+ return 1;
+ }
+
+ for (int i = 0; i < 5; i++)
+ {
+ if (strcmp(argv[i + 2], "-") == 0)
+ args[i] = NULL;
+ else if (strcmp(argv[i + 2], "=") == 0)
+ args[i] = "";
+ else
+ args[i] = argv[i + 2];
+ }
+
+ password = PQpassfileLookup(args[1], args[2], args[3], args[4], args[0]);
+
+ if (!password)
+ {
+ fprintf(stderr, "no password found\n");
+ return 1;
+ }
+
+ printf("%s\n", password);
+ PQfreemem(password);
+ return 0;
+}
+
int
main(int argc, char *argv[])
{
@@ -31,7 +82,14 @@ main(int argc, char *argv[])
print_ssl_library();
return 0;
}
+ else if ((argc > 1) && !strcmp(argv[1], "--passfile"))
+ return test_passfile_lookup(argc, argv);
+ else if ((argc > 1) && !strcmp(argv[1], "--passfile-defaults"))
+ {
+ print_passfile_defaults();
+ return 0;
+ }
- printf("currently only --ssl is supported\n");
+ printf("currently only --ssl, --passfile and --passfile-defaults are supported\n");
return 1;
}
--
2.43.0
[text/x-patch] v3-0001-libpq-Do-not-leave-password-residue-in-passwordFr.patch (3.2K, ../../5b8e0436-853d-4b5e-8546-1862faf84cc6@gmail.com/3-v3-0001-libpq-Do-not-leave-password-residue-in-passwordFr.patch)
download | inline diff:
From c97cdbe57b54d86707f9580a6dc52e25688fedd6 Mon Sep 17 00:00:00 2001
From: Diego <mrstephenamell@gmail.com>
Date: Mon, 14 Sep 2026 11:53:22 -0300
Subject: [PATCH v3 1/2] libpq: Do not leave password residue in
passwordFromFile()'s result
passwordFromFile() copied the remainder of the matching password file
line with strdup() and then de-escaped the copy in place. The copy
held everything up to the end of the line, and every escape sequence
shrinks the string by one byte, so the tail of the escaped password,
and any further fields on the line, remained in the allocation past
the terminating zero byte. Callers that clear the password before
freeing it, such as pqReleaseConnHosts() with explicit_bzero(p,
strlen(p)), could not reach those bytes.
De-escape the password within the line buffer instead, which is
cleared with explicit_bzero() before it is freed, and copy only the
de-escaped password. The returned string is unchanged, and the
function now writes nothing past its terminating zero byte.
The de-escaping dates from 8d15e3ec4fc; the explicit_bzero() clearing
added later by 74a308cf522 did not account for it.
Reported-by: Denis Smirnov <darthunix@gmail.com>
Discussion: https://postgr.es/m/D35D0ABB-755E-4EDE-B535-5C7578B3D3DC@gmail.com
---
src/interfaces/libpq/fe-connect.c | 30 +++++++++++++++++++++---------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2ce128da157..a86564e192a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8000,6 +8000,10 @@ pwdfMatchesString(char *buf, const char *token)
/*
* Get a password from the password file. Return value is malloc'd.
*
+ * The returned string holds nothing but the de-escaped password and its
+ * terminating zero byte, so callers can clear it with
+ * explicit_bzero(ret, strlen(ret)); pqReleaseConnHosts() relies on this.
+ *
* On failure, *errmsg is set to an error to be returned. It is
* left NULL on success, or if no password could be found.
*/
@@ -8116,6 +8120,23 @@ passwordFromFile(const char *hostname, const char *port,
*p1,
*p2;
+ /*
+ * De-escape the password in place, within the line buffer,
+ * before copying it out. Copying first and de-escaping the
+ * copy would leave the tail of the escaped password, and
+ * anything following it on the line, in the result past the
+ * terminating zero byte, where a caller that clears
+ * strlen(ret) bytes (as pqReleaseConnHosts() does) cannot
+ * reach it. The line buffer itself is cleared below.
+ */
+ for (p1 = p2 = t; *p1 != ':' && *p1 != '\0'; ++p1, ++p2)
+ {
+ if (*p1 == '\\' && p1[1] != '\0')
+ ++p1;
+ *p2 = *p1;
+ }
+ *p2 = '\0';
+
ret = strdup(t);
fclose(fp);
@@ -8128,15 +8149,6 @@ passwordFromFile(const char *hostname, const char *port,
return NULL;
}
- /* De-escape password. */
- for (p1 = p2 = ret; *p1 != ':' && *p1 != '\0'; ++p1, ++p2)
- {
- if (*p1 == '\\' && p1[1] != '\0')
- ++p1;
- *p2 = *p1;
- }
- *p2 = '\0';
-
return ret;
}
}
base-commit: bd1244343332419df6b34be38883ee0af3227030
--
2.43.0
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-17 07:17 Denis Smirnov <darthunix@gmail.com>
parent: Diego <mrstephenamell@gmail.com>
0 siblings, 1 reply; 8+ messages in thread
From: Denis Smirnov @ 2026-09-17 07:17 UTC (permalink / raw)
To: Diego <mrstephenamell@gmail.com>; +Cc: Grigorev Jurij <ju.grigorev@ftdata.ru>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Diego,
Thanks for v3. Both patches look good to me. I have no further
comments.
One separate note for future work: even after 0001, copies of the
password can still remain in memory:
- The FILE buffer holds the whole .pgpass file, and fclose() frees
it without clearing it.
- On long lines, realloc() in enlargePQExpBuffer() can leave old
copies behind.
- markPQExpBufferBroken() also frees memory without clearing it
when an allocation fails.
- SCRAM and cleartext authentication make more copies later.
These issues already existed, and I don't see a practical way to
exploit them. They should not delay the new API. Maybe you would
like to look at them separately later.
Best regards,
Denis Smirnov
> On 16 Sep 2026, at 20:39, Diego <mrstephenamell@gmail.com> wrote:
>
> Hi Denis, hi Yuriy,
>
> Thanks to both of you.v3 attached, now as a series of two patches:
>
> - v3-0001 fixes the residue Denis found, in passwordFromFile() itself.
> - v3-0002 is the API patch, rebased on 0001, with Yuriy's wording.
>
> > The new docs now say this, but passwordFromFile() leaves part of the
> > original password after removing escapes in place:
> > Password in .pgpass: pa\\ss\:word
> > Returned buffer:pa\ss:word\0d\0
> > A caller using explicit_bzero(password, strlen(password)) before
> > PQfreemem() leaves the final 'd' untouched. Could we zero this tail in
> > passwordFromFile() before returning? The caller does not know the
> > original allocation size.
>
> Confirmed, and it is a bit wider than the last character: strdup(t)
> copies the rest of the line, and the in-place loop only overwrites the
> de-escaped length, so everything after that point -- the tail of the
> escaped password and any further fields on the line -- survives past
> the terminator.With a line such as
>
> host:5432:db:user:pw:extra:fields:here
>
> the allocation ends up as "pw\0extra:fields:here\0".
>
> It is also not specific to the new function.Connection establishment
> stores the same allocation in conn->connhost[i].password, and
> pqReleaseConnHosts() clears it with explicit_bzero(p, strlen(p)), so the
> same bytes have been left behind in libpq's own cleanup: they have been
> there since the de-escaping was added in 8d15e3ec4fc (2011), and the
> explicit_bzero() that fails to reach them dates from 74a308cf522 (2019).
> That is why 0001 is a separate patch: it stands on its own against
> master, touches only passwordFromFile(), and could be back-patched if a
> committer thinks that is worth it -- I have no strong opinion either
> way.For what it is worth, it cherry-picks cleanly onto REL_19_STABLE;
> on REL_18_STABLE down to REL_14_STABLE the only conflict is the
> function's header comment, which is a single line there, and the code
> hunks apply.
>
> Rather than zeroing the tail after the fact, 0001 moves the existing
> de-escape loop above the strdup(), so it runs in place on the line
> buffer -- which is already cleared with explicit_bzero(buf.data,
> buf.maxlen) on every exit -- and strdup() then copies only the
> de-escaped password.The loop body is unchanged, the returned string
> is byte-identical, and the allocation is exactly strlen() + 1 by
> construction, so there is nothing a caller needs to know.If you
> would rather have a one-line explicit_bzero() of the tail instead,
> that is easy to do, but it keeps the oversized copy around and the
> documentation could not promise anything about strlen().
>
> 0002 depends on 0001: it does not apply on bare master, and the
> sentence in its docs and commit message about overwriting strlen()
> bytes is only true on top of it.Squashing the two is fine by me if a
> committer prefers that; if 0001 is dropped, those sentences go with it.
>
> How I checked it, on master @ bd124434333:
>
> * the libpq TAP suite and authentication/001_password, on 0001 alone
> and on the full series: all green (007_passfile now has 20
> subtests), no new compiler warnings, pgindent clean.
> * an out-of-tree corpus of 21 lookups against a password file that
> covers the escaping corners (your example, a lone trailing
> backslash, an escaped colon as the last character, an empty
> password, fields after the password, a 10 kB password, 3000 escaped
> colons, a CRLF line, a four-field line, a wildcard line) run
> through v2 and v3: identical output and exit code for all 21.
> * a small harness that searches for the expected leftover bytes past
> the terminator, within malloc_usable_size() and under
> MALLOC_PERTURB_ so untouched slack cannot be mistaken for data: on
> v2 it finds them in 5 of the 21 lines ("d" in your example,
> "extra:fields:here", 2999 bytes of the escaped-colon case); on v3
> it finds them in none.
>
> There is no in-tree test for this, because nothing public can observe
> bytes past the terminator without undefined behaviour; the corpus and
> the harness are outside the tree.
>
> With that, the sentence in the docs about clearing the result became a
> real contract: the string holds nothing but the password and libpq
> writes nothing past its terminating zero byte, so overwriting strlen()
> bytes before freeing it is sufficient.The comment above
> PQpassfileLookup() says the same.
>
> > Noticed one small wording issue in both the commit message and the
> > documentation.They say that PGPASSFILE is the only environment
> > variable consulted by PQpassfileLookup().Strictly speaking, when the
> > default password file location is used, pqGetHomeDirectory() consults
> > HOME on Unix.The new TAP test relies on this behavior as well.
> > Perhaps this could instead say:
> > Other libpq connection-parameter environment variables are not
> > applied to the lookup keys; in particular, PGHOST and PGPORT are
> > ignored.
>
> Right -- fixed with your sentence, verbatim, in both the docs and the
> commit message, and "the default password file location" in the docs
> now points at the pgpass section, which already covers HOME (and
> %APPDATA% on Windows).I also added a TAP case for an escaped colon
> at the end of the password.I did not add one for fields after the
> password, per your earlier point about not testing undocumented parser
> behaviour; the out-of-tree corpus above includes that line.
>
> One thing I expect to be asked, so let me say it up front: the new
> function has no error channel.A lookup that finds nothing, a missing
> or badly-permissioned file, no home directory, and an allocation
> failure all come back as NULL.That mirrors what connection
> establishment does when the password file yields nothing -- the connect
> path only turns the out-of-memory case into a hard error -- and it
> keeps the function a plain wrapper around the existing lookup.If an
> error out-parameter is preferred I can add one; I did not want to
> design more API than the use case needs.Relatedly, the default-file
> fallback in PQpassfileLookup() repeats a few lines of the connect path;
> I can factor a small static helper if that is wanted.
>
> Both patches apply on master in order, most recently checked against
> a4f18fd8f28.I'll leave the CF entry at Needs review.
>
> Thanks,
> Diego<v3-0002-libpq-Add-PQpassfileLookup.patch><v3-0001-libpq-Do-not-leave-password-residue-in-passwordFr.patch>
^ permalink raw reply [nested|flat] 8+ messages in thread
* Re: [PATCH] libpq: Add PQpassfileLookup()
@ 2026-09-18 17:38 Diego <mrstephenamell@gmail.com>
parent: Denis Smirnov <darthunix@gmail.com>
0 siblings, 0 replies; 8+ messages in thread
From: Diego @ 2026-09-18 17:38 UTC (permalink / raw)
To: Denis Smirnov <darthunix@gmail.com>; +Cc: Grigorev Jurij <ju.grigorev@ftdata.ru>; Jacob Champion <jacob.champion@enterprisedb.com>; pgsql-hackers@lists.postgresql.org <pgsql-hackers@lists.postgresql.org>
Hi Denis,
Thank you, and thanks for the review that found the residue in the first
place.
> One separate note for future work: even after 0001, copies of the
> password can still remain in memory:
Agreed on all four, and I had noticed two of them while checking 0001: on
an allocation failure, enlargePQExpBuffer() ends up in
markPQExpBufferBroken(), which free()s the block still holding the partially
read line and sets maxlen to 0, so the explicit_bzero(buf.data, buf.maxlen)
further down becomes a no-op on the replacement buffer. I left it alone
because fixing it properly means touching pqexpbuffer.c, which is a
different patch and a different discussion.
The stdio buffer is the largest of the four by far, it can hold the whole
file, not just one line, and it is also the only one that is there on
every successful lookup rather than only on an error path. If nobody
objects I will look at that one separately once this is settled, probably
with setvbuf() on an owned buffer that can be cleared before fclose().
I'll leave those out of this thread so they do not hold up the API. Since
you and Yuriy are both happy with the series, I'll move the CF entry to
Ready for Committer.
Thanks,
Diego
^ permalink raw reply [nested|flat] 8+ messages in thread
end of thread, other threads:[~2026-09-18 17:38 UTC | newest]
Thread overview: 8+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-09-04 14:06 [PATCH] libpq: Add PQpassfileLookup() Diego <mrstephenamell@gmail.com>
2026-09-07 11:15 ` Grigorev Jurij <ju.grigorev@ftdata.ru>
2026-09-09 12:33 ` Diego <mrstephenamell@gmail.com>
2026-09-12 13:26 ` Grigorev Jurij <ju.grigorev@ftdata.ru>
2026-09-13 05:20 ` Denis Smirnov <darthunix@gmail.com>
2026-09-16 13:39 ` Diego <mrstephenamell@gmail.com>
2026-09-17 07:17 ` Denis Smirnov <darthunix@gmail.com>
2026-09-18 17:38 ` Diego <mrstephenamell@gmail.com>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox