PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

Started by Vladimir Savin22 days ago13 messagesbugs
Beta feature

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.

won't retrysuccessCI history

This thread has been committed, so CI has stopped here. Anything below is the last result it produced.

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:t254019
psql -h localhost -U postgres

Built from patchset v10 (message #10), October 14, 2026 at 03:52 PM.

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 t254019_10 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t254019_10 && git checkout t254019_10

Patchset v10 (message #10) is on t254019_10

Jump to latest
#1Vladimir Savin
vladimir@encord.com

Hi,

A backend process is terminated by SIGSEGV in run-time partition pruning
when a statement contains a FOR UPDATE CTE that has to recheck a
concurrently
updated row (EvalPlanQual) and a separate CTE that updates a partitioned
table
through an Append with run-time pruning. It reproduces on REL_18_6 and on
master.

== Steps to reproduce

Attached epq-prune-repro.sql, run against a freshly initdb'd cluster:

psql -X -d postgres -f epq-prune-repro.sql

It needs contrib/dblink, which it uses to open the second session.

The enable_* settings only force the nested loop plan on these small tables;
we first hit this with a plan the planner picked on its own (Update ->
Nested
Loop -> CTE Scan / Append with run-time pruning).

== Output I got (REL_18_6, built with --enable-cassert; master gives the
same)

psql:

COMMIT
psql:epq-prune-repro.sql:44: ERROR: 08006: server closed the connection
unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
invalid socket
CONTEXT: while executing query on dblink connection named "s2"
LOCATION: dblink_res_error, dblink.c:2863

Server log (log_error_verbosity = verbose):

2026-10-01 14:07:00.975 UTC [13783] LOG: 00000: client backend (PID
13824) was terminated by signal 11: Segmentation fault
2026-10-01 14:07:00.975 UTC [13783] DETAIL: Failed process was running:
WITH inputs AS MATERIALIZED (
... (the session 2 query above)

Backtrace of the backend core (REL_18_6):

#0 ExecEvalExprSwitchContext (state=0x7f7f7f7f7f7f7f7f,
econtext=0xaaaaf14182f0, isNull=0xffffe735e23f) at
../../../src/include/executor/executor.h:440
#1 partkey_datum_from_expr (context=0xaaaaf1418568, expr=0xaaaaf13fb440,
stateidx=0, value=0xffffe735e240, isnull=0xffffe735e23f) at partprune.c:3823
#2 perform_pruning_base_step (context=0xaaaaf1418568,
opstep=0xaaaaf13fb3f0) at partprune.c:3503
#3 get_matching_partitions (context=0xaaaaf1418568,
pruning_steps=0xaaaaf13fb4c0) at partprune.c:881
#4 find_matching_subplans_recurse (prunedata=0xaaaaf14184d0,
pprune=0xaaaaf14184d8, initial_prune=false, validsubplans=0xffffe735e488,
validsubplan_rtis=0x0)
#5 ExecFindMatchingSubPlans (prunestate=0xaaaaf1418480,
initial_prune=false, validsubplan_rtis=0x0) at execPartition.c:2535
#6 choose_next_subplan_locally (node=0xaaaaf143fe58) at nodeAppend.c:584
#7 ExecAppend (pstate=0xaaaaf143fe58) at nodeAppend.c:330
#8 ExecProcNode (node=0xaaaaf143fe58) at
../../../src/include/executor/executor.h:315
#9 ExecNestLoop (pstate=0xaaaaf142d410) at nodeNestloop.c:159
#10 ExecProcNode (node=0xaaaaf142d410) at
../../../src/include/executor/executor.h:315
#11 ExecModifyTable (pstate=0xaaaaf142c260) at nodeModifyTable.c:4280
#12 ExecProcNode (node=0xaaaaf142c260) at
../../../src/include/executor/executor.h:315
...

On master the frames are the same (partprune.c:3841, execPartition.c:2642,
nodeModifyTable.c:4497), also with state=0x7f7f7f7f7f7f7f7f.

== Output I expected

The session 2 statement completes and dblink_get_result returns updated = 8.
That is what the patched build below returns.

== Builds and settings

