test(tests): pin the prologue scan order the interior refusals depend on - #326
Conversation
Two refusals in the Intel candidate scan decide by asking whether an address is already a candidate: _opensInsideAnEarlierPrologue, which declines a prologue match beginning exactly where an earlier one ends, and the hotpatch adjustment beside it. Both are directional, and hold only because the prologue the match sits inside is scanned first. Nothing said so. The behavioural tests do fail when the order changes, but they fail as an unexpected address, which invites quieting them by editing the expected set and silently drops the suppression the rule was measured to be worth. Three tests state the orders instead. The base family is scanned before the 64-bit family, which is what the clang pair the interior rule was written for depends on. A pattern that is the tail of a longer one is scanned after it, which covers the MSVC hotpatch pad over the bare frame prologue and any pattern added later that ends with one already listed. A 32-bit scan reaches only the base family. The CET pad's position is deliberately not pinned. Scanned after the base family it does seed push rbp; mov rbp, rsp four bytes into every endbr64 entry, 19,536 times over the 260-cell built C/C++ matrix and across 40 of its cells, but the entry is recovered from the pad and the interior candidate is absorbed into it: TP, FP and FN are identical either way on the nine cells where the pattern is densest.
|
Reviewed here and good to go. And your correction to the write-up is accepted — "not a comment, not a test" was wrong on the test half. The behavioural cases do fail on both reorder shapes; what they lack is legibility, and being quietened by editing an expected set is exactly how the suppression would have gone away quietly. The second site is a real gap in the item as I filed it. Stating both invariants over the pattern lists rather than over the pairs that exist today is the part that will still be working in a year: a prologue added later that ends with one already listed is covered without anyone remembering to come back here. Leaving the CET pad's position unpinned is also the right call, and the measurement for why — 19,536 interior seeds over 40 cells, no reported function moved, TP/FP/FN identical on the nine densest — is what makes it a decision rather than an omission. Pinning a position that carries nothing would only make the order harder to change for the reasons it should be. |
Item 5 of #322. Test-only.
What is going on
Two refusals in the Intel candidate scan decide by asking whether an address is already a candidate:
_opensInsideAnEarlierPrologue, which declines a prologue match that begins exactly where an earlier one ends, and the hotpatch adjustment a few lines below it. Nothing revisits a match once a later pattern seeds the prologue in front of it, so both are directional — they hold only because the prologue the match sits inside happens to be scanned first.One correction to the finding
The issue says the order is pinned by "not a comment, not a test". The comment part is right; the test part is not. The behavioural tests in this file already fail on both reorder shapes — 2 failures when the two family scans are swapped, 2 when a single pattern moves across the family boundary.
What they lack is legibility. They fail as
AssertionError: 4194324 unexpectedly found in {4194320, 4194324}, which names an address rather than an invariant, and the cheapest way to make that green again is to edit the expected set — which silently drops the suppression the rule was measured to be worth. These three fail as what they are.And one instance the finding missed
Sweeping the class turned up a second site: the hotpatch adjustment at
intel/FunctionCandidateManager.py:704carries the same dependency, and its own comment states it — the pad "is itself a DEFAULT_PROLOGUES entry scanned before the bare form, so it is already a candidate here". That order is withinDEFAULT_PROLOGUES, not across the two families, so a test pinning only the family order leaves it uncovered. Verified: moving the bare form ahead of the pad breakstestTheHotpatchPadIsStillTheEntryItAlwaysWasand nothing else.The relation there is containment —
\x55\x8b\xecis the tail of\x8b\xff\x55\x8b\xec— so the test states it over the pattern lists rather than over the pair that exists today, and a prologue added later that ends with one already listed is covered without anyone remembering to come back.The question the issue left open, measured
It occurs, in one shape, and it is harmless.
Enumerating every ordered pair of seeded patterns whose scan order leaves the refusal unable to fire, and counting occurrences over the 260-cell built C/C++ matrix, gives exactly one populated pair:
endbr64followed immediately bypush rbp; mov rbp, rsp, 19,536 times across 40 of the 260 cells. The CET pad is scanned after the base family, so the frame prologue four bytes into every such entry is seeded before the pad that precedes it — the interior match the rule exists to refuse.It reaches the candidate set and stops there. On
lua_gcc-x64_O0the prologue scan seeds 1,058 candidates atentry + 4of a truth start, matching that cell's raw count exactly; the entry is still recovered from the pad and the interior candidate is absorbed into it. Scanning the pad first instead leaves the report identical on the nine cells where the pattern is densest:Pooled over the six densest cells; the three smaller ones checked separately are identical too. So the pad's position is deliberately left unpinned, and the docstring says why — pinning a position that carries nothing would only make the order harder to change for the reasons it should be.
That also answers whether this wants the other fix the issue offers, making the accumulation order-independent. On this evidence it does not: the one place the directionality is reachable costs nothing in the report, and buying that back would mean restructuring a whole-binary scan and re-measuring the 912 false positives the rule was landed on.
Changed
tests/testInteriorPrologueSuppression.py, +98, nosrc/change.scannedPrologueOrder()records the patternslocatePrologueCandidateshands to the seeding scan.testTheBaseFamilyIsScannedBeforeThe64BitFamily— the family order the interior rule depends on.testAContainingPrologueIsScannedBeforeThePatternItEndsWith— the containment order the hotpatch rule depends on, stated over the pattern lists.testA32BitScanSeedsOnlyTheBaseFamily— the 64-bit family is bitness-gated, so nothing at 32 bits can depend on it.Validation
python -m pytest tests/→ 2063 passed, 1 skipped, 2593 subtestsruff check .andruff format --check .→ cleanmake typecheck→ exit 0, 261 diagnostics, identical to master and 0 error-level