Skip to content

[AUTOMATED] feat(p2): cortexmpriv — fold the Cortex-M isCurrentModePrivileged() guard (DIV-99) - #357

Merged
mahaloz merged 2 commits into
mainfrom
feat/cortexmpriv
Aug 29, 2026
Merged

[AUTOMATED] feat(p2): cortexmpriv — fold the Cortex-M isCurrentModePrivileged() guard (DIV-99)#357
mahaloz merged 2 commits into
mainfrom
feat/cortexmpriv

Conversation

@mahaloz

@mahaloz mahaloz commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What

specs/Ghidra/Processors/ARM/data/languages/ARMTHUMBinstructions.sinc models every Cortex-M (VERSION_7M) MRS/MSR as

b:1 = isCurrentModePrivileged();
if (!b) goto <notPriv>;
<the real effect>

Twelve constructors, all inside @if defined(VERSION_7M). kuna lowers that literally, so each MRS/MSR costs one basic block and two CFG edges that exist in no source. Ghidra shares the defect exactly (GED 26 on NuttX nxsched_process_scheduler); IDA and Binary Ninja model MRS/MSR as a plain intrinsic and score 6 and 0.

How

A synthesized <callotherfixup> for the ARM user op isCurrentModePrivileged returning the constant 1, installed through UserOpManage::manual_call_other_fixup at architecture bootstrap. specs/ is untouched — that is the point of doing this as a fixup rather than a spec edit (git diff main...feat/cortexmpriv -- specs/ is empty).

The option is read at the CALLOTHER consumption gate (decompile_drive::is_injected_userop), not at registration, because the payload is installed before any option line is processed.

Default OFF, in AGGRESSIVE_OVERRIDES. "The core is privileged" is a modelling judgement, not a proof — Cortex-M Thread mode can run unprivileged — which is exactly what an option is for.

Measured

Sample set: +2 overall-perfect, both verified individually through decbench's own GEDMetric:

case GED off GED on
O0-riot-os-hello-world-thread_sleep 24 0
O0-nuttx-nuttx-nxsched_process_scheduler 26 0
O0-nuttx-nuttx-nxsig_find_pendingsignal 18 9 (improvement)
O2-noinline-freertos-RTOSDemo-xFirstTimerHandler 84 14 (improvement)

The author explicitly corrected the brief here: it is +2, not +4 — the other two named cases improve but do not reach GED 0.

Corpus (the eight Cortex-M projects, the only population that can change; 90 slices / 31,044 functions in both arms): to GED-perfect 93, off GED-perfect 0 (McNemar z = 9.64), improved 336 / worsened 98, aggregate GED −6,004 (−7,033 removed against +1,029 added).

Zero collateral, measured not assumed: over the 26 guard-bearing binaries (46,747 bodies) discovery is identical on all 26 (same function set, same error count), and of 868 changed bodies all 868 are guard-bearing.

Speed flat, established with a null control rather than a bare number: running the identical harness off-vs-itself reads −4.8%, so anything inside ±5% at that rep count is noise. The two highest-rep readings are nuttx −0.7% and chibios +0.2% (both min-of-21). Off ARM the payload is never registered, so the cost is structurally zero.

Honest caveat carried in the DIV row: 98 of 434 changed functions score worse on GED (+1,029 against −7,033). That is the metric, not lost information.

Gates

  • make test675/675 PARITY OK (no re-pin; the datatest corpus has no Cortex-M bytechunk and cannot observe this)
  • make test-stages574/574 PARITY OK with the new two-pass tests/stages/kuna-cortexmpriv.xml
  • make rust-testgreen, 330 result blocks, 0 failures
  • make check-spec — green in lenient and strict mode
  • kuna catalog --check — green

