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 444501 066c5f6d20c759fcf65f8065010d32db34230e2e
parent 444500 614d705be06c5439212d5f3a955eb6e3e8e8c2b1
child 444502 52c26719155e2007d14380528ce092b0ebc2014b
push id8527
push userCallek@gmail.com
push dateThu, 11 Jan 2018 21:05:50 +0000
treeherdermozilla-beta@95342d212a7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcj
bugs1419070
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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")));