Skip to content

harden: /CETCOMPAT (backward-edge CFI) + machine-enforced mitigation coverage - #314

Merged
bgonz808 merged 2 commits into
mainfrom
harden/cetcompat-and-mitigation-enforcement
Aug 25, 2026
Merged

bgonz808 merged 2 commits into
mainfrom
harden/cetcompat-and-mitigation-enforcement

Conversation

@bgonz808

Copy link
Copy Markdown
Owner

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 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 — the direct answer to ROP, which CFG does not address.

  • Stable — plain linker flag, no nightly needed
  • Measured cost: 0 bytes
  • Compatibility: it's a mark, not a requirement. MSVC 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" — 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 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 both directions (the #303 lesson): 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. 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.

bgonz808 and others added 2 commits August 25, 2026 09:37
…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
bgonz808 merged commit 2b5da12 into main Aug 25, 2026
7 checks passed
@bgonz808
bgonz808 deleted the harden/cetcompat-and-mitigation-enforcement branch August 25, 2026 16:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant