harden: /CETCOMPAT (backward-edge CFI) + machine-enforced mitigation coverage - #314
Merged
Merged
Conversation
…coverage
Two additions, both from the nightly-feature research, and both made ENFORCED rather than
merely set — a mitigation we asked for is not a mitigation we have.
/CETCOMPAT — the missing half of control-flow integrity. `control-flow-guard=checks` has
protected indirect CALLS since the beginning; nothing protected RETURNS. /CETCOMPAT marks
the image compatible with the CPU's hardware shadow stack, which keeps a protected copy of
return addresses and faults on divergence. That is the direct answer to ROP, which CFG does
not address.
* STABLE — a plain linker flag, no nightly needed.
* Measured cost: 0 bytes.
* COMPATIBILITY, since it is a fair question: it is a MARK, not a requirement. Microsoft's
docs describe it as telling the linker to "mark the binary as CET Shadow Stack-
compatible". Windows enables shadow stacks only where CPU and OS support them AND the
image opts in; on hardware without CET there is nothing to enable. Every x86_64 CPU
still runs the binary — the ones with CET run it with more protection. x64-only per the
same docs, which is our only target.
-Z deny-partial-mitigations=stack-protector,control-flow-guard — turns a comment into an
invariant. The config asserted "build-std rebuilds std with it too, so protection is
whole-binary". That was true and unverified. Now the build FAILS if any crate in the graph,
std included, is compiled without those mitigations. Partial application is the dangerous
case precisely because the binary carries the flag and the reassurance while leaving gaps.
Upstream intends this as the default (rust-lang/rust#157941); adopting it now means passing
by construction rather than by luck.
ENFORCEMENT, not just configuration. CET does NOT live in the optional header's
DllCharacteristics like ASLR/DEP/CFG — it travels in a debug-directory entry of type 20
(IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS), so it needed its own reader. CET_COMPAT is now
measured structurally and ratcheted with the rest: lose it and the gate fires. Recorded
under its own name rather than folded into CONTROL_FLOW_GUARD because the two are
independently settable and defend opposite edges — collapsing them would let one vanish
while the manifest still looked complete.
Verified in both directions, per the #303 lesson: the decoder reports CET_COMPAT absent on
every binary built so far (none used the flag), and unit tests pin the positive case,
including that an unrelated extended bit must not read as CET.
EXPECTED on the next build: the caps gate will fire with "hardening CHANGED: binary has
CET_COMPAT, manifest does not declare it". That is the ratchet working; the manifest is
deliberately not pre-updated, because it should describe measured reality rather than assert
a posture ahead of measuring it.
OS-SCOPING, since it was asked: no change needed. These flags live under
[target.x86_64-pc-windows-msvc], so they are already applied per-target by construction. A
future Linux target would carry its own section and would not inherit any of this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… change
The caps ratchet fired on the first build with the new flags, as predicted:
verify-caps: FAIL - 1 manifest violation(s) for hp-thermal:
- hardening CHANGED: binary has CET_COMPAT, manifest does not declare it
Three things are established by that single line, none of which were true before:
1. /CETCOMPAT ACTUALLY LANDED. The reader had only ever been exercised in the negative
direction, because no binary we had built carried the flag. It now reports the bit on a
real artifact, so the positive path is proven on something other than a unit test.
2. deny-partial-mitigations PASSED. The build succeeded, which means every crate in the
graph -- std and its dependencies included -- really is compiled with stack-protector
and control-flow-guard. The config has claimed whole-binary coverage for a long time;
that claim is now machine-checked rather than asserted.
3. The ratchet caught a posture change on the first opportunity, which is what it is for.
Recording CET_COMPAT is the sign-off. Exactly ONE violation was reported: the import surface
did not move, so the two new flags changed protection without changing what the binary
reaches for.
Scope note: this covers hp-thermal only. The producer's RUSTFLAGS for the tool fleet do not
yet include /CETCOMPAT, so the tools still have forward-edge CFI and no backward edge.
Extending it there costs a full producer run and re-bless of all six digests, so it is left
as a deliberate follow-up rather than folded in here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bgonz808
added a commit
that referenced
this pull request
Aug 30, 2026
… fleet Everything upstream of this RC changed since rc.2 (2026-08-25): * tools: all six rebuilt by the DECLARED toolchain (nightly-2026-07-23) after #321 exposed that the pin was installed but never activated; provenance verified from the shipped bytes (#323/#328); CET_COMPAT fleet-wide; build-std on cargo-auditable; all VT panels 0-detection; cargo-acl on a frozen resolution (anyhow 1.0.104, #330) * app config: unchanged since rc.2 (/CETCOMPAT + deny-partial-mitigations, #314) — but this RC is the first BUILT with the blessed pinned-toolchain byte-producing tool (cargo-auditable 6cf437ed830b) Toolchain: nightly-2026-07-23 (unchanged; 38d soaked). Supersedes the abandoned rc.3 candidate branch (14cc57a, nightly-2026-08-25) which defeated the toolchain soak and was never tagged. RC++ until the full gate suite proves it; publishes as v0.3.2 (PATCH++) when ready. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bgonz808
added a commit
that referenced
this pull request
Aug 30, 2026
… fleet (#336) Everything upstream of this RC changed since rc.2 (2026-08-25): * tools: all six rebuilt by the DECLARED toolchain (nightly-2026-07-23) after #321 exposed that the pin was installed but never activated; provenance verified from the shipped bytes (#323/#328); CET_COMPAT fleet-wide; build-std on cargo-auditable; all VT panels 0-detection; cargo-acl on a frozen resolution (anyhow 1.0.104, #330) * app config: unchanged since rc.2 (/CETCOMPAT + deny-partial-mitigations, #314) — but this RC is the first BUILT with the blessed pinned-toolchain byte-producing tool (cargo-auditable 6cf437ed830b) Toolchain: nightly-2026-07-23 (unchanged; 38d soaked). Supersedes the abandoned rc.3 candidate branch (14cc57a, nightly-2026-08-25) which defeated the toolchain soak and was never tagged. RC++ until the full gate suite proves it; publishes as v0.3.2 (PATCH++) when ready. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two additions from the nightly-feature research, both made enforced rather than merely set — a mitigation we asked for is not a mitigation we have.
/CETCOMPAT— the missing half of control-flow integritycontrol-flow-guard=checkshas protected indirect calls since the beginning. Nothing protected returns./CETCOMPATmarks the image compatible with the CPU's hardware shadow stack, which keeps a protected copy of return addresses and faults on divergence — the direct answer to ROP, which CFG does not address.-Z deny-partial-mitigations=stack-protector,control-flow-guardTurns a comment into an invariant. The config asserted "build-std rebuilds std with it too, so protection is whole-binary" — true, and unverified. Now the build fails if any crate in the graph, std included, is compiled without those mitigations.
Partial application is the dangerous case precisely because the binary carries the flag and the reassurance while leaving gaps. Upstream intends this as the default (rust-lang/rust#157941) — adopting now means passing by construction rather than by luck.
Enforcement, not just configuration
CET does not live in the optional header's
DllCharacteristicslike ASLR/DEP/CFG — it travels in a debug-directory entry of type 20 (IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS), so it needed its own reader.CET_COMPATis now measured structurally and ratcheted with the rest: lose it and the gate fires.Recorded under its own name rather than folded into
CONTROL_FLOW_GUARDbecause the two are independently settable and defend opposite edges — collapsing them would let one vanish while the manifest still looked complete.Verified both directions (the #303 lesson): decoder reports
CET_COMPATabsent on every binary built so far (none used the flag), and unit tests pin the positive case — including that an unrelated extended bit must not read as CET. 84 tests pass.Expected on the next build
The caps gate will fire:
hardening CHANGED: binary has CET_COMPAT, manifest does not declare it. That's the ratchet working. Manifests are deliberately not pre-updated — they should describe measured reality, not assert a posture ahead of measuring it.On OS-scoping (asked, and already correct)
No change needed: these flags live under
[target.x86_64-pc-windows-msvc], so they're applied per-target by construction. A future Linux target would carry its own section and inherit none of this.