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

From: Victor Rusaleev <virusa(at)gmail(dot)com>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Subject: Subject: Re: PG18: use-after-free in exec partition pruning after an EPQ recheck in LockRows
Date: 2026-10-03 10:17:20
Message-ID: CAA_xEx409dLbZh5DM6U5wa3S=ZaFTh+7=34xL+vt0H7_CdkRbA@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

In-Reply-To: <CAP2G_gGpV=rKMLuET4RW-qO41GvBvi6Czsjsj16eT9hAwHxG6w(at)mail(dot)gmail(dot)com>
References: <CAP2G_gGpV=rKMLuET4RW-qO41GvBvi6Czsjsj16eT9hAwHxG6w(at)mail(dot)gmail(dot)com>

Hi,

Independent confirmation on a packaged release build, with a different
application query, plus a measured workaround.

Environment: PostgreSQL 18.6 (Debian package 18.6-1.pgdg13+2), x86_64.
JIT is not available on this build (llvmjit not installed).

Query shape: an outbox relay claims rows of a range-partitioned table
(5 partitions on created_at) in one statement:

WITH RECURSIVE keys AS (...),
heads AS (... CROSS JOIN LATERAL (... LIMIT $2) ...),
locked AS MATERIALIZED (
SELECT ... FROM outbox AS candidate JOIN heads ON <primary key>
WHERE <unpublished, lease expired>
ORDER BY ... LIMIT $1
FOR UPDATE OF candidate SKIP LOCKED),
candidates AS (SELECT ... FROM locked ...),
claimed AS (
UPDATE outbox SET status = 'PUBLISHING', lease_until = ...
FROM candidates
WHERE <primary key, including created_at>
RETURNING ...)
SELECT ... FROM claimed ...;

Several relay processes run this statement at the same time against the
same table.

Backtrace, identical in five core dumps (postgresql-18-dbgsym):

#0 ExecEvalExprSwitchContext (state=0x64, ...) executor.h:440
#1 partkey_datum_from_expr (stateidx=3) partprune.c:3823
#2 perform_pruning_base_step partprune.c:3503
#3 get_matching_partitions partprune.c:881
#4 find_matching_subplans_recurse (initial_prune=false) execPartition.c:2592
#5 ExecFindMatchingSubPlans execPartition.c:2535
#6 choose_next_subplan_locally nodeAppend.c:583
#7 ExecAppend nodeAppend.c:330
#9 ExecNestLoop nodeNestloop.c:159
#11 ExecModifyTable (CMD_UPDATE) nodeModifyTable.c:4280
#13 CteScanNext nodeCtescan.c:103

In two of the five dumps frame #0 is "??": a call through a freed function
pointer. es_epq_active is NULL at the crash. pprune->exec_context.planstate
and .exprstates point into memory allocated after the parent EState; the
"planstate" there has type 0xffffffff. This matches the analysis in the
thread: EvalPlanQualStart() shares es_part_prune_states, and
InitExecPartitionPruneContexts() re-points the shared exec_context at EPQ
memory that EvalPlanQualEnd() then frees.

Measured matrix. Each trial claims 1M rows; trial order interleaved; crash
means a backend terminated by signal 11:

enable_partition_pruning = on, 3-4 concurrent sessions: 4 of 4 crashed
(after 95-283 s)
enable_partition_pruning = on, 2 concurrent sessions: 0 of 2 crashed
enable_partition_pruning = off, 2-4 concurrent sessions: 0 of 6 crashed

One session alone never crashed.

Workaround in production use until a fixed release:

ALTER ROLE <relay_role> IN DATABASE <db> SET enable_partition_pruning = off;

With 5 partitions the claim throughput did not drop. This may help users
who wait for the fix, and may be worth a line in the release notes.

I did not test the attached patch.

Regards,
V.R.

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-10-03 16:17:03 BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t"
Previous Message shihao zhong 2026-10-03 04:12:43 Re: PostgreSQL 18.6/17.11: standby PANIC on restart after VM truncation