Hi, On REL_19_STABLE (e2b2267d24e, 19rc2) and master (206e71935e1), EXPLAIN (BUFFERS) without ANALYZE of a query that uses an index crashes the backend when auto_explain logs it with log_analyze on:
CREATE TABLE t (a int PRIMARY KEY, b int); LOAD 'auto_explain'; SET auto_explain.log_min_duration = 0; SET auto_explain.log_analyze = on; EXPLAIN (BUFFERS) SELECT * FROM t WHERE a = 5; server closed the connection unexpectedly #0 show_indexscan_info () #1 ExplainNode () #2 ExplainPrintPlan () #3 explain_ExecutorEnd () Index Scan, Index Only Scan and Bitmap Index Scan plans all crash. 18.6 is fine. f026fbf059f crashes and its parent does not. Since f026fbf059f the index scan nodes do not allocate their IndexScanInstrumentation under EXEC_FLAG_EXPLAIN_ONLY. auto_explain still prints such a plan with es->analyze set, because EXPLAIN (BUFFERS) asks for buffer instrumentation, and show_indexscan_info() (show_indexsearches_info() in 19) dereferences the NULL pointer. The attached patch treats the missing instrumentation as zero counts, so the log shows "(never executed)" and "Index Searches: 0" as it did before f026fbf059f, and adds cases to auto_explain's TAP test that crash without the fix. The master patch does not apply to REL_19_STABLE, so there is a separate version for it. I fixed this in explain.c rather than in auto_explain because ExplainNode already tolerates es->analyze without instrumentation, and ExplainPrintPlan() can be called by other extensions. -- Xiaoxuan Li Amazon Web Services
From: Xiaoxuan Li <[email protected]> Date: Fri, 13 Nov 2026 15:36:08 -0800 Subject: [PATCH v1] Fix auto_explain crash for EXPLAIN without ANALYZE on index scans Commit f026fbf059f turned the IndexScanInstrumentation of index scan, index-only scan and bitmap index scan nodes into a pointer that is allocated only when the node is going to be run. For a plan started with EXEC_FLAG_EXPLAIN_ONLY it stays NULL, but show_indexsearches_info() dereferenced it whenever es->analyze was set. EXPLAIN never sets es->analyze for such a plan, but auto_explain does. EXPLAIN (BUFFERS) without ANALYZE puts INSTRUMENT_BUFFERS into the query's instrument options, and with auto_explain.log_analyze enabled auto_explain then prints the plan with es->analyze set, which crashed the backend. Fix by treating a missing IndexScanInstrumentation as zero counts. That is what was shown before f026fbf059f, and what is shown for any other node that was never executed. Author: Xiaoxuan Li <[email protected]> Backpatch-through: 19 --- contrib/auto_explain/t/001_auto_explain.pl | 51 ++++++++++++++++++++++++++++ src/backend/commands/explain.c | 19 ++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl index f32a5e84f65..0df5202a484 100644 --- a/contrib/auto_explain/t/001_auto_explain.pl +++ b/contrib/auto_explain/t/001_auto_explain.pl @@ -135,6 +135,57 @@ unlike( qr/Query Identifier:/, "query identifier not logged with compute_query_id=regress, text mode"); +# EXPLAIN (BUFFERS) without ANALYZE. The plan is started but never run, yet +# with log_analyze enabled it is logged as if it had been analyzed (BUFFERS +# is what requests instrumentation; a plain EXPLAIN is logged bare). Check +# that each kind of index scan copes with that. +$log_contents = query_log($node, + q{EXPLAIN (BUFFERS) SELECT * FROM pg_proc WHERE proname = 'int4pl';}); + +like( + $log_contents, + qr/Index Scan using pg_proc_proname_args_nsp_index on pg_proc .*\(never executed\)/, + "index scan logged as never executed, text mode"); + +like( + $log_contents, + qr/Index Searches: 0/, + "index searches logged for index scan, text mode"); + +$log_contents = query_log($node, + q{EXPLAIN (BUFFERS) SELECT proname FROM pg_proc WHERE proname = 'int4pl';} +); + +like( + $log_contents, + qr/Index Only Scan using pg_proc_proname_args_nsp_index on pg_proc .*\(never executed\)/, + "index-only scan logged as never executed, text mode"); + +like( + $log_contents, + qr/Heap Fetches: 0/, + "heap fetches logged for index-only scan, text mode"); + +like( + $log_contents, + qr/Index Searches: 0/, + "index searches logged for index-only scan, text mode"); + +$log_contents = query_log( + $node, + q{EXPLAIN (BUFFERS) SELECT * FROM pg_proc WHERE proname = 'int4pl';}, + { "enable_indexscan" => "off" }); + +like( + $log_contents, + qr/Bitmap Index Scan on pg_proc_proname_args_nsp_index .*\(never executed\)/, + "bitmap index scan logged as never executed, text mode"); + +like( + $log_contents, + qr/Index Searches: 0/, + "index searches logged for bitmap index scan, text mode"); + # JSON format. $log_contents = query_log( $node, diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index a40d03d35f3..9c12f080dd4 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3880,14 +3880,23 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) if (!es->analyze) return; - /* Initialize counters with stats from the local process first */ + /* + * Initialize counters with stats from the local process first. + * + * The node's instrumentation is not allocated when the plan was started + * with EXEC_FLAG_EXPLAIN_ONLY. We can still get here in that case: for + * EXPLAIN (BUFFERS) without ANALYZE, auto_explain prints the plan with + * es->analyze set. Report zero counts then, like for any other node that + * was never executed. + */ switch (nodeTag(plan)) { case T_IndexScan: { IndexScanState *indexstate = ((IndexScanState *) planstate); - nsearches = indexstate->iss_Instrument->nsearches; + if (indexstate->iss_Instrument) + nsearches = indexstate->iss_Instrument->nsearches; SharedInfo = indexstate->iss_SharedInfo; break; } @@ -3895,7 +3904,8 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) { IndexOnlyScanState *indexstate = ((IndexOnlyScanState *) planstate); - nsearches = indexstate->ioss_Instrument->nsearches; + if (indexstate->ioss_Instrument) + nsearches = indexstate->ioss_Instrument->nsearches; SharedInfo = indexstate->ioss_SharedInfo; break; } @@ -3903,7 +3913,8 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) { BitmapIndexScanState *indexstate = ((BitmapIndexScanState *) planstate); - nsearches = indexstate->biss_Instrument->nsearches; + if (indexstate->biss_Instrument) + nsearches = indexstate->biss_Instrument->nsearches; SharedInfo = indexstate->biss_SharedInfo; break; } -- 2.54.0 (Apple Git-157)
v1-0001-Fix-auto_explain-crash-for-EXPLAIN-without-ANALYZE-on.patch
Description: Binary data
