Trap in PG_CATCH() without Throw

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
            if not found:

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 -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 --index ./index --nlines 20

We find xml.c and lets take a close look at it:

static bool
wellformed_xml(text *data, XmlOptionType xmloption_arg)
        bool            result;
        volatile xmlDocPtr doc = NULL;

        /* We want to catch any exceptions and return false */
                doc = xml_parse(data, xmloption_arg, true, GetDatabaseEncoding());
                result = true;
                result = false;

        if (doc)

        return result;

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:

  1. call xml_is_well_formed will lead enter ErrorContext
  2. then palloc will lead data structure allocated in ErrorContext
  3. then call xml_is_well_formed again will lead elog(ERROR one more time which will reset ErrorContext which will leads to the memory in step 2 become invalid
  4. 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:

  or array_length(
      random() ], 
      case when xml_is_well_formed('<a>') then random() else random() end
  ) > 1;

NOTE latest Postgres code get rid of the CATCH clause thanks to the commit:

commit c4939f121563f300b98b30837151753fc8255554
Author: Tom Lane <>
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().

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.