From b350f42d6567e9ca6248acf9a726227204868692 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Tue, 14 Nov 2017 11:39:29 +0100 Subject: [PATCH] Bug 1398268 - [u2f-hid-rs] Rewrite macOS IOHIDManager communication and state machine r=jcj Review: https://github.com/jcjones/u2f-hid-rs/pull/52 --- dom/webauthn/u2f-hid-rs/.gitignore | 1 + dom/webauthn/u2f-hid-rs/src/lib.rs | 4 +- dom/webauthn/u2f-hid-rs/src/macos/device.rs | 95 +------ dom/webauthn/u2f-hid-rs/src/macos/iohid.rs | 71 +++--- dom/webauthn/u2f-hid-rs/src/macos/iokit.rs | 64 ++++- dom/webauthn/u2f-hid-rs/src/macos/mod.rs | 202 ++++++--------- dom/webauthn/u2f-hid-rs/src/macos/monitor.rs | 236 +++++++++++------- .../u2f-hid-rs/src/macos/transaction.rs | 83 ++++++ 8 files changed, 413 insertions(+), 343 deletions(-) create mode 100644 dom/webauthn/u2f-hid-rs/src/macos/transaction.rs diff --git a/dom/webauthn/u2f-hid-rs/.gitignore b/dom/webauthn/u2f-hid-rs/.gitignore index 97455fc4af67..7bf5745b8a51 100644 --- a/dom/webauthn/u2f-hid-rs/.gitignore +++ b/dom/webauthn/u2f-hid-rs/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +**/*.rs.bk # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock diff --git a/dom/webauthn/u2f-hid-rs/src/lib.rs b/dom/webauthn/u2f-hid-rs/src/lib.rs index c5d592f353e8..17b4fec7a103 100644 --- a/dom/webauthn/u2f-hid-rs/src/lib.rs +++ b/dom/webauthn/u2f-hid-rs/src/lib.rs @@ -27,6 +27,9 @@ pub mod platform; #[path = "stub/mod.rs"] pub mod platform; +#[cfg(not(any(target_os = "macos")))] +mod khmatcher; + #[macro_use] extern crate log; extern crate rand; @@ -35,7 +38,6 @@ extern crate boxfnonce; extern crate runloop; mod consts; -mod khmatcher; mod u2ftypes; mod u2fprotocol; diff --git a/dom/webauthn/u2f-hid-rs/src/macos/device.rs b/dom/webauthn/u2f-hid-rs/src/macos/device.rs index 4d5c78e9df95..7b322e2f9591 100644 --- a/dom/webauthn/u2f-hid-rs/src/macos/device.rs +++ b/dom/webauthn/u2f-hid-rs/src/macos/device.rs @@ -2,70 +2,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -extern crate libc; extern crate log; -use std::fmt; -use std::io; -use std::io::{Read, Write}; -use std::slice; -use std::sync::mpsc::{channel, Sender, Receiver, RecvTimeoutError}; -use std::time::Duration; - -use core_foundation_sys::base::*; -use libc::c_void; - use consts::{CID_BROADCAST, HID_RPT_SIZE}; +use core_foundation_sys::base::*; +use platform::iokit::*; +use std::{fmt, io}; +use std::io::{Read, Write}; +use std::sync::mpsc::{Receiver, RecvTimeoutError}; +use std::time::Duration; use u2ftypes::U2FDevice; -use super::iokit::*; - const READ_TIMEOUT: u64 = 15; pub struct Device { device_ref: IOHIDDeviceRef, cid: [u8; 4], report_rx: Receiver>, - report_send_void: *mut c_void, - scratch_buf_ptr: *mut u8, } impl Device { - pub fn new(device_ref: IOHIDDeviceRef) -> Self { - let (report_tx, report_rx) = channel(); - let report_send_void = Box::into_raw(Box::new(report_tx)) as *mut c_void; - - let scratch_buf = [0; HID_RPT_SIZE]; - let scratch_buf_ptr = Box::into_raw(Box::new(scratch_buf)) as *mut u8; - - unsafe { - IOHIDDeviceRegisterInputReportCallback( - device_ref, - scratch_buf_ptr, - HID_RPT_SIZE as CFIndex, - read_new_data_cb, - report_send_void, - ); - } - + pub fn new(device_ref: IOHIDDeviceRef, report_rx: Receiver>) -> Self { Self { device_ref, cid: CID_BROADCAST, report_rx, - report_send_void, - scratch_buf_ptr, - } - } -} - -impl Drop for Device { - fn drop(&mut self) { - debug!("Dropping U2F device {}", self); - - unsafe { - // Re-allocate raw pointers for destruction. - let _ = Box::from_raw(self.report_send_void as *mut Sender>); - let _ = Box::from_raw(self.scratch_buf_ptr as *mut [u8; HID_RPT_SIZE]); } } } @@ -148,45 +109,3 @@ impl U2FDevice for Device { self.cid = cid; } } - -// This is called from the RunLoop thread -extern "C" fn read_new_data_cb( - context: *mut c_void, - _: IOReturn, - _: *mut c_void, - report_type: IOHIDReportType, - report_id: u32, - report: *mut u8, - report_len: CFIndex, -) { - unsafe { - let tx = &mut *(context as *mut Sender>); - - trace!( - "read_new_data_cb type={} id={} report={:?} len={}", - report_type, - report_id, - report, - report_len - ); - - let report_len = report_len as usize; - if report_len > HID_RPT_SIZE { - warn!( - "read_new_data_cb got too much data! {} > {}", - report_len, - HID_RPT_SIZE - ); - return; - } - - let data = slice::from_raw_parts(report, report_len).to_vec(); - - if let Err(e) = tx.send(data) { - // TOOD: This happens when the channel closes before this thread - // does. This is pretty common, but let's deal with stopping - // properly later. - warn!("Problem returning read_new_data_cb data for thread: {}", e); - }; - } -} diff --git a/dom/webauthn/u2f-hid-rs/src/macos/iohid.rs b/dom/webauthn/u2f-hid-rs/src/macos/iohid.rs index fffd5991b06b..dea1a9720db4 100644 --- a/dom/webauthn/u2f-hid-rs/src/macos/iohid.rs +++ b/dom/webauthn/u2f-hid-rs/src/macos/iohid.rs @@ -5,17 +5,14 @@ extern crate log; extern crate libc; -use std::io; - -use super::iokit::*; +use consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; use core_foundation_sys::base::*; use core_foundation_sys::dictionary::*; use core_foundation_sys::number::*; use core_foundation_sys::runloop::*; use core_foundation_sys::string::*; - -use consts::{FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID}; -use util::io_err; +use libc::c_void; +use platform::iokit::{CFRunLoopObserverContext, CFRunLoopObserverCreate}; pub struct IOHIDDeviceMatcher { dict: CFDictionaryRef, @@ -92,36 +89,48 @@ impl Drop for IOHIDDeviceMatcher { } } -pub struct IOHIDManager { - manager: IOHIDManagerRef, +pub struct CFRunLoopEntryObserver { + observer: CFRunLoopObserverRef, + // Keep alive until the observer goes away. + context_ptr: *mut CFRunLoopObserverContext, } -impl IOHIDManager { - pub fn new() -> io::Result { - let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) }; +impl CFRunLoopEntryObserver { + pub fn new(callback: CFRunLoopObserverCallBack, context: *mut c_void) -> Self { + let context = CFRunLoopObserverContext::new(context); + let context_ptr = Box::into_raw(Box::new(context)); - let rv = unsafe { IOHIDManagerOpen(manager, kIOHIDManagerOptionNone) }; - if rv != 0 { - return Err(io_err("Couldn't open HID Manager")); - } - - unsafe { - IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode) + let observer = unsafe { + CFRunLoopObserverCreate( + kCFAllocatorDefault, + kCFRunLoopEntry, + false as Boolean, + 0, + callback, + context_ptr, + ) }; - Ok(Self { manager }) - } - - pub fn get(&self) -> IOHIDManagerRef { - self.manager - } -} - -impl Drop for IOHIDManager { - fn drop(&mut self) { - let rv = unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) }; - if rv != 0 { - warn!("Couldn't close the HID Manager"); + Self { + observer, + context_ptr, } } + + pub fn add_to_current_runloop(&self) { + unsafe { + CFRunLoopAddObserver(CFRunLoopGetCurrent(), self.observer, kCFRunLoopDefaultMode) + }; + } +} + +impl Drop for CFRunLoopEntryObserver { + fn drop(&mut self) { + unsafe { + CFRelease(self.observer as *mut c_void); + + // Drop the CFRunLoopObserverContext. + let _ = Box::from_raw(self.context_ptr); + }; + } } diff --git a/dom/webauthn/u2f-hid-rs/src/macos/iokit.rs b/dom/webauthn/u2f-hid-rs/src/macos/iokit.rs index 9bf138b40cc2..436a49c3b215 100644 --- a/dom/webauthn/u2f-hid-rs/src/macos/iokit.rs +++ b/dom/webauthn/u2f-hid-rs/src/macos/iokit.rs @@ -7,11 +7,12 @@ extern crate core_foundation_sys; extern crate libc; -use libc::c_void; -use core_foundation_sys::base::{CFIndex, CFAllocatorRef}; +use core_foundation_sys::base::{Boolean, CFIndex, CFAllocatorRef, CFOptionFlags}; use core_foundation_sys::string::CFStringRef; -use core_foundation_sys::runloop::CFRunLoopRef; +use core_foundation_sys::runloop::{CFRunLoopRef, CFRunLoopObserverRef, CFRunLoopObserverCallBack}; use core_foundation_sys::dictionary::CFDictionaryRef; +use libc::c_void; +use std::ops::Deref; type IOOptionBits = u32; @@ -28,7 +29,7 @@ pub type IOHIDDeviceCallback = extern "C" fn(context: *mut c_void, pub type IOHIDReportType = IOOptionBits; pub type IOHIDReportCallback = extern "C" fn(context: *mut c_void, result: IOReturn, - sender: *mut c_void, + sender: IOHIDDeviceRef, report_type: IOHIDReportType, report_id: u32, report: *mut u8, @@ -50,8 +51,51 @@ pub struct IOHIDDeviceRef(*const c_void); unsafe impl Send for IOHIDDeviceRef {} unsafe impl Sync for IOHIDDeviceRef {} +pub struct SendableRunLoop(pub CFRunLoopRef); + +unsafe impl Send for SendableRunLoop {} + +impl Deref for SendableRunLoop { + type Target = CFRunLoopRef; + + fn deref(&self) -> &CFRunLoopRef { + &self.0 + } +} + +#[repr(C)] +pub struct CFRunLoopObserverContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, +} + +impl CFRunLoopObserverContext { + pub fn new(context: *mut c_void) -> Self { + Self { + version: 0 as CFIndex, + info: context, + retain: None, + release: None, + copyDescription: None, + } + } +} + #[link(name = "IOKit", kind = "framework")] extern "C" { + // CFRunLoop + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + // IOHIDManager pub fn IOHIDManagerCreate( allocator: CFAllocatorRef, @@ -68,6 +112,11 @@ extern "C" { callback: IOHIDDeviceCallback, context: *mut c_void, ); + pub fn IOHIDManagerRegisterInputReportCallback( + manager: IOHIDManagerRef, + callback: IOHIDReportCallback, + context: *mut c_void, + ); pub fn IOHIDManagerOpen(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; pub fn IOHIDManagerClose(manager: IOHIDManagerRef, options: IOHIDManagerOptions) -> IOReturn; pub fn IOHIDManagerScheduleWithRunLoop( @@ -84,11 +133,4 @@ extern "C" { report: *const u8, reportLength: CFIndex, ) -> IOReturn; - pub fn IOHIDDeviceRegisterInputReportCallback( - device: IOHIDDeviceRef, - report: *const u8, - reportLength: CFIndex, - callback: IOHIDReportCallback, - context: *mut c_void, - ); } diff --git a/dom/webauthn/u2f-hid-rs/src/macos/mod.rs b/dom/webauthn/u2f-hid-rs/src/macos/mod.rs index ea2782fe35cf..53c0f5cb7937 100644 --- a/dom/webauthn/u2f-hid-rs/src/macos/mod.rs +++ b/dom/webauthn/u2f-hid-rs/src/macos/mod.rs @@ -3,30 +3,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ extern crate log; -extern crate libc; - -use std::thread; -use std::time::Duration; mod device; -mod devicemap; mod iokit; mod iohid; mod monitor; - -use self::devicemap::DeviceMap; -use self::monitor::Monitor; +mod transaction; use consts::PARAMETER_SIZE; -use khmatcher::KeyHandleMatcher; -use runloop::RunLoop; +use platform::device::Device; +use platform::transaction::Transaction; +use std::thread; +use std::time::Duration; +use u2fprotocol::{u2f_init_device, u2f_register, u2f_sign, u2f_is_keyhandle_valid}; use util::{io_err, OnceCallback}; -use u2fprotocol::{u2f_register, u2f_sign, u2f_is_keyhandle_valid}; #[derive(Default)] pub struct PlatformManager { - // Handle to the thread loop. - thread: Option, + transaction: Option, } impl PlatformManager { @@ -47,56 +41,43 @@ impl PlatformManager { let cbc = callback.clone(); - let thread = RunLoop::new_with_timeout( - move |alive| { - let mut devices = DeviceMap::new(); - let monitor = try_or!(Monitor::new(), |e| callback.call(Err(e))); - let mut matches = KeyHandleMatcher::new(&key_handles); + // Start a new "sign" transaction. + let transaction = Transaction::new(timeout, cbc.clone(), move |device_ref, rx, alive| { + // Create a new device. + let dev = &mut Device::new(device_ref, rx); - 'top: while alive() && monitor.alive() { - for event in monitor.events() { - devices.process_event(event); - } + // Try initializing it. + if !u2f_init_device(dev) { + return; + } - // Query newly added devices. - matches.update(devices.iter_mut(), |device, key_handle| { - u2f_is_keyhandle_valid(device, &challenge, &application, key_handle) - .unwrap_or(false /* no match on failure */) - }); + // Iterate the exlude list and see if there are any matches. + // Abort the state machine if we found a valid key handle. + if key_handles.iter().any(|key_handle| { + u2f_is_keyhandle_valid(dev, &challenge, &application, key_handle) + .unwrap_or(false) /* no match on failure */ + }) + { + return; + } - // Iterate all devices that don't match any of the handles - // in the exclusion list and try to register. - for (path, device) in devices.iter_mut() { - if matches.get(path).is_empty() { - if let Ok(bytes) = u2f_register(device, &challenge, &application) { - callback.call(Ok(bytes)); - return; - } - } - - // Check to see if monitor.events has any hotplug events that we'll need - // to handle - if monitor.events().size_hint().0 > 0 { - debug!("Hotplug event; restarting loop"); - continue 'top; - } - } - - thread::sleep(Duration::from_millis(100)); + while alive() { + if let Ok(bytes) = u2f_register(dev, &challenge, &application) { + callback.call(Ok(bytes)); + break; } - callback.call(Err(io_err("aborted or timed out"))); - }, - timeout, - ); + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + }); - self.thread = Some(try_or!( - thread, - |_| cbc.call(Err(io_err("couldn't create runloop"))) - )); + // Store the transaction so we can cancel it, if needed. + self.transaction = Some(try_or!(transaction, |_| { + cbc.call(Err(io_err("couldn't create transaction"))) + })); } - pub fn sign( &mut self, timeout: u64, @@ -110,85 +91,58 @@ impl PlatformManager { let cbc = callback.clone(); - let thread = RunLoop::new_with_timeout( - move |alive| { - let mut devices = DeviceMap::new(); - let monitor = try_or!(Monitor::new(), |e| callback.call(Err(e))); - let mut matches = KeyHandleMatcher::new(&key_handles); + // Start a new "register" transaction. + let transaction = Transaction::new(timeout, cbc.clone(), move |device_ref, rx, alive| { + // Create a new device. + let dev = &mut Device::new(device_ref, rx); - 'top: while alive() && monitor.alive() { - for event in monitor.events() { - devices.process_event(event); + // Try initializing it. + if !u2f_init_device(dev) { + return; + } + + // Find all matching key handles. + let key_handles = key_handles + .iter() + .filter(|key_handle| { + u2f_is_keyhandle_valid(dev, &challenge, &application, key_handle) + .unwrap_or(false) /* no match on failure */ + }) + .collect::>>(); + + while alive() { + // If the device matches none of the given key handles + // then just make it blink with bogus data. + if key_handles.is_empty() { + let blank = vec![0u8; PARAMETER_SIZE]; + if let Ok(_) = u2f_register(dev, &blank, &blank) { + callback.call(Err(io_err("invalid key"))); + break; } - - // Query newly added devices. - matches.update(devices.iter_mut(), |device, key_handle| { - u2f_is_keyhandle_valid(device, &challenge, &application, key_handle) - .unwrap_or(false /* no match on failure */) - }); - - // Iterate all devices. - for (path, device) in devices.iter_mut() { - let key_handles = matches.get(path); - - // If the device matches none of the given key handles - // then just make it blink with bogus data. - if key_handles.is_empty() { - let blank = vec![0u8; PARAMETER_SIZE]; - if let Ok(_) = u2f_register(device, &blank, &blank) { - callback.call(Err(io_err("invalid key"))); - return; - } - - continue; - } - - // Otherwise, try to sign. - for key_handle in key_handles { - if let Ok(bytes) = u2f_sign( - device, - &challenge, - &application, - key_handle, - ) - { - callback.call(Ok((key_handle.to_vec(), bytes))); - return; - } - } - - // Check to see if monitor.events has any hotplug events that we'll - // need to handle - if monitor.events().size_hint().0 > 0 { - debug!("Hotplug event; restarting loop"); - continue 'top; + } else { + // Otherwise, try to sign. + for key_handle in &key_handles { + if let Ok(bytes) = u2f_sign(dev, &challenge, &application, key_handle) { + callback.call(Ok((key_handle.to_vec(), bytes))); + break; } } - - thread::sleep(Duration::from_millis(100)); } - callback.call(Err(io_err("aborted or timed out"))); - }, - timeout, - ); + // Sleep a bit before trying again. + thread::sleep(Duration::from_millis(100)); + } + }); - self.thread = Some(try_or!( - thread, - |_| cbc.call(Err(io_err("couldn't create runloop"))) - )); + // Store the transaction so we can cancel it, if needed. + self.transaction = Some(try_or!(transaction, |_| { + cbc.call(Err(io_err("couldn't create transaction"))) + })); } pub fn cancel(&mut self) { - if let Some(thread) = self.thread.take() { - thread.cancel(); + if let Some(mut transaction) = self.transaction.take() { + transaction.cancel(); } } } - -impl Drop for PlatformManager { - fn drop(&mut self) { - debug!("OSX PlatformManager dropped"); - self.cancel(); - } -} diff --git a/dom/webauthn/u2f-hid-rs/src/macos/monitor.rs b/dom/webauthn/u2f-hid-rs/src/macos/monitor.rs index 147b76b067a0..918e158db2c1 100644 --- a/dom/webauthn/u2f-hid-rs/src/macos/monitor.rs +++ b/dom/webauthn/u2f-hid-rs/src/macos/monitor.rs @@ -2,115 +2,175 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use std::io; -use std::sync::mpsc::{channel, Sender, Receiver, TryIter}; -use std::thread; - -use super::iohid::*; -use super::iokit::*; -use core_foundation_sys::runloop::*; -use runloop::RunLoop; - extern crate log; extern crate libc; + +use core_foundation_sys::base::*; +use core_foundation_sys::runloop::*; use libc::c_void; +use platform::iohid::*; +use platform::iokit::*; +use runloop::RunLoop; +use std::{io, slice}; +use std::collections::HashMap; +use std::sync::mpsc::{channel, Receiver, Sender}; +use util::io_err; -pub enum Event { - Add(IOHIDDeviceRef), - Remove(IOHIDDeviceRef), +struct DeviceData { + tx: Sender>, + runloop: RunLoop, } -pub struct Monitor { - // Receive events from the thread. - rx: Receiver, - // Handle to the thread loop. - thread: RunLoop, +pub struct Monitor +where + F: Fn(IOHIDDeviceRef, Receiver>, &Fn() -> bool) + Sync, +{ + manager: IOHIDManagerRef, + // Keep alive until the monitor goes away. + _matcher: IOHIDDeviceMatcher, + map: HashMap, + new_device_cb: F, } -impl Monitor { - pub fn new() -> io::Result { +impl Monitor +where + F: Fn(IOHIDDeviceRef, Receiver>, &Fn() -> bool) + Sync + 'static, +{ + pub fn new(new_device_cb: F) -> Self { + let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) }; + + // Match FIDO devices only. + let _matcher = IOHIDDeviceMatcher::new(); + unsafe { IOHIDManagerSetDeviceMatching(manager, _matcher.get()) }; + + Self { + manager, + _matcher, + new_device_cb, + map: HashMap::new(), + } + } + + pub fn start(&mut self) -> io::Result<()> { + let context = self as *mut Self as *mut c_void; + + unsafe { + IOHIDManagerRegisterDeviceMatchingCallback( + self.manager, + Monitor::::on_device_matching, + context, + ); + IOHIDManagerRegisterDeviceRemovalCallback( + self.manager, + Monitor::::on_device_removal, + context, + ); + IOHIDManagerRegisterInputReportCallback( + self.manager, + Monitor::::on_input_report, + context, + ); + + IOHIDManagerScheduleWithRunLoop( + self.manager, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode, + ); + + let rv = IOHIDManagerOpen(self.manager, kIOHIDManagerOptionNone); + if rv == 0 { + Ok(()) + } else { + Err(io_err(&format!("Couldn't open HID Manager, rv={}", rv))) + } + } + } + + pub fn stop(&mut self) { + // Remove all devices. + while !self.map.is_empty() { + let device_ref = *self.map.keys().next().unwrap(); + self.remove_device(&device_ref); + } + + // Close the manager and its devices. + unsafe { IOHIDManagerClose(self.manager, kIOHIDManagerOptionNone) }; + } + + fn remove_device(&mut self, device_ref: &IOHIDDeviceRef) { + if let Some(DeviceData { tx, runloop }) = self.map.remove(device_ref) { + // Dropping `tx` will make Device::read() fail eventually. + drop(tx); + + // Wait until the runloop stopped. + runloop.cancel(); + } + } + + extern "C" fn on_input_report( + context: *mut c_void, + _: IOReturn, + device_ref: IOHIDDeviceRef, + _: IOHIDReportType, + _: u32, + report: *mut u8, + report_len: CFIndex, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let mut send_failed = false; + + // Ignore the report if we can't find a device for it. + if let Some(&DeviceData { ref tx, .. }) = this.map.get(&device_ref) { + let data = unsafe { slice::from_raw_parts(report, report_len as usize).to_vec() }; + send_failed = tx.send(data).is_err(); + } + + // Remove the device if sending fails. + if send_failed { + this.remove_device(&device_ref); + } + } + + extern "C" fn on_device_matching( + context: *mut c_void, + _: IOReturn, + _: *mut c_void, + device_ref: IOHIDDeviceRef, + ) { + let this = unsafe { &mut *(context as *mut Self) }; + let (tx, rx) = channel(); + let f = &this.new_device_cb; - let thread = RunLoop::new(move |alive| -> io::Result<()> { - let tx_box = Box::new(tx); - let tx_ptr = Box::into_raw(tx_box) as *mut libc::c_void; - - // This will keep `tx` alive only for the scope. - let _tx = unsafe { Box::from_raw(tx_ptr as *mut Sender) }; - - // Create and initialize a scoped HID manager. - let manager = IOHIDManager::new()?; - - // Match only U2F devices. - let dict = IOHIDDeviceMatcher::new(); - unsafe { IOHIDManagerSetDeviceMatching(manager.get(), dict.get()) }; - - // Register callbacks. - unsafe { - IOHIDManagerRegisterDeviceMatchingCallback( - manager.get(), - Monitor::device_add_cb, - tx_ptr, - ); - IOHIDManagerRegisterDeviceRemovalCallback( - manager.get(), - Monitor::device_remove_cb, - tx_ptr, - ); + // Create a new per-device runloop. + let runloop = RunLoop::new(move |alive| { + // Ensure that the runloop is still alive. + if alive() { + f(device_ref, rx, alive); } + }); - // Run the Event Loop. CFRunLoopRunInMode() will dispatch HID - // input reports into the various callbacks - while alive() { - trace!("OSX Runloop running, handle={:?}", thread::current()); - - if unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, 0) } == - kCFRunLoopRunStopped - { - debug!("OSX Runloop device stopped."); - break; - } - } - debug!("OSX Runloop completed, handle={:?}", thread::current()); - - Ok(()) - })?; - - Ok(Self { rx, thread }) + if let Ok(runloop) = runloop { + this.map.insert(device_ref, DeviceData { tx, runloop }); + } } - pub fn events(&self) -> TryIter { - self.rx.try_iter() - } - - pub fn alive(&self) -> bool { - self.thread.alive() - } - - extern "C" fn device_add_cb( + extern "C" fn on_device_removal( context: *mut c_void, _: IOReturn, _: *mut c_void, - device: IOHIDDeviceRef, + device_ref: IOHIDDeviceRef, ) { - let tx = unsafe { &*(context as *mut Sender) }; - let _ = tx.send(Event::Add(device)); - } - - extern "C" fn device_remove_cb( - context: *mut c_void, - _: IOReturn, - _: *mut c_void, - device: IOHIDDeviceRef, - ) { - let tx = unsafe { &*(context as *mut Sender) }; - let _ = tx.send(Event::Remove(device)); + let this = unsafe { &mut *(context as *mut Self) }; + this.remove_device(&device_ref); } } -impl Drop for Monitor { +impl Drop for Monitor +where + F: Fn(IOHIDDeviceRef, Receiver>, &Fn() -> bool) + Sync, +{ fn drop(&mut self) { - debug!("OSX Runloop dropped"); - self.thread.cancel(); + unsafe { CFRelease(self.manager as *mut libc::c_void) }; } } diff --git a/dom/webauthn/u2f-hid-rs/src/macos/transaction.rs b/dom/webauthn/u2f-hid-rs/src/macos/transaction.rs new file mode 100644 index 000000000000..0ea7fd71d36e --- /dev/null +++ b/dom/webauthn/u2f-hid-rs/src/macos/transaction.rs @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate libc; + +use core_foundation_sys::runloop::*; +use libc::c_void; +use platform::iohid::CFRunLoopEntryObserver; +use platform::iokit::{IOHIDDeviceRef, SendableRunLoop}; +use platform::monitor::Monitor; +use std::io; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; +use util::{io_err, to_io_err, OnceCallback}; + +// A transaction will run the given closure in a new thread, thereby using a +// separate per-thread state machine for each HID. It will either complete or +// fail through user action, timeout, or be cancelled when overridden by a new +// transaction. +pub struct Transaction { + runloop: SendableRunLoop, + thread: Option>, +} + +impl Transaction { + pub fn new(timeout: u64, callback: OnceCallback, new_device_cb: F) -> io::Result + where + F: Fn(IOHIDDeviceRef, Receiver>, &Fn() -> bool) + Sync + Send + 'static, + T: 'static, + { + let (tx, rx) = channel(); + let cbc = callback.clone(); + let timeout = (timeout as f64) / 1000.0; + + let builder = thread::Builder::new(); + let thread = builder.spawn(move || { + // Add a runloop observer that will be notified when we enter the + // runloop and tx.send() the current runloop to the owning thread. + // We need to ensure the runloop was entered before unblocking + // Transaction::new(), so we can always properly cancel. + let context = &tx as *const _ as *mut c_void; + let obs = CFRunLoopEntryObserver::new(Transaction::observe, context); + obs.add_to_current_runloop(); + + // Create a new HID device monitor and start polling. + let mut monitor = Monitor::new(new_device_cb); + try_or!(monitor.start(), |e| cbc.call(Err(e))); + + // This will block until completion, abortion, or timeout. + unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, 0) }; + + // Close the monitor and its devices. + monitor.stop(); + + // Send an error, if the callback wasn't called already. + cbc.call(Err(io_err("aborted or timed out"))); + })?; + + // Block until we enter the CFRunLoop. + let runloop = rx.recv().map_err(to_io_err)?; + + Ok(Self { + runloop, + thread: Some(thread), + }) + } + + extern "C" fn observe(_: CFRunLoopObserverRef, _: CFRunLoopActivity, context: *mut c_void) { + let tx: &Sender = unsafe { &*(context as *mut _) }; + + // Send the current runloop to the receiver to unblock it. + let _ = tx.send(SendableRunLoop(unsafe { CFRunLoopGetCurrent() })); + } + + pub fn cancel(&mut self) { + // (This call doesn't block.) + unsafe { CFRunLoopStop(*self.runloop) }; + + // This must never be None. Ignore return value. + let _ = self.thread.take().unwrap().join(); + } +}