Skip to content

Commit cba6cf0

Browse files
committed
Enforce Sendness of CGEventTap callback
This was previously unsound.
1 parent d1ac1f9 commit cba6cf0

File tree

1 file changed

+25
-17
lines changed

1 file changed

+25
-17
lines changed

core-graphics/src/event.rs

+25-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use core::ffi::{c_ulong, c_void};
77
use core_foundation::{
88
base::{CFRelease, CFRetain, CFTypeID, TCFType},
99
mach_port::{CFMachPort, CFMachPortInvalidate, CFMachPortRef},
10+
runloop::{kCFRunLoopCommonModes, CFRunLoop},
1011
};
1112
use foreign_types::{foreign_type, ForeignType};
1213
use std::{mem::ManuallyDrop, ptr};
@@ -547,7 +548,7 @@ unsafe extern "C" fn cg_event_tap_callback_internal(
547548
/// use core_graphics::event::{CGEventTap, CGEventTapLocation, CGEventTapPlacement, CGEventTapOptions, CGEventType, CallbackResult};
548549
/// let current = CFRunLoop::get_current();
549550
///
550-
/// CGEventTap::with(
551+
/// CGEventTap::with_enabled(
551552
/// CGEventTapLocation::HID,
552553
/// CGEventTapPlacement::HeadInsertEventTap,
553554
/// CGEventTapOptions::Default,
@@ -556,15 +557,7 @@ unsafe extern "C" fn cg_event_tap_callback_internal(
556557
/// println!("{:?}", event.location());
557558
/// CallbackResult::Keep
558559
/// },
559-
/// |tap| {
560-
/// let loop_source = tap
561-
/// .mach_port()
562-
/// .create_runloop_source(0)
563-
/// .expect("Runloop source creation failed");
564-
/// current.add_source(&loop_source, unsafe { kCFRunLoopCommonModes });
565-
/// tap.enable();
566-
/// CFRunLoop::run_current();
567-
/// },
560+
/// || CFRunLoop::run_current(),
568561
/// ).expect("Failed to install event tap");
569562
/// ```
570563
#[must_use = "CGEventTap is disabled when dropped"]
@@ -574,38 +567,53 @@ pub struct CGEventTap<'tap_life> {
574567
}
575568

576569
impl CGEventTap<'static> {
577-
pub fn new<F: Fn(CGEventTapProxy, CGEventType, &CGEvent) -> CallbackResult + 'static>(
570+
pub fn new<F: Fn(CGEventTapProxy, CGEventType, &CGEvent) -> CallbackResult + Send + 'static>(
578571
tap: CGEventTapLocation,
579572
place: CGEventTapPlacement,
580573
options: CGEventTapOptions,
581574
events_of_interest: std::vec::Vec<CGEventType>,
582575
callback: F,
583576
) -> Result<Self, ()> {
584577
// SAFETY: callback is 'static so even if this object is forgotten it
585-
// will be valid to call.
578+
// will be valid to call. F is safe to send across threads.
586579
unsafe { Self::new_unchecked(tap, place, options, events_of_interest, callback) }
587580
}
588581
}
589582

590583
impl<'tap_life> CGEventTap<'tap_life> {
591-
pub fn with<R>(
584+
/// Configures an event tap with the supplied options and callback, then
585+
/// calls `with_fn`.
586+
///
587+
/// Note that the current thread run loop must run within `with_fn` for the
588+
/// tap to process events. The tap is destroyed when `with_fn` returns.
589+
pub fn with_enabled<R>(
592590
tap: CGEventTapLocation,
593591
place: CGEventTapPlacement,
594592
options: CGEventTapOptions,
595593
events_of_interest: std::vec::Vec<CGEventType>,
596594
callback: impl Fn(CGEventTapProxy, CGEventType, &CGEvent) -> CallbackResult + 'tap_life,
597-
with_fn: impl FnOnce(&Self) -> R,
595+
with_fn: impl FnOnce() -> R,
598596
) -> Result<R, ()> {
599597
// SAFETY: We are okay to bypass the 'static restriction because the
600598
// event tap is dropped before returning. The callback therefore cannot
601-
// be called after its lifetime expires.
599+
// be called after its lifetime expires. Since we only enable the tap
600+
// on the current thread run loop and don't hand it to user code, we
601+
// know that the callback will only be called from the current thread.
602602
let event_tap: Self =
603603
unsafe { Self::new_unchecked(tap, place, options, events_of_interest, callback)? };
604-
Ok(with_fn(&event_tap))
604+
let loop_source = event_tap
605+
.mach_port()
606+
.create_runloop_source(0)
607+
.expect("Runloop source creation failed");
608+
CFRunLoop::get_current().add_source(&loop_source, unsafe { kCFRunLoopCommonModes });
609+
event_tap.enable();
610+
Ok(with_fn())
605611
}
606612

607613
/// Caller is responsible for ensuring that this object is dropped before
608-
/// `'tap_life` expires.
614+
/// `'tap_life` expires. Either state captured by `callback` must be safe to
615+
/// send across threads, or the tap must only be installed on the current
616+
/// thread's run loop.
609617
pub unsafe fn new_unchecked(
610618
tap: CGEventTapLocation,
611619
place: CGEventTapPlacement,

0 commit comments

Comments
 (0)