CNA-BUG-083: PhoneApplicationService::getCurrentProperty()'s static instance detaches from an already-destroyed Game during static destruction
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.
The process-wide PhoneApplicationService is a function-local static holding a borrowed Game*; attached to a Game that is destroyed first, its destructor calls DetachEXT on the dead game's events.
- Identifier
CNA-BUG-083- Category
- Bug
- Subsystem
- Core & runtime
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (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::Phone::Shell::PhoneApplicationService::getCurrentProperty(), AttachEXT(Game&), DetachEXT() and ~PhoneApplicationService()
Expected behaviour
The header presents getCurrentProperty() as “the application's single lifecycle service” and AttachEXT as the one line a phone port writes. DetachEXT's documentation says the destructor calls it “so a game that outlives nothing in particular still leaves no dangling subscription behind”. Using the process-wide instance with an ordinary Game should end the process without undefined behaviour.
Actual behaviour at TARGET
getCurrentProperty() returns a function-local static PhoneApplicationService, destroyed during static destruction after main returns. AttachEXT stores a raw Game* and three event tokens; nothing in Game or elsewhere under modules/runtime knows about the service, so nothing clears the pointer when the game is destroyed. ~PhoneApplicationService calls DetachEXT, which calls Activated.Remove, Deactivated.Remove and Exiting.Remove through the stored pointer. When the Game lived on main's stack, the usual shape of a port, those calls erase from the handler vectors of a destroyed object: a use-after-free at exit.
Source locations
modules/phone/src/PhoneApplicationService.cpp— getCurrentProperty (function-local static), ~PhoneApplicationService, AttachEXT, DetachEXTmodules/phone/include/Microsoft/Phone/Shell/PhoneApplicationService.hpp— getCurrentProperty, AttachEXT and DetachEXT documentation; borrowed game_ pointermodules/phone/tests/Microsoft/Phone/Shell/PhoneApplicationServiceTests.cpp— every case uses a local service
Evidence
Checked by reading PhoneApplicationService.cpp and its header at 009d40f5, and Sharp Runtime's System::EventHandler::Remove (sharp-runtime next @ 41b918c9, a revision TARGET does not pin), which erases from the member vector with no liveness check. A search of modules/runtime finds no reference to the phone module. Unchanged since the earlier recorded pin. Not executed: the failure is undefined behaviour, so its visible symptom (a crash at exit, heap corruption, or nothing) depends on the allocator.
Focused reproduction
Illustrative (not compiled for this entry): the obvious port.
#include "Microsoft/Phone/Shell/PhoneApplicationService.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
int main()
{
Microsoft::Xna::Framework::Game game; // destroyed when main returns
Microsoft::Phone::Shell::PhoneApplicationService::getCurrentProperty().AttachEXT(game);
game.Run();
return 0;
} // static destruction: ~PhoneApplicationService -> DetachEXT -> game_->Activated.Remove(...) on a dead Game
Current tests
All nine cases in PhoneApplicationServiceTests.cpp construct a local service. DestroyingTheServiceUnsubscribesFromTheGame pins the safe order (the service dies first), and CurrentIsOneServiceForTheWholeProcess compares addresses without attaching. No test attaches the static instance to a game that is destroyed before it.
Regression test
A test in a child process (the failure happens during static destruction) that attaches getCurrentProperty() to a scoped Game, lets the game go out of scope and exits, run under AddressSanitizer. A fix could clear the pointer from the game's side (for example on Game::Disposed) or hold a weak lifetime token instead of the raw pointer.
Blast radius
Every phone port that attaches the process-wide Current service to a Game destroyed before static destruction, which is any game object that is not itself a static. Local services destroyed before their game, and services detached explicitly, are unaffected. Only executables that link CNA::Phone are involved; the CNA umbrella does not include it.
Workaround
Call PhoneApplicationService::getCurrentProperty().DetachEXT() after Run() returns, before the Game is destroyed.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Maintainer workflow
- I need to debug shutdown and lifetime behavior · Ownership and lifetime master map
- Known issues
- Bug index