BUG #19763: ManualBug2 NULL pointer dereference in make_build_data via examine_expression bypass of lookup_var_a
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254217psql -h localhost -U postgresBuilt from patchset v8 (message #8), October 22, 2026 at 03:33 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t254217_8 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t254217_8 && git checkout t254217_8Patchset v8 (message #8) is on t254217_8
The following bug has been logged on the website:
Bug reference: 19763
Logged by: Yuelin Wang
Email address: 1217816127@qq.com
PostgreSQL version: 19rc1
Operating system: Linux (Ubuntu 24.04, x86_64)
Description:
### What happens?
`make_build_data()` in `src/backend/statistics/extended_stats.c` (line 2586)
calls `examine_expression(expr, stattarget)` for each expression in a
statistics object without checking for a NULL return value.
The function `examine_expression()` can return NULL when a typanalyze
callback returns false for the given `attstattarget` value.
The invariant violated is: every element of `data->stats[]` returned by
`make_build_data()` must be a valid non-NULL `VacAttrStats` pointer before
any stat builder iterates over the array.
The upstream guard in `BuildRelationExtStatistics()` calls
`lookup_var_attr_stats()` first (line 169) and returns early when it yields
NULL.
However, `lookup_var_attr_stats()` calls `examine_attribute(expr)` (line
776) which sets `attstattarget = -1`, whereas `make_build_data()` calls
`examine_expression(expr, stattarget)` (line 2586) which sets `attstattarget
= stattarget` (a positive integer).
A typanalyze callback that behaves differently depending on `attstattarget`
can therefore pass `examine_attribute` (returning true) but fail
`examine_expression` (returning false, causing NULL).
The NULL then propagates through `make_build_data()` into stat builders such
as `statext_ndistinct_build()`, `statext_mcv_build()`, and
`statext_dependencies_build()`, all of which dereference `data->stats[i]`
without a NULL check.
The crash path confirmed by ASAN is:
```
make_build_data extended_stats.c:2586 examine_expression returns NULL for
stattarget-sensitive typanalyze
data->stats[idx] = NULL
BuildRelationExtStatistics extended_stats.c:206 calls
statext_ndistinct_build(data)
statext_ndistinct_build mvdistinct.c:125 iterates over data->stats[]
ndistinct_for_combination mvdistinct.c:459 VacAttrStats *colstat =
data->stats[combination[i]] == NULL
typid = colstat->attrtypid <- NULL dereference (SEGFAULT)
```
### To Reproduce
First build and install the `test_custom_types` extension with an additional
`int_custom_typanalyze_stattarget_sensitive` function added to
`src/test/modules/test_custom_types/test_custom_types.c`.
The function returns true (with valid `compute_stats` and `minrows`) when
`stats->attstattarget < 0` (the value set by `examine_attribute`), and
returns false when `stats->attstattarget >= 0` (the value set by
`examine_expression` with the actual statistics target).
Start a fresh PostgreSQL ASAN instance and run:
```sql
SELECT version();
CREATE EXTENSION test_custom_types;
CREATE TABLE t1 (id int, data int_custom);
INSERT INTO t1 SELECT i, (i % 100)::text::int_custom FROM generate_series(1,
1000) i;
CREATE OR REPLACE FUNCTION func_ic(p int_custom) RETURNS int_custom LANGUAGE
plpgsql AS $$ BEGIN RETURN p; END; $$;
ALTER TYPE int_custom SET (ANALYZE =
int_custom_typanalyze_stattarget_sensitive);
CREATE STATISTICS st1 ON func_ic(data), (id) FROM t1;
ANALYZE t1;
SELECT 'survived' AS result;
```
Trigger output:
```
version
--------------------------------------------------------------------
PostgreSQL 20devel on x86_64-linux, compiled by gcc-13.3.0, 64-bit
(1 row)
CREATE EXTENSION
CREATE TABLE
INSERT 0 1000
CREATE FUNCTION
ALTER TYPE
CREATE STATISTICS
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:
```
2026-10-15 08:38:12.423 +08 [2602104] LOG: starting PostgreSQL 20devel on
x86_64-linux, compiled by gcc-13.3.0, 64-bit
2026-10-15 08:38:12.423 +08 [2602104] LOG: listening on IPv4 address
"127.0.0.1", port 14064
2026-10-15 08:38:12.423 +08 [2602104] LOG: listening on Unix socket
"/tmp/.s.PGSQL.14064"
2026-10-15 08:38:12.432 +08 [2602109] LOG: database system was shut down at
2026-10-15 08:38:11 +08
2026-10-15 08:38:12.440 +08 [2602104] LOG: database system is ready to
accept connections
AddressSanitizer:DEADLYSIGNAL
2026-10-15 08:38:14.788 +08 [2602104] LOG: client backend (PID 2602233) was
terminated by signal 6: Aborted
2026-10-15 08:38:14.788 +08 [2602104] DETAIL: Failed process was running:
ANALYZE t1;
2026-10-15 08:38:14.788 +08 [2602104] LOG: terminating any other active
server processes
2026-10-15 08:38:14.793 +08 [2602104] LOG: all server processes terminated;
reinitializing
```
ASAN report:
```
==2602233==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000004
(pc 0x594bbf1a8535 bp 0x7ffcec007d70 sp 0x7ffcec007ca0 T0)
==2602233==The signal is caused by a READ memory access.
==2602233==Hint: address points to the zero page.
#0 0x594bbf1a8535 in ndistinct_for_combination
../src/backend/statistics/mvdistinct.c:459
#1 0x594bbf1a8535 in statext_ndistinct_build
../src/backend/statistics/mvdistinct.c:125
#2 0x594bbf192013 in BuildRelationExtStatistics
../src/backend/statistics/extended_stats.c:206
#3 0x594bbeb807f7 in do_analyze_rel
../src/backend/commands/analyze.c:645
#4 0x594bbeb83d20 in analyze_rel ../src/backend/commands/analyze.c:299
#5 0x594bbecef696 in vacuum ../src/backend/commands/vacuum.c:653
#6 0x594bbecf0e6c in ExecVacuum ../src/backend/commands/vacuum.c:467
#7 0x594bbf286a15 in standard_ProcessUtility
../src/backend/tcop/utility.c:860
#8 0x594bbf282d46 in PortalRunUtility ../src/backend/tcop/pquery.c:1153
#9 0x594bbf282fa4 in PortalRunMulti ../src/backend/tcop/pquery.c:1311
#10 0x594bbf283c64 in PortalRun ../src/backend/tcop/pquery.c:788
#11 0x594bbf27a670 in exec_simple_query
../src/backend/tcop/postgres.c:1297
#12 0x594bbf27e970 in PostgresMain ../src/backend/tcop/postgres.c:4946
#13 0x594bbf27134c in BackendMain
../src/backend/tcop/backend_startup.c:124
#14 0x594bbf092b51 in postmaster_child_launch
../src/backend/postmaster/launch_backend.c:268
#15 0x594bbf099c76 in BackendStartup
../src/backend/postmaster/postmaster.c:3650
#16 0x594bbf099c76 in ServerLoop
../src/backend/postmaster/postmaster.c:1727
#17 0x594bbf09cb29 in PostmasterMain
../src/backend/postmaster/postmaster.c:1414
#18 0x594bbe88e20d in main ../src/backend/main/main.c:227
SUMMARY: AddressSanitizer: SEGV ../src/backend/statistics/mvdistinct.c:459
in ndistinct_for_combination
==2602233==ABORTING
```
### Environment
- OS: Ubuntu 24.04.2 LTS, x86_64 (Linux 6.8.0-55-generic)
- PostgreSQL Version: PostgreSQL 20devel on x86_64-linux, compiled by
gcc-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
On 10/15/26 17:05, PG Bug reporting form wrote:
The following bug has been logged on the website:
Bug reference: 19763
Logged by: Yuelin Wang
Email address: 1217816127@qq.com
PostgreSQL version: 19rc1
Operating system: Linux (Ubuntu 24.04, x86_64)
Description:### What happens?
`make_build_data()` in `src/backend/statistics/extended_stats.c` (line 2586)
calls `examine_expression(expr, stattarget)` for each expression in a
statistics object without checking for a NULL return value.
The function `examine_expression()` can return NULL when a typanalyze
callback returns false for the given `attstattarget` value.
The invariant violated is: every element of `data->stats[]` returned by
`make_build_data()` must be a valid non-NULL `VacAttrStats` pointer before
any stat builder iterates over the array.
The upstream guard in `BuildRelationExtStatistics()` calls
`lookup_var_attr_stats()` first (line 169) and returns early when it yields
NULL.
However, `lookup_var_attr_stats()` calls `examine_attribute(expr)` (line
776) which sets `attstattarget = -1`, whereas `make_build_data()` calls
`examine_expression(expr, stattarget)` (line 2586) which sets `attstattarget
= stattarget` (a positive integer).
A typanalyze callback that behaves differently depending on `attstattarget`
can therefore pass `examine_attribute` (returning true) but fail
`examine_expression` (returning false, causing NULL).
The NULL then propagates through `make_build_data()` into stat builders such
as `statext_ndistinct_build()`, `statext_mcv_build()`, and
`statext_dependencies_build()`, all of which dereference `data->stats[i]`
without a NULL check.The crash path confirmed by ASAN is:
```
make_build_data extended_stats.c:2586 examine_expression returns NULL for
stattarget-sensitive typanalyze
data->stats[idx] = NULL
BuildRelationExtStatistics extended_stats.c:206 calls
statext_ndistinct_build(data)
statext_ndistinct_build mvdistinct.c:125 iterates over data->stats[]
ndistinct_for_combination mvdistinct.c:459 VacAttrStats *colstat =
data->stats[combination[i]] == NULL
typid = colstat->attrtypid <- NULL dereference (SEGFAULT)
```### To Reproduce
First build and install the `test_custom_types` extension with an additional
`int_custom_typanalyze_stattarget_sensitive` function added to
`src/test/modules/test_custom_types/test_custom_types.c`.
The function returns true (with valid `compute_stats` and `minrows`) when
`stats->attstattarget < 0` (the value set by `examine_attribute`), and
returns false when `stats->attstattarget >= 0` (the value set by
`examine_expression` with the actual statistics target).Start a fresh PostgreSQL ASAN instance and run:
```sql
SELECT version();
CREATE EXTENSION test_custom_types;
CREATE TABLE t1 (id int, data int_custom);
INSERT INTO t1 SELECT i, (i % 100)::text::int_custom FROM generate_series(1,
1000) i;
CREATE OR REPLACE FUNCTION func_ic(p int_custom) RETURNS int_custom LANGUAGE
plpgsql AS $$ BEGIN RETURN p; END; $$;
ALTER TYPE int_custom SET (ANALYZE =
int_custom_typanalyze_stattarget_sensitive);
CREATE STATISTICS st1 ON func_ic(data), (id) FROM t1;
ANALYZE t1;
SELECT 'survived' AS result;
```Trigger output:
```
version
--------------------------------------------------------------------
PostgreSQL 20devel on x86_64-linux, compiled by gcc-13.3.0, 64-bit
(1 row)CREATE EXTENSION
CREATE TABLE
INSERT 0 1000
CREATE FUNCTION
ALTER TYPE
CREATE STATISTICS
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:
```
2026-10-15 08:38:12.423 +08 [2602104] LOG: starting PostgreSQL 20devel on
x86_64-linux, compiled by gcc-13.3.0, 64-bit
2026-10-15 08:38:12.423 +08 [2602104] LOG: listening on IPv4 address
"127.0.0.1", port 14064
2026-10-15 08:38:12.423 +08 [2602104] LOG: listening on Unix socket
"/tmp/.s.PGSQL.14064"
2026-10-15 08:38:12.432 +08 [2602109] LOG: database system was shut down at
2026-10-15 08:38:11 +08
2026-10-15 08:38:12.440 +08 [2602104] LOG: database system is ready to
accept connections
AddressSanitizer:DEADLYSIGNAL
2026-10-15 08:38:14.788 +08 [2602104] LOG: client backend (PID 2602233) was
terminated by signal 6: Aborted
2026-10-15 08:38:14.788 +08 [2602104] DETAIL: Failed process was running:
ANALYZE t1;
2026-10-15 08:38:14.788 +08 [2602104] LOG: terminating any other active
server processes
2026-10-15 08:38:14.793 +08 [2602104] LOG: all server processes terminated;
reinitializing
```ASAN report:
```
==2602233==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000004
(pc 0x594bbf1a8535 bp 0x7ffcec007d70 sp 0x7ffcec007ca0 T0)
==2602233==The signal is caused by a READ memory access.
==2602233==Hint: address points to the zero page.
#0 0x594bbf1a8535 in ndistinct_for_combination
../src/backend/statistics/mvdistinct.c:459
#1 0x594bbf1a8535 in statext_ndistinct_build
../src/backend/statistics/mvdistinct.c:125
#2 0x594bbf192013 in BuildRelationExtStatistics
../src/backend/statistics/extended_stats.c:206
#3 0x594bbeb807f7 in do_analyze_rel
../src/backend/commands/analyze.c:645
#4 0x594bbeb83d20 in analyze_rel ../src/backend/commands/analyze.c:299
#5 0x594bbecef696 in vacuum ../src/backend/commands/vacuum.c:653
#6 0x594bbecf0e6c in ExecVacuum ../src/backend/commands/vacuum.c:467
#7 0x594bbf286a15 in standard_ProcessUtility
../src/backend/tcop/utility.c:860
#8 0x594bbf282d46 in PortalRunUtility ../src/backend/tcop/pquery.c:1153
#9 0x594bbf282fa4 in PortalRunMulti ../src/backend/tcop/pquery.c:1311
#10 0x594bbf283c64 in PortalRun ../src/backend/tcop/pquery.c:788
#11 0x594bbf27a670 in exec_simple_query
../src/backend/tcop/postgres.c:1297
#12 0x594bbf27e970 in PostgresMain ../src/backend/tcop/postgres.c:4946
#13 0x594bbf27134c in BackendMain
../src/backend/tcop/backend_startup.c:124
#14 0x594bbf092b51 in postmaster_child_launch
../src/backend/postmaster/launch_backend.c:268
#15 0x594bbf099c76 in BackendStartup
../src/backend/postmaster/postmaster.c:3650
#16 0x594bbf099c76 in ServerLoop
../src/backend/postmaster/postmaster.c:1727
#17 0x594bbf09cb29 in PostmasterMain
../src/backend/postmaster/postmaster.c:1414
#18 0x594bbe88e20d in main ../src/backend/main/main.c:227
SUMMARY: AddressSanitizer: SEGV ../src/backend/statistics/mvdistinct.c:459
in ndistinct_for_combination
==2602233==ABORTING
```
I tried reproducing it using the provided information, without much
success. Attached is the test_custom_types patch with the typanalyze
function I used, but it always ends for me with
WARNING: statistics object "public.st1" could not be computed for
relation "public.t1"
Please, provide a working reproducer, or clarify the instructions
regards
--
Tomas Vondra
On 10/15/26 20:52, Tomas Vondra wrote:
...
I tried reproducing it using the provided information, without much
success. Attached is the test_custom_types patch with the typanalyze
function I used, but it always ends for me withWARNING: statistics object "public.st1" could not be computed for
relation "public.t1"Please, provide a working reproducer, or clarify the instructions
After a bit more poking, I managed to make it crash. Attached are the
two pieces - patch for test_custom_types, and the SQL reproducer.
I haven't thought about how to fix it yet - it seems very unlikely a
typanalyze function would behave like this, but we should be more careful.
regards
--
Tomas Vondra
Hi,
I haven't thought about how to fix it yet - it seems very unlikely a
typanalyze function would behave like this, but we should be more
careful.
Here is a patch. It does for examine_expression() what 3b7a6fa1572
did for examine_attribute().
The test adds your typanalyze function to test_custom_types. It runs
your repro and also a single expression object, which builds only
expression statistics.
Thanks,
Shihao
Attachments:
v1-0001-Handle-failed-typanalyze-PG14-16.patch.txttext/plain; charset=US-ASCII; name=v1-0001-Handle-failed-typanalyze-PG14-16.patch.txtDownload+106-1
v1-0001-Handle-failed-typanalyze-when-building-extended-s.patchapplication/octet-stream; name=v1-0001-Handle-failed-typanalyze-when-building-extended-s.patchDownload+106-1
On Sun, Oct 18, 2026 at 10:43:03PM -0400, shihao zhong wrote:
Here is a patch. It does for examine_expression() what 3b7a6fa1572
did for examine_attribute().The test adds your typanalyze function to test_custom_types. It runs
your repro and also a single expression object, which builds only
expression statistics.
Thanks for the patch. A couple of comments as I have played with this
area in the recent past. I didn't think that this would be reachable
in practice, but well, that's a fancy typanalyze callback that you
have here.
+ /*
+ * The typanalyze function sees a different attstattarget here than in
+ * lookup_var_attr_stats(), so it may reject the expression now.
+ */
+ if (result->stats[idx] == NULL)
+ {
+ pfree(result);
+ return NULL;
+ }
I don't understand why this comment is adapted here; it just makes the
situation more confusing to me. This just points to the fact that
examine_expression() could return NULL depending on what the
typanalyze callback decides to return. What your patch does it fancy
enough in the callback by not having the function to be called twice.
Nothing like that would happen in practice, still I agree that the
backend should have more protections. I think that we should just
ditch this comment. It's plain enough that examine_expression() can
return NULL.
/* evaluate expressions (if the statistics object has any) */
data = make_build_data(onerel, stat, numrows, rows, stats, stattarget);
+ if (!data)
+ {
+ if (!IsAutoVacuumWorkerProcess())
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("statistics object \"%s.%s\" could not be computed for relation \"%s.%s\"",
+ stat->schema, stat->name,
+ get_namespace_name(onerel->rd_rel->relnamespace),
+ RelationGetRelationName(onerel)),
+ errtable(onerel)));
+ continue;
+ }
This warning is a duplicate, FWIW. Not optimal, just feels slightly
annoying. The patch is OK with this duplication, I guess..
If we begin to care about examine_expression(), I think that we ought
to also care about build_expr_data(), its second caller, no? It looks
like this could also be reached with a fancy undeterministic
typanalyze callback. I have not tested this hypothesis, but it would
be nice to make all the callers examine_expression() consistent.
+-- Same with a single expression, building only expression statistics.
+CREATE STATISTICS test_stats ON (func_int_custom(data)) FROM test_table;
+ANALYZE test_table;
+WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table"
This test is a duplicate of the first one. Let's remove it; it brings
nothing new.
--
Michael
Hi Michael,
I think that we should just ditch this comment.
Done.
This warning is a duplicate, FWIW.
v2 moves it into a small function, so there is only one copy.
If we begin to care about examine_expression(), I think that we
ought to also care about build_expr_data(), its second caller, no?
Yes, it can be reached. With a typanalyze function that only accepts the
first call with a real target, ANALYZE crashes in compute_expr_stats().
v2 checks the result there too and skips the object with the same
warning. I did not add a test for it, since it needs another fancy
callback.
This test is a duplicate of the first one.
Removed.
Thanks,
Shihao
On Tue, Oct 20, 2026 at 08:29:28PM -0400, shihao zhong wrote:
v2 checks the result there too and skips the object with the same
warning. I did not add a test for it, since it needs another fancy
callback.
Fancy typanalyze callbacks are what make the DNA of the test module
test_custom_types. I'd suggest to add a test for it. Your previous
duplicated test had little meaning. This would be a second test for a
separated code path, that acts as its own check.
--
Michael
Hi Michael,
Thanks. v3 adds a second typanalyze callback to test_custom_types,
int_custom_typanalyze_once.
Thanks,
Shihao
On Wed, Oct 21, 2026 at 11:23:28PM -0400, shihao zhong wrote:
Thanks. v3 adds a second typanalyze callback to test_custom_types,
int_custom_typanalyze_once.
The comments on top of each callback feel super bloated, as much as
the comments you have added in the two sequences of tests. These
could be tweaked much more, IMO..
report_ext_stats_not_computed() could be static inline.
We may want to hear from Tomas as well, as he has already commented on
this thread. There is a big conference in Europe this week, and my
guess is that he's now busy with the event and that it's going to take
some time before we get a reply.
@Tomas: Could you look at the patch once you have the occasion,
please?
--
Michael