Skip to content

fix: cancel CDN download tasks on dealloc to prevent use-after-free#410

Draft
mfazekas wants to merge 1 commit intorive-app:mainfrom
mfazekas:fix/cdn-asset-loader-use-after-free
Draft

fix: cancel CDN download tasks on dealloc to prevent use-after-free#410
mfazekas wants to merge 1 commit intorive-app:mainfrom
mfazekas:fix/cdn-asset-loader-use-after-free

Conversation

@mfazekas
Copy link

@mfazekas mfazekas commented Jan 16, 2026

Summary

Fixes a use-after-free crash in CDNFileAssetLoader when RiveFile deallocates while CDN downloads are still in progress.

Problem

When loading a Rive file with CDN-hosted assets (loadCdn: true), the following crash can occur:

  1. RiveFile is created and starts CDN asset downloads
  2. RiveFile is deallocated before downloads complete
  3. Download completion handler fires and calls [factory decodeImage:]
  4. RiveFactory holds a raw rive::Factory* pointer that is now invalid
  5. Crash: EXC_BAD_ACCESS in rive::Factory::decodeImage

Stack trace:

Thread 7 Crashed::  Dispatch queue: com.apple.NSURLSession-delegate
0   RiveRuntime                   	       0x10521ea40 rive::ImageAsset::renderImage(rive::rcp<rive::RenderImage>) + 88
1   RiveRuntime                   	       0x1051395f8 -[RiveImageAsset renderImage:] + 120 (RiveFileAsset.mm:94)
2   RiveRuntime                   	       0x1051255b8 __63-[CDNFileAssetLoader loadContentsWithAsset:andData:andFactory:]_block_invoke + 628 (CDNFileAssetLoader.mm:52)
3   CFNetwork                     	       0x18492dcac __53-[__NSURLSessionLocal _downloadTaskWithTaskForClass:]_block_invoke + 520
4   CFNetwork                     	       0x184949bc0 __58-[__NSCFLocalDownloadTask _private_completionAfterMetrics]_block_invoke_3 + 80
5   libdispatch.dylib             	       0x18017c788 _dispatch_call_block_and_release + 24
6   libdispatch.dylib             	       0x180197278 _dispatch_client_callout + 12
7   libdispatch.dylib             	       0x180185ad0 _dispatch_lane_serial_drain + 984
8   libdispatch.dylib             	       0x180186590 _dispatch_lane_invoke + 396
9   libdispatch.dylib             	       0x180191380 _dispatch_root_queue_drain_deferred_wlh + 288
10  libdispatch.dylib             	       0x1801909f4 _dispatch_workloop_worker_thread + 440
11  libsystem_pthread.dylib       	       0x1047fab88 _pthread_wqthread + 288
12  libsystem_pthread.dylib       	       0x1047f998c start_wqthread + 8

Solution

Track active NSURLSessionTask objects and cancel them in dealloc:

  • Add _activeTasks array to track in-flight downloads
  • Cancel all tasks when CDNFileAssetLoader deallocates
  • Remove tasks from array on completion (success or failure)

When tasks are canceled, the completion handler fires with NSURLErrorCancelled, the if (!error) check fails, and no invalid C++ objects are accessed.

Reproducing in React Native

Rapidly mounting/unmounting a <Rive> component with a CDN-hosted asset file triggers the crash.

React Native reproduction component
import { useState, useEffect } from 'react';
import { View, Button, Text, StyleSheet } from 'react-native';
import { RiveView, useRiveFile } from '@rive-app/react-native';
import type { Metadata } from '../../shared/metadata';

function RiveInstance() {
  const { riveFile } = useRiveFile(
    require('../../../assets/rive/out_of_band.riv')
  );

  if (!riveFile) return null;

  return <RiveView file={riveFile} style={styles.rive} autoPlay />;
}

export default function TestReloadCrash() {
  const [iteration, setIteration] = useState(0);
  const [running, setRunning] = useState(false);
  const [showRive, setShowRive] = useState(false);

  useEffect(() => {
    if (!running) return;

    setShowRive(true);
    const unmountTimer = setTimeout(() => setShowRive(false), 10);
    const nextTimer = setTimeout(() => setIteration((i) => i + 1), 50);

    return () => {
      clearTimeout(unmountTimer);
      clearTimeout(nextTimer);
    };
  }, [running, iteration]);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Reload Crash Test</Text>
      <Text style={styles.subtitle}>
        Rapidly mounts/unmounts RiveView to test for crashes
      </Text>
      <Text style={styles.iteration}>Iteration: {iteration}</Text>
      <Button
        title={running ? 'Stop' : 'Start'}
        onPress={() => setRunning(!running)}
      />
      {showRive && <RiveInstance />}
    </View>
  );
}

TestReloadCrash.metadata = {
  name: 'Reload Crash Test',
  description: 'Rapidly mounts/unmounts RiveView to test for crashes',
} satisfies Metadata;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 16,
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
    textAlign: 'center',
    marginBottom: 16,
  },
  iteration: {
    fontSize: 18,
    marginBottom: 16,
  },
  rive: {
    width: 200,
    height: 200,
  },
});

Reproducing in iOS

Add test to Example-iOS/Source/Examples/ and include hosted_assets.riv (a .riv file with CDN-hosted assets) in the bundle.

RaceConditionTest.swift
import UIKit
import RiveRuntime

class RaceConditionTestViewController: UIViewController {
    private var iterationCount = 0
    private var maxIterations = 500
    private var isRunning = false

    @objc private func startTest() {
        isRunning = true
        iterationCount = 0
        runNextIteration()
    }

    private func runNextIteration() {
        guard isRunning, iterationCount < maxIterations else { return }

        iterationCount += 1

        autoreleasepool {
            guard let url = Bundle.main.url(forResource: "hosted_assets", withExtension: "riv"),
                  let data = try? Data(contentsOf: url) else { return }
            let _ = try? RiveFile(data: data, loadCdn: true)
            // File released here, CDN download still in progress → crash without fix
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak self] in
            self?.runNextIteration()
        }
    }
}

Alternatives Considered

  1. Weak RenderContext in RiveFactory - check for nil before decode
  2. Strong RenderContext reference - keep context alive until downloads complete
  3. Nil checks in completion handler - bail out if decode returns nil
  4. Task cancellation (Implemented) - cancel tasks on dealloc, prevents callback from running

Track active URLSession tasks and cancel them when CDNFileAssetLoader
deallocates to prevent accessing invalid C++ objects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant