From f867383b12eba74be420539329107fed9047b1f8 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 7 Apr 2026 17:02:19 -0600 Subject: [PATCH] fix: exclusive serial port lock prevents USB port theft The mux now acquires an flock-based exclusive lock on the serial port before opening it. This prevents bridges or other processes from directly opening the same device while the mux owns it. The lock is held for the duration of the dongle session and auto-released on disconnect or crash. --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 6 +++--- Cargo.toml | 2 +- src/daemon.rs | 40 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb18e9..23164e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.2.1 — 2026-04-07 + +### Fixed + +- **Exclusive serial port lock.** The mux now acquires an `flock`-based + exclusive lock on the serial port before opening it. This prevents other + processes (bridges, other mux instances) from directly opening the same + device while the mux owns it. The lock is held for the duration of the + dongle session and auto-released on disconnect or crash. + ## 0.2.0 — 2026-04-07 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 68933b1..4ad22db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "donglora-client" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1936935d31ab56a8cd70ceecfd6d447f2628a73a123e1cfe37ee4987eab0fa70" +checksum = "343d750fe1e5dad94d4b31e6dbbb05214b39e66f3c94a2ff2a29b0031ffd2826" dependencies = [ "anyhow", "serialport", @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "donglora-mux" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7dc5260..73773b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "donglora-mux" -version = "0.2.0" +version = "0.2.1" edition = "2024" rust-version = "1.85" authors = ["Stephen Waits "] diff --git a/src/daemon.rs b/src/daemon.rs index 7f61473..54f26cf 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -156,12 +156,12 @@ impl MuxDaemon { } info!("opening dongle on {}", self.serial_port); - let serial = loop { + let (serial, _serial_lock_guard) = loop { if self.shutdown.is_cancelled() { return; } match open_and_ping(&self.serial_port).await { - Ok(s) => break s, + Ok(pair) => break pair, Err(e) => { debug!("could not open dongle: {e}"); tokio::select! { @@ -224,7 +224,39 @@ impl MuxDaemon { // ── Dongle I/O ───────────────────────────────────────────────────── -async fn open_and_ping(port: &str) -> anyhow::Result { +/// Lock file path for exclusive serial port access. +fn serial_lock_path(port: &str) -> String { + format!("/tmp/donglora-serial-{}.lock", port.replace('/', "_")) +} + +/// Open the serial port with an exclusive lock and verify the dongle responds to ping. +/// +/// Returns the serial stream and the lock. The caller must keep `_serial_lock` alive +/// for the duration of the dongle session. Dropping it releases the exclusive flock, +/// allowing other processes to claim the port. +async fn open_and_ping( + port: &str, +) -> anyhow::Result<(tokio_serial::SerialStream, fd_lock::RwLock)> { + // Acquire exclusive flock before opening the serial port. This prevents bridges + // or other processes from directly opening the same device while the mux owns it. + let lock_path = serial_lock_path(port); + let lock_file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(false) + .open(&lock_path) + .map_err(|e| anyhow::anyhow!("failed to open serial lock file {lock_path}: {e}"))?; + let mut lock = fd_lock::RwLock::new(lock_file); + // Acquire exclusive flock (non-blocking). Fails if another process holds it. + let guard = lock + .try_write() + .map_err(|_| anyhow::anyhow!("serial port {port} is locked by another process"))?; + // Forget the guard so its Drop doesn't call flock(UNLOCK). The underlying flock + // stays held as long as the file descriptor (owned by `lock`) remains open. + // When `lock` is dropped at the end of the dongle session, the fd closes and + // the OS releases the flock automatically. + std::mem::forget(guard); + let builder = tokio_serial::new(port, 115_200).timeout(Duration::from_secs(2)); let mut serial = tokio_serial::SerialStream::open(&builder).map_err(|e| anyhow::anyhow!("failed to open {port}: {e}"))?; @@ -260,7 +292,7 @@ async fn open_and_ping(port: &str) -> anyhow::Result for frame in reader.feed(&buf[..n]) { if frame.first() == Some(&0) { info!("dongle responded to Ping"); - return Ok(serial); + return Ok((serial, lock)); } } }