Inside a Truti CDC/RDC audit
A CDC tool that only counts crossings floods you with noise. One that hides the crossings it is unsure about lies to you. This is how an audit does neither: it classifies every crossing, recognizes the structures that are already safe, and refuses to print a clean verdict it did not earn.
The same async FIFO from Article 01, read one layer deeper.
In Article 01 the async FIFO simulated clean and still came back DIRTY in CDC. That single word was the headline. This article opens the report behind it, because the value of a CDC audit is not the verdict, it is the reasoning the verdict rests on. Every number below is quoted from one run of Truti on wz_async_fifo.sv, tool version 0.5.0-rc34.
An audit is a pipeline with gates, not a single scan
A crossing report is only trustworthy if the steps before it are trustworthy. Truti runs as contract-gated stages: elaboration builds the design, a clock and reset lattice is constructed, crossings are enumerated, safe structures are recognized and removed, and only what survives is classified and reported. Each stage has to satisfy its contract before the next one runs. If elaboration is incomplete or the clock intent is missing, the pipeline does not paper over it with a guess and a green tick. It stops and says what it is missing.
The reason for the gates is that a CDC finding is an inference, and an inference is only as good as its inputs. A tool that scans and reports without checking its own footing produces a number that looks like signoff and is not.
Setup is the analysis, not a warm-up
The report opens with two lines most tools bury:
input policy : pessimistic setup conf. : HIGH (0 blocking setup finding(s))
The input policy is pessimistic. When Truti cannot prove an input is synchronous, it treats it as asynchronous. That is the safe direction to be wrong in: a pessimistic policy over-reports rather than under-reports, so a crossing is never silently assumed away. Of the nine domains this FIFO presents, two are concrete clocks and seven are async inputs, which is exactly what a FIFO with independent write and read clocks plus asynchronous resets and controls should look like.
The setup confidence is HIGH with zero blocking setup findings. That line is the audit certifying its own footing before it says anything about crossings. If setup confidence were low, the correct verdict would be SETUP_BLOCKED, and the crossing counts would be withheld rather than reported as if they were complete. A CDC number is only meaningful once the tool can vouch for the setup it was computed from.
The verdict, and the crossing matrix under it
Here is the signoff report as the tool renders it in the browser.

Truti signoff report for wz_async_fifo. Verdict DIRTY, 8 must-fix crossings across 6 groups, with the domain-pair matrix and the recognized-safe inventory shown in the same view.
The verdict is unambiguous:
VERDICT: DIRTY SIGNOFF: FAIL reason : 8 must-fix crossing(s) open in-scope : MUST_FIX 8 (high-conf 8 / needs-confirm 0) REVIEW 3 findings : 11 reported | truncated 0 (safety cap 200000) -> zero findings lost
Two details in that block are the whole philosophy. The eight must-fix findings are all high-confidence, none deferred to a vague needs-confirm bucket. And truncated 0 ... zero findings lost is the tool promising it did not silently drop anything to make the list shorter. A report that can quietly truncate is a report you cannot sign.
The findings are not a flat list, they are organized by domain pair so you fix causes, not symptoms:
| Source domain → destination | Must-fix | Review |
|---|---|---|
| input.wrst_n → wz_async_fifo.wclk | 2 | 0 |
| input.winc → wz_async_fifo.wclk | 2 | 0 |
| input.rrst_n → wz_async_fifo.rclk | 2 | 0 |
| input.wdata → wz_async_fifo.wclk | 1 | 0 |
| input.rinc → wz_async_fifo.rclk | 1 | 0 |
| input.winc / wdata / rinc → reconverged | 0 | 3 |
Eight active domain pairs, classified findings per pair. Fixing the source of a pair collapses every finding under it.
What each finding says
The classified section names a root cause and a representative signal path for each group, down to the source line:
[MUST_FIX] 8 findings in 6 groups RDC_ASYNC_DEASSERT | input.wrst_n -> wz_async_fifo.wclk | 2 findings - wz_async_fifo.wrst_n -> wz_async_fifo.wbin wz_async_fifo.sv:58 - wz_async_fifo.wrst_n -> wz_sync2.q wz_async_fifo.sv:32 inst u_r2w CDC_UNSYNC_MULTIBIT | input.wdata -> wz_async_fifo.wclk | 1 finding - wz_async_fifo.wdata -> wz_async_fifo.mem__0 wz_async_fifo.sv:39 CDC_UNSYNC_SINGLE_BIT | input.winc -> wz_async_fifo.wclk | 1 finding - wz_async_fifo.winc -> wz_async_fifo.wbin wz_async_fifo.sv:58 [REVIEW] 3 findings in 3 groups CDC_RECONVERGENCE | input.wdata -> reconverged | 1 finding - wz_async_fifo.wdata -> wz_async_fifo.rdata wz_async_fifo.sv:48
The rule names are the teaching. RDC_ASYNC_DEASSERT is a reset-domain crossing: an asynchronous reset whose removal is not synchronized to the destination clock, which can release two flops on different cycles. CDC_UNSYNC_SINGLE_BIT and CDC_UNSYNC_MULTIBIT are data or control crossing a clock boundary without a synchronizer, the multibit case being worse because bits can land split across a clock edge. CDC_RECONVERGENCE is filed as REVIEW rather than must-fix because separately synchronized signals meeting again may or may not be a hazard worth fixing depending on intent. The tool does not pretend to know which; it flags it for a human and says so.
The structures it recognized as already safe
The part of the report that separates an audit from a grep is the recognized-safe inventory:
SYNCHRONIZER / PROTOCOL INVENTORY (recognized-safe structures) proven_2ff_synchronizer 100 Proven 2-flop synchronizer multibit_subsumed_single_bit 1 Single-bit facet subsumed by multibit finding (suppressed candidates were proved safe by structure or declared intent; they are visible here, never silently dropped)
One hundred two-flop synchronizers were recognized and removed from the finding list because they are correct by construction. A tool that reported them as crossings would bury the eight that matter under a hundred that do not. But notice the last line: the safe structures are shown, not deleted. You can audit what the tool decided was safe. Suppression you cannot inspect is indistinguishable from a bug, so Truti makes its suppressions visible by policy.
Four verdicts, and why the fourth one matters most
Every Truti run resolves to one of four words, and the design of the set is the point:
- CLEANNo must-fix crossings survive after safe structures are removed. Earned, not assumed.
- REVIEWOnly intent-dependent findings remain, such as reconvergence. A human decides.
- DIRTYMust-fix crossings are open, as here. Signoff fails and the report says why.
- SETUP_BLOCKEDThe tool could not establish confident footing, so it withholds a crossing verdict entirely.
SETUP_BLOCKED is the one that makes the other three trustworthy. A CDC tool that always returns CLEAN, REVIEW or DIRTY has no way to admit it did not have enough to answer, and a tool that cannot say I could not tell will eventually say CLEAN when it should not. The existence of a fourth, non-answer verdict is what lets the clean verdict mean something.
A CDC verdict is only as good as the reasoning printed beneath it. DIRTY on a page you can read, with the crossings named and the safe structures shown, is engineering. DIRTY with nothing behind it is a rumor.