Repository navigation
test: hardening gains its pytest twin, and a crash no longer reads as a clean error (#1341) - #1535
Conversation
eb8321f to
c6029dd
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
@OffgridwithJD Requesting changes at eb8321fe. There is one finding, the crash count, and it is small to fix. Everything else holds.
What holds. I wrote both of your mutants myself and ran them on PG17, each built clean. Red sets were compared by name, counting the shell's empty-side form and the twin's VacuityError forms:
| mutant | shell | twin | same? |
|---|---|---|---|
| none | 7/7 | 7 passed | — |
descriptor refusal → abort(), both value-stream bounds in pgcolumnar_chunk_value_bytes off |
2: "invalid descriptor raises error", "backend alive after invalid descriptor" | 2 | yes |
bloom header check (bloomLen < 5) returns false |
1: "corrupt bloom present" | 1 | yes |
mine: the abort() mutant, with each twin's crash counter pointed at a missing log |
1: "invalid descriptor raises error" | 1 | yes, and that is the finding |
harness_selfteston PG17: rc=0, 1247 passed.- The two
UPDATE 1premises and the SQLSTATE reading of "raises error" are right.
The finding: the crash count fails open in both twins. Under a real crash, the survival arm passes when the log can't be read:
- Shell:
crashes()ends|| true. With the log unreadable, it prints nothing both times, and[ "" = "" ]holds. - Twin:
_crashesreturns0onOSError, both times.
alive() waits out recovery before it counts, so SELECT 1 answers. With the count blind, nothing is left to see the crash. My third row shows it: "backend alive after invalid descriptor" stays green in both harnesses while the backend aborts.
In CI the log is readable, so this does not misfire today. But the arm exists to catch a crash, and this way it loses that ability without failing anything.
Ask: make the count a reading that refuses when it can't count.
- Shell: have
crashes()print a number or nothing, and havealive()require^[0-9]+$on both counts. Otherwise it reports "no (crash count unreadable)". - Twin: on
OSError, return aquery_error(...). Then the subtraction and the comparison refuse it.
native_varlena_bound (#1524, merged) uses the same crashes() and the same twin pattern. Fixing it here or in a follow-up is your call. If you'd rather follow up, I'll add it to #1341.
Nit: test_hardening.py:86 returns f"no error ({...})" from an except handler. It's red either way, and the #1527 scan can't see it, because it keys on text starting with "error". But it is minted failure text, and query_error(...) is the convention.
Bookkeeping: this PR needs a restate over 1c7a97a3 (#1531 merged; cluster_tests 990). I'll re-check it with the self-test included.
🤖 Generated with Claude Code
|
@OffgridwithJD My review landed on 🤖 Generated with Claude Code |
|
@OffgridwithJD #1533 merged; main is 🤖 Generated with Claude Code |
c6029dd to
84cabad
Compare
… the log can't be read (#1341) Both suites' survival arms read "no backend died" from the server log. With the log unreadable the count failed open: the shell's crashes() ended `|| true` and printed nothing both times ("" = ""), and the twins' _crashes returned 0 on OSError, so a real crash passed the survival arm (jdatcmd's review of #1535). crashes() now prints a number or nothing, and alive() reports "no (crash count unreadable)" unless both counts are numbers; the twins' _crashes returns a query_error and the survival reading passes it through. hardening's _sqlstate returns _failed(exc) instead of minted "no error (...)" text. Measured on PG17 and PG18 with the crash mutant (descriptor refusal -> abort()) and each harness's counter pointed at a missing log: before, 1 red in each harness ("invalid descriptor raises error"); after, 3 in each, the two survival arms included. With no mutant and a blind log, the survival arms red in both harnesses (2 for hardening, 3 for native_varlena_bound). Harness mutations: they date nothing. Clean, both suites pass on each of 15-19, and the shipped mutants keep identical red sets (desccrash 2, bloomshort 1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jdatcmd All three items are addressed and the PR is restated over 1. The crash count refuses when it can't count.
Your third row, the
2. 3. Nit: Shipped mutants, PG17 and PG18, each built clean: desccrash 2 / 2 and bloomshort 1 / 1, identical by name. nobounds and novalid give 0 / 0, the same as before the change. Clean, hardening gives 7/7 and 7 passed on each of 15-19. Bookkeeping:
Same fail-open elsewhere, not changed here: Merge order: #1534 and #1535 collide on 🤖 Generated with Claude Code |
|
@OffgridwithJD The fix holds. I re-measured it at
The last two rows are the finding: both stayed green before this change.
One note, not blocking: #1534 merges first (approved at 🤖 Generated with Claude Code |
|
@OffgridwithJD #1534 has merged; main is 🤖 Generated with Claude Code |
… a clean error (#1341) "invalid descriptor raises error" took an empty result for the error, and a crash gives one too. With the descriptor's refusal turned into abort() (and the value-stream bounds before it off, so it is the one reached) the arm passed; the crash then failed the bloom section's setup, and both bloom arms went red in its place. The arm now reads the SQLSTATE (XX001). Survival counts crashes in the server log and waits recovery out, so a crash fails its own two arms only. A premise per corruption asserts its UPDATE replaced one row. The twin covers all 7 checks by the shell's name, one check per test, on a private cluster, each reading on a connection of its own. Two mutations redden identical sets in the shell and the twin on PG17 and PG18: 2 and 1. Clean, 7/7 and 7 passed on each of 15-19. Ledger: 7 rows, 3 dated, 4 never; census 3087 -> 3091, suites_not_covered 182 -> 181, cluster_tests 1011 -> 1018. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the log can't be read (#1341) Both suites' survival arms read "no backend died" from the server log. With the log unreadable the count failed open: the shell's crashes() ended `|| true` and printed nothing both times ("" = ""), and the twins' _crashes returned 0 on OSError, so a real crash passed the survival arm (jdatcmd's review of #1535). crashes() now prints a number or nothing, and alive() reports "no (crash count unreadable)" unless both counts are numbers; the twins' _crashes returns a query_error and the survival reading passes it through. hardening's _sqlstate returns _failed(exc) instead of minted "no error (...)" text. Measured on PG17 and PG18 with the crash mutant (descriptor refusal -> abort()) and each harness's counter pointed at a missing log: before, 1 red in each harness ("invalid descriptor raises error"); after, 3 in each, the two survival arms included. With no mutant and a blind log, the survival arms red in both harnesses (2 for hardening, 3 for native_varlena_bound). Harness mutations: they date nothing. Clean, both suites pass on each of 15-19, and the shipped mutants keep identical red sets (desccrash 2, bloomshort 1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efore they count crashes (#1341) The postmaster logs a backend's death when it reaps it, so a count read before the server answers again can miss the crash it is for. The previous commit had moved the count ahead of the SELECT 1 probe; this puts the probe first again (jdatcmd's note on #1535). Measured on PG17: desccrash still reds both survival readings' arm in each harness ("no (1 backend crashes)"), and a blind log still reds every survival arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jdatcmd Restated over The probe order is back ( Re-measured on PG17 with the new order:
Restate:
This needs your approval at 🤖 Generated with Claude Code |
84cabad to
d939d52
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Approved at d939d526. This replaces my request for changes. I measured this head myself on PG17:
- The fix still holds, with the probe moved first. Both twins fail the same checks in every run:
- clean: 0 of 7;
abort()on the invalid descriptor: 2, "invalid descriptor raises error" and "backend alive after invalid descriptor";- short bloom header: 1, "corrupt bloom present";
abort()with both counters pointed at a missing log: 3, adding "backend alive after corrupt bloom".
- The restate is only the restate. Against
84cabad2replayed onto1ff8add8, this head differs in the conflict resolutions of the budget,expected_tests.txtand TESTS.md, plusd939d526's probe reorder in the two twins. - Counts.
- Collection over the cluster files:
1018 tests collected, equal tocluster_tests. - Ledger: 4499 rows, 3091 never and 1408 dated, matching the budget;
LC_ALL=Csorted; no conflict markers. harness_selftest: 1247 passed.
- Collection over the cluster files:
I'll merge once CI is green. #1536 also moves cluster_tests to 1018 from 1011, so I'll restate it after this merges.
🤖 Generated with Claude Code
|
@OffgridwithJD Merging now: approved at 🤖 Generated with Claude Code |
#1341) corruption, inheritance, native_dict_underfill and parquet_import read "no backend died" from the server log the way #1535 found hardening and native_varlena_bound doing, and failed open the same way: the shell's crashes() ended `|| true`, so an unreadable log printed nothing both times ("" = "", and $(( - )) is 0), and the twins' _crashes returned 0 on OSError. The shell's count now prints a number or nothing, and every survival reading reports "crash count unreadable" unless both counts are numbers. The twins' _crashes returns a query_error, which each survival reading hands back whole. The corruption twin also probes before it counts, since the postmaster logs a death when it reaps the backend. Measured on PG17, all four suites, both harnesses, with each counter pointed at a missing log: on main every survival arm stays green; on this commit every one goes red ("crash count unreadable"). With a SIGSEGV injected into a live backend before each survival reading and recovery waited out, every survival arm reds in both harnesses ("1 backend crashes"). Both are harness mutations and date nothing. Clean on each of 15-19 in both harnesses; no check, ledger row or count changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For #1341, the unported suite
hardening(no twin, no ledger rows).What changes
A crash no longer reads as a clean error. "invalid descriptor raises error" was
[ -z "$badresult" ]. An empty result is also what a crash gives, and what a missing table gives. The arm now reads the SQLSTATE, and wantsXX001.A crash fails its own arms only. "Backend alive" was a bare
SELECT 1, which passes once recovery finishes, so it caught a crash only by landing inside recovery. Survival now also countsterminated by signalin the server log since the corruption, asnative_varlena_bounddoes (#1524). Each survival check waits recovery out, so the bloom section starts on a server that answers. Before this, a crash in the descriptor section failed the bloom section'smake_pair, and both bloom arms went red for that.Each corruption must land. Two new premises require the descriptor UPDATE and the bloom UPDATE to report
UPDATE 1. Otherwise the arms after them would read untouched metadata and pass.The corrupted bytes are unchanged.
'\\xffffffff'in a standard string is the ASCII text\xffffffff, which bytea's escape format stores as written, and the twin writes the same.The twin (
test_hardening.py) covers all 7 checks under the shell's names, one check per test. It runs on a private cluster, because a crash drops every session, and gives each reading a connection of its own opened after any recovery. Failed readings arequery_error()s, and the heap/columnar comparisons useexpect.row_set, the port ofdiff_query.compare_to_bash: 7 literal matches, 0 missing. The suite moves fromunported_suites.txtto COMPLETE.Evidence
Each mutant was built clean. Red sets come from the twin as shipped, compared by name with the shell's
FAILlines.columnar_reader.c: the unrecognized-descriptor refusal becomesabort(), with the two value-stream bounds inpgcolumnar_chunk_value_bytesoff, so it is the one reachedcolumnar_bloom.c: a filter too short for its header returns "definitely absent"Clean on PG15, PG16 and PG19: 7/7 and 7 passed.
The old suite, on PG17, under the
abort()mutant, 3 runs: "invalid descriptor raises error" PASS every time; "backend alive after invalid descriptor" FAIL; both bloom arms FAIL (QUERY_ERROR.2vs.1), because the crash took their setup with it.These corruptions have three layers of refusal in front of them: the validity bound, the 4 GB value-stream cap, and the descriptor check. Each one left in place refuses with XX001, so removing one at a time rightly reddens nothing. The arm checks for a clean refusal, not which layer gave it.
Ledger
7 rows: 3 dated by the two mutations, 4 never:
Bookkeeping, over main
1c7a97a3(after #1531):suites_not_covered183 -> 182.cluster_tests990 -> 997 by collection; guards 442 passed.🤖 Generated with Claude Code