Skip to content

TUN inbound (Darwin): Wait() busy-spins instead of blocking, causing continuous high CPU / severe device heating on iOS #6579

Description

@Jidos86

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions