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.
This commit is contained in:
swaits committed 2026-04-07 18:04:52 -06:00
1 parent 903729ccbe
commit f867383b12
4 files changed
+50 -8

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# Changelog # 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 ## 0.2.0 — 2026-04-07
### Changed ### Changed
Generated
+3 -3
View File
@@ -161,9 +161,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "donglora-client" name = "donglora-client"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1936935d31ab56a8cd70ceecfd6d447f2628a73a123e1cfe37ee4987eab0fa70" checksum = "343d750fe1e5dad94d4b31e6dbbb05214b39e66f3c94a2ff2a29b0031ffd2826"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serialport", "serialport",
@@ -173,7 +173,7 @@ dependencies = [
[[package]] [[package]]
name = "donglora-mux" name = "donglora-mux"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "donglora-mux" name = "donglora-mux"
version = "0.2.0" version = "0.2.1"
edition = "2024" edition = "2024"
rust-version = "1.85" rust-version = "1.85"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
+36 -4
View File
@@ -156,12 +156,12 @@ impl MuxDaemon {
} }
info!("opening dongle on {}", self.serial_port); info!("opening dongle on {}", self.serial_port);
let serial = loop { let (serial, _serial_lock_guard) = loop {
if self.shutdown.is_cancelled() { if self.shutdown.is_cancelled() {
return; return;
} }
match open_and_ping(&self.serial_port).await { match open_and_ping(&self.serial_port).await {
Ok(s) => break s, Ok(pair) => break pair,
Err(e) => { Err(e) => {
debug!("could not open dongle: {e}"); debug!("could not open dongle: {e}");
tokio::select! { tokio::select! {
@@ -224,7 +224,39 @@ impl MuxDaemon {
// ── Dongle I/O ───────────────────────────────────────────────────── // ── Dongle I/O ─────────────────────────────────────────────────────
async fn open_and_ping(port: &str) -> anyhow::Result<tokio_serial::SerialStream> { /// 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<std::fs::File>)> {
// 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 builder = tokio_serial::new(port, 115_200).timeout(Duration::from_secs(2));
let mut serial = let mut serial =
tokio_serial::SerialStream::open(&builder).map_err(|e| anyhow::anyhow!("failed to open {port}: {e}"))?; 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<tokio_serial::SerialStream>
for frame in reader.feed(&buf[..n]) { for frame in reader.feed(&buf[..n]) {
if frame.first() == Some(&0) { if frame.first() == Some(&0) {
info!("dongle responded to Ping"); info!("dongle responded to Ping");
return Ok(serial); return Ok((serial, lock));
} }
} }
} }