Skip to content

TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning - #6580

Open
Jidos86 wants to merge 3 commits into
XTLS:mainfrom
Jidos86:fix-darwin-tun-wait-busy-spin
Open

TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning#6580
Jidos86 wants to merge 3 commits into
XTLS:mainfrom
Jidos86:fix-darwin-tun-wait-busy-spin

Conversation

@Jidos86

@Jidos86 Jidos86 commented Aug 2, 2026

Copy link
Copy Markdown

Fixes #6579.

What

DarwinTun.Wait() was procyield(1) -- a CPU-yield hint, not a real blocking wait. stack_gvisor_endpoint.go's dispatchLoop (a single dedicated goroutine) calls ReadPacket() then Wait() in a tight loop with no other throttling whenever the tun's non-blocking fd has nothing to read, so this pinned a full CPU core for the entire connected lifetime of the tunnel, independent of actual traffic volume.

For comparison, tun_android.go builds its link endpoint via gVisor's own fdbased.New(...) -- a properly blocking fd-based endpoint -- and never had this issue.

Impact

Observed causing severe, sustained device heating on a real iPhone 16 Pro during active tunnel connections -- severe enough that iOS's own thermal management disabled the camera flash ("iPhone needs to cool down before using flash"). See #6579 for the full writeup.

The fix

Adds a kqueue registered for EVFILT_READ on the tun fd, and has Wait() genuinely block on it (1s bounded timeout, so a racing Close() stays responsive) instead of yielding and immediately re-looping. Falls back to the original procyield behavior if kqueue setup ever fails, so this can only make things better or leave them unchanged, never worse.

Testing

  • Verified locally that the patch applies cleanly at this commit, and that the patched proxy/tun package (and the whole dependent libXray module) cross-compiles successfully for GOOS=darwin GOARCH=arm64.
  • Verified on a real iPhone 16 Pro: with only this fix applied (no other functional changes), a 10-minute active tunnel connection showed no measurable temperature increase, versus severe heating within a similar timeframe before the fix.

Note

This 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 well be a more elegant fix (maybe even adapting fdbased for Darwin the way Android uses it, if the fd differences allow it). I'm just sharing what fixed the issue in my own testing, not claiming this is the right solution -- happy for a maintainer to take a completely different approach.

DarwinTun.Wait() was procyield(1) -- a CPU-yield hint, not a real
blocking wait. stack_gvisor_endpoint.go's dispatchLoop (a single
dedicated goroutine) calls ReadPacket() then Wait() in a tight loop
with no other throttling whenever the tun's non-blocking fd has
nothing to read, so this pinned a full CPU core for the entire
connected lifetime of the tunnel, independent of actual traffic
volume -- observed causing severe, sustained device heating on a real
iPhone 16 Pro (severe enough that iOS's own thermal management
disabled the camera flash).

tun_android.go builds its link endpoint via gVisor's own
fdbased.New(...) -- a properly blocking fd-based endpoint -- and
never had this issue.

This adds a kqueue registered for EVFILT_READ on the tun fd, and has
Wait() genuinely block on it (1s bounded timeout, so a racing Close()
stays responsive) instead of yielding and immediately re-looping.
Falls back to the original procyield behavior if kqueue setup ever
fails, so this can only make things better or leave them unchanged,
never worse.

Verified locally: applies cleanly at this commit, and the patched
package (proxy/tun) plus the whole dependent libXray module
cross-compile successfully for GOOS=darwin GOARCH=arm64.

Not a gVisor-internals expert -- there may be a more elegant fix
(perhaps fdbased could be adapted for Darwin the way Android uses it,
if the fd differences allow it). This is what fixed the issue in
real-device testing; a maintainer may well prefer a different
approach.

Found and fixed with the help of Claude (Anthropic's AI coding
assistant).

Fixes XTLS#6579
@Fangliding

Copy link
Copy Markdown
Member

procyield 这里大抵是被滥用了 该删掉的

Jidos97 and others added 2 commits August 3, 2026 18:47
Reviewer feedback (Fangliding): the kqueue-setup-failure fallback still
called procyield(1) -- the exact busy-spin this whole change exists to
remove, just gated behind an edge case (kqueue setup failing, which
practically never happens on a real Darwin system) instead of always.
A genuine time.Sleep actually yields the CPU for a bounded duration,
unlike procyield's near-instant scheduler hint, which would let the
tight dispatchLoop caller (stack_gvisor_endpoint.go) spin just as hot as
before if this path were ever actually hit. The now-unused
//go:linkname procyield declaration is removed too rather than left as
dead code.

Verified via local cross-compile (darwin/arm64 and ios/arm64) --
go build/go vet both clean.
…usy-spin

# Conflicts:
#	proxy/tun/tun_darwin.go
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.

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

3 participants