Summary
proxy/tun/tun_darwin.go's DarwinTun.Wait() is just procyield(1) (a CPU-yield hint, not a real sleep):
// Wait some cpu cycles
func (t *DarwinTun) Wait() {
procyield(1)
}
stack_gvisor_endpoint.go's dispatchLoop (a single dedicated goroutine) calls ReadPacket() in a tight loop and, whenever the queue is empty, calls Wait() before immediately looping again, with no other throttling:
for {
select {
case <-ctx.Done():
return
default:
version, packet, err = e.device.ReadPacket()
if errors.Is(err, ErrQueueEmpty) {
e.device.Wait()
continue
}
...
}
}
Since procyield(1) isn't a real blocking wait, this pins a full CPU core for the entire connected lifetime of the tunnel, regardless of actual traffic.
For comparison, tun_android.go builds its link endpoint via gVisor's own fdbased.New(...) — a properly blocking fd-based endpoint — and doesn't implement a custom Wait()/ReadPacket() at all.
Impact
On a real iPhone 16 Pro, this caused severe, sustained device heating during any active tunnel connection — severe enough that iOS's own thermal management disabled the camera flash ("iPhone needs to cool down before using flash"). Confirmed via a clean before/after on a real device: with only this fix applied, a 10-minute connection showed no measurable temperature increase, versus severe heating within a similar timeframe before it.
Suggested fix
PR incoming: has Wait() block on a kqueue registered for EVFILT_READ on the tun fd (1s timeout so a racing Close() stays responsive), falling back to procyield if kqueue setup fails. Verified locally: applies cleanly at this commit (45cf289 / v26.6.27), patched package and the whole dependent module cross-compile for GOOS=darwin GOARCH=arm64.
Note
This bug was found and fixed with the help of Claude (Anthropic's AI coding assistant), while debugging a real thermal-management report on iOS. I'm not a gVisor internals expert — there may be a more elegant fix (maybe even using fdbased for Darwin like Android does, if the fd differences allow it). I'm just sharing what fixed it in my own testing, not claiming this is the right solution — happy for a maintainer to take a completely different approach.
Summary
proxy/tun/tun_darwin.go'sDarwinTun.Wait()is justprocyield(1)(a CPU-yield hint, not a real sleep):stack_gvisor_endpoint.go'sdispatchLoop(a single dedicated goroutine) callsReadPacket()in a tight loop and, whenever the queue is empty, callsWait()before immediately looping again, with no other throttling:Since
procyield(1)isn't a real blocking wait, this pins a full CPU core for the entire connected lifetime of the tunnel, regardless of actual traffic.For comparison,
tun_android.gobuilds its link endpoint via gVisor's ownfdbased.New(...)— a properly blocking fd-based endpoint — and doesn't implement a customWait()/ReadPacket()at all.Impact
On a real iPhone 16 Pro, this caused severe, sustained device heating during any active tunnel connection — severe enough that iOS's own thermal management disabled the camera flash ("iPhone needs to cool down before using flash"). Confirmed via a clean before/after on a real device: with only this fix applied, a 10-minute connection showed no measurable temperature increase, versus severe heating within a similar timeframe before it.
Suggested fix
PR incoming: has
Wait()block on akqueueregistered forEVFILT_READon the tun fd (1s timeout so a racingClose()stays responsive), falling back toprocyieldif kqueue setup fails. Verified locally: applies cleanly at this commit (45cf289 / v26.6.27), patched package and the whole dependent module cross-compile forGOOS=darwin GOARCH=arm64.Note
This bug was found and fixed with the help of Claude (Anthropic's AI coding assistant), while debugging a real thermal-management report on iOS. I'm not a gVisor internals expert — there may be a more elegant fix (maybe even using
fdbasedfor Darwin like Android does, if the fd differences allow it). I'm just sharing what fixed it in my own testing, not claiming this is the right solution — happy for a maintainer to take a completely different approach.