Spotted another bug, the game calls dos.library/Exit() instead of stdlib exit(). As a result tons of system resources get leaked, for example if the game is started from shell.
Ok. I think I found the lock problem. From the exec.doc/WaitIO:
WARNING
If this IORequest was "Quick" or otherwise finished BEFORE this
call, this function drops though immediately, with no call to
Wait(). A side effect is that the signal bit related the port may
remain set. Expect this.
...and, in couple of places you use Wait(1 << timerport->mp_SigBit) + GetMsg(timerport) + SendIO(timerio).
Since the WaitIO() can leave the signal set, this Wait() will get signal from the previous timer completion, and GetMsg() will return NULL. This means the actual IORequest is still active. Further SendIO() for the already active timerrequest nukes the system (regardless: it appears that the same timerequest is SendIO()d twice).
Quick fix would be to just WaitIO() instead of GetMsg().
[EDIT] It is verified now: I wrote a small runtime patch to fix the code, and I no longer get the lockups. [/EDIT]
Alternatively/additionally: to avoid getting the bogus signal, you could SetSignal(0, 1 << timerport->mp_SigBit); after each timer WaitIO(), making sure the signal is not left pending.
Finally, the game has a buggy CreateExtIO routine. It sets the ioreq->io_Message.mn_Node.ln_Type to NT_MESSAGE, when it should set it to NT_REPLYMSG. (This is possibly from some linklib, perhaps some VBCC provided one?)