- REL_18_6, commit 724edf9bde9d356724ad384a2e196edc3c9f80f7:
SELECT version() = PostgreSQL 18.6 on aarch64-unknown-linux-gnu, compiled
by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- master, commit 42e96cf2fe095c822f76bc94669ea875cb1e3351:
SELECT version() = PostgreSQL 20devel on aarch64-unknown-linux-gnu,
compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- configure: '--prefix=/pg' '--enable-cassert' '--enable-debug'
'--without-icu' 'CFLAGS=-O0 -g'
plus `make -C contrib/dblink install`. No other deviations from the
installation instructions.
- Cluster: `initdb -A trust`, started with pg_ctl. The only postgresql.conf
change from the
initdb defaults is log_error_verbosity = verbose. No environment
variables set.

Platform: Linux 7.0.12-linuxkit #1 SMP PREEMPT aarch64 (Docker Desktop on
macOS,
Apple silicon), Debian GNU/Linux 13 (trixie), glibc 2.41-12+deb13u4,
gcc 14.2.0-19, 14 CPUs, 23 GiB RAM.

Release builds: with these exact steps the stock postgres:18.6 Docker image
(Debian 18.6-1.pgdg13+2, aarch64) returns 8 and does not crash (3 of 3
runs).

Release builds do crash on the same query shape under concurrent load from
the
Hatchet workflow engine, whose UpdateDurableEventLogEntriesSatisfied query
has
this structure:
- postgres:18.6 (18.6-1.pgdg13+2) under a synthetic load test: a segfault
every few minutes. A core taken with the dbgsym package installed has the
same frames from ExecModifyTable (called from CteScanNext) up to
ExecEvalExprSwitchContext, with frame #0 at 0x000004a00000000c.
- Google Cloud SQL for PostgreSQL 18, production: "terminated by signal 11"
in that query roughly once a day.
- postgres:17.11 under the same synthetic load: no crashes in 9 runs.

Thanks,
Vladimir Savin

--
*This email and any files transmitted with it are confidential and intended
solely for the use of the individual or entity to whom they are addressed.
If you have received this email in error, please notify the sender
immediately by replying to this email and delete it from your system.
Please do not copy, distribute, or take action based on the contents of
this email if you are not the intended recipient. Any unauthorized use or
disclosure of this email's contents is strictly prohibited.*

Attachments:

epq-prune-repro.sqlapplication/octet-stream; name=epq-prune-repro.sqlDownload
#2Andrey Rachitskiy
pl0h0yp1@gmail.com
In reply to: Vladimir Savin (#1)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

чт, 1 окт. 2026 г. в 19:28, Vladimir Savin <vladimir@encord.com>:

Hi,

A backend process is terminated by SIGSEGV in run-time partition pruning
when a statement contains a FOR UPDATE CTE that has to recheck a
concurrently
updated row (EvalPlanQual) and a separate CTE that updates a partitioned
table
through an Append with run-time pruning. It reproduces on REL_18_6 and on
master.

== Steps to reproduce

Attached epq-prune-repro.sql, run against a freshly initdb'd cluster:

psql -X -d postgres -f epq-prune-repro.sql

It needs contrib/dblink, which it uses to open the second session.

The enable_* settings only force the nested loop plan on these small
tables;
we first hit this with a plan the planner picked on its own (Update ->
Nested
Loop -> CTE Scan / Append with run-time pruning).

== Output I got (REL_18_6, built with --enable-cassert; master gives the
same)

psql:

COMMIT
psql:epq-prune-repro.sql:44: ERROR: 08006: server closed the connection
unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
invalid socket
CONTEXT: while executing query on dblink connection named "s2"
LOCATION: dblink_res_error, dblink.c:2863

Server log (log_error_verbosity = verbose):

2026-10-01 14:07:00.975 UTC [13783] LOG: 00000: client backend (PID
13824) was terminated by signal 11: Segmentation fault
2026-10-01 14:07:00.975 UTC [13783] DETAIL: Failed process was running:
WITH inputs AS MATERIALIZED (
... (the session 2 query above)

Backtrace of the backend core (REL_18_6):

#0 ExecEvalExprSwitchContext (state=0x7f7f7f7f7f7f7f7f,
econtext=0xaaaaf14182f0, isNull=0xffffe735e23f) at
../../../src/include/executor/executor.h:440
#1 partkey_datum_from_expr (context=0xaaaaf1418568,
expr=0xaaaaf13fb440, stateidx=0, value=0xffffe735e240,
isnull=0xffffe735e23f) at partprune.c:3823
#2 perform_pruning_base_step (context=0xaaaaf1418568,
opstep=0xaaaaf13fb3f0) at partprune.c:3503
#3 get_matching_partitions (context=0xaaaaf1418568,
pruning_steps=0xaaaaf13fb4c0) at partprune.c:881
#4 find_matching_subplans_recurse (prunedata=0xaaaaf14184d0,
pprune=0xaaaaf14184d8, initial_prune=false, validsubplans=0xffffe735e488,
validsubplan_rtis=0x0)
#5 ExecFindMatchingSubPlans (prunestate=0xaaaaf1418480,
initial_prune=false, validsubplan_rtis=0x0) at execPartition.c:2535
#6 choose_next_subplan_locally (node=0xaaaaf143fe58) at nodeAppend.c:584
#7 ExecAppend (pstate=0xaaaaf143fe58) at nodeAppend.c:330
#8 ExecProcNode (node=0xaaaaf143fe58) at
../../../src/include/executor/executor.h:315
#9 ExecNestLoop (pstate=0xaaaaf142d410) at nodeNestloop.c:159
#10 ExecProcNode (node=0xaaaaf142d410) at
../../../src/include/executor/executor.h:315
#11 ExecModifyTable (pstate=0xaaaaf142c260) at nodeModifyTable.c:4280
#12 ExecProcNode (node=0xaaaaf142c260) at
../../../src/include/executor/executor.h:315
...

