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 '-'

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

✓

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 c1c316b9; 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 c1c316b9 (c1c316b9c7a846ce8002809c151fcd1af14942c9)
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

Evidence

Checked by reading at c1c316b9; the audit's own executed probe follows below. The Guide's local profiles cannot collide (their names pass isValidLocalGamertag), so only application-created gamers can. 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 that commit'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.

Re-verified at b0e97bb1: SanitizeStoreFileNameComponent is unchanged; since ac961f433 (GS-004l) the Guide's local profiles must pass isValidLocalGamertag (ASCII letters, digits and single spaces, matched case-insensitively), and the dispatcher no longer creates gamers of its own (0ab6cc3fe). Not executed.

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

Gamers created with arbitrary gamertags through SignedInGamer::CreateInternal or the C route cna_signed_in_gamer_create_ext while no service is configured: two names that sanitise alike share one achievements list. Local profiles from the Guide's sign-in are limited to ASCII letters, digits and single spaces and matched case-insensitively (LocalProfiles.cpp, isValidLocalGamertag), so two of them cannot collide, and service accounts keep their achievements on the service. Leaderboards are unaffected.

Workaround

Restrict profile names to ASCII letters, digits, -, _ and ., or keep names distinct after that mapping.

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

Known issues
Bug index