Skip to content

Commit 6545ba9

Browse files
committed
Prevent access to an unpinned buffer in BEFORE ROW UPDATE triggers.
When ExecBRUpdateTriggers switches to a new target tuple as a result of the EvalPlanQual logic, it must form a new proposed update tuple. Since commit 86dc900, that tuple (the result of ExecGetUpdateNewTuple) has been a virtual tuple that might contain pointers to by-ref fields of the new target tuple (in "oldslot"). However, immediately after that we materialize oldslot, causing it to drop its buffer pin, whereupon the by-ref pointers are unsafe to use. This is a live bug only when the new target tuple is in a different page than the original target tuple, since we do still hold a pin on the original one. (Before 86dc900, there was no bug because the EPQ plantree would hold a pin on the new target tuple; but now that's not assured.) To fix, forcibly materialize the new tuple before we materialize oldslot. This costs nothing since we would have done that shortly anyway. The real-world impact of this is probably minimal. A visible failure could occur if the new target tuple's buffer were recycled for some other page in the short interval before we materialize newslot within the trigger-calling loop; but that's quite unlikely given that we'd just touched that page. There's a larger hazard that some other process could prune and repack that page within the window. We have lock on the new target tuple, but that wouldn't prevent it being moved on the page. Alexander Lakhin and Tom Lane, per bug #17798 from Alexander Lakhin. Back-patch to v14 where 86dc900 came in. Discussion: https://postgr.es/m/17798-0907404928dcf0dd@postgresql.org
1 parent 15235ab commit 6545ba9

File tree

1 file changed

+23
-5
lines changed

1 file changed

+23
-5
lines changed

src/backend/commands/trigger.c

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2978,10 +2978,6 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
29782978
* received in newslot. Neither we nor our callers have any further
29792979
* interest in the passed-in tuple, so it's okay to overwrite newslot
29802980
* with the newer data.
2981-
*
2982-
* (Typically, newslot was also generated by ExecGetUpdateNewTuple, so
2983-
* that epqslot_clean will be that same slot and the copy step below
2984-
* is not needed.)
29852981
*/
29862982
if (epqslot_candidate != NULL)
29872983
{
@@ -2990,14 +2986,36 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
29902986
epqslot_clean = ExecGetUpdateNewTuple(relinfo, epqslot_candidate,
29912987
oldslot);
29922988

2993-
if (newslot != epqslot_clean)
2989+
/*
2990+
* Typically, the caller's newslot was also generated by
2991+
* ExecGetUpdateNewTuple, so that epqslot_clean will be the same
2992+
* slot and copying is not needed. But do the right thing if it
2993+
* isn't.
2994+
*/
2995+
if (unlikely(newslot != epqslot_clean))
29942996
ExecCopySlot(newslot, epqslot_clean);
2997+
2998+
/*
2999+
* At this point newslot contains a virtual tuple that may
3000+
* reference some fields of oldslot's tuple in some disk buffer.
3001+
* If that tuple is in a different page than the original target
3002+
* tuple, then our only pin on that buffer is oldslot's, and we're
3003+
* about to release it. Hence we'd better materialize newslot to
3004+
* ensure it doesn't contain references into an unpinned buffer.
3005+
* (We'd materialize it below anyway, but too late for safety.)
3006+
*/
3007+
ExecMaterializeSlot(newslot);
29953008
}
29963009

3010+
/*
3011+
* Here we convert oldslot to a materialized slot holding trigtuple.
3012+
* Neither slot passed to the triggers will hold any buffer pin.
3013+
*/
29973014
trigtuple = ExecFetchSlotHeapTuple(oldslot, true, &should_free_trig);
29983015
}
29993016
else
30003017
{
3018+
/* Put the FDW-supplied tuple into oldslot to unify the cases */
30013019
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
30023020
trigtuple = fdw_trigtuple;
30033021
}

0 commit comments

Comments
 (0)