CommitFest 6557 — PL/pgSQL FOREACH IN JSON ARRAY review

report · generated from canonical Markdown

Type
report
Source
postgresql-master
Slug
commitfest-6557-review
Tags
commitfest-6557pg-review-harness
Updated
2026-08-30T05:06:34.536998+00:00
Verdict / Summarywaiting-on-author
Front matter
schema: review-report-v2
slug: commitfest-6557-review
source: postgresql-master
target: "CommitFest 6557 — PL/pgSQL FOREACH ... IN JSON ARRAY (staged diff against HEAD 2fb8da5a245661287833b05a1b2e275ddf83bbd7)"
base: 2fb8da5a245661287833b05a1b2e275ddf83bbd7
head: 2fb8da5a245661287833b05a1b2e275ddf83bbd7
verdict: waiting-on-author
blocker_counts:
  verified: 2
  unverified: 0
seats:
  pg-patch-reviewer: opus
  pg-security-reviewer: opus
  pg-finding-verifier: coordinator (glm-5.2:cloud)
  pg-change-coverage-reviewer: coordinator (glm-5.2:cloud)
model: glm-5.2:cloud

CommitFest 6557 — PL/pgSQL FOREACH ... IN JSON ARRAY — Review Report

Source: postgresql-master Slug: commitfest-6557-review Scope identity: pg-cf6557-head-2fb8da5a245661287833b05a1b2e275ddf83bbd7-staged-f717b158 Target: uncommitted staged diff against HEAD 2fb8da5a245661287833b05a1b2e275ddf83bbd7 Date reviewed: 2026-08-30 Verdict: waiting-on-author (2 verified BLOCKERs)

Summary

Adds FOREACH <target> IN JSON ARRAY <expr> LOOP ... END LOOP to PL/pgSQL: iterate JSON/JSONB array elements and assign each to a scalar, composite, or list-of-scalars loop variable. New statement type PLPGSQL_STMT_FOREACH_JSON_A, unreserved keyword JSON, executor exec_stmt_foreach_json_a, helper JsonbValueToDatum, an extended plpgsql_exec_get_datum_type_info (new refname out-param and a new PLPGSQL_DTYPE_ROW lazy tupdesc build), and a new regression test plpgsql_foreach.

The architecture (shared base struct + grammar refactor via foreach_type, JsonbIteratorNext with skipNested, json_populate_type for composite population) is reasonable. However, the staged patch has two verified BLOCKERs that crash the server or corrupt the heap, one verified MAJOR correctness defect (FOUND), and significant test-coverage gaps. The patch's own regression test crashes the backend (SIGABRT) on the list-of-scalars and record-field cases — the committed expected output does not match actual behaviour. Not ready for committer as-is.

Findings

BLOCKER F1 — Server crash: missing TupleDescFinalize before BlessTupleDesc

CreateTemplateTupleDesc + TupleDescInitEntry/Collation and then calls BlessTupleDesc without first calling TupleDescFinalize. CreateTemplateTupleDesc initializes firstNonCachedOffsetAttr = -1 and relies on the caller to run TupleDescFinalize after populating entries; BlessTupleDesc asserts firstNonCachedOffsetAttr >= 0 (execTuples.c:2358). The missing TupleDescFinalize trips that assertion.

build aborts the backend at the foreach x, y, z in json array case and the foreach c.x, c.y, c.z case with TRAP: failed Assert("tupdesc->firstNonCachedOffsetAttr >= 0") at execTuples.c:2358. The committed expected/plpgsql_foreach.out claims those cases succeed.

compile context) and call TupleDescFinalize(row->rowtupdesc) before BlessTupleDesc. Concrete unified diff provided (bound to HEAD); validated: after applying, make installcheck reports all 14 plpgsql tests pass.

BLOCKER F2 — Use-after-free of row->rowtupdesc across cached function calls

CurrentMemoryContext, which (from exec_stmt_foreach_json_a) is the transient stmt_mcontext. PLpgSQL_row datums of DTYPE_ROW are NOT copied per execution (copy_plpgsql_datums uses outdatum = indatum), so the executor's row datum IS the cached function's datum. After the loop, MemoryContextReset(stmt_mcontext) frees the TupleDesc but row->rowtupdesc is not reset to NULL; on the next cached invocation the if (!row->rowtupdesc) guard is false (dangling non-NULL pointer) and the freed TupleDesc is dereferenced. Reachable by any role with CREATE/EXECUTE on a function using a multi-scalar FOREACH JSON ARRAY.

ARRAY ... corrupts the heap on the second invocation. The regression tests only use DO` blocks (anonymous, not cached), so the second invocation is never exercised.

Validated: a persistent cached function called three times in succession returned without crash after the repair.

MAJOR F3 — FOUND is never set to true after the JSON loop

never sets it true inside the WJB_ELEM branch; exec_set_found(estate, found) therefore always sets FOUND=false. Contradicts exec_stmt_foreach_a.

Validated: non-empty → FOUND=true, empty → FOUND=false after the one-line fix.

MAJOR F4 — Test suite does not cover FOUND, empty array, scalar/object/null rejection, SLICE rejection, or boolean elements

FOUND invariant are untested, which is why F1–F3 escaped. The committed expected output for list-of-scalars/record-field cases does not match actual behaviour (server crashes — F1).

scalar/object/null/SLICE rejection, and boolean-element cases). Validated by manual execution on the repaired build; the committed .out must be regenerated from a corrected build via make installcheck.

MAJOR COV-1 — Uncovered jbvBool conversion branch (coverage finding)

JsonbValueToDatum (lines 3096–3098, 3100) is never executed; a whole element-type conversion path is untested.

traverses the branch.

MINOR F5 — Ungrammatical grammar error message

MINOR F6 — Documentation typos

MINOR F7 — Stray #include "utils/builtins.h" in jsonb_util.c

Verification Summary

FindingSeverityClassStatusMethod
F1BLOCKERcorrectnessverifiedrepro (SIGABRT) + repair validation
F2BLOCKERsecurityverifiedstatic (plcache lifecycle) + repair validation
F3MAJORcorrectnessverifiedrepro (FOUND) + repair validation
F4MAJORtestsverifiedstatic + coverage
COV-1MAJORtestsverifiedverified-by-coverage
F5MINORmaintainabilityverifiedstatic-check
F6MINORdocsverifiedstatic-check
F7MINORmaintainabilityverifiedstatic-check

Repair Validation Summary

by apply-check + compile + targeted-test. After applying to a disposable scratch copy: build exit 0, make install exit 0, make installcheck → "All 14 tests passed"; a persistent cached function called 3× returned without crash. Patch SHA-256 bound to HEAD.

compile + targeted-test. FOUND=true after non-empty, false after empty.

execution of each case); the committed expected output must be regenerated from a corrected build.

(+compile for F7, +doc-build indicated for F6).

execution traversing the previously-uncovered branch.

A successful repair does not prove a finding, and a failed repair does not refute it. All repair patches are proposals governed by each finding's root mechanism and intended postcondition.

Changed-Code Coverage

src/pl/plpgsql/src/pl_exec.c, pl_funcs.c, pl_gram.y, pl_comp.c).

tree; configure --enable-debug --enable-cassert with -fprofile-arcs -ftest-coverage on the plpgsql module; make -j4; make install; initdb + pg_ctl start on a Unix-socket-only disposable cluster; make installcheck in src/pl/plpgsql/src (14/14 passed on the repaired tree); counters collected from plpgsql.so .gcda via gcov -b. Fresh counter epoch; no integrity errors.

Raw metrics

MetricValue
changed executable lines127
covered changed lines120
compiler-uncovered changed lines7
structural uncovered changed lines0
changed lines without coverage coordinates0
changed branches54
covered changed branches26

Actionable uncovered lines (coverable-now)

FileFunctionLineReasonCovering test
src/pl/plpgsql/src/pl_exec.cJsonbValueToDatum3096,3097,3098,3100jbvBool body; no boolean-element testforeach x in json array '[true,false,null]' with x boolean
src/pl/plpgsql/src/pl_exec.cexec_stmt_foreach_json_a3157null-expression ereportforeach x in json array null::jsonb
src/pl/plpgsql/src/pl_exec.cexec_stmt_foreach_json_a3199scalar-rejection ereportforeach x in json array '5'
src/pl/plpgsql/src/pl_exec.cexec_stmt_foreach_json_a3203object-rejection ereportforeach x in json array '{"a":1}'

Uncovered branches

by the F4 test additions.

(assert cannot fire in normal operation).

by calling a cached function twice.

require a loop body containing EXIT/CONTINUE; requires-special-harness-or- injection-point. Concrete EXIT/CONTINUE test examples provided.

Non-instrumented changes

doc/src/sgml/plpgsql.sgml (docs), Makefile/meson.build (build lists), pl_unreserved_kwlist.h (keyword generator input), plpgsql_foreach.sql/ .out (test input/expected), jsonb_util.c (stray include only).

Residual limitations

Coverage is qualified by the exact executed test set (plpgsql_foreach + the existing plpgsql regress suite). The LOOP_RC_PROCESSING EXIT/CONTINUE branches require additional loop-body tests to cover; the Assert failure branch is not coverable in normal operation.

pgsql-hackers thread ledger

Reconciled thread (Thread-ID 638323, "POC: PLpgSQL FOREACH IN JSON ARRAY"), 2026-01-24 to 2026-08-03. Ledger report: commitfest-6557-review-thread-ledger (source postgresql-master).

routing the feature through SubscriptRoutines/iterator separation; the staged patch reverts to a simpler non-SubscriptRoutines design and that discussion is unresolved.

Maintainer CC

Pavel Stehule (author); Jim Jones, Chao Li (reviewers); Tom Lane (design objection).

Aggregated Test Plan

  1. F1/F2: cached multi-scalar FOREACH JSON ARRAY function called 3×.
  2. F3: FOUND after non-empty and empty FOREACH JSON ARRAY.
  3. F4/COV-1: empty array, scalar/object/null-expression/SLICE rejection,

boolean element, null element to composite.

  1. LOOP_RC: EXIT and CONTINUE inside a FOREACH JSON ARRAY body.

Residual risk

The deeper composite-population reliability issue (the list-of-scalars field-by-name assignment produces NULLs under -O0/cached-function conditions while the DO-block -O2 path populates correctly) was observed during verification and is tied to the json_populate_type cache / rowtupdesc interaction; it is not raised as a separate finding here but warrants investigation once F1/F2 are fixed. The unresolved Tom Lane SubscriptRoutines design objection is a separate committer-level architectural concern.

Deterministic verdict

waiting-on-author — 2 verified BLOCKERs (F1 crash, F2 UAF) plus 1 verified MAJOR (F3 FOUND) and test-coverage MAJORs. After F1/F2/F3 and the F4 test additions are addressed, this would move to needs-review (pending the Tom Lane design-objection resolution), then to ready-for-committer after the nits.