The Verilog code has a simulation race condition, which means that the simulation results are often unpredictable.
In some cases, it is fine to tolerate this uncertainty, but I suspect that is not the case for you.
The issue is that you are changing din at the same time as the clk rising edge. Since you are sampling din at that edge using this sensitivity list:
always@(posedge clk, negedge rstn)
the simulator does not guarantee that it will sample din before or after the posedge of clk. It is indeterminate, and the simulator is allowed to choose one or the other. This can vary from simulator to simulator, or even between different versions of the same simulation software.
If you want din to be truly synchronous to the clock (and I suspect you do), then you need to drive din the same way you drive stable_4_clk, namely:
@(posedge clk)
- Using nonblocking assignments (
<=)
Currently, you drive din with # delays and blocking assignments (=). This leads to the simulation race condition. This is a way to code the testbench to avoid the race condition:
initial begin
rstn=0;
#10 rstn=1;
@(posedge clk);
din <= 1;
@(posedge clk);
din <= 0;
@(posedge clk);
din <= 1;
repeat (5) @(posedge clk);
din <= 0;
#10 $finish;
end
With those changes, I see stable_4_clk[0] change from 0 to 1 at time 50ns. This is the expected time since it is the first clock cycle after din has been stable for at least 2 clock cycles.

Using $past is handy, but one disadvantage is that you can not see the value of $past(din) in the waveforms; you must assume its value when debugging.
An alternate is to add logic for din edge detection, which is very common.