Skip to content

ibex: clear cpuctrlsts.sync_exc_seen on dret, matching mret - #2487

Open
john-kearney wants to merge 1 commit into
lowRISC:masterfrom
john-kearney:dret-clear-sync-exc-seen
Open

ibex: clear cpuctrlsts.sync_exc_seen on dret, matching mret#2487
john-kearney wants to merge 1 commit into
lowRISC:masterfrom
john-kearney:dret-clear-sync-exc-seen

Conversation

@john-kearney

Copy link
Copy Markdown

Problem

cpuctrlsts.sync_exc_seen is the flag behind Ibex's double-fault detection:
it is set on every synchronous exception and cleared by mret
(rtl/ibex_cs_registers.sv, csr_restore_mret_i arm). The dret arm
(csr_restore_dret_i) restores priv_lvl only — it does not clear the
flag.

A debugger that halts the core inside an exception handler (a debug_req
halt) and then resumes somewhere else (set dpc, dret — the standard
GDB "jump"/skip-frame flow) abandons that handler. Per the documented
contract, a double fault is a synchronous exception occurring whilst
handling
a previous synchronous exception, cleared by mret
(doc/03_reference/exception_interrupts.rst,
:ref:double-fault-detect). After the dret, the core is no longer
handling anything, yet sync_exc_seen stays 1 indefinitely (until some
later unrelated mret or a software cpuctrl write).

Consequence: the next synchronous exception — any class, any context,
arbitrarily later — falsely pulses the double_fault_seen_o SoC escalation
output for one cycle and falsely latches the HW-set/SW-clear
cpuctrlsts.double_fault_seen audit bit. Consumers such as OpenTitan wire
double_fault_seen_o into escalation, so a routine debug session can cause
a spurious SoC escalation/lockout plus permanently false audit state.

Reproduction on simple_system (Verilator): illegal instruction → handler
asserts debug_req and spins → debug ROM sets dpc=resume, dret
unrelated benign ecall. Before this patch: double_fault_seen_o pulses
and double_fault_seen latches. After: no pulse, flag reads 0 after
dret.

Change

  • rtl/ibex_cs_registers.sv: in the csr_restore_dret_i (DRET) arm, clear
    sync_exc_seen exactly as the MRET arm does (cpuctrlsts_part_we = 1'b1; cpuctrlsts_part_d.sync_exc_seen = 1'b0;). Not parameter-gated — applies
    to every configuration.
  • rtl/ibex_top.sv: update the RVFI double-fault predictor in lockstep so
    it clears its modelled sync_exc_seen on dret (0x7b200073) as well
    as mret (0x30200073); otherwise the DoubleFaultPulse* assertions
    would diverge from the RTL after a debug session.
  • doc/03_reference/exception_interrupts.rst,
    doc/03_reference/cs_registers.rst: document that the flag is cleared by
    mret or dret.
  • examples/simple_system/rtl/ibex_simple_system.sv: small test-support
    addition — a DbgCtrl bus device (0x40000) to drive the debug_req pin
    from software and observe double_fault_seen_o pulses and live
    sync_exc_seen, enabling a directed regression test without a full debug
    module.

Design note

dret returning into the interrupted handler would, with this change,
arrive with sync_exc_seen cleared; if that handler then faults again
before its mret, the double fault is not flagged. This is the safe
direction: the debugger has taken control of the flow (and can re-arm the
flag with a single csrw if it deliberately resumes fault-storm
analysis), whereas the current behaviour escalates the SoC on an exception
that is provably not a double fault. If maintainers prefer, the clear
could be restricted to drets that redirect away from the handler
(dpc outside the handler range), at the cost of tracking handler bounds
the hardware does not currently have.

Known trade-off: a dret that resumes inside the handler (adversarial differential test)

The unconditional clear is deliberately not semantically identical to mret. mret is
the defined terminator of the handling window, so no true double fault can ever straddle an
mret. dret has a semantics mret lacks: the plain debugger continue (halt on a
breakpoint inside a handler, dret with dpc never written) resumes in place, inside the
still-open handling window
— and with this change it wipes the tracking flag. Every
subsequent fault in that handler is then invisible to double-fault detection until the next
synchronous exception re-arms the flag (a false-clean cpuctrlsts.double_fault_seen audit
bit for a real double fault during a debugged handling window — the harder-to-detect failure
mode for a HW-set/SW-clear audit bit).

We believe the trade is the right one, and say so explicitly:

  • Capability-equivalence: the laundering sequence requires debug mode; that same actor
    could always clear the bit directly (cpuctrlsts bit 6 is SW-writable) — the fix changes a
    default for a trusted actor, it grants no new capability.
  • Error asymmetry: the unfixed false positive is unbounded (a stale flag from a debug
    session escalates the SoC on any later, unrelated exception, arbitrarily later); the new
    false negative is bounded to the current handling window and re-arms on the next sync
    exception.
  • No internal escalation is affected: double_fault_seen_o is a dedicated SoC output
    only; alert_major_internal_o does not include it.

This is proven, not argued: a differential directed test
(launder.S (same gist), runner run_launder.sh (same gist), watchdogged) runs
the identical instruction flow on unfixed and fixed RTL. Console markers are
live MMIO/CSR readouts; K0 = handled-fault+mret sanity, s = live
sync_exc_seen after an in-handler dret-continue, L = real double fault
(illegal insn inside the still-open handler, no mret ever executed)
pulses, a = bit 7 latched, M = mret-boundary count, N/b = genuine
no-debug nested double fault (control, must stay detected):

marker UNFIXED FIXED
s (flag after in-handler dret-continue) 1 0
L (real double fault in handler pulses) 1 (pulse @t264) 0 — laundered, as documented above
a (bit 7 latched) 1 0 (false-clean)
N / b (genuine no-debug double fault) 2 / 1 1 / 1 — detection intact, flag re-arms

Pinned expected output on fixed RTL: K0s0L0a0M0N1b1X with exactly 1 pulse
(the genuine control event); unfixed: K0s1L1a1M0N2b1X, 2 pulses. Trace
windows (gist: trace-window-{fixed,unfixed}-dret.log): dret@t198 with nocsrw dpc, resume @t206 inside the handler, flag read @t222 is the only divergence. If maintainers prefer, the clear could be restricted to drets that redirect away from the handler (dpc` outside handler bounds),
at the cost of handler-extent tracking the hardware does not currently have
— happy to prototype either variant.

Debuggers that deliberately resume into handlers during fault-storm analysis
should re-arm the flag with a single csrw (bit 6); if this PR lands, a
follow-up sentence in doc/03_reference/exception_interrupts.rst covering
the resume-in-place caveat would be appropriate (kept out of this patch to
keep the doc hunks minimal — the trade-off is fully stated here).

Testing

  • Directed regression on simple_system (Verilator 5.050, default
    config), sequence: two unrelated sync exceptions straddling a
    debug session that abandons the handler via dpc+dret
    (the regression program (gist: https://gist.github.com/john-kearney/61911098ab6a70e8b8bad969748eaca8), runner fixes/run-tests.sh):
    • unfixed RTL: fails — sync_exc_seen still reads 1 after dret,
      following ecall pulses double_fault_seen_o and latches bit 7;
    • fixed RTL: passes — flag reads 0 after dret, no pulse, bit 7 clear
      (also re-ran the original bug-demonstrating binary: pulse gone).
  • Adversarial differential test (second directed test, pinned expectations
    above): launder.S (same gist)K0s0L0a0M0N1b1X, 1 pulse on fixed RTL;
    the laundering window it demonstrates is the documented trade-off of this
    change, and the suppression is a visible, pinned decision rather than a
    silent pass.
  • simple_system smoke test (fib + 16 handled sync exceptions, no debug):
    identical pass before and after the change; mret-clearing behaviour
    unchanged.
  • Not run here (out of scope for this environment): the UVM/DV suite
    (dv/uvm/core_ibex) and riscv-compliance; the RVFI predictor change is
    expected to be exercised by those. The ibex_top.sv predictor block is
    ifdef RVFI and not compiled into simple_system.

Refs: double-fault escalation contract in
doc/03_reference/exception_interrupts.rst (:ref:double-fault-detect);
double_fault_seen_o documented as a dedicated escalation output in
doc/02_user/integration.rst.

sync_exc_seen is set on every synchronous exception and cleared by mret,
but the dret arm only restores priv_lvl. A debug session that abandons an
exception handler (halt inside handler, set dpc, dret) therefore leaves
the flag set indefinitely, so the next synchronous exception - any class,
arbitrarily later - falsely pulses double_fault_seen_o and latches the
double_fault_seen audit bit. Consumers such as OpenTitan wire
double_fault_seen_o into escalation, so a routine debug session can cause
a spurious SoC escalation.

Clear the flag in the csr_restore_dret_i arm exactly as the MRET arm does;
update the lockstep RVFI double-fault predictor and the documentation
accordingly. simple_system gains a small DbgCtrl bus device to drive
debug_req and observe the flag for the directed regression.

Reproduction, regression and a differential (laundering-tradeoff) test:
https://gist.github.com/john-kearney/61911098ab6a70e8b8bad969748eaca8

Signed-off-by: John Kearney <john@authensor.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Thank you for your submission, we really appreciate it.

Like many open-source projects, we ask that you sign the Contributor License Agreement before we can accept your contribution.

The CLA ensures that all users of the project are granted rights to use the submission.

Before signing the CLA, please ensure that you have the authority from your organisation to grant these rights.

You will be asked to sign the CLA when you first contribute to each lowRISC repository, and will be asked to re-sign if the CLA changes.

Each individual who has committed in this Pull Request should sign the CLA by posting a Pull Request Comment containing the text below.


I have read the CLA Document. By submitting this pull request comment, I am hereby confirming my acceptance of the terms of the CLA Document and my agreement to be legally bound by its terms.


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

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.

2 participants