On Mon, Sep 21, 2026 at 07:35, Chao Li wrote:
> PFA v2 - addressed Kirill's comment.
Hi Chao,
I reviewed and tested v2.
I reproduced the over-read on master (0019a1ecf4c) with an
AddressSanitizer build. Without the patch, each of these prompts
triggers a heap-buffer-overflow at prompt.c:103:
%:aa %:x %: pre %:aa %`echo hi %`
With v2 applied, all of them run cleanly, and well-formed prompts
(%:aa:, %`echo hi`, %/%R%# and a few others) render exactly as
before. The psql TAP tests and the core regression tests also pass
under ASan.
The fix looks correct to me. Since strcspn() stops at either the
delimiter or the terminating NUL, p[1] is always one of the two, so
the pointer can no longer step past the end of the string.
For the back branches, v2 doesn't apply as-is because master changed
free() to pfree() next to these lines in 30652b356d. The attached
v2-backbranch-REL_14-19.patch.txt adjusts only that context; the
code change itself is identical. It applies to REL_14_STABLE through
REL_19_STABLE. I built and tested REL_14_STABLE with ASan: the same
six cases fail before the fix and run cleanly after it.
I've also attached an optional TAP test (0002, on top of v2), since
nothing currently exercises custom prompts. It passes with the fix,
and without it ASan aborts psql at the first unterminated case. In a
normal build it passed both with and without the fix, so it only
reliably catches this under ASan. Feel free to leave it out if you
don't think it's worth adding.
This looks ready for committer to me.
Thanks,
Ishjaap Singh
From 73bb0a0e75bf589aae3c463d1d442552da364384 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <[email protected]>
Date: Mon, 21 Sep 2026 11:26:23 +0800
Subject: [PATCH v2] psql: Avoid over-reading unterminated prompt escapes
When parsing the %:name: and %`command` prompt escapes, get_prompt()
assumed that the terminating delimiter was present. If it was absent, the
parser positioned its pointer at the prompt string's NUL terminator. The
loop increment then advanced the pointer one byte past the string, and the
next loop condition dereferenced it.
Only advance over the delimiter when it is present.
Author: Chao Li <[email protected]>
Reviewed-by: Kirill Reshke <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/bin/psql/prompt.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/bin/psql/prompt.c b/src/bin/psql/prompt.c
index 681574bee79..1c79a829090 100644
--- a/src/bin/psql/prompt.c
+++ b/src/bin/psql/prompt.c
@@ -333,7 +333,9 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack
cstack)
(void) pg_strip_crlf(buf);
free(file);
- p += cmdend + 1;
+ p += cmdend;
+ if (p[1] != '\0')
+ p++;
break;
}
@@ -348,7 +350,9 @@ get_prompt(promptStatus_t prompt_status, ConditionalStack
cstack)
if (val)
strlcpy(buf, val,
sizeof(buf));
free(name);
- p += nameend + 1;
+ p += nameend;
+ if (p[1] != '\0')
+ p++;
break;
}
--
2.50.1 (Apple Git-155)
From e617b9cf0adf52427dde5ab804d0a8e75d55e7e6 Mon Sep 17 00:00:00 2001
From: Ishjaap Singh <[email protected]>
Date: Sat, 24 Oct 2026 17:01:52 +0000
Subject: [PATCH v2] psql: Add test for unterminated prompt escapes
Exercise %:name: and %`command` prompt escapes, both terminated and
missing their closing delimiter. Without the preceding fix, the
unterminated cases read past the end of the prompt string. In a build
with AddressSanitizer the test then fails; in a normal build the
over-read usually goes unnoticed and the test passes either way.
---
src/bin/psql/meson.build | 1 +
src/bin/psql/t/040_prompt.pl | 49 ++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+)
create mode 100644 src/bin/psql/t/040_prompt.pl
diff --git a/src/bin/psql/meson.build b/src/bin/psql/meson.build
index 922b2845..1106698e 100644
--- a/src/bin/psql/meson.build
+++ b/src/bin/psql/meson.build
@@ -78,6 +78,7 @@ tests += {
't/010_tab_completion.pl',
't/020_cancel.pl',
't/030_pager.pl',
+ 't/040_prompt.pl',
],
},
}
diff --git a/src/bin/psql/t/040_prompt.pl b/src/bin/psql/t/040_prompt.pl
new file mode 100644
index 00000000..6367aeec
--- /dev/null
+++ b/src/bin/psql/t/040_prompt.pl
@@ -0,0 +1,49 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test psql prompt escapes that are missing their closing delimiter.
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# If we don't have IO::Pty, forget it, because IPC::Run depends on that
+# to support pty connections
+eval { require IO::Pty; };
+if ($@)
+{
+ plan skip_all => 'IO::Pty is needed to run this test';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->start;
+
+my $h = $node->interactive_psql('postgres');
+
+# Check that the prompt is displayed as expected after setting PROMPT1.
+sub check_prompt
+{
+ my ($prompt, $pattern, $annotation) = @_;
+
+ my $out = $h->query_until($pattern, "\\set PROMPT1 '$prompt'\n");
+ like($out, $pattern, $annotation);
+}
+
+$h->query_until(qr/=# $/, "\\set myvar VAL\n");
+
+check_prompt('%:myvar:> ', qr/VAL> $/, 'terminated %:name: escape');
+check_prompt('%`echo cmd`> ', qr/cmd> $/, 'terminated %`command` escape');
+
+# An unterminated escape uses the rest of the prompt string as the
+# variable name or command. This used to read past the end of the string.
+check_prompt('X%:myvar', qr/XVAL$/, 'unterminated %:name escape');
+check_prompt('Y%`echo cmd', qr/Ycmd$/, 'unterminated %`command escape');
+check_prompt('Z%:', qr/Z$/, 'unterminated empty %: escape');
+check_prompt('W%`', qr/W$/, 'unterminated empty %` escape');
+
+$h->quit or die "psql returned $?";
+
+done_testing();
--
2.43.0