CommitFest 7371 Review: Fix missing tts_tid in ExecForceStoreHeapTuple
code-review · generated from canonical Markdown
code-review
postgresql-master
commitfest-7371-review2026-09-30T17:46:12.480374+00:00
Front matter
schema: review-report-v2 slug: commitfest-7371-review source: postgresql-master target: CommitFest 7371 — Fix missing tts_tid in ExecForceStoreHeapTuple base: b69356cd789fe963447177a0077c12c6c322c203 head: b69356cd789fe963447177a0077c12c6c322c203 verdict: waiting-on-author diff_sha: 8e414d3c6e19a2146b1fbb1c7c71fb8a6180f2ea3fff37b995144cdb20aff927 model: glm-5.2:cloud date: 2026-09-30
CommitFest 7371 — Review Report: Fix missing tts_tid in ExecForceStoreHeapTuple
Summary
Verdict: waiting-on-author (1 verified BLOCKER: test gap)
The patch adds slot->tts_tid = tuple->t_self; in the TTS_IS_BUFFERTUPLE branch of ExecForceStoreHeapTuple (src/backend/executor/execTuples.c:1768). This is a correct, minimal, one-line bug fix that restores the slot's tts_tid invariant after ExecClearTuple invalidated it, matching the established convention of tts_heap_store_tuple (line 497) and tts_buffer_heap_store_tuple (line 962).
The bug
KNN GiST scans with lossy distance recheck use a reorder queue in nodeIndexscan.c. When popping a tuple from the queue, ExecForceStoreHeapTuple is called with a buffer tuple slot. The TTS_IS_BUFFERTUPLE branch copies the tuple via heap_copytuple but previously did not set slot->tts_tid from tuple->t_self, leaving it at the InvalidItemPointer (4294967295,0) set by ExecClearTuple. With FOR UPDATE, the invalid tid is passed to heap_lock_tuple, which extends the relation with an orphan block before aborting with "attempted to lock invisible tuple".
Blocker split
| Status | Count |
|---|---|
| Verified | 1 |
| Unverified | 0 |
Findings
— BLOCKER — tests — Regression test does not exercise the buggy reorder-queue pop path
Location: src/test/regress/sql/gist.sql:246-251
Mechanism: The staged test queries ORDER BY v <-> point(50, 50) LIMIT 1 over circles centered at (i,i) with radius 0.5. For circle id=50, the query point (50,50) is inside the bounding box, so bbox distance = 0 and true distance = 0, making was_exact = true. With LIMIT 1 and an empty queue, the tuple is returned immediately without ever calling reorderqueue_push/reorderqueue_pop. The buggy ExecForceStoreHeapTuple pop path at nodeIndexscan.c:257 is NOT exercised.
Failure: A future regression reintroducing the missing tts_tid would not be caught. The test passes with or without the fix.
Verification: verified by static analysis and two-run validation. A replacement test was designed (querying point(0,0) outside all bounding boxes, forcing 100 reorder-queue pops) and validated: PASS on HEAD (with fix, all ctids valid), FAIL on BASE (without fix, all ctids = (4294967295,0), ERROR: attempted to lock invisible tuple).
Repair: Replace the weak test block with the strong test. The repair patch (f1-strong-test.patch) was applied, built, and pg_regress gist passed.
— MINOR — correctness — tts_tableOid not set in BUFFERTUPLE branch (pre-existing)
Location: src/backend/executor/execTuples.c:1759-1774
Mechanism: The TTS_IS_BUFFERTUPLE branch of ExecForceStoreHeapTuple does not set slot->tts_tableOid, unlike ExecStoreHeapTuple (line 1649) and ExecStoreBufferHeapTuple (line 1690) which both set slot->tts_tableOid = tuple->t_tableOid. Pre-existing, surfaced by this patch.
Verification: verified by static source comparison across sibling functions.
Repair: Add slot->tts_tableOid = tuple->t_tableOid; in the same branch.
— NIT — maintainability — No comment explaining why tts_tid must be set
Location: src/backend/executor/execTuples.c:1768
Mechanism: The patched line has no comment. ExecClearTuple invalidated tts_tid, and slot_getsysattr reads ctid from slot->tts_tid (not from the copied tuple). A future refactor might remove the line as "redundant".
Verification: verified by static analysis.
Repair: Add a two-line comment explaining the invariant restoration.
Verification Summary
| Finding | Severity | Class | Verification Status | Method |
|---|---|---|---|---|
| F-1 | BLOCKER | tests | verified | static analysis + two-run validation |
| F-2 | MINOR | correctness | verified | static source comparison |
| F-3 | NIT | maintainability | verified | static analysis |
All three findings are verified. The test-author designed and two-run validated a replacement test for F-1 (PASS on HEAD, FAIL on BASE). The verifier independently confirmed the analysis and validated all repair patches in a disposable worktree (build + pg_regress gist PASS).
Repair Validation Summary
| Finding | Repair | Validation | Methods |
|---|---|---|---|
| F-1 | Replace weak test with strong test (query point(0,0), 100 pops) | passed | apply-check, diff-check, compile, targeted-test, two-run |
| F-2 | Add slot->tts_tableOid = tuple->t_tableOid; | passed | apply-check, diff-check, compile, targeted-test |
| F-3 | Add comment explaining tts_tid restoration | passed | apply-check, diff-check, compile, targeted-test, static-check |
Repair patches:
- F-1 (strong test): SHA-256
22cb791e6426eed3fa76ce995167c13f6cb1ea1658d9398f8f2719bee9334881 - F-2+F-3 (tableOid + comment, includes original fix): SHA-256
ac7a5896f6631687428e4963b2ef548b86ad6c4c99f48e5721fdfe76dfb994bd
All patches are concrete git unified diffs bound to frozen HEAD b69356cd789fe963447177a0077c12c6c322c203.
Changed-Code Coverage
Coverage requirement: required (1 changed instrumentable path)
Changed instrumentable path: src/backend/executor/execTuples.c (line 1768)
Test commands
make -C src/backend/executor CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" execTuples.o make -s -j4 && make -s install initdb --no-sync -D <cov-pgdata> && pg_ctl start rm -f src/backend/executor/*.gcda src/test/regress/pg_regress --inputdir=src/test/regress --bindir=<inst>/bin --user=regress gist gcov src/backend/executor/execTuples.c
Execution
- Lane: host-disposable (disposable worktree copy at clean HEAD + staged diff + strong test)
- Counter epoch: fresh (stale .gcda files removed before test run)
- Test result:
ok 1 - gist 2275 ms/All 1 tests passed.— PASS - Integrity errors: none
Raw metrics
| Metric | Count |
|---|---|
| Changed executable lines | 1 |
| Covered changed lines | 1 |
| Compiler uncovered changed lines | 0 |
| Uncovered changed lines | 0 |
| Structural uncovered changed lines | 0 |
| Changed lines without coverage coordinates | 0 |
| Changed branches | 0 |
| Covered changed branches | 0 |
Touched functions
| Path | Function | Lines | Entered | Whole-function lines | Whole-function branches |
|---|---|---|---|---|---|
src/backend/executor/execTuples.c | ExecForceStoreHeapTuple | 1751–1788 | yes (113 calls) | 7/8 covered | 3/5 covered (2 pre-existing gaps, not introduced by patch) |
gcov evidence for line 1768
function ExecForceStoreHeapTuple called 113 returned 100% blocks executed 94%
111: 1768: slot->tts_tid = tuple->t_self;Line 1768 (the changed line) was executed 111 times — fully covered.
Uncovered regions
None. The single changed executable line is fully covered.
Uncovered branches
None. The changed line is an unconditional assignment with no branch.
Non-instrumented changes
| Path | Reason |
|---|---|
src/test/regress/sql/gist.sql | Test SQL input; not compilable C, no gcov counters. Acts as the exercising harness. |
src/test/regress/expected/gist.out | Expected regression output; not executable, no coverage counters. |
Excluded changed coordinates
None.
COV-* findings
None. The changed line is fully covered and introduces no new branch.
Security Review
No security findings. The patch is a minimal, correct, one-line fix that:
- Has no memory-safety issues (struct copy by value, safe memory context)
- Introduces no privilege escalation, SQL injection, or catalog race
- Closes a low-privilege storage-bloat/locking-misbehavior bug
pgsql-hackers Thread Ledger
Status: no-thread-found
Reconstruction: Searched the pgsql-hackers mail archive. Found only the initial patch submission (Message-ID CAM6Zo8wZOLnCWRO_tuuXVX9J4N4JN6GsEnk8WJtT0=_0zy-1dw@mail.gmail.com, 2026-09-01, by virender.cse@gmail.com). No review replies, author follow-ups, or committer responses were found.
All three findings (F-1, F-2, F-3) have disposition new — they were not raised in the thread.
Aggregated Test Plan
- Strong reorder-queue regression test (F-1 repair): queries
point(0,0)
to force 100 reorder-queue pops through the TTS_IS_BUFFERTUPLE branch of ExecForceStoreHeapTuple, asserts all ctids are valid, and exercises FOR UPDATE row locking. Validated: PASS on HEAD, FAIL on BASE.
- Coverage confirmation: gcov shows line 1768 hit 111 times by the gist
regress suite with the strong test applied.
Provenance / Model Routes
| Seat | Agent | Model |
|---|---|---|
| Orchestrator | pg-review-harness | glm-5.2:cloud |
| Patch review | pg-patch-reviewer | glm-5.2:cloud |
| Security review | pg-security-reviewer | glm-5.2:cloud |
| Test author | pg-test-author | glm-5.2:cloud |
| Finding verifier | pg-finding-verifier | glm-5.2:cloud |
| Coverage reviewer | pg-change-coverage-reviewer | glm-5.2:cloud |
Model routing: user-specified override glm-5.2:cloud applied to all seats.
Deterministic Verdict
waiting-on-author
Rationale: F-1 is a verified BLOCKER (the regression test does not exercise the buggy path). The production fix itself is correct and safe, but the author must replace the weak test with the strong test before this patch is ready for committal. F-2 (MINOR) and F-3 (NIT) are recommended improvements that can be addressed in the same revision.
Residual Risk
- The production fix is correct and matches established conventions.
- The strong test (F-1 repair) has been designed and two-run validated but is
proposal-only; the author must apply it.
- The
tts_tableOidgap (F-2) is pre-existing and low-impact in the
reorder-queue path (all tuples from the same relation).
- The virtual branch of
ExecForceStoreHeapTuplealso leavestts_tidinvalid
(informational, flagged by the patch-reviewer as out of scope).