Difficulty: Hard · Puzzle 2 of 6 · Topic: Real Interview Rounds: End-to-End Debug Walkthrough
Interviewer says the waveform looks idle and clean, but simulation never exits. Use objection-centric reasoning, state why the code never reaches its drop, and close with a robust patch.
task body();
starting_phase.raise_objection(this);
fork
begin
wait(cfg_done); // cfg_done never comes in the failing run
run_traffic();
starting_phase.drop_objection(this);
end
begin
#1ms;
if (!cfg_done) \`uvm_error("CFG", "timeout");
disable fork; // intent: stop the waiter
end
join // waits for BOTH threads
endtaskMention +UVM_OBJECTION_TRACE as first high-yield evidence, then ask which thread can still finish.
1) Symptom: run_phase stall with a quiet waveform and a cfg timeout error in the log.
2) Hypotheses: leaked objection, a thread that never finishes, or a blocked mailbox/semaphore.
3) Instrumentation: +UVM_OBJECTION_TRACE shows this sequence still holds its objection; per-thread lifecycle logs show thread 1 never leaves wait(cfg_done).
4) Root cause: the timeout thread's disable fork has no child processes, so it kills nothing (verified in simulation). The fork uses join, which waits for both threads, so thread 1 sits in wait(cfg_done) forever and the drop after it never runs.
5) Fix: make the timeout actually end the wait and put the drop where it always runs: fork begin fork wait(cfg_done); #1ms; join_any disable fork; end join; if (!cfg_done) uvm_error(...) else run_traffic(); then drop_objection after the block. The enclosing fork begin ... end join keeps disable fork from killing unrelated threads.Answer: Actual bug: the timeout thread's disable fork cannot stop the waiter (it has no children), and join waits for both threads, so the drop in thread 1 is never reached when cfg_done never arrives. Fix: race wait(cfg_done) against the timeout with fork..join_any inside an isolating fork begin..end join, then drop the objection after it on every path.
Checked by running the code in Verilator 5; the printed output supports the answer above. Plain-SV model of the timeout thread: disable fork with no children kills nothing and join keeps waiting; the join_any race drops the objection.
Candidates say 'disable fork killed the drop path' because it sounds right; here it kills nothing, and the real hang is join waiting for a thread that never finishes. Use the objection trace to prove which thread is stuck.
How would you redesign timeout handling so it cannot bypass critical cleanup? Model answer: Structure it as: raise, then a single isolated fork..join_any race (event vs timeout) inside fork begin..end join, then one drop after the block on every path; error reporting is a flag set by the timeout branch, never an early exit.
The waves are clean, so I use the objection trace to find which thread is actually stuck. The root cause is that the timeout thread's disable fork stops nothing, because it has no children, and the join waits for both threads, so the drop in the first thread is never reached when cfg_done never arrives. I confirm it with the trace. The fix is to race the wait against the timeout with fork and join_any inside an isolating fork begin end join, then drop the objection after it on every path.