CNA-BUG-168: LocalGamerServicesStore maps distinct gamertags to one achievements file when they differ only in characters outside A-Z, a-z, 0-9, '.', '_' and '-'
Evidence basis: source-verified at the pinned commit; executed for this entry (the Evidence section names exactly what was run); 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.
SanitizeStoreFileNameComponent replaces every other byte with an underscore, so gamertags such as "Ann B" and "Ann_B", or two same-length non-ASCII names, share one achievements file and see each other's achievements.
- Identifier
CNA-BUG-168- Category
- Bug
- Subsystem
- Networking & gamer services
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Reproduced: executed for this entry (the Evidence section names exactly what was run)
- Tests touching this area
- Yes: see Current tests
- Affected contract
- SignedInGamer::AwardAchievement and GetAchievements persistence (CNA::Internal::GamerServices::SaveEarnedAchievementEXT, LoadEarnedAchievementsEXT, SanitizeStoreFileNameComponent)
Expected behaviour
Achievements belong to one gamer: GetAchievements returns what that gamertag earned and nothing another gamer earned.
Actual behaviour at TARGET
The achievements file is achievements/<SanitizeStoreFileNameComponent(gamertag)>.json (LocalGamerServicesStore.cpp). The sanitiser keeps ASCII letters, digits, -, _ and ., replaces every other byte with _ (each byte of a multi-byte UTF-8 character separately) and maps an empty name to _. The mapping is many-to-one and the file does not record the original gamertag, so "Ann B", "Ann_B" and "Ann!B" read and write one list, as do two non-ASCII names whose characters have the same byte lengths. Leaderboards are not affected in the same way: each board file stores every entry's raw gamertag.
Source locations
modules/gamer-services/src/Internal/LocalGamerServicesStore.cpp— SanitizeStoreFileNameComponent; LoadEarnedAchievementsEXT and SaveEarnedAchievementEXT name the file by the sanitised gamertagmodules/gamer-services/src/Xna/SignedInGamer.cpp— AwardAchievement and EndGetAchievements key the store by gamertag
Evidence
Checked by reading at 009d40f5; the audit's own executed probe follows below. The four gamertags the dispatcher creates sanitise to distinct names, so only application-created gamers can collide. The gamer-services deep dive states that the mapping "does not make identities unique" but not the consequence.
Independent re-verification: Reproduced by the audit at 009d40f5. A probe built from TARGET's LocalGamerServicesStore.cpp, PropertyDictionary.cpp and the storage sources (g++ -O0; the Sharp Runtime types came from a sibling checkout that TARGET does not pin) sanitised “Ann B”, “Ann_B” and “Ann!B” to the same name Ann_B, and the two non-ASCII names “éx” and “šx” to the same __x. After SaveEarnedAchievementEXT("Ann B", "first_steps", ...) one file, achievements/Ann_B.json, existed, and LoadEarnedAchievementsEXT for “Ann_B” and for “Ann!B” each returned that entry, while “Bob” returned none. The four gamers the dispatcher creates sanitise to distinct names, so only application-created gamers collide.
Focused reproduction
// Illustrative; the audit's executed probe is described under Evidence.
auto ann = SignedInGamer::CreateInternal("Ann B");
auto ann2 = SignedInGamer::CreateInternal("Ann_B");
ann.AwardAchievement("first_steps"); // writes achievements/Ann_B.json
auto list = ann2.GetAchievements(); // contains first_steps
Current tests
SignedInGamerTest.AchievementsAreIsolatedPerGamertag in GamerServicesGamerTests.cpp uses gamertags that sanitise to different names; no test uses two that sanitise to the same one.
Regression test
Two gamertags that differ only in a space, punctuation or a non-ASCII character, each awarded a different key, each reading back only its own; a collision-free encoding (percent-encoding or a hash suffix, with the gamertag stored in the file) satisfies it.
Blast radius
Games that create local profiles with free-form names: two players' achievements merge. Leaderboards and the four stub gamers are unaffected.
Workaround
Restrict profile names to ASCII letters, digits, -, _ and ., or keep names distinct after that mapping.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Deep dives
- GamerServices contract: persistence
- Known issues
- Bug index