Rebased onto voidtailreturn (#356); all six hard-coded catalog counts reconciled to base+both (127 settables, tier 28/52/47, live 39, static rows 126, corpus 222 files) and the DIV renumbered 99. An independent adversarial review re-ran every gate in its own worktree and re-scored both flips: verdict ship.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FvpHGrvHyKDNmLSPVnZw2G

mahaloz and others added 2 commits August 29, 2026 17:32
…ivileged() guard (DIV-99)

`ARMTHUMBinstructions.sinc` models every VERSION_7M (Cortex-M) special-register
move as a RUNTIME privilege test rather than as one operation:

    b:1 = isCurrentModePrivileged();
    if (!b) goto <notPriv>;
    <the real effect>
  <notPriv>

Twelve constructors do it — `mrs`/`msr` against ipsr, primask, basepri,
basepri_max, faultmask and control — so kuna, lowering the model literally, gives
EVERY MRS/MSR one extra basic block and two extra CFG edges that exist in no
source.  A four-instruction `irq_disable`/`irq_restore` pair therefore lands four
phantom branches in the middle of an otherwise straight-line function; RIOT's
`thread_sleep` and NuttX's `nxsched_process_scheduler` score GED 24 and 26
against straight-line source for that reason alone.  Upstream Ghidra shares the
defect; IDA and Binary Ninja model MRS/MSR as a plain intrinsic and score 6 and 0
on the same functions.

`p2_lift/kuna_cortexmpriv.rs` supplies a `<callotherfixup>` body for the user op
— the constant `1` — so the guard condition constant-folds, its block and edges
die, and the real effect survives untouched.  The payload is synthesized in Rust
rather than added to a `.cspec`, so the vendored spec tree stays byte-identical
to upstream, and `architecture.rs (register_cortexmpriv_fixup)` installs it
between the cspec `<callotherfixup>` dispatch and `parse_inject_all` — only where
a local `.sla` can compile it, only on a language whose translator presents the
user op, and only when no compiler spec has already specialized it.

"The core is privileged" is a MODELLING JUDGEMENT, not a proof (Cortex-M Thread
mode can run unprivileged, and there those moves really do read as zero), so it
ships as `option cortexmpriv`, default off and ON in the `aggressive` preset —
which `auto` selects for anything under 500 KiB, i.e. essentially every Cortex-M
image.  Because the architecture bootstraps at `load file`, before any `option`
line, the flag is read at the CONSUMPTION seam instead:
`decompile_drive.rs (is_injected_userop)`, the one live per-CALLOTHER predicate
that sees the applied options.

Measured over the 26 guard-bearing decbench binaries (46,747 function bodies,
`decompile-all --json --max-fn-seconds 120`, exactly as the benchmark runs it):

    discovery            identical on all 26 (nA == nB), errors identical
    changed bodies       868
    of which guarded     868  (ZERO non-guard collateral)
    `cortexmpriv off`    byte-identical to unmodified HEAD on all 46,747 bodies

plus 10 control binaries (4,148 bodies, one of them ARM with no MRS/MSR):
0 changed lines against HEAD at the shipped default.

The synthesized payload compiles at architecture bootstrap on all 15 ARM language
variants (LE and BE, v4 through v8-m), and is a measured no-op on AArch64 and
x86-64.

Gates: `make test` 675/675 PARITY OK (no re-pin), `make test-stages` 572/572
PARITY OK (568 -> 572, baseline re-recorded for the new stage test),
`make check-spec` green in lenient and strict mode, `kuna catalog --check` OK.
Catalog 125 -> 126 settables (transform tier 51 -> 52).  Stages corpus 220 -> 221
files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FvpHGrvHyKDNmLSPVnZw2G
…surement it rests on

The evidence half of the `cortexmpriv` change: the DIV-99 row in `docs/history.md` and
`docs/decbench/cortexmpriv-measurement.md`, the file it points at.  Also moves
`register_cortexmpriv_fixup` below `init_userops_and_fixups` so it stops stealing that
function's doc comment (pure relocation, no code change).

Corpus-scale and bidirectional (`scripts.decbench.optsweep`, 90 Cortex-M slices over
O0 / O2 / O2-noinline, 31,044 functions scored in BOTH arms, one kuna binary invoked
exactly as decbench's `kuna_raw` backend does; baseline arm = the shipped branch
defaults, variant arm = `--option cortexmpriv off`, i.e. main HEAD):

    to GED-perfect        93        off GED-perfect      0     (McNemar z = 9.64)
    improved             336        worsened            98
    aggregate GED    236,133  ->  230,129               -6,004
    GED removed/added -7,033 / +1,029                   6.8 : 1

Controls all clean: 30,600 unchanged bodies score identically, 0 slug-only diffs, and
the baseline arm agrees with the tree's published per-function verdict on
30,612/31,044 (98.6%).  `betaflight` is 533 KB, so `auto` picks `reliable`, the preset
never reaches it, and its two arms are identical -- a control inside the sweep.

DecBench sample set: +2.  Only 49 of the 250 sit on a guard-bearing binary; all 49
re-scored in both arms, four change, none regresses:

    O0-riot-os-hello-world-thread_sleep                24 -> 0   FLIP
    O0-nuttx-nuttx-nxsched_process_scheduler           26 -> 0   FLIP
    O2-noinline-freertos-RTOSDemo-xFirstTimerHandler   84 -> 14
    O0-nuttx-nuttx-nxsig_find_pendingsignal            18 -> 9

Neither flip was perfect on any metric before, so both are genuine `overall` gains.

Speed: flat, and the row says so with a null control rather than a bare number.  The
same arm compared against itself on cf2.elf reads -4.81% at min-of-9, so nothing inside
+-5% at that rep count is signal on this box; the two min-of-21 readings (nuttx -0.7%,
chibios/ch.elf +0.2%) are the usable ones.

Stated plainly in both files: 98 of the 434 changed functions score WORSE on GED.  That
is the metric, not lost recovery -- GED counts blocks and edges, so deleting phantom
blocks widens the node deficit wherever kuna's CFG is already smaller than the source's.
The 336 wins have a median source CFG of 4 nodes, the 98 losses a median of 11, and the
emitted `on` body is always the `off` body minus the guards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FvpHGrvHyKDNmLSPVnZw2G
@mahaloz
mahaloz merged commit de9177f into main Aug 29, 2026
9 checks passed
@mahaloz
mahaloz deleted the feat/cortexmpriv branch August 29, 2026 17:59
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