On master the frames are the same (partprune.c:3841, execPartition.c:2642,
nodeModifyTable.c:4497), also with state=0x7f7f7f7f7f7f7f7f.

== Output I expected

The session 2 statement completes and dblink_get_result returns updated =
8.
That is what the patched build below returns.

== Builds and settings

- REL_18_6, commit 724edf9bde9d356724ad384a2e196edc3c9f80f7:
SELECT version() = PostgreSQL 18.6 on aarch64-unknown-linux-gnu,
compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- master, commit 42e96cf2fe095c822f76bc94669ea875cb1e3351:
SELECT version() = PostgreSQL 20devel on aarch64-unknown-linux-gnu,
compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- configure: '--prefix=/pg' '--enable-cassert' '--enable-debug'
'--without-icu' 'CFLAGS=-O0 -g'
plus `make -C contrib/dblink install`. No other deviations from the
installation instructions.
- Cluster: `initdb -A trust`, started with pg_ctl. The only
postgresql.conf change from the
initdb defaults is log_error_verbosity = verbose. No environment
variables set.

Platform: Linux 7.0.12-linuxkit #1 SMP PREEMPT aarch64 (Docker Desktop on
macOS,
Apple silicon), Debian GNU/Linux 13 (trixie), glibc 2.41-12+deb13u4,
gcc 14.2.0-19, 14 CPUs, 23 GiB RAM.

Release builds: with these exact steps the stock postgres:18.6 Docker image
(Debian 18.6-1.pgdg13+2, aarch64) returns 8 and does not crash (3 of 3
runs).

Release builds do crash on the same query shape under concurrent load from
the
Hatchet workflow engine, whose UpdateDurableEventLogEntriesSatisfied query
has
this structure:
- postgres:18.6 (18.6-1.pgdg13+2) under a synthetic load test: a segfault
every few minutes. A core taken with the dbgsym package installed has the
same frames from ExecModifyTable (called from CteScanNext) up to
ExecEvalExprSwitchContext, with frame #0 at 0x000004a00000000c.
- Google Cloud SQL for PostgreSQL 18, production: "terminated by signal 11"
in that query roughly once a day.
- postgres:17.11 under the same synthetic load: no crashes in 9 runs.

Thanks,
Vladimir Savin

*This email and any files transmitted with it are confidential and
intended solely for the use of the individual or entity to whom they are
addressed. If you have received this email in error, please notify the
sender immediately by replying to this email and delete it from your
system. Please do not copy, distribute, or take action based on the
contents of this email if you are not the intended recipient. Any
unauthorized use or disclosure of this email's contents is strictly
prohibited.*

Hi, Vladimir!

Thanks for the report.

I can reproduce it on the master branch.

EvalPlanQualStart() shares the parent's PartitionPruneState so that EPQ
initializes the same Append and MergeAppend subplans (8741e48e5dd).
ExecInit of those nodes called InitExecPartitionPruneContexts() again.
That replaced exec prune ExprStates with ones allocated in the EPQ
memory context. EvalPlanQualEnd() frees that context.

A later CTE that updates a partitioned table through Nested Loop plus
Append with run-time pruning then used the dangling ExprStates and
could SIGSEGV in partkey_datum_from_expr().

Skip InitExecPartitionPruneContexts() when es_epq_active is set. A
case is added to eval-plan-qual.

--
Regards,
Rachitskiy Andrey

Attachments:

t254019_2
0001-Don-t-rebuild-exec-partition-prune-state-during-Eval.patchtext/x-patch; charset=US-ASCII; name=0001-Don-t-rebuild-exec-partition-prune-state-during-Eval.patchDownload+64-2
#3Vladimir Savin
vladimir@encord.com
In reply to: Andrey Rachitskiy (#2)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

Hi Andrey,

Thank you for looking into this!

Is there anything else I can do to help, or are we all set?

Regards,
Vladimir

On Thu, 1 Oct 2026 at 17:15, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

This is the first time you're receiving an email from this person. Make
sure you check the email address to confirm their identity before
interacting with the email.

чт, 1 окт. 2026 г. в 19:28, Vladimir Savin <vladimir@encord.com>:

Hi,

A backend process is terminated by SIGSEGV in run-time partition pruning
when a statement contains a FOR UPDATE CTE that has to recheck a
concurrently
updated row (EvalPlanQual) and a separate CTE that updates a partitioned
table
through an Append with run-time pruning. It reproduces on REL_18_6 and on
master.

== Steps to reproduce

Attached epq-prune-repro.sql, run against a freshly initdb'd cluster:

psql -X -d postgres -f epq-prune-repro.sql

It needs contrib/dblink, which it uses to open the second session.

The enable_* settings only force the nested loop plan on these small
tables;
we first hit this with a plan the planner picked on its own (Update ->
Nested
Loop -> CTE Scan / Append with run-time pruning).

== Output I got (REL_18_6, built with --enable-cassert; master gives the
same)

psql:

COMMIT
psql:epq-prune-repro.sql:44: ERROR: 08006: server closed the
connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
invalid socket
CONTEXT: while executing query on dblink connection named "s2"
LOCATION: dblink_res_error, dblink.c:2863

Server log (log_error_verbosity = verbose):

2026-10-01 14:07:00.975 UTC [13783] LOG: 00000: client backend (PID
13824) was terminated by signal 11: Segmentation fault
2026-10-01 14:07:00.975 UTC [13783] DETAIL: Failed process was running:
WITH inputs AS MATERIALIZED (
... (the session 2 query above)

Backtrace of the backend core (REL_18_6):

#0 ExecEvalExprSwitchContext (state=0x7f7f7f7f7f7f7f7f,
econtext=0xaaaaf14182f0, isNull=0xffffe735e23f) at
../../../src/include/executor/executor.h:440
#1 partkey_datum_from_expr (context=0xaaaaf1418568,
expr=0xaaaaf13fb440, stateidx=0, value=0xffffe735e240,
isnull=0xffffe735e23f) at partprune.c:3823
#2 perform_pruning_base_step (context=0xaaaaf1418568,
opstep=0xaaaaf13fb3f0) at partprune.c:3503
#3 get_matching_partitions (context=0xaaaaf1418568,
pruning_steps=0xaaaaf13fb4c0) at partprune.c:881
#4 find_matching_subplans_recurse (prunedata=0xaaaaf14184d0,
pprune=0xaaaaf14184d8, initial_prune=false, validsubplans=0xffffe735e488,
validsubplan_rtis=0x0)
#5 ExecFindMatchingSubPlans (prunestate=0xaaaaf1418480,
initial_prune=false, validsubplan_rtis=0x0) at execPartition.c:2535
#6 choose_next_subplan_locally (node=0xaaaaf143fe58) at
nodeAppend.c:584
#7 ExecAppend (pstate=0xaaaaf143fe58) at nodeAppend.c:330
#8 ExecProcNode (node=0xaaaaf143fe58) at
../../../src/include/executor/executor.h:315
#9 ExecNestLoop (pstate=0xaaaaf142d410) at nodeNestloop.c:159
#10 ExecProcNode (node=0xaaaaf142d410) at
../../../src/include/executor/executor.h:315
#11 ExecModifyTable (pstate=0xaaaaf142c260) at nodeModifyTable.c:4280
#12 ExecProcNode (node=0xaaaaf142c260) at
../../../src/include/executor/executor.h:315
...

On master the frames are the same (partprune.c:3841, execPartition.c:2642,
nodeModifyTable.c:4497), also with state=0x7f7f7f7f7f7f7f7f.

== Output I expected

The session 2 statement completes and dblink_get_result returns updated =
8.
That is what the patched build below returns.

== Builds and settings

- REL_18_6, commit 724edf9bde9d356724ad384a2e196edc3c9f80f7:
SELECT version() = PostgreSQL 18.6 on aarch64-unknown-linux-gnu,
compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- master, commit 42e96cf2fe095c822f76bc94669ea875cb1e3351:
SELECT version() = PostgreSQL 20devel on aarch64-unknown-linux-gnu,
compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- configure: '--prefix=/pg' '--enable-cassert' '--enable-debug'
'--without-icu' 'CFLAGS=-O0 -g'
plus `make -C contrib/dblink install`. No other deviations from the
installation instructions.
- Cluster: `initdb -A trust`, started with pg_ctl. The only
postgresql.conf change from the
initdb defaults is log_error_verbosity = verbose. No environment
variables set.

Platform: Linux 7.0.12-linuxkit #1 SMP PREEMPT aarch64 (Docker Desktop on
macOS,
Apple silicon), Debian GNU/Linux 13 (trixie), glibc 2.41-12+deb13u4,
gcc 14.2.0-19, 14 CPUs, 23 GiB RAM.

Release builds: with these exact steps the stock postgres:18.6 Docker
image
(Debian 18.6-1.pgdg13+2, aarch64) returns 8 and does not crash (3 of 3
runs).

Release builds do crash on the same query shape under concurrent load
from the
Hatchet workflow engine, whose UpdateDurableEventLogEntriesSatisfied
query has
this structure:
- postgres:18.6 (18.6-1.pgdg13+2) under a synthetic load test: a segfault
every few minutes. A core taken with the dbgsym package installed has
the
same frames from ExecModifyTable (called from CteScanNext) up to
ExecEvalExprSwitchContext, with frame #0 at 0x000004a00000000c.
- Google Cloud SQL for PostgreSQL 18, production: "terminated by signal
11"
in that query roughly once a day.
- postgres:17.11 under the same synthetic load: no crashes in 9 runs.

Thanks,
Vladimir Savin

*This email and any files transmitted with it are confidential and
intended solely for the use of the individual or entity to whom they are
addressed. If you have received this email in error, please notify the
sender immediately by replying to this email and delete it from your
system. Please do not copy, distribute, or take action based on the
contents of this email if you are not the intended recipient. Any
unauthorized use or disclosure of this email's contents is strictly
prohibited.*

Hi, Vladimir!

Thanks for the report.

I can reproduce it on the master branch.

EvalPlanQualStart() shares the parent's PartitionPruneState so that EPQ
initializes the same Append and MergeAppend subplans (8741e48e5dd).
ExecInit of those nodes called InitExecPartitionPruneContexts() again.
That replaced exec prune ExprStates with ones allocated in the EPQ
memory context. EvalPlanQualEnd() frees that context.

A later CTE that updates a partitioned table through Nested Loop plus
Append with run-time pruning then used the dangling ExprStates and
could SIGSEGV in partkey_datum_from_expr().

Skip InitExecPartitionPruneContexts() when es_epq_active is set. A
case is added to eval-plan-qual.

--
Regards,
Rachitskiy Andrey

--
*This email and any files transmitted with it are confidential and intended
solely for the use of the individual or entity to whom they are addressed.
If you have received this email in error, please notify the sender
immediately by replying to this email and delete it from your system.
Please do not copy, distribute, or take action based on the contents of
this email if you are not the intended recipient. Any unauthorized use or
disclosure of this email's contents is strictly prohibited.*

#4Andrey Rachitskiy
pl0h0yp1@gmail.com
In reply to: Vladimir Savin (#3)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

пт, 2 окт. 2026 г. в 00:36, Vladimir Savin <vladimir@encord.com>:

Is there anything else I can do to help, or are we all set?

Hi, Vladimir!

The patch fixes the issue, and now we just need someone to review and
approve it. So at this point we just have to wait for the maintainers to
take a look when they have time.

Also, a small request for future threads: please reply inline
(bottom-posting) — trim quotes and answer below the relevant part.

Thanks!

--
Regards,
Rachitskiy Andrey

#5Vladimir Savin
vladimir@encord.com
In reply to: Andrey Rachitskiy (#4)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Fri, 2 Oct 2026 at 09:04, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

Also, a small request for future threads: please reply inline
(bottom-posting) — trim quotes and answer below the relevant part.

Got it, thank you Andrey!

--
*This email and any files transmitted with it are confidential and intended
solely for the use of the individual or entity to whom they are addressed.
If you have received this email in error, please notify the sender
immediately by replying to this email and delete it from your system.
Please do not copy, distribute, or take action based on the contents of
this email if you are not the intended recipient. Any unauthorized use or
disclosure of this email's contents is strictly prohibited.*

#6David Rowley
dgrowleyml@gmail.com
In reply to: Andrey Rachitskiy (#2)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Fri, 2 Oct 2026 at 05:16, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

Skip InitExecPartitionPruneContexts() when es_epq_active is set. A
case is added to eval-plan-qual.

uhh, that's a pretty horrible fix. So partition pruning needs to know
what EPQ is now?

I think it would be much better to fix with the attached, which adds
an "initialized" flag to the PartitionPruneState.

Seemingly, this is all fallout from bb3ec16e1. Prior to that, the
EPQ's EState and the main executor one had different
PartitionPruneState as they were created during executor startup by
the Append / MergeAppend nodes. 8741e48e5 fixed one issue with that,
but it turns out there were repercussions to sharing the
PartitionPruneState with the EPQ's EState.

I also created a much smaller isolation test to check this works.

David

Attachments:

t254019_6
v2-0001-Fix-incorrect-init-of-run-time-pruning-for-EPQ-re.patchapplication/octet-stream; name=v2-0001-Fix-incorrect-init-of-run-time-pruning-for-EPQ-re.patchDownload+89-1
#7Andrey Rachitskiy
pl0h0yp1@gmail.com
In reply to: David Rowley (#6)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

чт, 8 окт. 2026 г. в 13:08, David Rowley <dgrowleyml@gmail.com>:

On Fri, 2 Oct 2026 at 05:16, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

Skip InitExecPartitionPruneContexts() when es_epq_active is set. A
case is added to eval-plan-qual.

uhh, that's a pretty horrible fix. So partition pruning needs to know
what EPQ is now?

I think it would be much better to fix with the attached, which adds
an "initialized" flag to the PartitionPruneState.

Hi David!

Thanks for the review.

Agreed. Skipping on es_epq_active was answering the wrong question.
The invariant is that InitExecPartitionPruneContexts() must not run
twice on a shared PartitionPruneState.
Your initialized flag is the right fix.

LGTM

--
Regards,
Rachitskiy Andrey

#8David Rowley
dgrowleyml@gmail.com
In reply to: Andrey Rachitskiy (#7)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Thu, 8 Oct 2026 at 21:30, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

Agreed. Skipping on es_epq_active was answering the wrong question.
The invariant is that InitExecPartitionPruneContexts() must not run
twice on a shared PartitionPruneState.
Your initialized flag is the right fix.

Pushed.

David

#9Andrew Dunstan
andrew@dunslane.net
In reply to: David Rowley (#8)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On 2026-10-08 Th 6:04 PM, David Rowley wrote:

On Thu, 8 Oct 2026 at 21:30, Andrey Rachitskiy <pl0h0yp1@gmail.com> wrote:

Agreed. Skipping on es_epq_active was answering the wrong question.
The invariant is that InitExecPartitionPruneContexts() must not run
twice on a shared PartitionPruneState.
Your initialized flag is the right fix.

Pushed.

per buildfarm, this is an ABI break on release 18, apparently needs an
update to .abi-compliance-history

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#10Zsolt Parragi
zsolt.parragi@percona.com
In reply to: Andrew Dunstan (#9)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

Hello

An automated claude review pointed out a leftover issue after the fix.

per buildfarm, this is an ABI break on release 18, apparently needs an
update to .abi-compliance-history

The attached patch fixing that removes the initialized field again,
which should also take care of this.

EPQ EState has another problem, which the initialized flag doesn't
address: the ExprContext of the PartitionPruneState is created by
CreatePartitionPruneState() for the parent EState, so exec pruning in
the EPQ reads the parent's es_param_exec_vals. If the pruning depends
on a value from the re-fetched row, the EPQ prunes with the value from
the old row version, and can remove the partition it actually needs.

This isn't caused by 1b5dd3a24. Before it the EPQ re-initialized the
exec contexts, but with the same parent ExprContext, so this goes back
to 8741e48e5.

Repro:

CREATE TABLE p (a int, b int) PARTITION BY LIST (a);
CREATE TABLE p1 PARTITION OF p FOR VALUES IN (1);
CREATE TABLE p2 PARTITION OF p FOR VALUES IN (2);
INSERT INTO p VALUES (1, 100), (2, 200);
CREATE TABLE t (id int PRIMARY KEY, k int, v int, done bool DEFAULT false);
INSERT INTO t VALUES (1, 1, 100);

-- s1
BEGIN;
UPDATE t SET k = 2, v = 200 WHERE id = 1;
-- s2, blocks
UPDATE t SET done = true WHERE t.v = (SELECT p.b FROM p WHERE p.a =
t.k) RETURNING *;
-- s1
COMMIT;

s2 returns UPDATE 0. The recheck sees k = 2, the SubPlan sets $0 = 2
in the EPQ EState, but pruning still sees $0 = 1 from the parent, so
only p1 is kept, the scan finds nothing, and the subquery returns
NULL.
With enable_partition_pruning = off the row is updated as expected.

As David said upthread, before bb3ec16e1 the EPQ had its own
PartitionPruneStates, the sharing only came with 8741e48e5, which
fixed the missing states after that commit. The attached patch goes
back to separate states: it gives the EPQ EState its own
PartitionPruneStates, created from the parent's PartitionPruneInfos,
and keeps borrowing es_part_prune_results so the same subplans are
initialized, which was the reason for the sharing. As nothing is
shared anymore, I removed the initialized flag again. This also covers
the use-after-free, nothing in the parent state is touched by the EPQ
anymore, the existing test still passes, and so does Vladimir's
epq-prune-repro.sql.

The patch also adds the case above to the
eval-plan-qual-partition-prune isolation test.

Attachments:

t254019_10
v1-0001-Fix-EPQ-run-time-pruning-using-the-parent-s-PARAM.patchapplication/octet-stream; name=v1-0001-Fix-EPQ-run-time-pruning-using-the-parent-s-PARAM.patchDownload+65-18
#11David Rowley
dgrowleyml@gmail.com
In reply to: Andrew Dunstan (#9)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Sat, 10 Oct 2026 at 03:28, Andrew Dunstan <andrew@dunslane.net> wrote:

per buildfarm, this is an ABI break on release 18, apparently needs an
update to .abi-compliance-history

Thanks for highlighting that. I'll look at this once the RC1 commit
embargo is lifted. Seems Zsolt has uncovered a reason to warrant the
removal of the new flag. So it doesn't seem right to add the exception
if it'll disappear soon.

David

#12David Rowley
dgrowleyml@gmail.com
In reply to: Vladimir Savin (#1)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Thu, 15 Oct 2026 at 13:12, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Could we prioritize getting that straight? Alvaro's beff3cd00 et al
have created an independent ABI break, but I don't think we can
update .abi-compliance-history for that until this earlier issue
is resolved.

It's on my list for today.

David

#13David Rowley
dgrowleyml@gmail.com
In reply to: Zsolt Parragi (#10)
Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows

On Sun, 11 Oct 2026 at 00:51, Zsolt Parragi <zsolt.parragi@percona.com> wrote:

EPQ EState has another problem, which the initialized flag doesn't
address: the ExprContext of the PartitionPruneState is created by
CreatePartitionPruneState() for the parent EState, so exec pruning in
the EPQ reads the parent's es_param_exec_vals. If the pruning depends
on a value from the re-fetched row, the EPQ prunes with the value from
the old row version, and can remove the partition it actually needs.

This isn't caused by 1b5dd3a24. Before it the EPQ re-initialized the
exec contexts, but with the same parent ExprContext, so this goes back
to 8741e48e5.

Thanks for finding this and for putting the patch together.

Pushed.

David