Minimal reproducible example
https://github.com/eliotgevers/expo-font-ota-repro
Steps to reproduce
- Install the pinned dependencies, export the iOS bundle, and start the local update server as described in the reproduction README.
- Build and launch the app on an iOS simulator using
expo run:ios --configuration Release.
- Wait for Font loaded, then tap Download and restart.
The embedded bundle loads one Inter font with useFonts, without rendering it. The downloaded bundle contains the same font and renders it after Updates.reloadAsync().
Expected: the downloaded update renders the font and stays open.
Actual: the process terminates with a null EXC_BAD_ACCESS through CFStringFind → RCTGetFontWeight → RCTFontWithFontProperties during Fabric layout. Loading without rendering before the reload is important: an existing UIKit font object can mask the registration loss.
There are no icon libraries, invalid font files, application unregister calls, or native modifications in the stock reproduction.
Source-level finding
In FontLoaderModule.loadAsync, the process-wide alias still refers to the embedded asset URL after the JS runtime restarts. The update supplies a different URL for the same PostScript name:
- Unregistering the incoming URL reports
notRegistered (201).
- Registering that URL reports
duplicatedName (305), because the embedded URL still owns the name. registerFont currently accepts this as success.
- The alias is moved to the incoming URL.
- Stale-URL cleanup successfully unregisters the embedded URL.
- Neither URL now provides a registration. Alias lookup can return nil to React Native font selection.
An instrumented run recorded this sequence and a nil UIFont for Inter-Regular. The unmodified native package reproduced the same crash independently. The same stale cleanup exists in the multi-face loading path.
A candidate change distinguishes successful/already-registered URLs from duplicate-name collisions and skips stale cleanup when the incoming registration still depends on another URL. It preserves the current behavior of accepting a duplicated name.
Related reports
Environment and verification
See the reproduction README and recorded evidence for exact package versions and controls. All runtime tests use iOS Release builds and real locally served Expo updates, not Metro refresh.
Pinned reproduction: Expo 58.0.0-preview.5, expo-font 58.0.2, expo-updates 58.0.7, React Native 0.88.0-rc.2, React 19.3.0, @expo-google-fonts/inter 0.4.2. The same stock crash was also observed on preview 4 / expo-updates 58.0.6.
Toolchain: macOS 27.0, Xcode 27.0 (27A266a), iOS 27 Simulator, Node 26.8.1, Bun, CocoaPods 1.16.2 (invoked by Expo).
Current upstream main at 80a28600623dea779f27c6e2e2a6cea4d754dc7a has the same affected font-loader and registration-helper code. The SDK 57 control was blocked before JavaScript by an unrelated iOS 27 scene-lifecycle trap; no stable-SDK font result is claimed.
Expo Doctor Diagnostics
Running 20 checks on your project...
20/20 checks passed. No issues detected!
Minimal reproducible example
https://github.com/eliotgevers/expo-font-ota-repro
Steps to reproduce
expo run:ios --configuration Release.The embedded bundle loads one Inter font with
useFonts, without rendering it. The downloaded bundle contains the same font and renders it afterUpdates.reloadAsync().Expected: the downloaded update renders the font and stays open.
Actual: the process terminates with a null
EXC_BAD_ACCESSthroughCFStringFind → RCTGetFontWeight → RCTFontWithFontPropertiesduring Fabric layout. Loading without rendering before the reload is important: an existing UIKit font object can mask the registration loss.There are no icon libraries, invalid font files, application unregister calls, or native modifications in the stock reproduction.
Source-level finding
In
FontLoaderModule.loadAsync, the process-wide alias still refers to the embedded asset URL after the JS runtime restarts. The update supplies a different URL for the same PostScript name:notRegistered(201).duplicatedName(305), because the embedded URL still owns the name.registerFontcurrently accepts this as success.An instrumented run recorded this sequence and a nil
UIFontforInter-Regular. The unmodified native package reproduced the same crash independently. The same stale cleanup exists in the multi-face loading path.A candidate change distinguishes successful/already-registered URLs from duplicate-name collisions and skips stale cleanup when the incoming registration still depends on another URL. It preserves the current behavior of accepting a duplicated name.
Related reports
Environment and verification
See the reproduction README and recorded evidence for exact package versions and controls. All runtime tests use iOS Release builds and real locally served Expo updates, not Metro refresh.
Pinned reproduction: Expo 58.0.0-preview.5, expo-font 58.0.2, expo-updates 58.0.7, React Native 0.88.0-rc.2, React 19.3.0, @expo-google-fonts/inter 0.4.2. The same stock crash was also observed on preview 4 / expo-updates 58.0.6.
Toolchain: macOS 27.0, Xcode 27.0 (27A266a), iOS 27 Simulator, Node 26.8.1, Bun, CocoaPods 1.16.2 (invoked by Expo).
Current upstream
mainat80a28600623dea779f27c6e2e2a6cea4d754dc7ahas the same affected font-loader and registration-helper code. The SDK 57 control was blocked before JavaScript by an unrelated iOS 27 scene-lifecycle trap; no stable-SDK font result is claimed.Expo Doctor Diagnostics