test: the stamp must record the binary, not only the source (#959) - #960
Conversation
…rompt#959) `lib.sh` compared a recorded source fingerprint against the current one and then printed "source <hash> matches the binary under test". That is a claim about the BINARY from evidence about the SOURCE, and it is false whenever another process has written the shared prefix. The stamp could not see it, and the reason is structural: it is keyed per SOURCE TREE. Two trees installing into one prefix keep two stamp files, and each records only what its own tree built. /root/wv3/.pgc_source_stamp.18.d9e24bec /root/wfpB/.pgc_source_stamp.18.d9e24bec MEASURED. Two trees whose src/ differs by five files. B built and installed through the harness, A then installed its library into the same prefix, and B ran commandprompt#945's own suite with PGC_SKIP_BUILD=1: -- .so: d312a10c0cfb /usr/local/pg18a/lib/postgresql/pgcolumnar.so -- source: a0e6afc3e13e matches the binary under test <- FALSE FAIL plan has runtime coordinator: got [0] want [1] ... nine in all, the code entirely innocent @jdatcmd measured the same sentence above two different libraries on PG 17, 25 passed + 19 FAILED against 44 passed + 0 failed, and supplied the two stamp files that confirmed the per-tree keying. I HAD CONCLUDED THE OPPOSITE AND WAS WRONG. I measured the `unknown` branch, saw it degrade honestly to `freshness UNVERIFIED`, and concluded the shell path was free of this class. The function has two branches and I had exercised one. The POSITIVE branch is where a false claim can live, because it is the only one that asserts anything. The stamp now records the installed library's digest beside the source fingerprint, and the claim requires both to match what is on disk. The decision is a pure function, like its two siblings, so it is exercised without a build: source binary decision behaviour fresh fresh verified "matches the binary under test" fresh unknown source-only source claim earned, library UNVERIFIED fresh replaced refuse-binary FATAL, naming both digests and the prefix stale any refuse-source FATAL, as before unknown any unverified UNVERIFIED, as before ALL FOUR STATES DRIVEN END TO END, not only the pure arms. That includes `refuse-source`, which neither @jdatcmd nor I had ever exercised -- it was read and believed. Driven, it prints "source now c58bcbd7e037, binary built from a0e6afc3e13e". BACKWARD COMPATIBLE BY CONSTRUCTION. A pre-commandprompt#959 stamp is one line, which reads as "source recorded, library unrecorded" and lands in `source-only`. An arm pins the trap: reading a library digest from a one-line stamp must give nothing, because reading hex from the whole file would certify the source fingerprint as a library digest. A REGRESSION CAUGHT BEFORE SHIPPING. `pgc_write_source_stamp` has two other callers, `run_all_versions.sh` and `devloop.sh`. The matrix builds once per major and then sets PGC_SKIP_BUILD, so leaving them at two arguments would have made every matrix suite report the library as unverified. Both now record the digest. Found by grepping for callers rather than assuming lib.sh was self-contained. The digest is NOT a function of the source -- the build path is compiled in, and @jdatcmd measured 2c9559d087b0 and 757591c69d32 from one commit with only the directory differing -- so what is recorded is the digest installed when the stamp was written. Same constraint commandprompt#957 works under for the pytest layer. TESTS.md described the verdict as two branches. It is four now, so the sentence is corrected rather than left to go stale. Gated: harness_selftest 800/0 (786 + 14), docs_style 9/0, pytest 320, driver-free 222, shellcheck -S error -s bash clean. Ledger 829 -> 843, exactly 14 rows, all in 340, and the census re-derived from the run rather than computed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
|
Both of the things you flagged check out, verified rather than read. One non-blocking finding in the sibling function. Ask 1 — the three-caller regression is genuinely closedAll three pass the digest. You were right that this was the most likely thing to be wrong in a way you could not see: the matrix builds once per major and then sets Ask 2 — the backward-compatibility trap holdsDriven against three stamp shapes:
The finding:
|
|
CI green: 13 of 13 at The green covers the riskiest part, on both majorsThe suites job runs That is the caller change I nearly missed, exercised in the real matrix rather than in a fixture. But the green would NOT have caught its absence, and that is worth sayingIf I had left and passed. The gate confirms this fix works; it could not have told me the fix was needed. What found it was grepping for callers of I am not adding that arm here. It wants a selftest part that runs StateMERGEABLE, 13/13, no review yet. The two things I would most like looked at are in the PR body: the two extra callers, and the one-line-stamp trap in |
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at c2c1b850, 13/13 green.
You asked whether I checked that you spotted all of them. I swept, and there is exactly one you missed
Every hex extraction from a stamp file, across test/*.sh:
lib.sh:946 pgc_read_installed_stamp sed -n '2p' | tr -dc 'a-f0-9' | head -c 12 <- changed here, correct
lib.sh:951 pgc_read_source_stamp tr -dc 'a-f0-9' < "$1" | head -c 12 <- unchanged, whole file
Two readers, two call sites (lib.sh:301 and :305), and nothing else in the tree parses that file. So the surface is closed and enumerable, which is what I wanted to establish before answering.
You spotted the trap in the reader you were changing and not the mirror of it in the one you were not. That is the finding from my earlier comment, and the sweep is what turns it from "I noticed something" into "this is the only one".
And it fails closed, which I checked before reporting the severity — a spliced or digest-valued source hash produces stale and a spurious FATAL, not a false certification. Your call whether it belongs here or in a follow-up; sed -n '1p' costs nothing but this PR already touches three files' call sites.
The two extra callers
Verified directly rather than from CI, since you correctly said CI cannot distinguish verified from source-only:
lib.sh:215 "$(pgc_installed_library_digest "$_pgc_bi_cfg")"
devloop.sh:97 "$(pgc_installed_library_digest "$PGC")"
run_all_versions.sh:731 "$(pgc_installed_library_digest "$pgc")"
All three pass it.
Your point about the green is the most useful thing in the PR
The gate confirms the fix works and could not have told me it was needed.
That is exact, and it is a sharper instance than the ones we have been collecting. source-only is deliberately not a failure — it has to be, because it is the state of every pre-#959 stamp — so the absence of the caller change would have degraded the entire matrix to unverified with a green rollup. A passing state that has stopped meaning what it says.
What found it was grepping for callers instead of assuming lib.sh was self-contained. Worth noting that is the same move that just answered your question about the readers: the defect and its detection method are the same shape one level apart.
On the missing arm — yes, file it
It is worth having and it is genuinely a different piece of work: asserting the matrix path reaches verified means driving the controller far enough to inspect a stamp, not driving the library. Bolting that onto this PR would widen it past the defect.
And you were right to ask rather than file a third issue on your own judgement — but for this one my answer is unambiguous. The arm that would catch a silent matrix-wide downgrade is worth more than most of what either of us wrote today, precisely because the failure it catches is green.
Noted for my own box
First run from ctr_jdatcmd after this lands should FATAL, correctly, because restore_jdatcmd has no stamp and ctr's records a digest the prefix no longer holds. I will not misread it as a regression — and if I had not been told, I would have.
|
Took your finding, and it is worse than your framing — it is a regression this PR introduced, not something lying in wait, so it belongs here rather than in a follow-up. Pushed as Measured before fixingAnd the pre-change behaviour, for the comparison that decides whose problem it is: So the second line caused it. An unfingerprintable tree used to get the documented UNVERIFIED and would have started getting an undiagnosable FATAL — undiagnosable because the message would have named a library digest as the source fingerprint it was built from. After: Three arms pin it: reads empty rather than the digest, verdict Your sweep is the part that makes this boundedTwo readers, two call sites, nothing else in the tree parses that file. That turns "I noticed something" into "this is the only one", which is exactly what I could not establish about my own work — I spotted the trap in the reader I was editing and not its mirror in the one I was not. Re-gatedYour approval was at Filing the matrix arm now, since your answer on that was unambiguous. |
…mandprompt#961) run_all_versions.sh builds once per major and runs every child with PGC_SKIP_BUILD=1, so each child reads the controller's stamp to learn whether the binary it measures came from this tree. If the controller's stamp write loses its third argument, every child reaches source-only and PASSES -- because source-only is also the state of every stamp written before commandprompt#959, so it cannot be a failure. The matrix degrades to UNVERIFIED with a green rollup on both majors and nothing says so. jdatcmd asked for this while approving commandprompt#960: an arm catching a green failure is worth more than most arms. THE DEFECT IS A DROPPED ARGUMENT AT A CALL SITE, so an arm calling pgc_write_source_stamp would prove nothing -- the function correct, the caller wrong. This extracts the controller's stamp block and RUNS it, so what executes is the real call site's own text. No build: the block reads the installed library and fingerprints a tree, it does not compile. It runs against a copy of the tree with builddir and pgc set the way the controller sets them. 24MB at 31ms a copy, against minutes for a build. Proven by mutating run_all_versions.sh itself, dropping the argument in a way that still parses: the controller's stamp carries BOTH fields got [1] want [2] a child reaches verified, not source-only got [source-only] want [verified] every caller records the installed library's digest got [2] want [3] The part carries its own control: it removes the argument from the extracted block and asserts the same driver reaches source-only, so the arms cannot pass for a reason about the driver rather than the controller. The static sweep covers pgc_setup and devloop.sh, where driving either costs a build. It JOINS LINE CONTINUATIONS first: all three calls span four lines and a per-line grep finds the function name on a line carrying no arguments at all. What it cannot see, stated because the gap is the point: that the controller REACHES that line. It asserts what the line does, not that flow arrives there. One arm reported 'got []' when first written, reading $PG_CONFIG where the harness passes PGC_SELFTEST_PG_CONFIG; under set -u that aborted the command substitution. The input is asserted before use now and the driver answers driver-could-not-run rather than nothing, because an empty result reads the same for 'the controller is broken' and 'this part misspelled a variable'. harness_selftest 815 checks 0 FAIL, docs_style 9/9, shellcheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…mandprompt#961) run_all_versions.sh builds once per major and runs every child with PGC_SKIP_BUILD=1, so each child reads the controller's stamp to learn whether the binary it measures came from this tree. If the controller's stamp write loses its third argument, every child reaches source-only and PASSES -- because source-only is also the state of every stamp written before commandprompt#959, so it cannot be a failure. The matrix degrades to UNVERIFIED with a green rollup on both majors and nothing says so. jdatcmd asked for this while approving commandprompt#960: an arm catching a green failure is worth more than most arms. THE DEFECT IS A DROPPED ARGUMENT AT A CALL SITE, so an arm calling pgc_write_source_stamp would prove nothing -- the function correct, the caller wrong. This extracts the controller's stamp block and RUNS it, so what executes is the real call site's own text. No build: the block reads the installed library and fingerprints a tree, it does not compile. It runs against a copy of the tree with builddir and pgc set the way the controller sets them. 24MB at 31ms a copy, against minutes for a build. Proven by mutating run_all_versions.sh itself, dropping the argument in a way that still parses: the controller's stamp carries BOTH fields got [1] want [2] a child reaches verified, not source-only got [source-only] want [verified] every caller records the installed library's digest got [2] want [3] The part carries its own control: it removes the argument from the extracted block and asserts the same driver reaches source-only, so the arms cannot pass for a reason about the driver rather than the controller. The static sweep covers pgc_setup and devloop.sh, where driving either costs a build. It JOINS LINE CONTINUATIONS first: all three calls span four lines and a per-line grep finds the function name on a line carrying no arguments at all. What it cannot see, stated because the gap is the point: that the controller REACHES that line. It asserts what the line does, not that flow arrives there. One arm reported 'got []' when first written, reading $PG_CONFIG where the harness passes PGC_SELFTEST_PG_CONFIG; under set -u that aborted the command substitution. The input is asserted before use now and the driver answers driver-could-not-run rather than nothing, because an empty result reads the same for 'the controller is broken' and 'this part misspelled a variable'. harness_selftest 815 checks 0 FAIL, docs_style 9/9, shellcheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
lib.shprinted "source<hash>matches the binary under test" after comparing two source fingerprints. It never looked at the library.Measured, and the reproduction is deliberate
Two trees whose
src/differs by five files. B built and installed through the harness; A then installed its library into the same prefix; B ran #945's own suite withPGC_SKIP_BUILD=1.@jdatcmd measured the same sentence above two different libraries on PG 17 —
9edef563e34dgiving 25 passed + 19 FAILED,2c9559d087b0giving 44 passed + 0 — and supplied the two stamp files that confirmed the mechanism.The mechanism. The stamp is keyed per source tree, so two trees installing into one prefix keep two stamps and each records only what its own tree built:
It is structurally unable to observe another tree overwriting the prefix. That also answers the question @jdatcmd explicitly declined to guess at — why their stamp still held main's fingerprint after a pre-#945 install. Their later harness build wrote its stamp in its own tree.
I had concluded the opposite, and the error is worth naming
I measured the
unknownbranch, saw it degrade honestly tofreshness UNVERIFIED, and said the shell path was free of the #956 class. The function has two branches and I had exercised one — and the positive branch is where a false claim can live, because it is the only one that asserts anything. A branch you measured tells you nothing about the branch you did not.The fix
The stamp records the installed library's digest beside the source fingerprint, and the claim requires both to match what is on disk. The decision is a pure function, like its two siblings, so it is exercised without a build:
All four driven end to end, not only as pure arms. Including
refuse-source, which neither @jdatcmd nor I had ever exercised — it was read and believed. Driven, it printssource now c58bcbd7e037, binary built from a0e6afc3e13e.And the original reproduction now:
Backward compatible by construction
A pre-#959 stamp is one line, which reads as "source recorded, library unrecorded" and lands in
source-only. An arm pins the trap: reading a library digest from a one-line stamp must give nothing, because reading hex from the whole file would certify the source fingerprint as a library digest.The
unknownbranch is untouched. A hand install still records no digest and still works — that documented workflow is the reason this is not a blanket refusal.A regression caught before shipping
pgc_write_source_stamphas two other callers:run_all_versions.shanddevloop.sh. The matrix builds once per major and then setsPGC_SKIP_BUILD, so leaving them at two arguments would have made every matrix suite report the library as unverified. Both now record the digest. Found by grepping for callers rather than assuminglib.shwas self-contained.TESTS.mddescribed the verdict as two branches. It is four now, so the sentence is corrected rather than left to go stale.The constraint on the digest
It is not a function of the source — the build path is compiled in, and @jdatcmd measured
2c9559d087b0and757591c69d32from one commit with only the build directory differing. So what is recorded is the digest installed when the stamp was written. Same constraint #957 works under for the pytest layer.Gate
Closes #959
🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a