CNA-BUG-166: GamerServicesDispatcher::Initialize deletes every SignedInGamer in the installed collection, including gamers the application created
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.
Initialize frees whatever gamers Gamer::getSignedInGamersProperty() currently holds before creating its four stub gamers, so gamers a game published with setSignedInGamersProperty, the pattern leaderboard reads require, are deleted behind their owner's back.
- Identifier
CNA-BUG-166- Category
- Bug
- Subsystem
- Networking & gamer services
- 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
- GamerServicesDispatcher::Initialize(System::IServiceProvider&) (also reached through GamerServicesComponent::Initialize) together with Gamer::setSignedInGamersProperty(SignedInGamerCollection*)
Expected behaviour
GamerCollection.hpp states the ownership contract: gamer collections are non-owning views, "The creator of a Gamer-derived object is exclusively responsible for freeing it", and Initialize's free-before-replace loop owns "the 4 stub SignedInGamers it creates". A later Initialize should free only gamers an earlier Initialize created.
Actual behaviour at TARGET
Initialize in GamerServicesDispatcher.cpp iterates *Gamer::getSignedInGamersProperty() and deletes every element before installing four new stub gamers. Its comment calls the first run "a harmless no-op ... since getSignedInGamersProperty() lazily returns an empty collection until Initialize() has run at least once", but setSignedInGamersProperty is public (documented as XNA's internal setter exposed) and is exactly how CNA's own leaderboard demo and tests make their gamers visible to LeaderboardReader::Read. If Initialize runs afterwards, for example when a GamerServicesComponent is initialised, it deletes those application-owned objects: a unique_ptr owner later deletes the object a second time, a stack object is deleted as if it were heap-allocated, and leaderboard entries that borrowed the gamers dangle.
Source locations
modules/gamer-services/src/Xna/GamerServicesDispatcher.cpp— GamerServicesDispatcher::Initialize deletion loop and its first-run commentmodules/gamer-services/include/Microsoft/Xna/Framework/GamerServices/GamerCollection.hpp— ownership contract: the creator frees, collections never domodules/gamer-services/include/Microsoft/Xna/Framework/GamerServices/Gamer.hpp— setSignedInGamersProperty documented as XNA's internal setter made publicmodules/gamer-services/src/Xna/GamerServicesComponent.cpp— GamerServicesComponent::Initialize calls the dispatchermodules/gamer-services/examples/demo_leaderboard_viewer/src/LeaderboardGame.cpp— publishes unique_ptr-owned gamers for LeaderboardReader::Readmodules/gamer-services/tests/Microsoft/Xna/Framework/GamerServices/GamerServicesGamerTests.cpp— SignedInGamersGuard publishes stack gamers
Evidence
Checked by reading at 009d40f5; not executed. The demo never calls Initialize and the tests install an empty collection before any Initialize, so no current CNA program triggers the deletion; a game that combines the two documented patterns does. GetFreedGamerCountForTesting counts deletions but cannot tell whose objects they were.
Focused reproduction
// Illustrative; not compiled or run for this entry. The spelling is the demo's.
std::unique_ptr<SignedInGamer> mine(
new SignedInGamer(SignedInGamer::CreateInternal("Player01")));
Gamer::setSignedInGamersProperty(new SignedInGamerCollection(
SignedInGamerCollection::CreateInternal({mine.get()})));
GamerServicesDispatcher::Initialize(services); // deletes *mine
// ~unique_ptr deletes it again at scope exit
Current tests
The harness mode behind GamerServicesDispatcherHangRegressionTest that checks "a second Initialize frees exactly the previous four gamers" only ever installs Initialize-created gamers. No test publishes application gamers and then initialises.
Regression test
A harness mode that publishes an application-owned gamer (a subclass that counts destructor calls, or an ASan build), calls Initialize, and asserts the object is still alive; the fix is for Initialize to track and free only the gamers it created.
Blast radius
Games that publish their own SignedInGamer objects and also initialise gamer services afterwards, directly or through GamerServicesComponent: memory corruption at teardown or earlier. Games that rely on the four stub gamers, or publish their own gamers without initialising, are unaffected.
Workaround
Initialise gamer services first (add the component before publishing gamers) and never again afterwards; the four stub gamers replaced by your collection are then leaked, which is harmless.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Maintainer workflow
- Ownership map: devices
- Deep dives
- GamerServices contract: local identities
- Known issues
- Bug index