Skip to content

Commit e5a5ce4

Browse files
authored
fix(ios): keep RiveFile alive until decode* completes (#225) (#266)
Fixes #225. The crash was a use-after-free: `RiveFile` deallocated while asset downloads are in-flight, leaving `RiveFactory` with a dangling `rive::Factory*` pointer. Concurrent downloads widened the timing window, making it look like a threading race. Fix: strong `self` capture in decode closures keeps `activeFileRef` (and thus `RiveFile`/`RenderContext`) alive until decode completes. All decode types use a background serial queue via a `decodeAndApply` helper. Also upgrades rive-ios from 6.18.2 to 6.20.4 (includes CDN use-after-free fix from rive-app/rive-ios#449). Verified with 320+ mount/unmount stress cycles without crash.
1 parent 3748643 commit e5a5ce4

2 files changed

Lines changed: 49 additions & 42 deletions

File tree

example/ios/Podfile.lock

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,7 +1357,7 @@ PODS:
13571357
- React-jsiexecutor
13581358
- React-RCTFBReactNativeSpec
13591359
- ReactCommon/turbomodule/core
1360-
- react-native-safe-area-context (5.6.2):
1360+
- react-native-safe-area-context (5.7.0):
13611361
- DoubleConversion
13621362
- glog
13631363
- hermes-engine
@@ -1372,8 +1372,8 @@ PODS:
13721372
- React-hermes
13731373
- React-ImageManager
13741374
- React-jsi
1375-
- react-native-safe-area-context/common (= 5.6.2)
1376-
- react-native-safe-area-context/fabric (= 5.6.2)
1375+
- react-native-safe-area-context/common (= 5.7.0)
1376+
- react-native-safe-area-context/fabric (= 5.7.0)
13771377
- React-NativeModulesApple
13781378
- React-RCTFabric
13791379
- React-renderercss
@@ -1383,7 +1383,7 @@ PODS:
13831383
- ReactCommon/turbomodule/bridging
13841384
- ReactCommon/turbomodule/core
13851385
- Yoga
1386-
- react-native-safe-area-context/common (5.6.2):
1386+
- react-native-safe-area-context/common (5.7.0):
13871387
- DoubleConversion
13881388
- glog
13891389
- hermes-engine
@@ -1407,7 +1407,7 @@ PODS:
14071407
- ReactCommon/turbomodule/bridging
14081408
- ReactCommon/turbomodule/core
14091409
- Yoga
1410-
- react-native-safe-area-context/fabric (5.6.2):
1410+
- react-native-safe-area-context/fabric (5.7.0):
14111411
- DoubleConversion
14121412
- glog
14131413
- hermes-engine
@@ -1754,7 +1754,7 @@ PODS:
17541754
- React-logger (= 0.79.2)
17551755
- React-perflogger (= 0.79.2)
17561756
- React-utils (= 0.79.2)
1757-
- RiveRuntime (6.18.2)
1757+
- RiveRuntime (6.20.4)
17581758
- RNCAsyncStorage (2.2.0):
17591759
- DoubleConversion
17601760
- glog
@@ -1904,7 +1904,7 @@ PODS:
19041904
- ReactCommon/turbomodule/core
19051905
- RNWorklets
19061906
- Yoga
1907-
- RNRive (0.4.2):
1907+
- RNRive (0.4.7):
19081908
- DoubleConversion
19091909
- glog
19101910
- hermes-engine
@@ -1928,7 +1928,7 @@ PODS:
19281928
- ReactCodegen
19291929
- ReactCommon/turbomodule/bridging
19301930
- ReactCommon/turbomodule/core
1931-
- RiveRuntime (= 6.18.2)
1931+
- RiveRuntime (= 6.20.4)
19321932
- Yoga
19331933
- RNScreens (4.18.0):
19341934
- DoubleConversion
@@ -2347,7 +2347,7 @@ SPEC CHECKSUMS:
23472347
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
23482348
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
23492349
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
2350-
react-native-safe-area-context: 0b8555c40461feb7198e999912a3446602e7c601
2350+
react-native-safe-area-context: 26634d9b636a98ceee20cb6fa5dc946922f1e90f
23512351
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
23522352
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
23532353
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
@@ -2379,12 +2379,12 @@ SPEC CHECKSUMS:
23792379
ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584
23802380
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
23812381
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
2382-
RiveRuntime: 55c7a7badd9a8389d20fc8a75b7c6accc851b69a
2382+
RiveRuntime: f99ddd1d4b6a420dea5943ff52502e8f92ae7a92
23832383
RNCAsyncStorage: a1c8cc8a99c32de1244a9cf707bf9d83d0de0f71
23842384
RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d
23852385
RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5
23862386
RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344
2387-
RNRive: c02b3545abcf477d074945c5103f9f4bc9d8d672
2387+
RNRive: 042cbd7de1e5dd7e11aac076ade185926717543b
23882388
RNScreens: f38464ec1e83bda5820c3b05ccf4908e3841c5cc
23892389
RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b
23902390
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748

ios/ReferencedAssetLoader.swift

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func createAssetFileError(_ assetName: String) -> NitroRiveError {
1111
}
1212

1313
final class ReferencedAssetLoader {
14+
private static let decodeQueue = DispatchQueue(label: "com.rive.asset-decode")
1415
private var activeLoadCount = 0
1516
private var activeFileRef: RiveFile?
1617

@@ -35,43 +36,49 @@ final class ReferencedAssetLoader {
3536
RCTLogError("\(error)")
3637
}
3738

39+
/// Decodes an asset on a background serial queue, then applies the result
40+
/// on the main thread. The `[self]` capture keeps `activeFileRef` alive
41+
/// until completion, preventing use-after-free on the factory.
42+
private func decodeAndApply<T>(
43+
decode: @escaping () -> T?,
44+
apply: @escaping (T) -> Void,
45+
completion: @escaping () -> Void
46+
) {
47+
Self.decodeQueue.async { [self] in
48+
let result = decode()
49+
DispatchQueue.main.async {
50+
if let result { apply(result) }
51+
completion()
52+
_ = self
53+
}
54+
}
55+
}
56+
3857
private func processAssetBytes(
3958
_ data: Data, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void
4059
) {
41-
if data.isEmpty == true {
60+
if data.isEmpty {
4261
completion()
4362
return
4463
}
45-
DispatchQueue.global(qos: .background).async {
46-
switch asset {
47-
case let imageAsset as RiveImageAsset:
48-
let decodedImage = factory.decodeImage(data)
49-
DispatchQueue.main.async {
50-
imageAsset.renderImage(decodedImage)
51-
completion()
52-
}
53-
case let fontAsset as RiveFontAsset:
54-
let decodedFont = factory.decodeFont(data)
55-
DispatchQueue.main.async {
56-
fontAsset.font(decodedFont)
57-
completion()
58-
}
59-
case let audioAsset as RiveAudioAsset:
60-
guard let decodedAudio = factory.decodeAudio(data) else {
61-
DispatchQueue.main.async {
62-
completion()
63-
}
64-
return
65-
}
66-
DispatchQueue.main.async {
67-
audioAsset.audio(decodedAudio)
68-
completion()
69-
}
70-
default:
71-
DispatchQueue.main.async {
72-
completion()
73-
}
74-
}
64+
switch asset {
65+
case let imageAsset as RiveImageAsset:
66+
decodeAndApply(
67+
decode: { factory.decodeImage(data) },
68+
apply: { imageAsset.renderImage($0) },
69+
completion: completion)
70+
case let fontAsset as RiveFontAsset:
71+
decodeAndApply(
72+
decode: { factory.decodeFont(data) },
73+
apply: { fontAsset.font($0) },
74+
completion: completion)
75+
case let audioAsset as RiveAudioAsset:
76+
decodeAndApply(
77+
decode: { factory.decodeAudio(data) },
78+
apply: { audioAsset.audio($0) },
79+
completion: completion)
80+
default:
81+
completion()
7582
}
7683
}
7784

0 commit comments

Comments
 (0)