CNA-BUG-268: A NetworkSession BeginCreate, BeginFind or BeginJoin callback that calls the matching End* frees the action and the closure that is executing, and Begin* returns the freed pointer

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.

InvokeActiveActionCallback runs the AsyncCallback inline from BeginCreate, BeginFind and BeginJoin; the matching End* deletes the action, including the std::function that is executing, so captures read after End* and the IAsyncResult* that Begin* then returns point at freed memory. BeginJoinInvited always refuses and never invokes a callback.

Identifier
CNA-BUG-268
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
Microsoft::Xna::Framework::Net::NetworkSession::BeginCreate/BeginFind/BeginJoin/BeginJoinInvited with a callback that calls the matching End*

Expected behaviour

XNA's asynchronous pattern lets the AsyncCallback call the matching End* method to obtain the result, and the IAsyncResult that Begin* returned stays valid until the game drops it.

Actual behaviour at TARGET

NetworkSession.cpp calls action->Callback(*action) from inside BeginCreate, BeginFind and BeginJoin (through InvokeActiveActionCallback). EndFind deletes activeAction_ first thing, EndJoin right after copying the fields it needs, and EndCreate as soon as the session exists (or in its catch path). The action owns the const std::function Callback that is running, so a callback that calls the matching End* destroys its own closure: captures read after End* returns live in freed storage, and the IAsyncResult& it was given dangles. The function also returns the pointer it saved before invoking, so Begin* hands the caller an IAsyncResult* that is already freed, and a later IsCompleted call on it is a use after free. BeginJoinInvited (both overloads) throws GamerServicesNotAvailableException before it creates an action and EndJoinInvited throws ArgumentException, so that pair is not affected. The source comment treats the re-entrant case only as a stale-null problem and fixes that by saving the pointer first. CNA's own test NetworkSessionTest.BeginCreateCallbackCanReentrantlyCallEndCreate uses exactly this idiom (its callback assigns through a by-reference capture after EndCreate returns).

Source locations

Evidence

Read at 009d40f5; nothing was executed, so the hazard is derived from the code: the undefined behaviour needs a callback that touches its captures or the returned result after calling End*, which is the idiomatic shape of XNA's asynchronous samples. Found by the C10 conservation reviewer (unit ch51) and re-verified by the orchestrator against InvokeActiveActionCallback and EndFind. It is distinct from CNA-GAP-070 (the Begin/End pairs are synchronous by design). NetworkSessionTest.BeginCreateCallbackCanReentrantlyCallEndCreate covers the path but passes in an ordinary build, because the freed memory is still intact when the callback reads its capture.

Focused reproduction

No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.

Current tests

NetworkSessionTests.BeginCreateInvokesCallbackExactlyOnceWithCorrectIdentity pins the inline callback, and NetworkSessionTest.BeginCreateCallbackCanReentrantlyCallEndCreate calls EndCreate from inside it and writes through a by-reference capture afterwards, so the idiom is exercised; nothing checks for the use of freed memory.

Regression test

Call BeginFind with a callback that calls EndFind and then reads a captured variable, under AddressSanitizer, and check the pointer returned by Begin*; the fix is to defer the deletion until the callback has returned.

Blast radius

Games that follow XNA's asynchronous pattern and call End* from the callback of a NetworkSession Begin/End pair (create, find, join): a read of a capture after End*, or any use of the returned result, is a use after free. Callbacks whose last action is End* and that keep no result are much less exposed.

Workaround

Do not call End* from inside the callback: keep the IAsyncResult that the callback receives and call End* from the game loop after Begin* has returned, and do not use the pointer that Begin* returns.

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

Known issues
Bug index