Difficulty: Medium · Puzzle 5 of 6 · Topic: Debugging Riddles
A test sometimes passes then later crashes with timeout fatal. Why can this happen?
// broken: the losing branch is never cancelled
fork
begin wait(done); end
begin #1000 `uvm_fatal("TIMEOUT", "No completion"); end
join_any
// fixed: wrap, then disable only this block's children
fork begin
fork
begin wait(done); end
begin #1000 `uvm_fatal("TIMEOUT", "No completion"); end
join_any
disable fork;
end joinWhat happens to the losing forked branch after join_any?
1) join_any returns when the first branch finishes (e.g. done becomes true).
2) The other branch keeps running unless explicitly killed.
3) The timeout branch later reaches #1000 and calls the fatal, even though the operation succeeded. It shows up only if the simulation is still running 1000 time units after the fork, which is why it is 'sometimes' (simulated: fatal at t=1010).
4) Add disable fork after join_any, but wrap it in an outer fork begin ... end join. disable fork kills all child processes of the calling process, not just the two you just forked. Without the wrapper it also killed an unrelated earlier fork...join_none thread in the same task: the scoreboard thread that should have printed at t=50 never did. With the wrapper only the timeout was killed and the earlier thread finished normally.
5) An alternative is a structured timeout utility, or process handles (follow-up).Answer: Missing `disable fork` leaves timeout thread alive; it fires later as a false fatal.
Checked by running the code in Verilator 5; the printed output supports the answer above. Plain-SV model of the broken fork/join_any (no UVM library, `uvm_fatal replaced by $display): the done branch wins at t=20, yet the timeout branch still fires at 1010. A bare disable fork stops the timeout but also kills the unrelated earlier thread, which never prints at t=50. Wrapped in fork begin ... end join, disable fork kills only the timeout: no fatal and the earlier thread still prints at t=50.
Adding a bare disable fork. It also kills unrelated background threads forked earlier by the same process, which then silently stop doing their job.
How would you write the same logic using `process` handles for explicit kill? Model answer: process t; fork begin t = process::self(); #1000 `uvm_fatal("TIMEOUT", "No completion"); end join_none wait(t != null); wait(done); t.kill(); The wait(t != null) avoids killing before the branch has started; only that thread is killed.
The join_any returns as soon as the first branch finishes, but the other branch keeps running. So the timeout thread is still alive, and a thousand time units later it fires the fatal even though the operation succeeded; that is why it only fails sometimes. The fix is disable fork after the join_any, but wrapped in an outer fork begin end join, because disable fork kills all children of the calling process, not just these two. The trap is a bare disable fork, which silently kills unrelated background threads.