From 2b81261b32fa637c3275f5d627e6aaac6bf5e8bf Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 12 Apr 2026 02:07:10 +0300 Subject: [PATCH] fix(windows): prevent use-after-free crash in Skia bitmap during dispose asComposeImageBitmap() shares native pixel memory with the source Bitmap. When dispose() closed the double-buffer bitmaps, Compose could still be rendering the last frame on the AWT-EventQueue thread, causing Skia's _nMakeFromBitmap to read freed memory (EXCEPTION_ACCESS_VIOLATION in skiko-windows-x64.dll). Instead of calling close() explicitly, nullify references and let the Skia Managed cleaner free them once all holders (including Compose) drop their references. --- .../windows/WindowsVideoPlayerState.kt | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt index 571c975..1517582 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt @@ -346,19 +346,10 @@ class WindowsVideoPlayerState : VideoPlayerState { // Free bitmaps and frame buffers bitmapLock.write { - val currentFrame = _currentFrame - if (currentFrame != null && - currentFrame !== skiaBitmapA && - currentFrame !== skiaBitmapB - ) { - currentFrame.close() - } _currentFrame = null currentFrameState.value = null - // Clean up double-buffering bitmaps - skiaBitmapA?.close() - skiaBitmapB?.close() + // Don't close bitmaps — see comment in releaseAllResources(). skiaBitmapA = null skiaBitmapB = null skiaBitmapWidth = 0 @@ -391,19 +382,16 @@ class WindowsVideoPlayerState : VideoPlayerState { // Free bitmaps and frame buffers bitmapLock.write { - val currentFrame = _currentFrame - if (currentFrame != null && - currentFrame !== skiaBitmapA && - currentFrame !== skiaBitmapB - ) { - currentFrame.close() - } _currentFrame = null currentFrameState.value = null - // Clean up double-buffering bitmaps - skiaBitmapA?.close() - skiaBitmapB?.close() + // Do NOT close the double-buffer bitmaps here: the ImageBitmap + // exposed via currentFrameState shares the same native pixel memory + // (asComposeImageBitmap is zero-copy). Compose may still be rendering + // the last frame on the AWT-EventQueue thread. Closing now would free + // the native memory while Skia reads it, causing an access violation. + // Nullifying the references lets the Skia Managed cleaner release them + // once Compose (and any other holder) drops its reference. skiaBitmapA = null skiaBitmapB = null skiaBitmapWidth = 0