From 67090d3aa14cec95af702aa01e1f3800c0c8e13c Mon Sep 17 00:00:00 2001 From: Steven Storie Date: Sat, 10 Oct 2026 04:02:29 -0400 Subject: [PATCH] fix: a refused privacy-mode request no longer turns off another connection's (#16507) * fix: a refused privacy-mode request no longer turns off another connection's `Connection::turn_on_privacy`'s error arm tore down whatever privacy mode was active, whoever owned it. With connection A in privacy mode, connection B's request is correctly refused with OCCUPIED, and the error arm then called `turn_off_privacy_to_msg(INVALID_PRIVACY_MODE_CONN_ID, ..)`. That id bypasses the ownership check in `check_off_conn_id`, so A's privacy mode went away: the screen was un-blanked and local input un-hooked. The call passes `state: None`, so no `set_privacy_mode_state` goes out either and A was never told, leaving its client showing privacy mode as on. `check_privacy_mode_changed` doesn't cover it, as it only sends PrvOnByOther while a privacy mode is still active. Pass `self.inner.id`, as the sibling error path above already does. `check_off_conn_id` then cleans up after this connection when it owns privacy mode, and refuses when another connection does. The bypass itself stays: `ipc::Data::Close`, `reset_all` and `TurnOnGuard` need an unconditional teardown. Its contract is now noted on `check_off_conn_id`, since that is what the buggy call site got wrong. Co-Authored-By: Claude Opus 5 Signed-off-by: Steven Storie * fix: disconnect other remote sessions when privacy mode starts Signed-off-by: fufesou * fix: exclude privacy disconnect hook from iOS builds Signed-off-by: fufesou * fix: validate privacy mode before disconnecting other sessions Signed-off-by: fufesou * chore Signed-off-by: fufesou --------- Signed-off-by: Steven Storie Signed-off-by: fufesou Co-authored-by: Claude Opus 5 Co-authored-by: fufesou --- src/privacy_mode.rs | 28 ++++++++++++++++++++++++++++ src/server/connection.rs | 17 +++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index 234004d15..5757dee44 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -75,6 +75,11 @@ pub trait PrivacyMode: Sync + Send { Ok(false) } + /// `INVALID_PRIVACY_MODE_CONN_ID` deliberately bypasses the ownership check, meaning + /// "turn it off whoever owns it": shutdown (`ipc::Data::Close`), `reset_all`, and + /// `TurnOnGuard`, which undoes a half-finished turn-on however far it got. A caller + /// acting for one connection must pass that connection's own id, or it can turn off + /// privacy mode belonging to another. #[inline] fn check_off_conn_id(&self, conn_id: i32) -> ResultType<()> { let pre_conn_id = self.pre_conn_id(); @@ -289,6 +294,29 @@ fn turn_on_privacy_sync(impl_key: &str, conn_id: i32) -> Option Some(privacy_mode_lock.as_mut()?.turn_on_privacy(conn_id)) } +#[cfg(not(target_os = "ios"))] +pub(crate) fn close_other_remote_connections_for_privacy(owner_id: i32) { + use crate::server::{AuthConnType, AUTHED_CONNS}; + + let privacy_mode_lock = PRIVACY_MODE.lock().unwrap(); + // Ownership can change while the caller validates the capturer. + if privacy_mode_lock.as_ref().map(|mode| mode.pre_conn_id()) != Some(owner_id) { + return; + } + for connection in AUTHED_CONNS.lock().unwrap().iter() { + if connection.conn_type != AuthConnType::Remote || connection.conn_id == owner_id { + continue; + } + if let Err(err) = connection.sender.send(crate::ipc::Data::Close) { + hbb_common::log::debug!( + "Failed to close connection {} for privacy mode: {}", + connection.conn_id, + err + ); + } + } +} + #[inline] pub fn turn_off_privacy(conn_id: i32, state: Option) -> Option> { Some( diff --git a/src/server/connection.rs b/src/server/connection.rs index 56666eec7..eddd0c521 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1145,6 +1145,14 @@ impl Connection { }, Some(data) = rx_from_authed.recv() => { match data { + ipc::Data::Close => { + conn.send(crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnByOther, + String::new(), + )).await; + conn.on_close("privacy mode", false).await; + break; + } #[cfg(all(target_os = "windows", feature = "flutter"))] ipc::Data::PrinterData(data) => { if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.control_permissions) { @@ -5142,6 +5150,7 @@ impl Connection { 5_000, ); if err_msg.is_empty() { + privacy_mode::close_other_remote_connections_for_privacy(self.inner.id); crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOnSucceeded, impl_key, @@ -5168,10 +5177,10 @@ impl Connection { Some(Err(e)) => { log::error!("Failed to turn on privacy mode. {}", e); if privacy_mode::is_in_privacy_mode() { - let _ = Self::turn_off_privacy_to_msg( - privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, - String::new(), - ); + // Use this connection's ID so a failed request cannot disable another + // connection's privacy mode: INVALID_PRIVACY_MODE_CONN_ID would bypass + // the ownership check (see `check_off_conn_id`). + let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new()); } crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed,