Postgres wraps C language’s siglongjmp
using macros to provide code style similar to other languages Python, Java, and C++ to handle exceptions. Refer to the src/include/utils/elog.h
in Postgres source code to study the skills of Postgres. Chapter 4 of David R Hanson’s book C Interfaces and Implementations also serves as a good reference.
Let me copy and paste the comments of Postgres here:
* API for catching ereport(ERROR) exits. Use these macros like so:
*
* PG_TRY();
* {
* ... code that might throw ereport(ERROR) ...
* }
* PG_CATCH();
* {
* ... error recovery code ...
* }
* PG_END_TRY();
*
* (The braces are not actually necessary, but are recommended so that
* pgindent will indent the construct nicely.) The error recovery code
* can either do PG_RE_THROW to propagate the error outwards, or do a
* (sub)transaction abort. Failure to do so may leave the system in an
* inconsistent state for further processing.
If you don’t rethrow errors out in PG_CATCH()
clause, the CurrentMemoryContext
will be left in ErrorContext
. If you still not to manually change the context, then things might be wrong.
I write a script to search the code of Postgres to see if there is any code matching the above pattern. The script is so simple that it just uses text search instead of semantic analysis of C code.
#-*- coding: utf-8 -*-
import argparse
import re
def handle_one_file(fn, nlines):
with open(fn) as f:
lines = f.readlines()
for ln, line in enumerate(lines):
if "PG_CATCH();" in line:
checks = lines[ln:(ln + nlines)]
found = False
for c in checks:
if "PG_RE_THROW();" in c or "ReThrowError" in c or "MemoryContextSwitchTo" in c:
found = True
break
if not found:
print("{fn}:{ln}".format(fn=fn,ln=ln+1))
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='check_pg_catch')
parser.add_argument('--index', type=str, help='index file contains all paths of c files')
parser.add_argument('--nlines', type=int, help='search pg_catch in how many next lines')
args = parser.parse_args()
with open(args.index) as f:
for line in f:
handle_one_file(line.strip(), args.nlines)
Run the above script on Postgres 15_STABLE:
gpadmin@gpdev:~/z$ pypy3 check_pg_catch.py -h
usage: check_pg_catch [-h] [--index INDEX] [--nlines NLINES]
optional arguments:
-h, --help show this help message and exit
--index INDEX index file contains all paths of c files
--nlines NLINES search pg_catch in how many next lines
gpadmin@gpdev:~/z$ pypy3 check_pg_catch.py --index ./index --nlines 20
/home/gpadmin/workspace/postgres/contrib/jsonb_plpython/jsonb_plpython.c:374
/home/gpadmin/workspace/postgres/src/backend/postmaster/autovacuum.c:2494
/home/gpadmin/workspace/postgres/src/backend/postmaster/autovacuum.c:2691
/home/gpadmin/workspace/postgres/src/backend/utils/adt/xml.c:4247
/home/gpadmin/workspace/postgres/src/pl/plpgsql/src/pl_handler.c:374
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_cursorobject.c:129
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_cursorobject.c:244
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_cursorobject.c:359
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_cursorobject.c:460
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_exec.c:239
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_spi.c:136
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_spi.c:262
/home/gpadmin/workspace/postgres/src/pl/plpython/plpy_spi.c:328
/home/gpadmin/workspace/postgres/src/pl/tcl/pltcl.c:2235
/home/gpadmin/workspace/postgres/src/pl/tcl/pltcl.c:2411
/home/gpadmin/workspace/postgres/src/pl/tcl/pltcl.c:2637
/home/gpadmin/workspace/postgres/src/pl/tcl/pltcl.c:2864
We find xml.c
and lets take a close look at it:
#ifdef USE_LIBXML
static bool
wellformed_xml(text *data, XmlOptionType xmloption_arg)
{
bool result;
volatile xmlDocPtr doc = NULL;
/* We want to catch any exceptions and return false */
PG_TRY();
{
doc = xml_parse(data, xmloption_arg, true, GetDatabaseEncoding());
result = true;
}
PG_CATCH();
{
FlushErrorState();
result = false;
}
PG_END_TRY();
if (doc)
xmlFreeDoc(doc);
return result;
}
#endif
This function’s call chain is: wellformed_xml <-- xml_is_well_formed
, so calling the UDF xml_is_well_formed
will lead to CurrentMemoryContext
be set to ErrorContext
. Let’s use GDB to confirm:
./configure --prefix=/home/gpadmin/local/pg --with-libxml --enable-debug --enable-cassert CFLAGS='-O0' CXXFLAGS='-O0';make -j 8 -s install
We have to compile Postgres with libxml. Then create a running instance of Postgres and use psql to connect to it, use GDB to set break point at the UDF and run a SQL that will hit the CATCH clause. You can see after the UDF it is in ErrorContext
, which is bad.


And one of my colleague mentions that Postgres mailing list has a discussion: PG_CATCH used without PG_RETHROW. In this thread, one developer post the same question but not mention memorycontext, and Tom Lane replied that nothing is wrong. But I think there is bug due to the global variable CurrentMemoryContext
are not correctly restored. Lets try to build a bad case.
The idea is:
- call
xml_is_well_formed
will lead enterErrorContext
- then
palloc
will lead data structure allocated inErrorContext
- then call
xml_is_well_formed
again will leadelog(ERROR
one more time which will resetErrorContext
which will leads to the memory in step 2 become invalid - lets try to reference the pointer in step 2, this will lead to bad result (PANIC or wrong).
I use array
function and operator to implement the above step 2, a SQL lead to error is:
select
xml_is_well_formed('<a>')
or array_length(
array_append(
array[random(),
random() ],
case when xml_is_well_formed('<a>') then random() else random() end
),
1
) > 1;
NOTE latest Postgres code get rid of the CATCH clause thanks to the commit:
commit c4939f121563f300b98b30837151753fc8255554
Author: Tom Lane <tgl@sss.pgh.pa.us>
Date: Fri Dec 16 11:10:36 2022 -0500
Clean up dubious error handling in wellformed_xml().
For other cases of the pattern, if it is in pl
, it will abort subtransaction and restore the context, and for in autovacuum
it also restore the context after PG_END()
.