Unity version
2022.3.3.62f2
Unity editor platform
macOS
AVPro Video edition
Core
AVPro Video version
3.3.4
Device hardware
iphone all series. (11,12,13,14,15,16)
Which iOS version are you using?
26.5.0
Texture format
BGRA
Audio output
Unity
Any other Media Player component configuration required to reproduce the issue.
No response
Which output component(s) are you using?
No response
Any other component configuration required to reproduce the issue.
No response
The issue
AVPro Video 3.3.4 — iOS GfxDeviceWorker crash in Plugin.playerSetup(playerRef:) (poisoned playerRef, use-after-free)
1. Summary
We are seeing a recurring, hard-to-reproduce iOS crash on the Unity render thread (UnityGfxDeviceWorker) inside AVPro's own native render-event dispatch. A symbolicated stack (matching dSYM available) shows the crash in:
@objc Plugin.playerSetup(playerRef:) + 60
<- _AVPPluginRenderEventFunction + 88
<- GfxDevice::InsertCustomMarkerCallbackAndDataWithFlags(...) // Unity IssuePluginEvent mechanism
<- GfxDeviceWorker::RunCommand(...)
The playerRef passed into playerSetup is a poisoned/invalid pointer (0x8000000000000000); the fault is a byte read of playerRef + 0x10. This is a classic use-after-free / dangling-handle dereference on the render thread.
We use multiple MediaPlayer instances concurrently and create/destroy/cross-fade them frequently. We have already implemented extensive per-player render-thread synchronization (GPU-fence-gated Close and Release, drain-before-Open), and the crash still occurs. Our analysis indicates the remaining cause is a cross-player race on AVPro's shared native state that per-player app-side synchronization cannot cover. We would like RenderHeads' help to confirm the mechanism and fix it at the source.
2. Environment
|
|
| AVPro Video |
3.3.4 (Apple native plugin version 3.3.4) |
| Unity |
2022.3.62f2, IL2CPP |
| Platform |
iOS 18.7.8, iPhone 11 (iPhone12,1, A13), arm64, Metal |
| Graphics fence support |
SystemInfo.supportsGraphicsFence == true on this device |
| Build type |
Release, UnityFramework linked as .framework; AVPro static lib linked into UnityFramework |
| Occurrence |
Intermittent in production; ~4 minutes into a session (not cold start); during normal gameplay with video activity |
We have the exact matching dSYM (UnityFramework UUID 7B219BD0-3113-3D53-8ECB-9ABC47C9946C) and can provide it or any further symbolication on request.
3. Crash detail (symbolicated)
Exception: EXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS at 0x8000000000000010, ESR 0x92000006 (Data Abort, byte read, translation fault).
Note: Apple's report annotates "possible pointer authentication failure", but UnityFramework is arm64 (not arm64e) — there is no PAC here. This is a plain wild-pointer dereference; the annotation is a red herring.
Crashed thread — UnityGfxDeviceWorker:
0 @objc Plugin.playerSetup(playerRef:) + 60
1 _AVPPluginRenderEventFunction + 88
2 GfxDevice::InsertCustomMarkerCallbackAndDataWithFlags(void (*)(int, void*), int, CustomMarkerCallbackFlags, void*, unsigned long) GfxDevice.cpp:2592
3 GfxDeviceWorker::RunCommand(ThreadedStreamBuffer&) GfxDeviceWorker.cpp
4 GfxDeviceWorkerAutoreleasePoolProxy GfxDeviceMetal.mm:5886
5 GfxDeviceWorker::RunExt(ThreadedStreamBuffer&) GfxDeviceWorker.cpp:375
6 GfxDeviceWorker::RunGfxDeviceWorker(void*) GfxDeviceWorker.cpp:333
7 Thread::RunThreadWrapper(void*) Thread.cpp:112
Register analysis (crash frame):
x20 = 0x8000000000000000 <- this is the playerRef argument
far = 0x8000000000000010 <- fault address = playerRef + 0x10 (a field read)
pc = playerSetup + 60
So playerSetup was invoked with playerRef = 0x8000000000000000 and faulted reading a field at offset +0x10. The playerRef itself is already invalid by the time the worker dispatches this custom render event — i.e. the player object the render command refers to has been freed/poisoned, or the reference resolved from the render-event data no longer maps to a live player.
Main thread (com.apple.main-thread) at crash time — provided for context; it is only a synchronization point, not the cause:
... ScriptableRenderContext::ExtractAndExecuteRenderPipeline
-> UniversalRenderPipeline.Render -> RenderCameraStack -> RenderSingleCamera
-> GfxDevice::ExecuteAsync -> ScriptableRenderLoopDraw -> BatchRenderer::ApplyShaderPass
-> ShaderLab compile -> GfxDeviceClient::CreateGpuProgram // blocked on the render-thread semaphore
The main thread is inside the normal URP render loop, blocked on the render-thread sync semaphore (creating a GPU shader program). The GfxDeviceWorker is independently draining its command queue and hits the AVPro custom render event with the bad playerRef. The main-thread work is unrelated to the fault; it merely establishes that a normal per-frame render submission is in flight.
4. How we use AVPro (so you can reason about the trigger)
Our title is a casual puzzle game with many short videos (cutscenes, level-complete clips, story/character videos, activity popups). Key facts about our usage:
- Multiple native players are alive simultaneously. At steady state there are typically 8+ native
AVPlayers: a manager pool pre-warms 4 "queue managers", each owning two MediaPlayers (A/B) for cross-fade = 8 players; plus embedded players in certain UI forms; plus on-demand growth.
- Cross-fade keeps two players of a manager live at once. During a transition we open a second
MediaPlayer while the first is still rendering, and blend them with Graphics.Blit into a RenderTexture. So concurrent Setup/Render of two players of the same logical unit is normal.
- Frequent create/destroy. Our UI framework instantiates a form on open and
Destroys it on close (no instance pool at the UI layer). Any form with an embedded MediaPlayer therefore triggers a real native AVPPluginMakePlayer on open and AVPPlayerRelease on close. The highest-frequency case is the level-complete video (every level). We also grow the manager pool by Instantiate on demand when concurrency exceeds the pre-warm count.
- Net effect: native
AVPPluginMakePlayer / AVPPlayerClose / AVPPlayerRelease are called often, from the main thread, while other players are actively being rendered by GfxDeviceWorker.
Native lifecycle entry points we observe in the plugin (3.3.4, PlatformMediaPlayer)
- Make —
PlatformMediaPlayer ctor: AVPPluginMakePlayer(_playerSettings) → CreateCommandBuffers() → Graphics.ExecuteCommandBuffer(_setupCommandBuffer). This enqueues the Setup render command, which the worker later executes via _AVPPluginRenderEventFunction → Plugin.playerSetup(playerRef:) (this is exactly our crash site).
- Close —
CloseMedia(): AVPPlayerClose(_player) + Texture2D.Destroy(_texturePlanes[]).
- Release —
Dispose(): Graphics.ExecuteCommandBuffer(_freeResourcesCommandBuffer) → AVPPlayerRelease(_player).
All three appear to bake the raw _player handle into render commands (Setup/Render/FreeResources) that run on GfxDeviceWorker, with no visible handle validation on the native side when the event is dispatched.
5. Root-cause analysis
Observation: The crash is on the establish/dispatch path (playerSetup), not on a Free/teardown path. For a single player, the ordering Setup → Render → Free is well-defined and we can fence it. So a single player's own lifecycle does not explain a poisoned playerRef arriving at playerSetup.
Hypothesis (cross-player race on shared native state): Because many native players coexist and are made/released frequently from the main thread, we believe one player's main-thread AVPPluginMakePlayer / AVPPlayerRelease mutates AVPro's shared Plugin state (e.g. an internal registry/table that maps a render-event playerRef/id to a live player object) concurrently with GfxDeviceWorker dispatching a different player's playerSetup/playerRender event. The worker then resolves/dereferences a playerRef that has just been invalidated → the poisoned 0x8000000000000000 we see.
Supporting evidence from our side:
- The faulting
playerRef (0x8000…0000) does not belong to the command's own just-created player (a freshly made player's pointer would not be that sentinel), consistent with a stale/foreign entry being read.
- Our entire C# orchestration and native-binding layer contains no locking whatsoever (no
lock/Monitor/Interlocked/Mutex) around AVPPluginMakePlayer / AVPPlayerRelease / AVPPlayerClose. And even if it did, a C# lock cannot serialize against AVPro's native GfxDeviceWorker thread. So the safety of concurrent main-thread lifecycle calls vs. worker-thread render-event dispatch rests entirely inside the plugin.
- Per-player GPU fences (which we have) serialize "player X's Free vs X's own render drain." They structurally cannot serialize "player X's Make/Release (main thread) vs the worker dispatching player Y's Setup."
We acknowledge this is an inference about closed-source Swift internals — hence the questions in §7. But the symbolicated crash landing squarely in playerSetup with a poisoned playerRef, combined with our concurrency pattern and lack of any cross-player serialization, points strongly here.
6. Mitigations we have already shipped (per-player)
We mention these so you know the "obvious" fixes are already in place and did not stop the crash — reinforcing that the remaining cause is cross-player / inside the plugin:
- Fence-gated native Release. In
PlatformMediaPlayer.Dispose() we insert a GraphicsFence (CPUSynchronisation, PixelProcessing) right after ExecuteCommandBuffer(_freeResourcesCommandBuffer), and defer AVPPlayerRelease on a DontDestroyOnLoad runner until fence.passed == true (floor of 2 frames, 120-frame leak fallback; falls back to frames when fence unsupported).
- Fence-gated native Close. A helper (
MediaPlayerSafeClose) sets enabled=false, waits one frame, inserts a GraphicsFence, waits for fence.passed, then calls CloseMedia() — so AVPPlayerClose + Texture2D.Destroy do not tear down textures/outputs still referenced by in-flight render commands.
- Drain-before-Open. We
await a render drain before any OpenMedia (which internally calls CloseMedia), and we defer the fade-to player's OpenMedia by a frame after CloseMedia.
- Churn reduction. A persistent
DontDestroyOnLoad host owns the level-complete players so they are made once and reused (Open/Close only, no Make/Release per level). The manager pool reuses instances via SetActive rather than destroying them.
None of these can gate the cross-player window, and the crash persists on builds containing all of the above.
7. What we would like from RenderHeads
- Thread safety of the
Plugin registry. In 3.3.4 (Apple), is the internal mapping of render-event playerRef/player-id → player object safe against concurrent main-thread AVPPluginMakePlayer/AVPPlayerRelease and GfxDeviceWorker render-event dispatch? If not, that is very likely our root cause.
- Handle validation before dereference. Does
_AVPPluginRenderEventFunction / Plugin.playerSetup(playerRef:) validate that playerRef still refers to a live player before dereferencing (e.g. read +0x10)? Could a freed/unknown ref be turned into a safe no-op instead of a UAF (generation counter / handle table / validity check)?
- Supported concurrent multi-player usage. What is the officially supported pattern for running many players concurrently with frequent create/destroy and cross-fade? Is there a required serialization, a maximum live count, or a mandated teardown ordering we are violating?
- Any known fix / regression. Is this a known issue in 3.3.4, and is there a fix in a later 3.x we should upgrade to?
8. Reproduction & assets we can provide
- The full
.ips crash report and the matching dSYM (UUID above).
- Additional symbolicated threads on request.
- We can attempt a minimal reproduction project that isolates the pattern (several
MediaPlayers created/destroyed rapidly + cross-fade of two players while others render), without our game code. Please confirm this would help and whether you have a preferred repro template.
Thank you — we are happy to run instrumented builds or test candidate fixes.
Media information
No response
Log output
Unity version
2022.3.3.62f2
Unity editor platform
macOS
AVPro Video edition
Core
AVPro Video version
3.3.4
Device hardware
iphone all series. (11,12,13,14,15,16)
Which iOS version are you using?
26.5.0
Texture format
BGRA
Audio output
Unity
Any other Media Player component configuration required to reproduce the issue.
No response
Which output component(s) are you using?
No response
Any other component configuration required to reproduce the issue.
No response
The issue
AVPro Video 3.3.4 — iOS GfxDeviceWorker crash in
Plugin.playerSetup(playerRef:)(poisoned playerRef, use-after-free)1. Summary
We are seeing a recurring, hard-to-reproduce iOS crash on the Unity render thread (
UnityGfxDeviceWorker) inside AVPro's own native render-event dispatch. A symbolicated stack (matching dSYM available) shows the crash in:The
playerRefpassed intoplayerSetupis a poisoned/invalid pointer (0x8000000000000000); the fault is a byte read ofplayerRef + 0x10. This is a classic use-after-free / dangling-handle dereference on the render thread.We use multiple
MediaPlayerinstances concurrently and create/destroy/cross-fade them frequently. We have already implemented extensive per-player render-thread synchronization (GPU-fence-gated Close and Release, drain-before-Open), and the crash still occurs. Our analysis indicates the remaining cause is a cross-player race on AVPro's shared native state that per-player app-side synchronization cannot cover. We would like RenderHeads' help to confirm the mechanism and fix it at the source.2. Environment
SystemInfo.supportsGraphicsFence == trueon this device.framework; AVPro static lib linked into UnityFrameworkWe have the exact matching dSYM (UnityFramework UUID
7B219BD0-3113-3D53-8ECB-9ABC47C9946C) and can provide it or any further symbolication on request.3. Crash detail (symbolicated)
Exception:
EXC_BAD_ACCESS (SIGSEGV),KERN_INVALID_ADDRESS at 0x8000000000000010,ESR 0x92000006(Data Abort, byte read, translation fault).Crashed thread —
UnityGfxDeviceWorker:Register analysis (crash frame):
So
playerSetupwas invoked withplayerRef = 0x8000000000000000and faulted reading a field at offset+0x10. The playerRef itself is already invalid by the time the worker dispatches this custom render event — i.e. the player object the render command refers to has been freed/poisoned, or the reference resolved from the render-event data no longer maps to a live player.Main thread (
com.apple.main-thread) at crash time — provided for context; it is only a synchronization point, not the cause:The main thread is inside the normal URP render loop, blocked on the render-thread sync semaphore (creating a GPU shader program). The
GfxDeviceWorkeris independently draining its command queue and hits the AVPro custom render event with the badplayerRef. The main-thread work is unrelated to the fault; it merely establishes that a normal per-frame render submission is in flight.4. How we use AVPro (so you can reason about the trigger)
Our title is a casual puzzle game with many short videos (cutscenes, level-complete clips, story/character videos, activity popups). Key facts about our usage:
AVPlayers: a manager pool pre-warms 4 "queue managers", each owning twoMediaPlayers (A/B) for cross-fade = 8 players; plus embedded players in certain UI forms; plus on-demand growth.MediaPlayerwhile the first is still rendering, and blend them withGraphics.Blitinto aRenderTexture. So concurrent Setup/Render of two players of the same logical unit is normal.Destroys it on close (no instance pool at the UI layer). Any form with an embeddedMediaPlayertherefore triggers a real nativeAVPPluginMakePlayeron open andAVPPlayerReleaseon close. The highest-frequency case is the level-complete video (every level). We also grow the manager pool byInstantiateon demand when concurrency exceeds the pre-warm count.AVPPluginMakePlayer/AVPPlayerClose/AVPPlayerReleaseare called often, from the main thread, while other players are actively being rendered byGfxDeviceWorker.Native lifecycle entry points we observe in the plugin (3.3.4,
PlatformMediaPlayer)PlatformMediaPlayerctor:AVPPluginMakePlayer(_playerSettings)→CreateCommandBuffers()→Graphics.ExecuteCommandBuffer(_setupCommandBuffer). This enqueues the Setup render command, which the worker later executes via_AVPPluginRenderEventFunction → Plugin.playerSetup(playerRef:)(this is exactly our crash site).CloseMedia():AVPPlayerClose(_player)+Texture2D.Destroy(_texturePlanes[]).Dispose():Graphics.ExecuteCommandBuffer(_freeResourcesCommandBuffer)→AVPPlayerRelease(_player).All three appear to bake the raw
_playerhandle into render commands (Setup/Render/FreeResources) that run onGfxDeviceWorker, with no visible handle validation on the native side when the event is dispatched.5. Root-cause analysis
Observation: The crash is on the establish/dispatch path (
playerSetup), not on a Free/teardown path. For a single player, the ordering Setup → Render → Free is well-defined and we can fence it. So a single player's own lifecycle does not explain a poisonedplayerRefarriving atplayerSetup.Hypothesis (cross-player race on shared native state): Because many native players coexist and are made/released frequently from the main thread, we believe one player's main-thread
AVPPluginMakePlayer/AVPPlayerReleasemutates AVPro's sharedPluginstate (e.g. an internal registry/table that maps a render-eventplayerRef/id to a live player object) concurrently withGfxDeviceWorkerdispatching a different player'splayerSetup/playerRenderevent. The worker then resolves/dereferences aplayerRefthat has just been invalidated → the poisoned0x8000000000000000we see.Supporting evidence from our side:
playerRef(0x8000…0000) does not belong to the command's own just-created player (a freshly made player's pointer would not be that sentinel), consistent with a stale/foreign entry being read.lock/Monitor/Interlocked/Mutex) aroundAVPPluginMakePlayer/AVPPlayerRelease/AVPPlayerClose. And even if it did, a C# lock cannot serialize against AVPro's nativeGfxDeviceWorkerthread. So the safety of concurrent main-thread lifecycle calls vs. worker-thread render-event dispatch rests entirely inside the plugin.We acknowledge this is an inference about closed-source Swift internals — hence the questions in §7. But the symbolicated crash landing squarely in
playerSetupwith a poisonedplayerRef, combined with our concurrency pattern and lack of any cross-player serialization, points strongly here.6. Mitigations we have already shipped (per-player)
We mention these so you know the "obvious" fixes are already in place and did not stop the crash — reinforcing that the remaining cause is cross-player / inside the plugin:
PlatformMediaPlayer.Dispose()we insert aGraphicsFence(CPUSynchronisation,PixelProcessing) right afterExecuteCommandBuffer(_freeResourcesCommandBuffer), and deferAVPPlayerReleaseon aDontDestroyOnLoadrunner untilfence.passed == true(floor of 2 frames, 120-frame leak fallback; falls back to frames when fence unsupported).MediaPlayerSafeClose) setsenabled=false, waits one frame, inserts aGraphicsFence, waits forfence.passed, then callsCloseMedia()— soAVPPlayerClose+Texture2D.Destroydo not tear down textures/outputs still referenced by in-flight render commands.awaita render drain before anyOpenMedia(which internally callsCloseMedia), and we defer the fade-to player'sOpenMediaby a frame afterCloseMedia.DontDestroyOnLoadhost owns the level-complete players so they are made once and reused (Open/Close only, no Make/Release per level). The manager pool reuses instances viaSetActiverather than destroying them.None of these can gate the cross-player window, and the crash persists on builds containing all of the above.
7. What we would like from RenderHeads
Pluginregistry. In 3.3.4 (Apple), is the internal mapping of render-eventplayerRef/player-id → player object safe against concurrent main-threadAVPPluginMakePlayer/AVPPlayerReleaseandGfxDeviceWorkerrender-event dispatch? If not, that is very likely our root cause._AVPPluginRenderEventFunction/Plugin.playerSetup(playerRef:)validate thatplayerRefstill refers to a live player before dereferencing (e.g. read+0x10)? Could a freed/unknown ref be turned into a safe no-op instead of a UAF (generation counter / handle table / validity check)?8. Reproduction & assets we can provide
.ipscrash report and the matching dSYM (UUID above).MediaPlayers created/destroyed rapidly + cross-fade of two players while others render), without our game code. Please confirm this would help and whether you have a preferred repro template.Thank you — we are happy to run instrumented builds or test candidate fixes.
Media information
No response
Log output