diff --git a/change/@react-native-windows-cli-5f26c350-5ea4-40e4-b502-99c38d1c3ed1.json b/change/@react-native-windows-cli-5f26c350-5ea4-40e4-b502-99c38d1c3ed1.json new file mode 100644 index 00000000000..c91a8ea7da8 --- /dev/null +++ b/change/@react-native-windows-cli-5f26c350-5ea4-40e4-b502-99c38d1c3ed1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Defer PowerShell discovery until a command or health check needs it so CLI configuration can load without Windows build tools.", + "packageName": "@react-native-windows/cli", + "email": "14055146+shirakaba@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-eba3d592-2ea0-4067-8880-2beea6638837.json b/change/react-native-windows-eba3d592-2ea0-4067-8880-2beea6638837.json new file mode 100644 index 00000000000..2f6076908a1 --- /dev/null +++ b/change/react-native-windows-eba3d592-2ea0-4067-8880-2beea6638837.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix use-after-free crash when an Image is destroyed while its download is still in flight", + "packageName": "react-native-windows", + "email": "gordomacmaster@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts b/packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts index 74d4797811a..66da14b782c 100644 --- a/packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts +++ b/packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts @@ -17,7 +17,7 @@ import type { import {findPowerShell} from '@react-native-windows/find-dotnet-tools'; import {HealthCheckList} from './healthCheckList'; -const powershell = findPowerShell(); +let powershell: string | undefined; export function getHealthChecks(): HealthCheckCategory[] | undefined { // #8471: There are known cases where the dependencies script will error out. @@ -68,6 +68,7 @@ function getHealthChecksUnsafe(): HealthCheckCategory[] | undefined { getDiagnostics: async () => { let needsToBeFixed = true; try { + powershell ??= findPowerShell(); await execa( `"${powershell}" -ExecutionPolicy Unrestricted -NoProfile "${rnwDepScriptPath}" -NoPrompt -Check ${id}`, ); @@ -78,6 +79,21 @@ function getHealthChecksUnsafe(): HealthCheckCategory[] | undefined { }; }, runAutomaticFix: async ({loader, logManualInstallation}) => { + try { + powershell ??= findPowerShell(); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : undefined; + logManualInstallation({ + healthcheck: `react-native-windows dependency "${id}"`, + message: `Error finding PowerShell${ + errorMessage ? `: ${errorMessage}` : '' + }`, + }); + loader.fail(); + return; + } + const command = `"${powershell}" -ExecutionPolicy Unrestricted -NoProfile "${rnwDepScriptPath}" -Check ${id}`; try { const {exitCode} = await execa(command, {stdio: 'inherit'}); diff --git a/packages/@react-native-windows/cli/src/utils/commandWithProgress.ts b/packages/@react-native-windows/cli/src/utils/commandWithProgress.ts index 98a99520265..63774bd38ab 100644 --- a/packages/@react-native-windows/cli/src/utils/commandWithProgress.ts +++ b/packages/@react-native-windows/cli/src/utils/commandWithProgress.ts @@ -48,7 +48,7 @@ export function newSpinner(text: string) { return ora(options).start(); } -const powershell = findPowerShell(); +let powershell: string | undefined; export async function runPowerShellScriptFunction( taskDescription: string, @@ -58,6 +58,8 @@ export async function runPowerShellScriptFunction( errorCategory: CodedErrorType, useAppxCompatibility = false, ) { + powershell ??= findPowerShell(); + try { const printException = verbose ? '$_;' : ''; const importAppx = useAppxCompatibility diff --git a/vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp b/vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp index 045b3eb1ed8..ac05ea87127 100644 --- a/vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp @@ -186,6 +186,16 @@ facebook::react::ImageRequest WindowsImageManager::requestImage( auto weakObserverCoordinator = (std::weak_ptr) imageRequest.getSharedObserverCoordinator(); + // ImageResponseObserverCoordinator copies its observer list under a lock but dereferences the raw + // observer pointers after releasing it. Observers are added and removed on the UI thread (from + // ImageComponentView::setStateAndResubscribeImageResponseObserver), and that is also where the + // owning ImageComponentView - and with it the WindowsImageResponseObserver - is destroyed. Notifying + // the coordinator from the download/completion threads therefore races that teardown and can call + // into a freed observer. Marshal every notification onto the UI thread so subscription and + // notification are serialized on the same thread. Image decoding deliberately stays off the UI + // thread; only the notification itself is posted. + auto uiDispatcher = m_reactContext.UIDispatcher(); + auto rnImageSource = winrt::Microsoft::ReactNative::Composition::implementation::MakeImageSource(imageSource); auto provider = m_uriImageManager->TryGetUriImageProvider(m_reactContext.Handle(), rnImageSource); @@ -202,21 +212,39 @@ facebook::react::ImageRequest WindowsImageManager::requestImage( source.sourceType = ImageSourceType::Download; source.body = imageSource.body; - auto progressCallback = [weakObserverCoordinator](int64_t loaded, int64_t total) { - if (auto observerCoordinator = weakObserverCoordinator.lock()) { - float progress = total > 0 ? static_cast(loaded) / static_cast(total) : 1.0f; - observerCoordinator->nativeImageResponseProgress(progress, loaded, total); - } + auto progressCallback = [weakObserverCoordinator, uiDispatcher](int64_t loaded, int64_t total) { + float progress = total > 0 ? static_cast(loaded) / static_cast(total) : 1.0f; + uiDispatcher.Post([weakObserverCoordinator, progress, loaded, total]() { + if (auto observerCoordinator = weakObserverCoordinator.lock()) { + observerCoordinator->nativeImageResponseProgress(progress, loaded, total); + } + }); }; imageResponseTask = GetImageRandomAccessStreamAsync(source, progressCallback); } - imageResponseTask.Completed([weakObserverCoordinator](auto asyncOp, auto status) { - auto observerCoordinator = weakObserverCoordinator.lock(); - if (!observerCoordinator) { + imageResponseTask.Completed([weakObserverCoordinator, uiDispatcher](auto asyncOp, auto status) { + if (weakObserverCoordinator.expired()) { return; } + auto postComplete = [weakObserverCoordinator, uiDispatcher](auto image) { + uiDispatcher.Post([weakObserverCoordinator, image = std::move(image)]() { + if (auto observerCoordinator = weakObserverCoordinator.lock()) { + observerCoordinator->nativeImageResponseComplete(facebook::react::ImageResponse(image, nullptr /*metadata*/)); + } + }); + }; + + auto postFailure = [weakObserverCoordinator, + uiDispatcher](std::shared_ptr errorInfo) { + uiDispatcher.Post([weakObserverCoordinator, errorInfo = std::move(errorInfo)]() { + if (auto observerCoordinator = weakObserverCoordinator.lock()) { + observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(errorInfo)); + } + }); + }; + switch (status) { case winrt::Windows::Foundation::AsyncStatus::Completed: { auto imageResponse = asyncOp.GetResults(); @@ -224,23 +252,22 @@ facebook::react::ImageRequest WindowsImageManager::requestImage( winrt::get_self(imageResponse); auto imageResultOrError = selfImageResponse->ResolveImage(); if (imageResultOrError.image) { - observerCoordinator->nativeImageResponseComplete( - facebook::react::ImageResponse(imageResultOrError.image, nullptr /*metadata*/)); + postComplete(std::move(imageResultOrError.image)); } else { - observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(imageResultOrError.errorInfo)); + postFailure(std::move(imageResultOrError.errorInfo)); } break; } case winrt::Windows::Foundation::AsyncStatus::Canceled: { auto errorInfo = std::make_shared(); errorInfo->error = FormatHResultError(winrt::hresult_error(asyncOp.ErrorCode())); - observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(errorInfo)); + postFailure(std::move(errorInfo)); break; } case winrt::Windows::Foundation::AsyncStatus::Error: { auto errorInfo = std::make_shared(); errorInfo->error = FormatHResultError(winrt::hresult_error(asyncOp.ErrorCode())); - observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(errorInfo)); + postFailure(std::move(errorInfo)); break; } }