Fix use-after-free in XMLHttpRequest::Send continuation#190
Merged
bkaradzic-microsoft merged 1 commit intoJun 9, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a use-after-free in the XMLHttpRequest::Send async completion path by ensuring the JS wrapper (and thus the underlying Napi::ObjectWrap) remains alive until the request settles.
Changes:
- Add a strong
Napi::ObjectReference“anchor” stored in astd::shared_ptrand captured by the async completion continuation to prevent GC from collecting the wrapper mid-request. - Document the rationale inline and align the approach with the existing
FileReaderanchoring idiom.
Comments suppressed due to low confidence (1)
Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp:282
- The completion continuation captures
anchoronly to extend the wrapper lifetime, but never uses it. Withwarnings_as_errors(XMLHttpRequest)enabled, many compilers warn on unused lambda captures (e.g.-Wunused-lambda-captureunder-Wall), which can break the build. Make the intent explicit by marking the capture as used inside the lambda body.
.then(m_runtimeScheduler, arcana::cancellation::none(), [this, anchor{std::move(anchor)}](const arcana::expected<void, std::exception_ptr>& result) {
// Run on every outcome -- transport exception OR underlying request succeeded but ended in a non-2xx
// status (e.g. a missing local file on UWP, where UrlLib silently retains status 0). The previous
// success-only continuation here skipped readyState=Done / loadend / error and let the JS observer
// hang.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
bghgary
approved these changes
Jun 9, 2026
The completion continuation scheduled by Send captures `this` raw with no lifetime guarantee. If the JS XMLHttpRequest wrapper is garbage-collected while the request is in flight (e.g. once the requesting script drops its reference), the wrapper -- and the C++ object behind it -- is destroyed before the continuation runs on the runtime scheduler, so dereferencing m_request / m_eventHandlerRefs / m_readyState faults (access violation). Anchor the wrapper for the duration of the request with a strong Napi::ObjectReference held in a shared_ptr captured by the continuation, mirroring FileReader's existing anchor pattern. The anchor is released automatically once the request settles and the lambda is destroyed, so there is no member self-reference to clear and no extra teardown path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
69f6449 to
2e8a89e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
XMLHttpRequest::Sendschedules its completion continuation capturingthisraw, witharcana::cancellation::none()and no lifetime guarantee:m_request.SendAsync() .then(arcana::inline_scheduler, ..., [sendRegion]{ ... }) .then(m_runtimeScheduler, arcana::cancellation::none(), [this](const arcana::expected<void, std::exception_ptr>& result) { const auto statusCode = arcana::underlying_cast(m_request.StatusCode()); // <-- AV on freed `this` SetReadyState(ReadyState::Done); ... m_eventHandlerRefs.clear(); });Nothing keeps the JS
XMLHttpRequestwrapper alive across the async window. If the wrapper is garbage-collected while the request is in flight (the requesting script dropped its reference, the scene/loader finished), the wrapper and the C++ object behind it are destroyed before the continuation runs on the runtime scheduler. Dereferencingm_request/m_eventHandlerRefs/m_readyStatethen faults.Observed as a flaky access violation in CI (BabylonNative), stack top:
i.e. the
[this]completion lambda running on a deadthis.Fix
Anchor the wrapper for the duration of the request with a strong
Napi::ObjectReferenceheld in ashared_ptrand captured by the continuation. This is the same idiom already used inFileReader(see the anchor inPolyfills/File/Source/FileReader.cpp). The anchor is released automatically when the request settles and the lambda is destroyed — no member self-reference, no extra teardown path, and the continuation capture stays small (arcana sizes the task payload to the callable).Validation
XMLHttpRequestpolyfill compiles clean against napi + arcana.ran=2 passed=2 failed=0, exit 0 — no regression.The original crash is a GC/teardown-timing race, so it reproduces only intermittently; the fix removes the race by construction (the wrapper cannot be collected before the continuation runs).