From 9a8fe9a148722df58ab17d1f8342667210598e25 Mon Sep 17 00:00:00 2001 From: voidsip Date: Tue, 3 Nov 2026 02:43:47 -0600 Subject: [PATCH] Release Windows clipboard memory locks after reading --- src/native/windows/clipboard.rs | 81 ++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/native/windows/clipboard.rs b/src/native/windows/clipboard.rs index 630ac7dca..f319fd84f 100644 --- a/src/native/windows/clipboard.rs +++ b/src/native/windows/clipboard.rs @@ -1,3 +1,4 @@ +use winapi::shared::minwindef::HGLOBAL; use winapi::um::winbase::{GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, GMEM_MOVEABLE}; use winapi::um::winuser::CF_UNICODETEXT; use winapi::um::winuser::{ @@ -25,6 +26,16 @@ impl Drop for ClipboardGuard { } } +struct GlobalUnlockGuard(HGLOBAL); + +impl Drop for GlobalUnlockGuard { + fn drop(&mut self) { + unsafe { + GlobalUnlock(self.0); + } + } +} + unsafe fn get_raw_clipboard() -> Option> { // https://docs.microsoft.com/en-us/windows/win32/dataxchg/about-the-clipboard @@ -41,11 +52,16 @@ unsafe fn get_raw_clipboard() -> Option> { return None; } + read_clipboard_data(clipboard_data) +} + +unsafe fn read_clipboard_data(clipboard_data: HGLOBAL) -> Option> { let data_ptr = GlobalLock(clipboard_data) as *const u16; if data_ptr.is_null() { return None; } - let data_size = GlobalSize(clipboard_data) as usize; + let _lock_guard = GlobalUnlockGuard(clipboard_data); + let data_size = GlobalSize(clipboard_data) / std::mem::size_of::(); let slice = std::slice::from_raw_parts(data_ptr, data_size); let len = slice.iter().position(|b| *b == 0).unwrap_or(data_size); @@ -100,3 +116,66 @@ impl crate::native::Clipboard for WindowsClipboard { } } } +#[cfg(test)] +mod tests { + use super::*; + use winapi::um::winbase::{GlobalFlags, GlobalFree, GMEM_LOCKCOUNT}; + + struct Allocation(HGLOBAL); + + impl Drop for Allocation { + fn drop(&mut self) { + unsafe { + GlobalFree(self.0); + } + } + } + + #[test] + fn reading_clipboard_data_releases_the_memory_lock() { + unsafe { + let text = "hello 🦀\0ignored".encode_utf16().collect::>(); + let allocation = Allocation(GlobalAlloc(GMEM_MOVEABLE, text.len() * 2)); + assert!(!allocation.0.is_null()); + let data = GlobalLock(allocation.0) as *mut u16; + assert!(!data.is_null()); + ptr::copy_nonoverlapping(text.as_ptr(), data, text.len()); + GlobalUnlock(allocation.0); + for _ in 0..3 { + assert_eq!( + read_clipboard_data(allocation.0).unwrap(), + "hello 🦀".encode_utf16().collect::>() + ); + assert_eq!(GlobalFlags(allocation.0) & GMEM_LOCKCOUNT, 0); + } + } + } + + #[test] + fn unterminated_clipboard_data_stays_within_the_allocation() { + unsafe { + let allocation = Allocation(GlobalAlloc(GMEM_MOVEABLE, 16)); + assert!(!allocation.0.is_null()); + let data = GlobalLock(allocation.0) as *mut u8; + assert!(!data.is_null()); + let size = GlobalSize(allocation.0); + ptr::write_bytes(data, 0x41, size); + GlobalUnlock(allocation.0); + assert_eq!( + read_clipboard_data(allocation.0).unwrap(), + vec![0x4141; size / 2] + ); + assert_eq!(GlobalFlags(allocation.0) & GMEM_LOCKCOUNT, 0); + } + } + + #[test] + fn discarded_clipboard_data_does_not_get_locked() { + unsafe { + let allocation = Allocation(GlobalAlloc(GMEM_MOVEABLE, 0)); + assert!(!allocation.0.is_null()); + assert!(read_clipboard_data(allocation.0).is_none()); + assert_eq!(GlobalFlags(allocation.0) & GMEM_LOCKCOUNT, 0); + } + } +}