| 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
| 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 |