Skip to content

runtime: fix signal_recv/sleepTicks with threads scheduler and use MAP_NORESERVE - #5929

Open
brho wants to merge 1 commit into
tinygo-org:devfrom
brho:pr-runtime-threads-signal-mmap
Open

brho wants to merge 1 commit into
tinygo-org:devfrom
brho:pr-runtime-threads-signal-mmap

Conversation

@brho

@brho brho commented Oct 19, 2026

Copy link
Copy Markdown
  • When !hasScheduler (-scheduler=threads), sleepTicks previously waited on and cleared signalFutex, racing with signalWatcher and signal_recv. Wait on a local futex in sleepTicks when !hasScheduler, and wait directly on signalFutex in signal_recv instead of calling task.Pause() (which is a no-op under -scheduler=threads).
  • Pass MAP_NORESERVE when reserving the 1 GiB virtual heap in allocateHeap() on Linux and Darwin so strict overcommit settings do not reject untouched virtual address space.

…P_NORESERVE

- When `!hasScheduler` (`-scheduler=threads`), `sleepTicks` previously
  waited on and cleared `signalFutex`, racing with `signalWatcher` and
  `signal_recv`. Wait on a local futex in `sleepTicks` when
  `!hasScheduler`, and wait directly on `signalFutex` in `signal_recv`
  instead of calling `task.Pause()` (which is a no-op under
  `-scheduler=threads`).
- Pass `MAP_NORESERVE` when reserving the 1 GiB virtual heap in
  `allocateHeap()` on Linux and Darwin so strict overcommit settings do
  not reject untouched virtual address space.
@dgryski

dgryski commented Oct 22, 2026

Copy link
Copy Markdown
Member

Thanks for this. The MAP_NORESERVE change looks right to me. The flag values match Linux (0x4000) and Darwin (0x40).

I think signal_recv under -scheduler=threads has a lost wakeup. signalWatcher also does signalFutex.Swap(0), so it competes with signal_recv for the futex.

  • signal_recv stores 0 to signalFutex and loads receivedSignals, which is 0.
  • A signal arrives. The handler sets the bit and sets the futex to 1.
  • signalWatcher wakes and swaps the futex back to 0. checkSignals does nothing, because signalRecvWaiter is nil without a scheduler.
  • signal_recv calls signalFutex.Wait(0) and blocks, even though receivedSignals is nonzero. It stays blocked until another signal arrives.

The window is tiny, so I couldn't hit it naturally in 200k ping-pong iterations. With a 200µs busy-wait between the Load and the Wait, a single signal.Notify + kill(getpid(), SIGUSR1) hangs in 3 of 3 runs. With the watcher disabled, it passes.

A fix is to give signal_recv its own futex that only it clears:

 var signalRecvWaiter atomic.Pointer[task.Task]
 
+// Futex signal_recv waits on under -scheduler=threads. Only signal_recv clears it.
+var signalRecvFutex futex.Futex
+
 func signal_recv() uint32 {
 	for {
 		if !hasScheduler {
-			signalFutex.Store(0)
+			signalRecvFutex.Store(0)
 		}
 		val := receivedSignals.Load()
 		if val == 0 {
 			if !hasScheduler {
-				signalFutex.Wait(0)
+				signalRecvFutex.Wait(0)
 				continue
 			}

and in tinygo_signal_handler, after the existing wake:

 	if signalFutex.Swap(1) == 0 {
 		signalFutex.WakeAll()
 	}
+
+	if !hasScheduler {
+		signalRecvFutex.Store(1)
+		signalRecvFutex.WakeAll()
+	}

The handler sets the receivedSignals bit before it sets this futex. signal_recv stores 0 before it loads receivedSignals. If the load sees 0, the handler's store of 1 comes after the store of 0, and nothing else clears it, so Wait(0) returns immediately. With this change and the same 200µs delay, 5 of 5 runs pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants