Bug 1419070 - [u2f-hid-rs] Implement per-device threads on Linux, don't use KeyHandleMatcher r=jcj
authorTim Taubert <ttaubert@mozilla.com>
Mon, 20 Nov 2017 18:25:54 +0100
changeset 437214 066c5f6d20c759fcf65f8065010d32db34230e2e
parent 437213 614d705be06c5439212d5f3a955eb6e3e8e8c2b1
child 437215 52c26719155e2007d14380528ce092b0ebc2014b
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +0000
reviewersjcj
bugs1419070
milestone59.0a1
Bug 1419070 - [u2f-hid-rs] Implement per-device threads on Linux, don't use KeyHandleMatcher r=jcj This patch rewrites the Linux backend to have a structure similar to the macOS one [1]. We'll have one thread per device, which means that a device that is stuck or unresponsive won't break other devices. [1] https://github.com/jcjones/u2f-hid-rs/pull/52 Review: https://github.com/jcjones/u2f-hid-rs/pull/56 https://github.com/jcjones/u2f-hid-rs/commit/7d9f31a8a2c807153f712c5e4943b8e60d92674b
dom/webauthn/u2f-hid-rs/src/lib.rs
dom/webauthn/u2f-hid-rs/src/linux/mod.rs
dom/webauthn/u2f-hid-rs/src/linux/monitor.rs
dom/webauthn/u2f-hid-rs/src/linux/transaction.rs
dom/webauthn/u2f-hid-rs/src/macos/mod.rs
--- a/dom/webauthn/u2f-hid-rs/src/lib.rs
+++ b/dom/webauthn/u2f-hid-rs/src/lib.rs
@@ -22,17 +22,17 @@ pub mod platform;
 #[cfg(any(target_os = "windows"))]
 #[path = "windows/mod.rs"]
 pub mod platform;
 
 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
 #[path = "stub/mod.rs"]
 pub mod platform;
 
-#[cfg(not(any(target_os = "macos")))]
+#[cfg(target_os = "windows")]
 mod khmatcher;
 
 #[macro_use]
 extern crate log;
 extern crate rand;
 extern crate libc;
 extern crate boxfnonce;
 extern crate runloop;
--- a/dom/webauthn/u2f-hid-rs/src/linux/mod.rs
+++ b/dom/webauthn/u2f-hid-rs/src/linux/mod.rs
@@ -1,172 +1,149 @@
 /* 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/. */
 
 use std::time::Duration;
 use std::thread;
 
 mod device;
-mod devicemap;
 mod hidraw;
 mod monitor;
+mod transaction;
 
 use consts::PARAMETER_SIZE;
-use khmatcher::KeyHandleMatcher;
-use runloop::RunLoop;
+use platform::device::Device;
+use platform::transaction::Transaction;
 use util::{io_err, OnceCallback};
-use u2fprotocol::{u2f_is_keyhandle_valid, u2f_register, u2f_sign};
+use u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign};
 
-use self::devicemap::DeviceMap;
-use self::monitor::Monitor;
-
+#[derive(Default)]
 pub struct PlatformManager {
-    // Handle to the thread loop.
-    thread: Option<RunLoop>,
+    transaction: Option<Transaction>,
 }
 
 impl PlatformManager {
     pub fn new() -> Self {
-        Self { thread: None }
+        Default::default()
     }
 
     pub fn register(
         &mut self,
         timeout: u64,
         challenge: Vec<u8>,
         application: Vec<u8>,
         key_handles: Vec<Vec<u8>>,
         callback: OnceCallback<Vec<u8>>,
     ) {
         // Abort any prior register/sign calls.
         self.cancel();
 
         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);
+        let transaction = Transaction::new(timeout, cbc.clone(), move |path, alive| {
+            // Create a new device.
+            let dev = &mut match Device::new(path) {
+                Ok(dev) => dev,
+                _ => return,
+            };
 
-                while alive() && monitor.alive() {
-                    // Add/remove devices.
-                    for event in monitor.events() {
-                        devices.process_event(event);
-                    }
+            // Try initializing it.
+            if !dev.is_u2f() || !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 exclude 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;
-                            }
-                        }
-                    }
-
-                    // Wait a little before trying again.
-                    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")))
-        ));
+        self.transaction = Some(try_or!(transaction, |_| {
+            cbc.call(Err(io_err("couldn't create transaction")))
+        }));
     }
 
     pub fn sign(
         &mut self,
         timeout: u64,
         challenge: Vec<u8>,
         application: Vec<u8>,
         key_handles: Vec<Vec<u8>>,
         callback: OnceCallback<(Vec<u8>, Vec<u8>)>,
     ) {
         // Abort any prior register/sign calls.
         self.cancel();
 
         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);
+        let transaction = Transaction::new(timeout, cbc.clone(), move |path, alive| {
+            // Create a new device.
+            let dev = &mut match Device::new(path) {
+                Ok(dev) => dev,
+                _ => return,
+            };
 
-                while alive() && monitor.alive() {
-                    // Add/remove devices.
-                    for event in monitor.events() {
-                        devices.process_event(event);
-                    }
-
-                    // 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);
+            // Try initializing it.
+            if !dev.is_u2f() || !u2f_init_device(dev) {
+                return;
+            }
 
-                        // 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;
-                        }
+            // 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::<Vec<_>>();
 
-                        // 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;
-                            }
+            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;
+                    }
+                } 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;
                         }
                     }
-
-                    // Wait a little before trying again.
-                    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")))
-        ));
+        self.transaction = Some(try_or!(transaction, |_| {
+            cbc.call(Err(io_err("couldn't create transaction")))
+        }));
     }
 
     // This blocks.
     pub fn cancel(&mut self) {
-        if let Some(thread) = self.thread.take() {
-            thread.cancel();
+        if let Some(mut transaction) = self.transaction.take() {
+            transaction.cancel();
         }
     }
 }
--- a/dom/webauthn/u2f-hid-rs/src/linux/monitor.rs
+++ b/dom/webauthn/u2f-hid-rs/src/linux/monitor.rs
@@ -1,24 +1,21 @@
 /* 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/. */
 
+use libc::{c_int, c_short, c_ulong};
 use libudev;
 use libudev::EventType;
-
-use libc::{c_int, c_short, c_ulong};
-
+use runloop::RunLoop;
+use std::collections::HashMap;
 use std::ffi::OsString;
 use std::io;
 use std::os::unix::io::AsRawFd;
-use std::sync::mpsc::{channel, Receiver, TryIter};
-
-use runloop::RunLoop;
-use util::to_io_err;
+use std::sync::Arc;
 
 const UDEV_SUBSYSTEM: &'static str = "hidraw";
 const POLLIN: c_short = 0x0001;
 const POLL_TIMEOUT: c_int = 100;
 
 fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> {
     let nfds = fds.len() as c_ulong;
 
@@ -26,95 +23,110 @@ fn poll(fds: &mut Vec<::libc::pollfd>) -
 
     if rv < 0 {
         Err(io::Error::from_raw_os_error(rv))
     } else {
         Ok(())
     }
 }
 
-pub enum Event {
-    Add(OsString),
-    Remove(OsString),
+pub struct Monitor<F>
+where
+    F: Fn(OsString, &Fn() -> bool) + Sync,
+{
+    runloops: HashMap<OsString, RunLoop>,
+    new_device_cb: Arc<F>,
 }
 
-impl Event {
-    fn from_udev(event: libudev::Event) -> Option<Self> {
+impl<F> Monitor<F>
+where
+    F: Fn(OsString, &Fn() -> bool) + Send + Sync + 'static,
+{
+    pub fn new(new_device_cb: F) -> Self {
+        Self {
+            runloops: HashMap::new(),
+            new_device_cb: Arc::new(new_device_cb),
+        }
+    }
+
+    pub fn run(&mut self, alive: &Fn() -> bool) -> io::Result<()> {
+        let ctx = libudev::Context::new()?;
+
+        let mut enumerator = libudev::Enumerator::new(&ctx)?;
+        enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
+
+        // Iterate all existing devices.
+        for dev in enumerator.scan_devices()? {
+            if let Some(path) = dev.devnode().map(|p| p.to_owned().into_os_string()) {
+                self.add_device(path);
+            }
+        }
+
+        let mut monitor = libudev::Monitor::new(&ctx)?;
+        monitor.match_subsystem(UDEV_SUBSYSTEM)?;
+
+        // Start listening for new devices.
+        let mut socket = monitor.listen()?;
+        let mut fds = vec![
+            ::libc::pollfd {
+                fd: socket.as_raw_fd(),
+                events: POLLIN,
+                revents: 0,
+            },
+        ];
+
+        while alive() {
+            // Wait for new events, break on failure.
+            poll(&mut fds)?;
+
+            if let Some(event) = socket.receive_event() {
+                self.process_event(event);
+            }
+        }
+
+        // Remove all tracked devices.
+        self.remove_all_devices();
+
+        Ok(())
+    }
+
+    fn process_event(&mut self, event: libudev::Event) {
         let path = event.device().devnode().map(
             |dn| dn.to_owned().into_os_string(),
         );
 
         match (event.event_type(), path) {
-            (EventType::Add, Some(path)) => Some(Event::Add(path)),
-            (EventType::Remove, Some(path)) => Some(Event::Remove(path)),
-            _ => None,
+            (EventType::Add, Some(path)) => {
+                self.add_device(path);
+            }
+            (EventType::Remove, Some(path)) => {
+                self.remove_device(path);
+            }
+            _ => { /* ignore other types and failures */ }
+        }
+    }
+
+    fn add_device(&mut self, path: OsString) {
+        let f = self.new_device_cb.clone();
+        let key = path.clone();
+
+        let runloop = RunLoop::new(move |alive| if alive() {
+            f(path, alive);
+        });
+
+        if let Ok(runloop) = runloop {
+            self.runloops.insert(key, runloop);
+        }
+    }
+
+    fn remove_device(&mut self, path: OsString) {
+        if let Some(runloop) = self.runloops.remove(&path) {
+            runloop.cancel();
+        }
+    }
+
+    fn remove_all_devices(&mut self) {
+        while !self.runloops.is_empty() {
+            let path = self.runloops.keys().next().unwrap().clone();
+            self.remove_device(path);
         }
     }
 }
-
-pub struct Monitor {
-    // Receive events from the thread.
-    rx: Receiver<Event>,
-    // Handle to the thread loop.
-    thread: RunLoop,
-}
-
-impl Monitor {
-    pub fn new() -> io::Result<Self> {
-        let (tx, rx) = channel();
-
-        let thread = RunLoop::new(move |alive| -> io::Result<()> {
-            let ctx = libudev::Context::new()?;
-            let mut enumerator = libudev::Enumerator::new(&ctx)?;
-            enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
-
-            // Iterate all existing devices.
-            for dev in enumerator.scan_devices()? {
-                if let Some(path) = dev.devnode().map(|p| p.to_owned().into_os_string()) {
-                    tx.send(Event::Add(path)).map_err(to_io_err)?;
-                }
-            }
-
-            let mut monitor = libudev::Monitor::new(&ctx)?;
-            monitor.match_subsystem(UDEV_SUBSYSTEM)?;
-
-            // Start listening for new devices.
-            let mut socket = monitor.listen()?;
-            let mut fds = vec![
-                ::libc::pollfd {
-                    fd: socket.as_raw_fd(),
-                    events: POLLIN,
-                    revents: 0,
-                },
-            ];
-
-            // Loop until we're stopped by the controlling thread, or fail.
-            while alive() {
-                // Wait for new events, break on failure.
-                poll(&mut fds)?;
-
-                // Send the event over.
-                let udev_event = socket.receive_event();
-                if let Some(event) = udev_event.and_then(Event::from_udev) {
-                    tx.send(event).map_err(to_io_err)?;
-                }
-            }
-
-            Ok(())
-        })?;
-
-        Ok(Self { rx, thread })
-    }
-
-    pub fn events<'a>(&'a self) -> TryIter<'a, Event> {
-        self.rx.try_iter()
-    }
-
-    pub fn alive(&self) -> bool {
-        self.thread.alive()
-    }
-}
-
-impl Drop for Monitor {
-    fn drop(&mut self) {
-        self.thread.cancel();
-    }
-}
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/u2f-hid-rs/src/linux/transaction.rs
@@ -0,0 +1,43 @@
+/* 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/. */
+
+use platform::monitor::Monitor;
+use runloop::RunLoop;
+use std::ffi::OsString;
+use std::io;
+use util::{io_err, OnceCallback};
+
+pub struct Transaction {
+    // Handle to the thread loop.
+    thread: Option<RunLoop>,
+}
+
+impl Transaction {
+    pub fn new<F, T>(timeout: u64, callback: OnceCallback<T>, new_device_cb: F) -> io::Result<Self>
+    where
+        F: Fn(OsString, &Fn() -> bool) + Sync + Send + 'static,
+        T: 'static,
+    {
+        let thread = RunLoop::new_with_timeout(
+            move |alive| {
+                // Create a new device monitor.
+                let mut monitor = Monitor::new(new_device_cb);
+
+                // Start polling for new devices.
+                try_or!(monitor.run(alive), |e| callback.call(Err(e)));
+
+                // Send an error, if the callback wasn't called already.
+                callback.call(Err(io_err("aborted or timed out")));
+            },
+            timeout,
+        )?;
+
+        Ok(Self { thread: Some(thread) })
+    }
+
+    pub fn cancel(&mut self) {
+        // This must never be None.
+        self.thread.take().unwrap().cancel();
+    }
+}
--- a/dom/webauthn/u2f-hid-rs/src/macos/mod.rs
+++ b/dom/webauthn/u2f-hid-rs/src/macos/mod.rs
@@ -45,17 +45,17 @@ impl PlatformManager {
             // Create a new device.
             let dev = &mut Device::new(device_ref, rx);
 
             // Try initializing it.
             if !u2f_init_device(dev) {
                 return;
             }
 
-            // Iterate the exlude list and see if there are any matches.
+            // Iterate the exclude 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;
             }
@@ -102,17 +102,17 @@ impl PlatformManager {
 
             // 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::<Vec<&Vec<u8>>>();
+                .collect::<Vec<_>>();
 
             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")));