nocfbot-0029-Fix-nav_slot-pass-by-ref-dangling-pointer.txt
text/plain
Filename: nocfbot-0029-Fix-nav_slot-pass-by-ref-dangling-pointer.txt
Type: text/plain
Part: 28
Message:
Re: Row pattern recognition
From c33aca418319816bde883d1ad6b07f7effbdddea Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH 29/40] Fix nav_slot pass-by-ref dangling pointer in RPR
navigation
When a DEFINE expression contains multiple navigation calls targeting
different positions (e.g., PREV(x,1) > PREV(x,2)), the second call
re-fetches nav_slot, freeing the previous tuple via pfree. Any
pass-by-ref datum extracted from the first navigation becomes a
dangling pointer. Fix by copying pass-by-ref results into per-tuple
memory in the RESTORE step.
---
src/backend/executor/execExpr.c | 5 ++
src/backend/executor/execExprInterp.c | 20 +++++++
src/include/executor/execExpr.h | 2 +
src/test/regress/expected/rpr.out | 80 +++++++++++++++++++++++++++
src/test/regress/sql/rpr.sql | 34 ++++++++++++
5 files changed, 141 insertions(+)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 6349a564a98..8d8da67e79f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1304,7 +1304,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
/* Emit RESTORE opcode: restore original slot */
scratch.opcode = EEOP_RPR_NAV_RESTORE;
+ scratch.resvalue = resv;
+ scratch.resnull = resnull;
scratch.d.rpr_nav.winstate = winstate;
+ get_typlenbyval(nav->resulttype,
+ &scratch.d.rpr_nav.resulttyplen,
+ &scratch.d.rpr_nav.resulttypbyval);
ExprEvalPushStep(state, &scratch);
break;
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 2ec579732cc..e2d41c3098f 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6156,6 +6156,13 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
* When slot swap was elided (target == currentpos), this is a harmless
* no-op since saved and current slots are identical.
* The caller is responsible for updating any local slot cache.
+ *
+ * For pass-by-reference result types, the result datum points into
+ * nav_slot's tuple memory. If a subsequent navigation in the same
+ * expression re-fetches nav_slot for a different position, the old
+ * tuple is freed, leaving a dangling pointer. We prevent this by
+ * copying pass-by-ref results into per-tuple memory, which survives
+ * until the next ResetExprContext.
*/
void
ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
@@ -6164,4 +6171,17 @@ ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
WindowAggState *winstate = op->d.rpr_nav.winstate;
econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+
+ /* Stabilize pass-by-ref result against nav_slot re-fetch */
+ if (!op->d.rpr_nav.resulttypbyval &&
+ !*op->resnull)
+ {
+ MemoryContext oldContext;
+
+ oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+ *op->resvalue = datumCopy(*op->resvalue,
+ false,
+ op->d.rpr_nav.resulttyplen);
+ MemoryContextSwitchTo(oldContext);
+ }
}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 834800a4062..e6b2ab30406 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -703,6 +703,8 @@ typedef struct ExprEvalStep
Datum *offset_value; /* offset value(s), or NULL */
bool *offset_isnull; /* offset null flag(s) */
/* For compound nav: offset_value[0] = inner, [1] = outer */
+ int16 resulttyplen; /* RESTORE: result type length */
+ bool resulttypbyval; /* RESTORE: result pass-by-value? */
} rpr_nav;
/* for EEOP_AGG_*DESERIALIZE */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 04ec25d4cf5..32aa8bc3722 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1635,6 +1635,86 @@ WINDOW w AS (
company2 | 07-10-2023 | 1300 | | | 0
(20 rows)
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+ company | tdate | tdate_text | first_value | last_value | count
+----------+------------+------------+-------------+------------+-------
+ company1 | 07-01-2023 | 07-01-2023 | | | 0
+ company1 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company1 | 07-03-2023 | 07-03-2023 | | | 0
+ company1 | 07-04-2023 | 07-04-2023 | | | 0
+ company1 | 07-05-2023 | 07-05-2023 | | | 0
+ company1 | 07-06-2023 | 07-06-2023 | | | 0
+ company1 | 07-07-2023 | 07-07-2023 | | | 0
+ company1 | 07-08-2023 | 07-08-2023 | | | 0
+ company1 | 07-09-2023 | 07-09-2023 | | | 0
+ company1 | 07-10-2023 | 07-10-2023 | | | 0
+ company2 | 07-01-2023 | 07-01-2023 | | | 0
+ company2 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company2 | 07-03-2023 | 07-03-2023 | | | 0
+ company2 | 07-04-2023 | 07-04-2023 | | | 0
+ company2 | 07-05-2023 | 07-05-2023 | | | 0
+ company2 | 07-06-2023 | 07-06-2023 | | | 0
+ company2 | 07-07-2023 | 07-07-2023 | | | 0
+ company2 | 07-08-2023 | 07-08-2023 | | | 0
+ company2 | 07-09-2023 | 07-09-2023 | | | 0
+ company2 | 07-10-2023 | 07-10-2023 | | | 0
+(20 rows)
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+ company | tdate | nprice | first_value | last_value | count
+----------+------------+--------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | 200 | 150 | 2
+ company1 | 07-03-2023 | 150 | | | 0
+ company1 | 07-04-2023 | 140 | | | 0
+ company1 | 07-05-2023 | 150 | 150 | 90 | 2
+ company1 | 07-06-2023 | 90 | | | 0
+ company1 | 07-07-2023 | 110 | 110 | 120 | 3
+ company1 | 07-08-2023 | 130 | | | 0
+ company1 | 07-09-2023 | 120 | | | 0
+ company1 | 07-10-2023 | 130 | | | 0
+ company2 | 07-01-2023 | 50 | | | 0
+ company2 | 07-02-2023 | 2000 | 2000 | 1500 | 2
+ company2 | 07-03-2023 | 1500 | | | 0
+ company2 | 07-04-2023 | 1400 | | | 0
+ company2 | 07-05-2023 | 1500 | 1500 | 60 | 2
+ company2 | 07-06-2023 | 60 | | | 0
+ company2 | 07-07-2023 | 1100 | 1100 | 1200 | 3
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | | | 0
+(20 rows)
+
--
-- FIRST/LAST navigation
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index a05b429ce74..724d460b2da 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,40 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
);
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+
--
-- FIRST/LAST navigation
--
--
2.50.1 (Apple Git-155)