fix(visibilitymap): enforce columnar relation check and pre-lock ownership - #748
Conversation
…rship Functions pgcolumnar.vm_selftest and pgcolumnar.vm_is_visible previously did not check if the target relation was a columnar table via PgColumnarIsColumnarRelation, allowing callers to manipulate or read the visibility map fork of standard PostgreSQL heap relations. Additionally, table_open was invoked prior to checking ownership. This commit: 1. Adds PgColumnarIsColumnarRelation check to both visibility map functions. 2. Exports PgColumnarRequireTableOwnerByOid to validate table ownership before requesting relation locks.
jdatcmd
left a comment
There was a problem hiding this comment.
Reviewed on the merged tree, pg17 in my container. The defect is real and the fix works.
I confirmed both rather than reading them, because the PR asserts them without a
demonstration.
The vulnerability, reproduced on main
Both functions accept a heap relation, and vm_selftest does not merely inspect it: it
calls PgColumnarVMSetVisible, so it sets an all-visible bit in a heap relation's visibility
map fork. A wrongly-set all-visible bit is how an index-only scan returns rows without
checking visibility, so this is a wrong-answers vector on a table this extension does not own
the format of.
main with #748
vm_selftest on a HEAP table, as its OWNER no error SQLSTATE=42809
vm_is_visible on a HEAP table, as its OWNER no error SQLSTATE=42809
vm_is_visible on the COLUMNAR table (control) no error no error
SQLSTATE and not error text, per the house rule. The control matters: a guard that refused
everything would look identical on the first two rows.
Ownership before table_open is right and matches the reasoning already written at
columnar_vacuum.c:191. Checking ownership before the columnar check is also right: a
non-owner now learns nothing about whether the relation is columnar.
entry_point_privilege.sh, index_only.sh and debug_hook_privilege.sh all pass on the
merged tree, so the reordering did not disturb the existing 42501 arms.
Requesting changes: there is no test
This is the one thing I am asking for. The 42809 behaviour is entirely untested, so it can
regress to the pre-#748 state silently and every suite stays green. The house rule is that a
fix needs a removal proof: the mutation, and the exact check that goes red.
test/entry_point_privilege.sh is the natural home. It already enumerates
vm_is_visible and vm_selftest by name in GUARDED_BEHAVIOURAL, and it already reads
SQLSTATE rather than text (its own header argues the point at lines 30-33). It needs a heap
arm beside the unprivileged arm, with the columnar call as the positive control.
Here is the probe I used, so you do not have to re-derive it:
stx() { # -> SQLSTATE=..., or empty when the call succeeded
psql -At -v ON_ERROR_STOP=0 -c "DO \$p\$ BEGIN PERFORM $1;
EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'SQLSTATE=%', SQLSTATE; END \$p\$;" 2>&1 |
grep -oE 'SQLSTATE=[0-9A-Z]{5}' | head -1
}The mutation that must redden it is deleting one PgColumnarIsColumnarRelation block.
Two smaller things
The PR body lost every inline code span. It currently opens:
anddid not verify if the target relation was a columnar relation (`), allowing callers
to inspect or manipulate...
Every function name is gone, so the summary names nothing. That text is what the merge commit
carries as the record.
Not blocking, but worth a follow-up: this raises 42809 for "not a columnar table",
while #747 -- merged two hours ago -- added the same condition to set_options in plpgsql,
which raises the plpgsql default. Measured on the merged tree:
set_options on heap (#747, plpgsql) SQLSTATE=P0001
vm_is_visible on heap (#748, C) SQLSTATE=42809
One condition, two SQLSTATEs, so a caller cannot handle it uniformly. 42809 is the correct
one; #747's would need USING ERRCODE = 'wrong_object_type'. Your PR is the one that is
right, which is why I am not asking you to change it.
|
Reviewing this as a PR I did not write, despite the attribution — the branch is on my fork and jdatcmd has already confirmed the defect and the fix, and asked for a test. I agree with all of
|
You are right, and the miss was mine. Confirmed on my own box.I wrote that "the reordering did not disturb the existing 42501 arms" on the strength of three Derived properly, from the identifiers the diff touches, the suites naming I ran two of those four, plus Built from the #748 merged tree, md5 of the installed library printed beside the result so it Reproduces exactly, including the point that matters: "the owner can still call One thing to add, since neither of us had run it: Where that leaves the PRI still think the fix is right, for the reason my reproduction showed: On the attributionBoth of these PRs carry your account and you say you did not push them. That is the exact My review's other two points stand: there is still no test for the 42809 behaviour, and the |
|
Thanks for running the fourth suite. Nothing further from me on the finding itself — we agree, and you reproduced it independently The
|
|
The SQLSTATE split is fixed in #757: Raised here because it lands under both of these: #748 adds a second guard raising the same sentence, and #749 wants a test that asserts a SQLSTATE. Landing #757 first means neither is built against a codebase that answers the same message with two codes. |
…ommandprompt#749) Addresses the two review findings on this PR. ## 1. drop_projection was ordered differently from the four beside it It checked PgColumnarIsColumnarRelation BEFORE the ownership check, so a caller with no rights to an arbitrary relation learned whether it was columnar. recluster, compact_rewrite and compact do not. Ownership now comes first, which is also the order commandprompt#748 uses in the visibility-map entry points. ## 2. There was no test, and a SQLSTATE assertion would not have been one On unfixed main these calls ARE eventually refused -- just after the lock request. Asserting 42501 alone therefore passes on both orderings and proves nothing. Contention is the observable that separates them, which is the argument test/vacuum_lock_privilege.sh already makes for vacuum/vacuum_sorted/cluster (commandprompt#568), so the five new entry points are asserted there rather than in a new suite. add_projection takes ShareLock and the other four take ShareUpdateExclusiveLock; all conflict with the AccessExclusiveLock the suite already holds, so an unfixed caller queues exactly as vacuum did. Removal proof, the same tests against main's src/: add_projection ... got [locktimeout] want [owner] drop_projection ... got [locktimeout] want [owner] recluster ... got [locktimeout] want [owner] compact_rewrite ... got [locktimeout] want [owner] compact ... got [locktimeout] want [owner] drop_projection tells a non-owner nothing about the relation type: got [typedisclosed] want [owner] `locktimeout` is the proof: the caller reached a four second lock timeout before its ownership was ever checked. Positive controls added beside them, because a guard that refused every caller would satisfy every deny arm: the owner can still compact, and can still add and drop a projection. vacuum_lock_privilege 18 checks PASSED. docs_style, entry_point_privilege, projection_privilege, native_ownership and native_compact also pass, all run against a build made from this tree (md5 8efa9694027e) rather than whatever was installed last. CHANGELOG entry added; the PR changed a public function's error code for a non-owner on a non-columnar relation (42809 -> 42501) and said nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ually makes
The suite was red on PG17 and PG18 and the description said the suite
passed. Its fixture is a heap table, so four of its assertions -- one of
them its own positive control, "the owner can still call vm_selftest" --
encoded the behaviour this PR removes.
## Two fixtures, because the guarantee changed shape
vmvictim columnar -- the ownership boundary still applies here, so the
refused/allowed arms live on it
vmheap heap -- refused for EVERYONE now, owner included
That heap arm is stronger than the ownership arm it replaces: no caller
reaches a heap relation's visibility map at all, rather than only
non-owners being turned away.
## What moved, and why it had to
The index-only-scan consequence arms now run against vmheap. A columnar
relation plans as Custom Scan (PgColumnarScan) and offers no Index Only
Scan to corrupt -- measured, not assumed -- and the heap table is where
the blast radius this suite describes always was.
The columnar victim also lost its PRIMARY KEY: inserting 20,000 rows into
a columnar table with a unique index exhausts the lock table here
("You might need to increase max_locks_per_transaction"). It needs no key
for the ownership arms.
## The proof, and what is only a control
Removal proof, the same suite against main's src/:
vm_selftest refuses a heap relation even for its owner (commandprompt#748):
got [] want [SQLSTATE=42809]
vm_is_visible refuses a heap relation even for its owner (commandprompt#748):
got [] want [SQLSTATE=42809]
`got []` is the call succeeding, which is the defect.
The scan-consequence arms are labelled CONTROLS in the file, because they
do NOT discriminate: sweeping every block of vmheap as the owner, the
index-only scan still returned 10000 on both arms. They guard a future
change that refuses the call while still writing to the fork; they are
not evidence for this one, and saying otherwise would be the vacuous-test
shape this file already warns about.
SQLSTATE and not message text: 42809 is wrong_object_type from C. A grep
for "not a columnar table" is also satisfied by set_options's plpgsql
guard, which raises a different SQLSTATE for the same sentence.
30 checks PASSED with the fix, 2 RED without, both run against builds made
from the tree under test (572882e1704a and 9774fc9c7049) rather than
whatever was installed last.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushed
|
…not commandprompt#403 Raised on review. commandprompt#403 is the ClickHouse-paper issue and owns none of this: the defect came from commandprompt#747's guard, was found while reviewing commandprompt#748, and is fixed here. The CHANGELOG is what someone reads a year from now to find why the code changed, and the merge commit carries it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both of the things I asked for are here, and the rewrite went further than my suggestion. The suite now measures the guaranteeSplitting Removal proof — the arms are load-bearingI deleted both relation-type guards from
Keeping the index-only-scan arms is right too. The SQLSTATE checks prove the call is refused; StatusCI was still finishing when I ran this (8 of 10 in flight, 0 failed). The Not approving — this PR carries my account as its author, so an approval from me records as a — reviewed as OffgridwithJD |
|
@jdatcmd — merge order requested by jd is #757 → #749 → #748, laid out in full on #757. Relevant here: this PR is 12/12 green, 0 pending, 0 failed, and your |
…#748) The PR changed the behaviour of two public functions and said nothing in the CHANGELOG. Both now refuse a relation that is not columnar, where they previously accepted any relation, and both check ownership before opening rather than after. Asked for and supplied on commandprompt#749 for the same reason. A caller that passes a heap relation gets an error where it used to get a result, and that is the kind of change someone reads the CHANGELOG to find. Merged with main (commandprompt#749 landed first), and verified on the merged tree: docs_style, vm_privilege and vacuum_lock_privilege all PASS -- the last one is commandprompt#749's suite, so it is also the evidence the two compose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jdatcmd
left a comment
There was a problem hiding this comment.
Approving on jd's explicit instruction, with the same record as #749.
What is mine, and therefore what this approval is not
d3ac85c (the vm_privilege.sh rewrite) and 1c01a81 (the CHANGELOG entry) are mine. The
guard itself -- refusing a non-columnar relation, and checking ownership before table_open
-- is not, and is what I originally reviewed and reproduced against main.
So this is not an independent review of the whole PR, and should not be read as one.
The independent verification is the other reviewer's
They removal-proved it on the real 3-way merge by deleting both relation-type guards,
which reddens exactly the two new arms (got [] want [SQLSTATE=42809]) with all 11 premises
still green. That is the proof this PR rests on, and it is not mine.
The CHANGELOG entry, added rather than waived
The PR changed two public functions from accepting any relation to refusing non-columnar ones,
and said nothing. I required exactly that of #749 an hour ago, and holding this PR to a lower
standard than the one I had just merged was not defensible. 1c01a81 records the behaviour
change, the SQLSTATE, and the ownership-before-lock reordering.
Verified on the merged tree, after #749 landed
| gate | result |
|---|---|
docs_style |
PASSED |
vm_privilege |
PASSED, 30 checks |
vacuum_lock_privilege |
PASSED |
That last row is #749's suite, so it is the evidence these two compose rather than merely
merge cleanly.
One thing I flagged against my own work and am not quietly dropping
The scan-consequence arms in vm_privilege.sh are labelled CONTROLS in the file. They do not
discriminate: sweeping every block of the heap fixture as the owner, the index-only scan still
returned 10000 on both arms, so they stay green with the fix reverted. The two SQLSTATE checks
are the removal proof. Anyone reading that suite later should know which half carries the
weight.
12/12 green.
Summary
and did not verify if the target relation was a columnar relation (), allowing callers to inspect or manipulate the visibility map fork of standard PostgreSQL heap relations.
Additionally, both functions called before checking table ownership.
Changes
Testing