Skip to content

SFA: avoid ANR when handling DEVICE_IDLE_MODE_CHANGED #4481

Description

@soasurs

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:

soasurs/sing-box-for-android@a27714a

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_anr or am_crash event 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.

Activity

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

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