CNA-BUG-049: Accelerometer, Gyroscope, Compass and Motion declare Dispose(bool) public, so an external Dispose(false) marks the sensor disposed without cleanup

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

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.

SensorBase declares Dispose(bool) protected, but the four sensor classes redeclare it public; Dispose(false) sets the disposed flag without Stop, unregistration or releasing the platform lease, and the destructor then skips cleanup.

Identifier
CNA-BUG-049
Category
Bug
Subsystem
Input
Status
Narrowed (partially fixed; describes only what survives)
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Microsoft::Devices::Sensors::Accelerometer::Dispose(bool), Gyroscope::Dispose(bool), Compass::Dispose(bool) and Motion::Dispose(bool); the IDisposable pattern of SensorBase<T>

Expected behaviour

In the .NET pattern CNA mirrors, Dispose(bool) is protected and only Dispose() and the destructor path call it. SensorBase.hpp declares it protected, and CNA's remediation plan item REMED-DEVICES-002 says all four classes should do the same, with a compile-time accessibility test.

Actual behaviour at TARGET

Accelerometer.hpp, Gyroscope.hpp, Compass.hpp and Motion.hpp each declare void Dispose(bool disposing) override; in a public section. Each implementation sends !disposing straight to SensorBase<T>::Dispose(false), which only sets disposed_ and wakes waiters: no Stop(), no instance-count decrement, no platform subsystem release, no nulling of the owner control block. The destructors call Dispose(true) only when not already disposed, so after an external Dispose(false) nothing ever cleans up. For a started Accelerometer or Gyroscope, the registration Start() made with the shared PlatformSensorSubsystem keeps naming the object; once it is destroyed, the next platform sensor event is dispatched through that registration into freed memory (Accelerometer.cpp). The per-class instance counter stays raised, so repeated misuse reaches the limit of 10 simultaneous instances. The avatar half of the original report is fixed: AvatarRenderer.hpp declares Dispose(bool) protected.

Source locations

Evidence

Checked by reading the four headers, SensorBase.hpp, the four implementations and the subsystem at 009d40f5; not executed. The use-after-free follows from reading RegisterStartedInstanceLocked/UnregisterStartedInstanceLocked (only Stop and Dispose(true) unregister); it was not provoked under a sanitizer. CNA's own remediation plan records the defect as REMED-DEVICES-002, still NOT STARTED in REMEDIATION_PROGRESS.md. No test calls Dispose(false) on a sensor. The sensors deep dive already tells callers to use the parameterless Dispose().

Independently observed as a separate finding (merged): The overrides are public although SensorBase declares Dispose(bool) protected; calling Dispose(false) from outside sets the disposed flag without stopping the sensor, releasing its platform registration or decrementing the instance count.

Focused reproduction

// Illustrative; not compiled or run for this entry (SDL3 platform with an accelerometer).
{
    Microsoft::Devices::Sensors::Accelerometer accel;
    accel.Start();          // registered with the shared accelerometer subsystem
    accel.Dispose(false);   // public in CNA: marks disposed, no Stop(), no unregistration
}                           // ~Accelerometer sees IsDisposed and skips Dispose(true)
// The next accelerometer event from the platform is dispatched to the destroyed object.

Current tests

AccelerometerTests, GyroscopeTests, CompassTests, MotionTests and SensorBaseTests (under modules/devices/tests/Microsoft/Devices/Sensors) cover Dispose(), concurrent disposal and disposal during dispatch through the public parameterless Dispose() and the destructor. None checks the accessibility of Dispose(bool) or the Dispose(false) path.

Regression test

The compile-time check REMED-DEVICES-002 asks for, for all four classes, for example template<class T> concept PublicDisposeBool = requires(T& t) { t.Dispose(false); }; with static_assert(!PublicDisposeBool<Accelerometer>), plus a runtime test that Dispose() on a started sensor leaves no subsystem registration behind.

Blast radius

Code that calls Dispose(bool) on one of the four sensors from outside, which XNA and Windows Phone code cannot do because the method is protected there, so only code written against CNA's headers is exposed. Dispose(), destruction without an explicit call and VibrateController are unaffected.

Workaround

Call only the parameterless Dispose(), or let the destructor run.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

User guide
Sensors guide
Known issues
Bug index