SFA: avoid ANR when handling DEVICE_IDLE_MODE_CHANGED
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 38.1k
- Forks
- 4.6k
- Avg merge
- 19d 15h
- Merged PRs (30d)
- 1
Description
Summary
SFA 1.14.0 can ANR while handling PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED because the registered broadcast receiver calls CommandServer.wake() synchronously.
I collected three ANR reports from the same device (one on 1.14.0-rc.4 and two on 1.14.0). All three show the Android main thread inside the JNI call to CommandServer.wake() while dispatching DEVICE_IDLE_MODE_CHANGED.
Since pull request creation is restricted in the SFA repository, the proposed Android patch is available here:
https://github.com/soasurs/sing-box-for-android/commit/a27714a2af3f4f9d5e267e483a03811efd618814
Observed blocking chain
A pprof capture taken while the call was blocked showed this exact goroutine chain:
CommandServer.Wake
→ pause.defaultManager.DeviceWake
→ pause.defaultManager.emit
→ wireguard.Endpoint.onPauseUpdated
→ wireguard-go.Device.Up
→ Peer.SendKeepalive
→ Peer.SendHandshakeInitiation
→ Peer.resolveEndpoints
→ dns.Router.Lookup
→ dns.Client.Lookup
The profile used a WireGuard endpoint with a domain peer address and persistent keepalive. On wake, Device.Up() synchronously initiated a handshake and resolved the peer endpoint. The DNS child goroutines were in HTTPSTransport and net.(*netFD).connect / IO wait, waiting for DoH TCP connection timeout while the physical network was recovering.
This was a network timeout, not a WireGuard mutex deadlock.
Reproduction and A/B
Environment:
- Xiaomi 2211133C (
fuxi) - Android 16 / API 36
- SFA 1.14.0, Android commit
5d5479d - sing-box v1.14.0 (
0b899587...) - wireguard-go
8bd032a91a30
Synthetic idle/wake transition:
adb shell dumpsys deviceidle force-idle
sleep 5
adb shell dumpsys deviceidle disable
# restore afterward:
adb shell dumpsys deviceidle unforce
adb shell dumpsys deviceidle enable
With WireGuard enabled, two instrumented wake calls took:
- 15,430 ms
- 10,014 ms
Thirteen consecutive pprof samples contained the synchronous stack above.
After removing WireGuard from the same configuration, five identical wake transitions took:
- 1 ms
- 4 ms
- 3 ms
- 2 ms
- 3 ms
Proposed fix
The patch makes the broadcast receiver only enqueue the latest idle state into a conflated channel. A dedicated Dispatchers.IO coroutine consumes updates serially and invokes CommandServer.pause() / wake() outside the broadcast thread. The scope and channel are closed when the service is destroyed.
Conflation is intentional: while a native transition is still running, only the latest requested Android idle state needs to be applied afterward.
Clean-build validation
I then installed a clean APK containing only the proposed patch, without pprof, watchdog, or timing instrumentation, and restored the WireGuard configuration.
Three wake cycles made the synchronous WireGuard endpoint path wait for at least:
- 10.010 seconds
- 12.672 seconds
- 27.226 seconds
Every cycle encountered DoH TCP timeout while resolving the WireGuard domain endpoint. Despite that:
- no
am_anroram_crashevent occurred; - the process PID did not change;
- the foreground VPN service remained alive.
Build checks also pass:
:app:spotlessCheck
:app:compileOtherDebugKotlin
I can provide the redacted ANR excerpts, filtered device logs, and representative full goroutine profiles if useful.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the Android broadcast receiver handling PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED and the CommandServer.wake() entry point. Review the proposed Android patch at commit a27714a2af3f4f9d5e267e483a03811efd618814, then run the listed adb idle/wake reproduction with WireGuard enabled. Done means wake handling no longer blocks the broadcast thread, no ANR occurs, and :app:spotlessCheck plus :app:compileOtherDebugKotlin pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, go, kotlin
- Domain
- mobile-dev, networking, observability
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100