CNA-PLAT-009: Android Compass and Motion: stopping waits at most five seconds for the sensor worker, after which the bridge is abandoned for good and a handler still running can outlive its owner
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
AndroidSensorBridge::Stop gives a wedged worker five seconds and then detaches it, marking the bridge permanently unable to start; a Compass or Motion destroyed after such a timeout, while a callback is still inside it, is an unsupported window.
- Identifier
CNA-PLAT-009- Category
- Platform limitation
- Subsystem
- Input
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- Microsoft::Devices::Sensors::Compass and Motion on Android (AndroidCompassBackend and AndroidMotionBackend over Detail::AndroidSensorBridge): Start(), Stop(), Dispose() and destruction
Expected behaviour
Stopping or disposing a sensor returns once the native sensor queue has stopped delivering, a later Start() can use the sensor again, and destroying the owner never races a callback that is running inside it.
Actual behaviour at TARGET
AndroidSensorBridge::Stop in AndroidSensorBridge.cpp, called from another thread, waits for the worker to finish for at most kNativeCallTimeout (five seconds). If Run() is stuck in ASensorManager_createEventQueue or ASensorEventQueue_enableSensor, or a handler blocks that long, Stop detaches the thread and sets abandoned_, which is never reset: every later Start() on that bridge returns false, and the abandoned native call cannot be cancelled. Compass::Dispose(bool) and Motion::Dispose(bool) null the owner in the SensorOwnerControlBlock first, so callbacks that have not yet passed their owner check do nothing; but SensorOwnerControlBlock.hpp documents as an accepted boundary that a callback which already passed the check and is running inside the owner is not protected against another thread completing the owner's destruction. Because Stop normally waits for the worker, that window opens when the bounded wait expires.
Source locations
modules/devices/src/Sensors/Detail/AndroidSensorBridge.cpp— Impl::abandoned_ and kNativeCallTimeout; Stop's bounded wait and detach; Start refuses an abandoned bridgemodules/devices/include/Microsoft/Devices/Sensors/Detail/SensorOwnerControlBlock.hpp— the documented remaining concurrent-destruction boundarymodules/devices/src/Sensors/Compass.cpp— Compass::Dispose(bool) nulls control_->owner before Stopmodules/devices/src/Sensors/Motion.cpp— Motion::Dispose(bool), same shapemodules/devices/include/Microsoft/Devices/Sensors/Detail/AndroidCompassBackend.hpp— each backend owns its own bridge
Evidence
Checked by reading at 009d40f5; not executed. The Android paths compile only under __ANDROID__; the host tests (AndroidSensorBridgeTests.cpp, AndroidCompassMathTests.cpp, AndroidMotionMathTests.cpp) cover the interval conversion and the math, not a device, and CNA's own comments call the abandoned state "a deliberate fatal backend-health state". How often NDK sensor calls stall, and what an abandoned worker does afterwards, is unverified on hardware. Classified as a platform limitation: a thread blocked in a native NDK call cannot be cancelled from C++, and the bound trades an indefinite hang for this state.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
AndroidSensorBridgeTests.cpp tests interval conversion and value counts on the host; CompassTests and MotionTests use fake backends, including destruction during a same-thread dispatch. Nothing drives the timeout or the abandoned state.
Regression test
An Android instrumented test, or a host test with a seam that stalls the startup call past the bound, asserting that Stop returns, that a later Start() on the same instance fails while a new instance succeeds, and a sanitizer run with a fake backend whose callback outlives a five-second stop.
Blast radius
Android games using Compass or Motion, the only platform on which those classes start. The worst case needs a native call or a handler that stalls for five seconds. Accelerometer, Gyroscope and desktop builds are unaffected.
Workaround
Keep sensor handlers short and non-blocking (hand work to the game thread), stop sensors before destroying them, and after a failed Start() construct a new Compass or Motion, which gets its own bridge.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Maintainer workflow
- Thread and callback map: sensors
- Deep dives
- Sensors and vibration: Compass
- Known issues
- Platform limitation index