BUG #19762: ManualBug1 tcn: NULL pointer dereference in triggered_change_notification() via strcpy_quoted

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 1217816127(at)qq(dot)com
Subject: BUG #19762: ManualBug1 tcn: NULL pointer dereference in triggered_change_notification() via strcpy_quoted
Date: 2026-10-15 15:02:57
Message-ID: 19762-04e5ab8e5459b958@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19762
Logged by: Yuelin Wang
Email address: 1217816127(at)qq(dot)com
PostgreSQL version: 19rc1
Operating system: Linux (Ubuntu 24.04, x86_64)
Description:

### What happens?

In `contrib/tcn/tcn.c` at line 165, the function
`triggered_change_notification()` calls `SPI_getvalue(trigtuple, tupdesc,
colno)` and passes the return value directly to `strcpy_quoted()` without
checking whether it is NULL:

```c
/* contrib/tcn/tcn.c:165 */
strcpy_quoted(&payload, SPI_getvalue(trigtuple, tupdesc, colno), '\'');
```

`SPI_getvalue()` returns NULL when the requested column value is SQL NULL
(it sets `isnull = true` and returns NULL from `heap_getattr`).
The function `strcpy_quoted()` defined at line 36 immediately dereferences
the pointer at line 39:

```c
static void
strcpy_quoted(StringInfo r, const char *s, const char q)
{
appendStringInfoCharMacro(r, q);
while (*s) /* line 39: unconditional dereference */
...
}
```

The violated invariant is: any caller of `strcpy_quoted` must guarantee the
string pointer `s` is non-NULL before passing it.
The Entry61 fix in `refint.c` showed the identical invariant being violated
by `quote_literal_cstr(nv)` where `nv` could be NULL.
Here the same class of bug appears: a nullable value returned by
`SPI_getvalue` is passed to a string-consuming function that dereferences
the pointer unconditionally.
The column `colno` comes from `index->indkey.values[i]` for the primary key
index.
Under normal DDL, primary key columns are NOT NULL, which prevents
`SPI_getvalue` from returning NULL.
However, the code contains no guard against NULL and will crash if
`SPI_getvalue` returns NULL for any reason (for example when a superuser
bypasses the NOT NULL constraint by directly updating
`pg_index.indisprimary` on an index whose key columns lack a NOT NULL
constraint, or if a future code path makes the invariant reachable through
other means).
The correct fix mirrors Entry61: replace the bare `SPI_getvalue(...)`
argument with a ternary expression that substitutes the unquoted SQL keyword
`NULL` when the value is NULL, exactly as `dblink.c` already does at lines
2197-2203.

### To Reproduce

Start a fresh PostgreSQL ASAN instance and run:

```sql
CREATE EXTENSION tcn;

CREATE TABLE tcn_test (id int, data text);

CREATE UNIQUE INDEX tcn_test_uniq ON tcn_test (id);

SET allow_system_table_mods = on;

UPDATE pg_index SET indisprimary = true
WHERE indexrelid = 'tcn_test_uniq'::regclass;

CREATE TRIGGER tcn_trig
AFTER INSERT OR UPDATE OR DELETE ON tcn_test
FOR EACH ROW EXECUTE FUNCTION triggered_change_notification();

INSERT INTO tcn_test VALUES (NULL, 'crash test');
```

Trigger output:

```
CREATE EXTENSION
CREATE TABLE
CREATE INDEX
SET
UPDATE 1
CREATE TRIGGER
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
connection to server was lost

=== pg.log (relevant lines) ===
AddressSanitizer:DEADLYSIGNAL
[postmaster] LOG: client backend (PID 2605793) was terminated by signal 6:
Aborted
[postmaster] DETAIL: Failed process was running: INSERT INTO tcn_test
VALUES (NULL, 'crash test');
[postmaster] LOG: terminating any other active server processes
[postmaster] LOG: all server processes terminated (reinitializing)

=== ASAN report ===
==2605793==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
(pc 0x7f1ed965ed8a bp 0x7fffdfe2c0b0 sp 0x7fffdfe2c090 T0)
==2605793==The signal is caused by a READ memory access.
==2605793==Hint: address points to the zero page.
#0 0x7f1ed965ed8a in strcpy_quoted /tmp/tcn_v2_FUbB/tcn.c:39
#1 0x7f1ed965ff08 in triggered_change_notification
/tmp/tcn_v2_FUbB/tcn.c:165
#2 0x60a8368cbd69 in ExecCallTriggerFunc trigger.c:2407
#3 0x60a8368d5875 in AfterTriggerExecute trigger.c:4620
#4 0x60a8368d65fd in afterTriggerInvokeEvents trigger.c:4861
#5 0x60a8368d7af9 in AfterTriggerEndQuery trigger.c:5247
#6 0x60a83697c0ec in standard_ExecutorFinish execMain.c:454
...
SUMMARY: AddressSanitizer: SEGV /tmp/tcn_v2_FUbB/tcn.c:39 in strcpy_quoted
==2605793==ABORTING
```

### Environment

- OS: Ubuntu 24.04.2 LTS, x86_64 (Linux 6.8.0-55-generic)
- PostgreSQL Version: PostgreSQL 20devel on x86_64-pc-linux-gnu, compiled by
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
- Client: psql, ASAN instrumented build (built from source with
-fsanitize=address)
- Hardware: N/A

### Identity Disclosure

- Full Name: Yuelin
- Affiliation: Individual

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-10-15 15:05:32 BUG #19764: ManualBug3 Assert failure in pgstat_setup_shared_refs crashes initdb post-bootstrap subprocess
Previous Message Sebastian Webber 2026-10-15 14:19:48 Re: BUG #19759: Hashed SetOp / RecursiveUnion pre-allocate up to 64 GB from the row estimate since 8f29467c57f