From a934df047d1ecdff50b8a823fea37db2910639e2 Mon Sep 17 00:00:00 2001 From: Autopilot Bot Date: Fri, 7 Aug 2026 02:11:53 -0700 Subject: [PATCH] Harden on-demand JS segment load path against missing cache file Summary: Harden the on-demand (lazily code-split) JS segment load path against a missing cache file. When RN lazily loads an on-demand JS segment, the file lives in an OS-purgeable / LRU-evictable on-demand cache and can already be gone by the time the segment is registered. On a missing file `JSBigFileString::fromPath` throws `std::runtime_error("... Could not open file: ...")`, and `ReactInstance::registerSegment` had no failure handling, so the throw surfaced as an unhandled JS exception and crashed the app. Fix (V4, addresses pieterdb's latest review feedback): - `JSBigFileString::fromPath`'s throwing contract is now documented in the header: it throws `std::runtime_error` when the file cannot be opened or read, and callers loading files that may legitimately be gone (an OS-purgeable / LRU-evictable cache) must catch and degrade gracefully. No behavior change to `fromPath` and no new factory. - `ReactInstance::registerSegment` builds the segment buffer on the calling thread (outside the `scheduleWork` callback) inside a try/catch. On the thrown exception it logs an ERROR and returns early, so a missing segment never schedules evaluation and the throw no longer escapes. The buffer is carried into the callback as a `std::shared_ptr` (`RuntimeScheduler` callbacks are copyable `std::function`s; `evaluateJavaScript` takes `const shared_ptr&`). This keeps file I/O off the JS thread. - Dropped the inline `access(R_OK)` pre-check: the `open()` failure inside `fromPath` is the single source of truth, removing the redundant check and its TOCTOU window. Changelog: [iOS][Fixed] - Avoid an unhandled exception in bridgeless `ReactInstance::registerSegment` when an on-demand JS segment file is missing from the cache at lazy-load time Reviewed By: zeyap, javache Differential Revision: D114761643 --- .../ReactCommon/cxxreact/JSBigString.h | 5 ++ .../react/runtime/ReactInstance.cpp | 53 ++++++++++++------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/react-native/ReactCommon/cxxreact/JSBigString.h b/packages/react-native/ReactCommon/cxxreact/JSBigString.h index 3668b3254625..a2365dee8962 100644 --- a/packages/react-native/ReactCommon/cxxreact/JSBigString.h +++ b/packages/react-native/ReactCommon/cxxreact/JSBigString.h @@ -140,6 +140,11 @@ class RN_EXPORT JSBigFileString : public JSBigString { size_t size() const override; int fd() const; + // Throws std::runtime_error when the file at sourceURL cannot be opened or + // read (e.g. it is missing). Callers that load files which may legitimately + // be gone at load time (such as an OS-purgeable / LRU-evictable cache) must + // catch this and degrade gracefully rather than let it surface as an + // unhandled exception. static std::unique_ptr fromPath(const std::string &sourceURL); private: diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index e6b8f129d5f4..288bde9d70f6 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -378,26 +378,39 @@ void ReactInstance::registerSegment( const std::string& segmentPath) { LOG(WARNING) << "Starting to run ReactInstance::registerSegment with segment " << segmentId; - runtimeScheduler_->scheduleWork([=](jsi::Runtime& runtime) { - TraceSection s("ReactInstance::registerSegment"); - auto tag = std::to_string(segmentId); - auto script = JSBigFileString::fromPath(segmentPath); - if (script->size() == 0) { - throw std::invalid_argument( - "Empty segment registered with ID " + tag + " from " + segmentPath); - } - - ReactMarker::logTaggedMarker( - ReactMarker::REGISTER_JS_SEGMENT_START, tag.c_str()); - LOG(WARNING) << "Starting to evaluate segment " << segmentId - << " in ReactInstance::registerSegment"; - runtime.evaluateJavaScript( - std::move(script), getSyntheticBundlePath(segmentId)); - LOG(WARNING) << "Finished evaluating segment " << segmentId - << " in ReactInstance::registerSegment"; - ReactMarker::logTaggedMarker( - ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); - }); + // Build the segment buffer off the JS thread: there's no need to block the + // JS thread on file I/O. The segment lives in an OS-purgeable / + // LRU-evictable on-demand cache and can already be gone by the time we get + // here, in which case fromPath throws. Catch it and return early so a + // missing segment degrades gracefully instead of surfacing as an unhandled + // exception. + std::shared_ptr script; + try { + script = JSBigFileString::fromPath(segmentPath); + } catch (const std::exception& e) { + LOG(ERROR) << "ReactInstance::registerSegment - could not load segment " + << segmentId << " from " << segmentPath << ": " << e.what(); + return; + } + if (script->size() == 0) { + throw std::invalid_argument( + "Empty segment registered with ID " + std::to_string(segmentId) + + " from " + segmentPath); + } + runtimeScheduler_->scheduleWork( + [script = std::move(script), segmentId](jsi::Runtime& runtime) { + TraceSection s("ReactInstance::registerSegment"); + auto tag = std::to_string(segmentId); + ReactMarker::logTaggedMarker( + ReactMarker::REGISTER_JS_SEGMENT_START, tag.c_str()); + LOG(WARNING) << "Starting to evaluate segment " << segmentId + << " in ReactInstance::registerSegment"; + runtime.evaluateJavaScript(script, getSyntheticBundlePath(segmentId)); + LOG(WARNING) << "Finished evaluating segment " << segmentId + << " in ReactInstance::registerSegment"; + ReactMarker::logTaggedMarker( + ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); + }); } namespace {