Compare commits

..

22 Commits

Author SHA1 Message Date
fufesou
cdfd986cb9 Merge branch 'master' into terminal-utf8-and-reconnect 2026-05-07 12:13:44 +08:00
fufesou
e47e5b38b6 fix(terminal): update hbb_common
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-07 12:12:37 +08:00
fufesou
6a53757f68 fix(terminal): comments utf-8 chunk accumulator
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-03 11:37:01 +08:00
fufesou
5a65d45244 fix(terminal): comments
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-03 09:53:56 +08:00
fufesou
e99829d709 fix(terminal): update hbb_common
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-30 21:43:38 +08:00
fufesou
071c6b1c12 fix(terminal): flag, retry output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 23:48:55 +08:00
fufesou
26a356d0f5 fix(terminal): reconnect, refactor
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 23:11:13 +08:00
fufesou
929a4e78ba fix(terminal): env en_US.UTF-8
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 22:30:39 +08:00
fufesou
18479129a2 fix: cap terminal reconnect replay output
- split reconnect replay backlog into capped chunks
  - mark terminal data replay chunks for client-side suppression
  - avoid using open-message text to suppress xterm replies
  - reuse default terminal padding value
  - remove misleading Enter-key normalization PR link

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 22:12:26 +08:00
fufesou
b516dfb15b fix(terminal): reconnect suppress next output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 20:54:04 +08:00
fufesou
d5568d9188 fix(terminal): windows&macos, charset utf-8
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-29 10:11:25 +08:00
fufesou
1745bab204 fix(terminal): schedule frame before flushing buffered output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:46:47 +08:00
fufesou
0eff404323 fix(terminal): remove invalid test
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:31:37 +08:00
fufesou
67b5484ded fix(terminal): avoid reconnect stalls and delayed layout writes
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:21:15 +08:00
fufesou
268827ef64 fix(terminal): merge reconnect backlog into replay output
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 20:11:18 +08:00
fufesou
c4542b4a5d fix(terminal): close terminal window on disconnect dialog
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 19:47:57 +08:00
fufesou
59f3060a04 fix(terminal): dialog, close window
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 18:40:49 +08:00
fufesou
0a1500a72a fix(terminal): reconnect, error handling
1. Terminal shows "^[[1;1R^[[2;2R^[[>0;0;0c"
2. NaN

```
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Converting object to an encodable object failed: NaN
...
```

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-28 17:58:51 +08:00
rustdesk
4f5c7db70a fix ios enter: https://github.com/rustdesk/rustdesk/issues/14907 2026-04-26 09:08:11 +08:00
fufesou
0112167029 fix(terminal): subtract with overflow
```
thread '<unnamed>' panicked at src\server\terminal_service.rs:476:17:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'tokio-runtime-worker' panicked at src\server\terminal_service.rs:1576:50:
called `Result::unwrap()` on an `Err` value: PoisonError { .. }
[2026-04-25T07:17:34Z ERROR librustdesk::server::service] Failed to join thread for service ts_9badd3fe-2411-4996-9f40-93c979009edd, Any { .. }
```

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-04-25 15:46:25 +08:00
rustdesk
0d77482a64 Fix terminal auto-reconnect freeze: reconnect resumes terminal output, while multi-tab reconnect avoids restoring duplicate tabs for terminals that are already open. 2026-04-25 01:07:00 +08:00
rustdesk
ca3ef2a1c3 fix: handle incomplete UTF-8 sequences in terminal output, rework on https://github.com/rustdesk/rustdesk/pull/14736 2026-04-25 00:44:23 +08:00
13 changed files with 260 additions and 4511 deletions

4
Cargo.lock generated
View File

@@ -5996,8 +5996,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
version = "0.7.3-6"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
version = "0.7.3-5"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
dependencies = [
"futures",
"libc",

View File

@@ -146,13 +146,7 @@ pub fn core_main() -> Option<Vec<String>> {
crate::portable_service::client::set_quick_support(_is_quick_support);
}
let mut log_name = "".to_owned();
// Keep portable-service logs under a stable directory name.
let has_portable_service_shmem_arg = args
.iter()
.any(|arg| arg.starts_with("--portable-service-shmem-name="));
if has_portable_service_shmem_arg {
log_name = "portable-service".to_owned();
} else if args.len() > 0 && args[0].starts_with("--") {
if args.len() > 0 && args[0].starts_with("--") {
let name = args[0].replace("--", "");
if !name.is_empty() {
log_name = name;

View File

@@ -1,28 +1,33 @@
#[path = "ipc/auth.rs"]
mod ipc_auth;
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[path = "ipc/fs.rs"]
mod ipc_fs;
use crate::{
common::CheckTestNatType,
privacy_mode::PrivacyModeState,
ui_interface::{get_local_option, set_local_option},
};
use bytes::Bytes;
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
#[cfg(not(windows))]
use std::{fs::File, io::prelude::*};
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::plugin::ipc::Plugin;
use crate::{
common::{is_server, CheckTestNatType},
privacy_mode,
privacy_mode::PrivacyModeState,
rendezvous_mediator::RendezvousMediator,
ui_interface::{get_local_option, set_local_option},
};
use bytes::Bytes;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use clipboard::ClipboardFile;
#[cfg(target_os = "linux")]
use hbb_common::anyhow;
use hbb_common::{
allow_err, bail, bytes,
bytes_codec::BytesCodec,
config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2},
config::{
self,
keys::{self, OPTION_ALLOW_WEBSOCKET},
Config, Config2,
},
futures::StreamExt as _,
futures_util::sink::SinkExt,
log, password_security as password, timeout,
@@ -33,55 +38,13 @@ use hbb_common::{
tokio_util::codec::Framed,
ResultType,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::authorize_service_scoped_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(windows)]
use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows,
};
#[cfg(target_os = "linux")]
use ipc_fs::terminal_count_candidate_uids;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{
check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir,
should_scrub_parent_entries_after_check_pid, write_pid,
};
use parity_tokio_ipc::{
Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes,
};
use serde_derive::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt;
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator};
// IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close";
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
pub(crate) const IPC_TOKEN_LEN: usize = 64;
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[inline]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum FS {
@@ -244,8 +207,6 @@ pub enum DataControl {
pub enum DataPortableService {
Ping,
Pong,
AuthToken(String),
AuthResult(bool),
ConnCount(Option<usize>),
Mouse((Vec<u8>, i32, String, u32, bool, bool)),
Pointer((Vec<u8>, i32)),
@@ -450,22 +411,6 @@ pub async fn start(postfix: &str) -> ResultType<()> {
Ok(stream) => {
let mut stream = Connection::new(stream);
let postfix = postfix.to_owned();
#[cfg(any(target_os = "linux", target_os = "macos"))]
if config::is_service_ipc_postfix(&postfix) {
if !authorize_service_scoped_ipc_connection(&stream, &postfix) {
continue;
}
}
#[cfg(windows)]
if postfix.is_empty() {
// Windows main IPC (`postfix == ""`) is authorized here.
// Other security-sensitive channels use dedicated authorization paths:
// - `_portable_service`: portable-service listener + handshake policy
// - service-scoped postfixes: service-specific listener/authorization
if !authorize_windows_main_ipc_connection(&stream, &postfix) {
continue;
}
}
tokio::spawn(async move {
loop {
match stream.next().await {
@@ -474,48 +419,9 @@ pub async fn start(postfix: &str) -> ResultType<()> {
break;
}
Ok(Some(data)) => {
// On Linux/macOS, the protected `_service` channel is used only for
// syncing config between root service and the active user process.
//
// NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those
// channels are handled by the dedicated uinput listener/protocol in
// `src/server/uinput.rs` and therefore do not share this Data enum
// allowlist. The SyncConfig allowlist here is intentionally scoped to the
// `_service` channel only.
//
// Keep this explicit branch to avoid policy drift between `_service` and
// uinput IPC paths while still minimizing exposed message surface here.
#[cfg(any(target_os = "linux", target_os = "macos"))]
if postfix == crate::POSTFIX_SERVICE {
if matches!(&data, Data::SyncConfig(_)) {
handle(data, &mut stream).await;
} else {
log::warn!(
"Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}",
postfix,
std::mem::discriminant(&data),
stream.peer_uid()
);
// Close the connection to avoid keeping a protected channel
// alive while repeatedly receiving invalid traffic.
break;
}
continue;
}
handle(data, &mut stream).await;
}
Ok(None) => {
// `Ok(None)` means a complete frame arrived but did not
// deserialize into `Data`. Peer close/reset is returned as
// `Err` by `ConnectionTmpl::next()`. Keep the historical
// ignore behavior except on the protected `_service` channel.
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
if postfix == crate::POSTFIX_SERVICE {
break;
}
}
}
_ => {}
}
}
});
@@ -530,77 +436,20 @@ pub async fn start(postfix: &str) -> ResultType<()> {
pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
let path = Config::ipc_path(postfix);
#[cfg(any(target_os = "linux", target_os = "macos"))]
let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?;
#[cfg(any(target_os = "linux", target_os = "macos"))]
let existing_listener_alive = check_pid(postfix).await;
#[cfg(any(target_os = "linux", target_os = "macos"))]
if should_scrub_parent_entries_after_check_pid(
should_scrub_parent_entries,
existing_listener_alive,
) {
scrub_secure_ipc_parent_dir(&path, postfix)?;
}
#[cfg(not(any(windows, target_os = "android", target_os = "ios")))]
check_pid(postfix).await;
let mut endpoint = Endpoint::new(path.clone());
let security_attrs = {
#[cfg(windows)]
{
if postfix == "_portable_service" {
portable_service_listener_security_attributes()
} else if should_allow_everyone_create_on_windows(postfix) {
SecurityAttributes::allow_everyone_create()
} else {
Ok(SecurityAttributes::empty())
}
}
#[cfg(not(windows))]
{
SecurityAttributes::allow_everyone_create()
}
};
match security_attrs {
match SecurityAttributes::allow_everyone_create() {
Ok(attr) => endpoint.set_security_attributes(attr),
Err(err) => {
log::error!("Failed to set ipc{} security: {}", postfix, err);
#[cfg(windows)]
if postfix == "_portable_service" {
// Fail closed for `_portable_service` when SDDL construction fails.
// This endpoint is security-critical and must not start with default ACLs.
return Err(err.into());
}
}
Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err),
};
match endpoint.incoming() {
Ok(incoming) => {
if postfix == crate::POSTFIX_SERVICE {
log::info!("Started protected ipc service server: postfix={}", postfix);
} else {
log::info!("Started ipc{} server at path: {}", postfix, &path);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
log::info!("Started ipc{} server at path: {}", postfix, &path);
#[cfg(not(windows))]
{
// NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable
// (0666) so the active (non-root) user process can connect. Authorization is
// enforced at accept-time for these channels, and the protected `_service`
// channel is further restricted by an explicit message allowlist (SyncConfig
// only).
let socket_mode = if config::is_service_ipc_postfix(postfix) {
0o0666
} else {
0o0600
};
if let Err(err) =
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode))
{
log::error!(
"Failed to set permissions on ipc{} socket at path {}: {}",
postfix,
&path,
err
);
std::fs::remove_file(&path).ok();
return Err(err.into());
}
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
write_pid(postfix);
}
Ok(incoming)
@@ -1104,116 +953,15 @@ async fn handle(data: Data, stream: &mut Connection) {
);
}
_ => {}
};
}
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _;
let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES];
let mut rng = OsRng;
rng.try_fill_bytes(&mut random_bytes).map_err(|err| {
hbb_common::anyhow::anyhow!(
"failed to generate portable service ipc token from OsRng: {}",
err
)
})?;
let mut token = String::with_capacity(IPC_TOKEN_LEN);
for byte in random_bytes {
let _ = write!(token, "{:02x}", byte);
}
Ok(token)
}
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false;
}
expected
.as_bytes()
.iter()
.zip(candidate.as_bytes().iter())
.fold(0u8, |diff, (left, right)| diff | (*left ^ *right))
== 0
}
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
token: &str,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
{
stream
.send(&Data::DataPortableService(DataPortableService::AuthToken(
token.to_owned(),
)))
.await?;
match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()),
Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => {
bail!("portable service ipc handshake was rejected by server")
}
Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"),
}
}
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
mut validate_token: F,
) -> ResultType<()>
where
T: AsyncRead + AsyncWrite + std::marker::Unpin,
// Token validators must use `constant_time_ipc_token_eq` or an equivalent
// fixed-length comparison; this handshake is part of the privilege boundary.
F: FnMut(&str) -> bool,
{
let authorized = match stream
.next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS)
.await?
{
Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => {
validate_token(&token)
}
Some(_) | None => false,
};
stream
.send(&Data::DataPortableService(DataPortableService::AuthResult(
authorized,
)))
.await?;
if !authorized {
bail!("portable service ipc handshake failed")
}
Ok(())
}
#[inline]
async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let client = timeout(ms_timeout, Endpoint::connect(path)).await??;
let client = timeout(ms_timeout, Endpoint::connect(&path)).await??;
Ok(ConnectionTmpl::new(client))
}
#[cfg(target_os = "linux")]
pub async fn connect_for_uid(
ms_timeout: u64,
uid: u32,
postfix: &str,
) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path_for_uid(uid, postfix);
connect_with_path(ms_timeout, &path).await
}
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn start_pa() {
@@ -1291,6 +1039,54 @@ pub async fn start_pa() {
}
}
#[inline]
#[cfg(not(windows))]
fn get_pid_file(postfix: &str) -> String {
let path = Config::ipc_path(postfix);
format!("{}.pid", path)
}
#[cfg(not(any(windows, target_os = "android", target_os = "ios")))]
async fn check_pid(postfix: &str) {
let pid_file = get_pid_file(postfix);
if let Ok(mut file) = File::open(&pid_file) {
let mut content = String::new();
file.read_to_string(&mut content).ok();
let pid = content.parse::<usize>().unwrap_or(0);
if pid > 0 {
use hbb_common::sysinfo::System;
let mut sys = System::new();
sys.refresh_processes();
if let Some(p) = sys.process(pid.into()) {
if let Some(current) = sys.process((std::process::id() as usize).into()) {
if current.name() == p.name() {
// double check with connect
if connect(1000, postfix).await.is_ok() {
return;
}
}
}
}
}
}
// if not remove old ipc file, the new ipc creation will fail
// if we remove a ipc file, but the old ipc process is still running,
// new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive
std::fs::remove_file(&Config::ipc_path(postfix)).ok();
}
#[inline]
#[cfg(not(windows))]
fn write_pid(postfix: &str) {
let path = get_pid_file(postfix);
if let Ok(mut file) = File::create(&path) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
file.write_all(&std::process::id().to_string().into_bytes())
.ok();
}
}
pub struct ConnectionTmpl<T> {
inner: Framed<T, BytesCodec>,
}
@@ -1754,10 +1550,9 @@ pub fn close_all_instances() -> ResultType<bool> {
}
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
pub async fn connect_to_user_session(usid: Option<u32>) -> ResultType<()> {
let mut stream = crate::ipc::connect_service(1000).await?;
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??;
Ok(())
}
@@ -1883,76 +1678,13 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> {
#[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")]
pub async fn get_terminal_session_count() -> ResultType<usize> {
let timeout_ms = 1_000;
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let candidate_uids = terminal_count_candidate_uids(effective_uid);
let mut last_err: Option<anyhow::Error> = None;
for candidate_uid in candidate_uids {
let socket_path = Config::ipc_path_for_uid(candidate_uid, "");
let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path))
.await
.map_err(|err| {
anyhow::anyhow!(
"Timeout connecting to terminal ipc at {}: {}",
socket_path,
err
)
});
let connection = match connect_result {
Ok(Ok(connection)) => connection,
Ok(Err(err)) => {
last_err = Some(anyhow::anyhow!(
"Failed to connect to terminal ipc at {}: {}",
socket_path,
err
));
continue;
}
Err(err) => {
last_err = Some(err);
continue;
}
};
let mut ipc_conn = ConnectionTmpl::new(connection);
if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await {
last_err = Some(anyhow::anyhow!(
"Failed to request terminal session count via ipc at {}: {}",
socket_path,
err
));
continue;
}
match ipc_conn.next_timeout(timeout_ms).await {
Ok(Some(Data::TerminalSessionCount(session_count))) => {
return Ok(session_count);
}
Ok(None) => {
last_err = Some(anyhow::anyhow!(
"Invalid response when requesting terminal session count via ipc at {}",
socket_path
));
}
Ok(other) => {
last_err = Some(anyhow::anyhow!(
"Unexpected response when requesting terminal session count via ipc at {}: {:?}",
socket_path,
other.map(|v| std::mem::discriminant(&v))
));
}
Err(err) => {
last_err = Some(anyhow::anyhow!(
"Failed to read terminal session count via ipc at {}: {}",
socket_path,
err
));
}
}
}
if let Some(err) = last_err {
Err(err.into())
} else {
Ok(0)
let ms_timeout = 1_000;
let mut c = connect(ms_timeout, "").await?;
c.send(&Data::TerminalSessionCount(0)).await?;
if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? {
return Ok(c);
}
Ok(0)
}
async fn handle_wayland_screencast_restore_token(
@@ -1983,30 +1715,9 @@ pub async fn set_install_option(k: String, v: String) -> ResultType<()> {
#[cfg(test)]
mod test {
use super::*;
#[test]
fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 120);
}
#[cfg(target_os = "linux")]
#[test]
fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let other_uid = effective_uid.saturating_add(1);
let postfix = "_cm";
// Default connect path targets the current effective uid.
assert_eq!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(effective_uid, postfix)
);
// A different uid yields a different socket path - this is the root cause of the
// cross-user regression when root spawns a user process but still connects as uid 0.
assert_ne!(
Config::ipc_path(postfix),
Config::ipc_path_for_uid(other_uid, postfix)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,951 +0,0 @@
#[cfg(target_os = "linux")]
use super::ipc_auth::active_uid;
use crate::ipc::{connect, Data};
use hbb_common::{config, log, ResultType};
use std::{
ffi::CString,
io::{Error, ErrorKind},
os::unix::ffi::OsStrExt,
path::Path,
};
struct FdGuard(i32);
impl Drop for FdGuard {
fn drop(&mut self) {
unsafe {
hbb_common::libc::close(self.0);
}
}
}
#[cfg(target_os = "linux")]
#[inline]
pub(crate) fn terminal_count_candidate_uids(effective_uid: u32) -> Vec<u32> {
if effective_uid != 0 {
return vec![effective_uid];
}
let mut candidates = Vec::with_capacity(2);
if let Some(uid) = active_uid().filter(|uid| *uid != 0) {
candidates.push(uid);
}
candidates.push(0);
candidates
}
#[inline]
fn expected_ipc_parent_mode(postfix: &str) -> u32 {
if config::is_service_ipc_postfix(postfix) {
0o0711
} else {
0o0700
}
}
fn open_ipc_parent_dir_fd(parent_c: &CString) -> std::io::Result<i32> {
let fd = unsafe {
hbb_common::libc::open(
parent_c.as_ptr(),
hbb_common::libc::O_RDONLY
| hbb_common::libc::O_DIRECTORY
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW,
)
};
if fd < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(fd)
}
}
// Remove one preexisting IPC artifact via an already-opened parent directory FD.
//
// Security intent:
// - Bind cleanup to the exact parent inode that passed O_NOFOLLOW + fstat checks.
// - Avoid path-based TOCTOU during scrub (e.g., parent path rename/swap race).
//
// Flow:
// 1) fstatat(..., AT_SYMLINK_NOFOLLOW) to inspect the target entry under parent_fd.
// 2) Decide file vs directory from st_mode.
// 3) unlinkat relative to parent_fd (AT_REMOVEDIR for directories).
//
// Error policy:
// - NotFound is treated as benign (already removed / raced away).
// - Other errors are surfaced explicitly.
fn remove_parent_entry_via_fd(
parent_fd: i32,
parent_dir: &Path,
entry_name: &str,
) -> ResultType<()> {
if entry_name.contains('/') {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"invalid ipc parent entry name (contains '/'): parent={}, entry={}",
parent_dir.display(),
entry_name
),
)
.into());
}
let entry_c = CString::new(entry_name.as_bytes().to_vec()).map_err(|err| {
Error::new(
ErrorKind::InvalidInput,
format!(
"invalid ipc parent entry name: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
})?;
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
let stat_rc = unsafe {
hbb_common::libc::fstatat(
parent_fd,
entry_c.as_ptr(),
&mut stat,
hbb_common::libc::AT_SYMLINK_NOFOLLOW,
)
};
if stat_rc != 0 {
let err = std::io::Error::last_os_error();
if err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
err.kind(),
format!(
"failed to stat preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
.into());
}
let is_dir = (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
== hbb_common::libc::S_IFDIR;
let unlink_flags = if is_dir {
hbb_common::libc::AT_REMOVEDIR
} else {
0
};
let unlink_rc =
unsafe { hbb_common::libc::unlinkat(parent_fd, entry_c.as_ptr(), unlink_flags) };
if unlink_rc != 0 {
let err = std::io::Error::last_os_error();
if err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
err.kind(),
format!(
"failed to remove preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}",
parent_dir.display(),
entry_name,
err
),
)
.into());
}
Ok(())
}
fn scrub_preexisting_ipc_parent_entries(
parent_fd: i32,
parent_dir: &Path,
postfix: &str,
) -> ResultType<()> {
let ipc_basename = format!("ipc{}", postfix);
remove_parent_entry_via_fd(parent_fd, parent_dir, &ipc_basename)?;
remove_parent_entry_via_fd(parent_fd, parent_dir, &format!("{}.pid", ipc_basename))?;
Ok(())
}
fn remove_ipc_socket_via_secure_parent_fd(postfix: &str) -> ResultType<()> {
let path = config::Config::ipc_path(postfix);
let parent_dir = Path::new(&path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(open_err) => {
if open_err.kind() == ErrorKind::NotFound {
return Ok(());
}
return Err(Error::new(
open_err.kind(),
format!(
"failed to open ipc parent dir for stale socket cleanup (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
open_err
),
)
.into());
}
};
let _fd_guard = FdGuard(fd);
remove_parent_entry_via_fd(fd, parent_dir, &format!("ipc{}", postfix))
}
// Purpose:
// - Harden the IPC parent directory before creating/listening socket files.
// - Prevent symlink/path-race abuse and reject unsafe owner/mode.
//
// Approach:
// - Open parent dir with O_NOFOLLOW/O_DIRECTORY and operate on that fd.
// - Validate inode type/owner/mode via fstat.
// - For protected service postfix, optionally adopt owner (root only), then scrub stale
// rustdesk IPC artifacts when directory trust boundary changed.
//
// Main steps:
// 1) Resolve parent path and open/create directory securely.
// 2) Verify directory inode type and owner uid.
// 3) Enforce expected mode via fchmod on opened fd.
// 4) Scrub stale IPC artifacts when owner/mode was unsafe before hardening.
//
// References:
// - open(2): O_NOFOLLOW/O_DIRECTORY/O_CLOEXEC
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): verify file type/metadata on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - fchown(2): adopt ownership when running as root
// https://man7.org/linux/man-pages/man2/chown.2.html
// - fchmod(2): enforce exact mode on opened fd
// https://man7.org/linux/man-pages/man2/fchmod.2.html
pub(crate) fn ensure_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<bool> {
let parent_dir = Path::new(path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
// Harden against common TOCTOU by opening the parent directory with O_NOFOLLOW (so the parent
// itself cannot be a symlink) and then operating on its FD (fstat/fchown/fchmod). This ensures
// we mutate the inode we opened, though it does not protect against symlinks in ancestor path
// components.
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(open_err) => {
// If the directory doesn't exist yet, create it with the expected mode. The parent
// dir is intended to be a single-level /tmp path, so mkdir is sufficient here.
if open_err.raw_os_error() == Some(hbb_common::libc::ENOENT) {
let expected_mode = expected_ipc_parent_mode(postfix);
let rc = unsafe {
hbb_common::libc::mkdir(
parent_c.as_ptr(),
expected_mode as hbb_common::libc::mode_t,
)
};
if rc != 0 {
let mkdir_err = std::io::Error::last_os_error();
// Handle a race where another process created the directory first.
if mkdir_err.raw_os_error() != Some(hbb_common::libc::EEXIST) {
return Err(Error::new(
mkdir_err.kind(),
format!(
"failed to mkdir ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
mkdir_err
),
)
.into());
}
}
match open_ipc_parent_dir_fd(&parent_c) {
Ok(fd) => fd,
Err(err) => {
return Err(Error::new(
err.kind(),
format!(
"failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
err
),
)
.into());
}
}
} else {
return Err(Error::new(
open_err.kind(),
format!(
"failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
open_err
),
)
.into());
}
}
};
let _fd_guard = FdGuard(fd);
let mut st: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut st as *mut _) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to stat ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
os_err
),
)
.into());
}
let mode = st.st_mode as u32;
let is_dir = (mode & (hbb_common::libc::S_IFMT as u32)) == (hbb_common::libc::S_IFDIR as u32);
if !is_dir {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"ipc parent is not directory: postfix={}, parent={}",
postfix,
parent_dir.display()
),
)
.into());
}
let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 };
let mut owner_uid = st.st_uid as u32;
let mut adopted_foreign_service_parent = false;
// Service-scoped IPC may be created by different privilege contexts historically.
// If running as root on protected service postfix, try adopting ownership first.
if owner_uid != expected_uid && expected_uid == 0 && config::is_service_ipc_postfix(postfix) {
let rc = unsafe {
hbb_common::libc::fchown(
fd,
expected_uid as hbb_common::libc::uid_t,
hbb_common::libc::gid_t::MAX,
)
};
if rc == 0 {
let mut st2: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut st2 as *mut _) } == 0 {
owner_uid = st2.st_uid as u32;
st = st2;
adopted_foreign_service_parent = true;
}
} else {
// Keep behavior unchanged; capture errno to ease diagnosing why chown failed.
let err = std::io::Error::last_os_error();
log::warn!(
"Failed to chown ipc parent dir, parent={}, postfix={}, expected_uid={}, rc={}, err={:?}",
parent_dir.display(),
postfix,
expected_uid,
rc,
err
);
}
}
if owner_uid != expected_uid {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"unsafe ipc parent owner, postfix={}, expected uid {expected_uid}, got {owner_uid}: {}",
postfix,
parent_dir.display()
),
)
.into());
}
let expected_mode = expected_ipc_parent_mode(postfix);
// Include special bits (setuid/setgid/sticky) to ensure the directory is hardened to the exact
// expected mode.
let current_mode = (st.st_mode as u32) & 0o7777;
let repaired_parent_mode = current_mode != expected_mode;
let had_untrusted_parent_mode = (current_mode & 0o022) != 0;
if repaired_parent_mode {
// Use fchmod on the opened fd to avoid path-race between check and chmod.
if unsafe { hbb_common::libc::fchmod(fd, expected_mode as hbb_common::libc::mode_t) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to chmod ipc parent dir: postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
os_err
),
)
.into());
}
}
let should_scrub =
repaired_parent_mode || adopted_foreign_service_parent || had_untrusted_parent_mode;
Ok(should_scrub)
}
pub(crate) fn scrub_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<()> {
let parent_dir = Path::new(path)
.parent()
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?;
let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?;
let fd = open_ipc_parent_dir_fd(&parent_c).map_err(|err| {
Error::new(
err.kind(),
format!(
"failed to open ipc parent dir for scrub (no-follow): postfix={}, parent={}, err={}",
postfix,
parent_dir.display(),
err
),
)
})?;
let _fd_guard = FdGuard(fd);
scrub_preexisting_ipc_parent_entries(fd, parent_dir, postfix)
}
#[inline]
pub(crate) fn get_pid_file(postfix: &str) -> String {
let path = config::Config::ipc_path(postfix);
format!("{}.pid", path)
}
// Purpose:
// - Write current process pid to pid file without following attacker-controlled symlinks.
// - Ensure the pid file is a regular file owned by the opened inode path.
//
// Approach:
// - Use libc open/fstat/write syscalls (FFI) so flags and inode validation are explicit.
// - Open file with O_NOFOLLOW/O_CLOEXEC and verify S_IFREG with fstat before write.
// - Keep unsafe scopes minimal and check syscall return values immediately.
//
// Main steps:
// 1) Secure-open pid file (without truncation).
// 2) Validate opened inode is a regular file owned by current euid.
// 3) Enforce pid file mode to 0600 and truncate via ftruncate after validation.
// 4) Write process id bytes through fd.
//
// Why not plain std::fs::write?
// - std::fs helpers cannot enforce this exact open-time hardening sequence
// (especially "open with O_NOFOLLOW, then fstat the same opened inode").
//
// References:
// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): verify file type on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - fchmod(2): enforce secure mode on reused pid file
// https://man7.org/linux/man-pages/man2/fchmod.2.html
// - ftruncate(2): truncate after validation
// https://man7.org/linux/man-pages/man2/ftruncate.2.html
// - write(2): write bytes via fd
// https://man7.org/linux/man-pages/man2/write.2.html
fn write_pid_file(path: &Path) -> ResultType<()> {
let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).map_err(|err| {
Error::new(
ErrorKind::InvalidInput,
format!("invalid pid file path '{}': {}", path.display(), err),
)
})?;
let flags = hbb_common::libc::O_WRONLY
| hbb_common::libc::O_CREAT
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW
| hbb_common::libc::O_NONBLOCK;
let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags, 0o0600) };
if fd < 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to open pid file with no-follow '{}': {}",
path.display(),
os_err
),
)
.into());
}
let _fd_guard = FdGuard(fd);
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to stat pid file '{}': {}", path.display(), os_err),
)
.into());
}
if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
!= (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t)
{
return Err(Error::new(
ErrorKind::PermissionDenied,
format!("pid file path is not a regular file: '{}'", path.display()),
)
.into());
}
let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 };
if stat.st_uid as u32 != expected_uid {
return Err(Error::new(
ErrorKind::PermissionDenied,
format!(
"pid file owner mismatch: expected uid {}, got {} for '{}'",
expected_uid,
stat.st_uid,
path.display()
),
)
.into());
}
if unsafe { hbb_common::libc::fchmod(fd, 0o600) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to chmod pid file '{}': {}", path.display(), os_err),
)
.into());
}
if unsafe { hbb_common::libc::ftruncate(fd, 0) } != 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!(
"failed to truncate pid file '{}': {}",
path.display(),
os_err
),
)
.into());
}
let bytes = std::process::id().to_string();
let buf = bytes.as_bytes();
// `write(2)` is allowed to return a short write even for regular files.
// PID content is tiny and usually written in one shot, but we still loop
// until all bytes are persisted so this path is semantically correct.
let mut written = 0usize;
while written < buf.len() {
let rc = unsafe {
hbb_common::libc::write(
fd,
buf[written..].as_ptr() as *const hbb_common::libc::c_void,
buf.len() - written,
)
};
if rc < 0 {
let os_err = std::io::Error::last_os_error();
return Err(Error::new(
os_err.kind(),
format!("failed to write pid file '{}': {}", path.display(), os_err),
)
.into());
}
if rc == 0 {
return Err(Error::new(
ErrorKind::WriteZero,
format!(
"failed to write pid file '{}': write returned 0 bytes",
path.display()
),
)
.into());
}
written += rc as usize;
}
Ok(())
}
#[inline]
pub(crate) fn write_pid(postfix: &str) {
let path = std::path::PathBuf::from(get_pid_file(postfix));
if let Err(err) = write_pid_file(&path) {
log::warn!(
"Failed to write pid file for postfix '{}', path='{}', err={}",
postfix,
path.display(),
err
);
}
}
// Purpose:
// - Read pid file safely and avoid trusting symlink/non-regular files.
//
// Approach:
// - Use libc open/fstat/read syscalls (FFI) to control flags and inode checks.
// - Open path with O_NOFOLLOW, validate opened fd via fstat, then read and parse.
// - Keep unsafe scopes minimal and check syscall return values immediately.
//
// Main steps:
// 1) Secure-open pid file read-only.
// 2) Ensure fd points to regular file.
// 3) Read bytes and parse usize pid.
//
// References:
// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK
// https://man7.org/linux/man-pages/man2/open.2.html
// - fstat(2): validate S_IFREG on opened fd
// https://man7.org/linux/man-pages/man2/fstat.2.html
// - read(2): read bytes via fd
// https://man7.org/linux/man-pages/man2/read.2.html
#[inline]
fn read_pid_file_secure(path: &Path) -> Option<usize> {
let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).ok()?;
let flags = hbb_common::libc::O_RDONLY
| hbb_common::libc::O_CLOEXEC
| hbb_common::libc::O_NOFOLLOW
| hbb_common::libc::O_NONBLOCK;
let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags) };
if fd < 0 {
return None;
}
let _fd_guard = FdGuard(fd);
let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() };
if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 {
return None;
}
if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t))
!= (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t)
{
return None;
}
let mut buffer = [0u8; 64];
let read_len = unsafe {
hbb_common::libc::read(
fd,
buffer.as_mut_ptr() as *mut hbb_common::libc::c_void,
buffer.len(),
)
};
if read_len <= 0 {
return None;
}
let content = String::from_utf8_lossy(&buffer[..read_len as usize]).to_string();
content.trim().parse::<usize>().ok()
}
#[inline]
async fn probe_existing_listener(postfix: &str) -> bool {
let Ok(mut stream) = connect(1000, postfix).await else {
return false;
};
if postfix != crate::POSTFIX_SERVICE {
return true;
}
if stream.send(&Data::SyncConfig(None)).await.is_err() {
return false;
}
matches!(
stream.next_timeout(1000).await,
Ok(Some(Data::SyncConfig(Some(_))))
)
}
pub(crate) async fn check_pid(postfix: &str) -> bool {
let pid_file = std::path::PathBuf::from(get_pid_file(postfix));
if let Some(pid) = read_pid_file_secure(&pid_file) {
if pid > 0 {
let mut sys = hbb_common::sysinfo::System::new();
sys.refresh_processes();
if let Some(p) = sys.process(pid.into()) {
if let Some(current) = sys.process((std::process::id() as usize).into()) {
if current.name() == p.name() && probe_existing_listener(postfix).await {
return true;
}
}
}
}
}
if probe_existing_listener(postfix).await {
return true;
}
// if not remove old ipc file, the new ipc creation will fail
// if we remove a ipc file, but the old ipc process is still running,
// new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive
if let Err(err) = remove_ipc_socket_via_secure_parent_fd(postfix) {
log::debug!(
"Failed to remove stale ipc socket via secure parent fd: postfix={}, err={}",
postfix,
err
);
}
false
}
#[inline]
pub(crate) fn should_scrub_parent_entries_after_check_pid(
should_scrub_parent_entries: bool,
existing_listener_alive: bool,
) -> bool {
should_scrub_parent_entries && !existing_listener_alive
}
#[cfg(test)]
mod tests {
#[test]
fn test_write_pid_file_rejects_symlink() {
use std::os::unix::fs::symlink;
let unique = format!(
"rustdesk-ipc-pid-file-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let target = base.join("target_pid");
std::fs::write(&target, b"origin").unwrap();
let link = base.join("pid_link");
symlink(&target, &link).unwrap();
let res = super::write_pid_file(&link);
assert!(res.is_err());
assert_eq!(std::fs::read_to_string(&target).unwrap(), "origin");
std::fs::remove_file(&link).ok();
std::fs::remove_file(&target).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_rejects_symlink_parent() {
use std::os::unix::fs::symlink;
let unique = format!(
"rustdesk-ipc-secure-dir-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
let real_dir = base.join("real");
let link_dir = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(&real_dir, &link_dir).unwrap();
let ipc_path = link_dir.join("ipc_service");
let res =
super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "_service");
assert!(res.is_err());
std::fs::remove_file(&link_dir).ok();
std::fs::remove_dir_all(&real_dir).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_creates_parent_with_expected_mode() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-secure-dir-create-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
// Intentionally choose a parent that does not exist to exercise the ENOENT -> mkdir branch.
let parent_dir = base.join("parent");
assert!(!parent_dir.exists());
let ipc_path = parent_dir.join("ipc");
let res = super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "");
// Restrictive umask can make mkdir create a stricter initial mode. In that case
// ensure_secure_ipc_parent_dir repairs it with fchmod and may request a scrub.
res.unwrap();
let md = std::fs::metadata(&parent_dir).unwrap();
assert!(md.is_dir());
let mode = md.permissions().mode() & 0o777;
assert_eq!(mode, 0o0700);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_scrub_preexisting_ipc_parent_entries_only_removes_target_postfix_artifacts() {
use std::os::unix::ffi::OsStrExt;
let unique = format!(
"rustdesk-ipc-scrub-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let ipc_file = base.join("ipc_service");
let ipc_pid_file = base.join("ipc_service.pid");
let ipc_other_postfix_file = base.join("ipc_uinput_1");
let keep_file = base.join("keep.txt");
let keep_dir = base.join("keep_dir");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
std::fs::write(&ipc_pid_file, b"1234").unwrap();
std::fs::write(&ipc_other_postfix_file, b"other-postfix").unwrap();
std::fs::write(&keep_file, b"keep").unwrap();
std::fs::create_dir_all(&keep_dir).unwrap();
let base_c = std::ffi::CString::new(base.as_os_str().as_bytes().to_vec()).unwrap();
let base_fd = super::open_ipc_parent_dir_fd(&base_c).unwrap();
let _base_guard = super::FdGuard(base_fd);
super::scrub_preexisting_ipc_parent_entries(base_fd, &base, "_service").unwrap();
assert!(!ipc_file.exists());
assert!(!ipc_pid_file.exists());
assert!(ipc_other_postfix_file.exists());
assert!(keep_file.exists());
assert!(keep_dir.exists());
std::fs::remove_file(&ipc_other_postfix_file).ok();
std::fs::remove_file(&keep_file).ok();
std::fs::remove_dir_all(&keep_dir).ok();
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_scrub_preexisting_ipc_parent_entries_should_bind_to_opened_inode_not_path() {
use std::os::unix::ffi::OsStrExt;
let unique = format!(
"rustdesk-ipc-scrub-fd-bind-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let trusted_parent = base.join("trusted_parent");
let trusted_parent_moved = base.join("trusted_parent_moved");
let attacker_parent = base.join("attacker_parent");
std::fs::create_dir_all(&trusted_parent).unwrap();
std::fs::create_dir_all(&attacker_parent).unwrap();
let trusted_ipc_file = trusted_parent.join("ipc_service");
let attacker_ipc_file = attacker_parent.join("ipc_service");
std::fs::write(&trusted_ipc_file, b"trusted").unwrap();
std::fs::write(&attacker_ipc_file, b"attacker").unwrap();
let trusted_parent_c =
std::ffi::CString::new(trusted_parent.as_os_str().as_bytes().to_vec()).unwrap();
let trusted_parent_fd = super::open_ipc_parent_dir_fd(&trusted_parent_c).unwrap();
let _trusted_parent_guard = super::FdGuard(trusted_parent_fd);
// Swap the path after the trusted inode has been opened.
std::fs::rename(&trusted_parent, &trusted_parent_moved).unwrap();
std::fs::rename(&attacker_parent, &trusted_parent).unwrap();
super::scrub_preexisting_ipc_parent_entries(trusted_parent_fd, &trusted_parent, "_service")
.unwrap();
// Expected secure behavior: scrub should target the inode that was opened before path swap.
assert!(
!trusted_parent_moved.join("ipc_service").exists(),
"trusted inode artifact should be removed even after path swap"
);
assert!(
trusted_parent.join("ipc_service").exists(),
"path-swapped attacker directory should not be scrubbed"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_keeps_service_artifacts_before_liveness_probe() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-secure-dir-order-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let parent_dir = base.join("service_parent");
std::fs::create_dir_all(&parent_dir).unwrap();
// Trigger "had_untrusted_service_parent_mode".
std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o777)).unwrap();
let ipc_file = parent_dir.join("ipc_service");
let ipc_pid_file = parent_dir.join("ipc_service.pid");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
std::fs::write(&ipc_pid_file, b"1234").unwrap();
let res =
super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "_service");
assert_eq!(res.unwrap(), true);
// Parent hardening should run first; artifacts should stay until liveness probe completes.
assert!(ipc_file.exists(), "ipc socket marker should be preserved");
assert!(ipc_pid_file.exists(), "pid marker should be preserved");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_ensure_secure_ipc_parent_dir_marks_non_service_mode_repair_for_scrub() {
use std::os::unix::fs::PermissionsExt;
let unique = format!(
"rustdesk-ipc-nonservice-mode-repair-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let base = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&base).unwrap();
let parent_dir = base.join("non_service_parent");
std::fs::create_dir_all(&parent_dir).unwrap();
std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
let ipc_file = parent_dir.join("ipc");
std::fs::write(&ipc_file, b"socket-placeholder").unwrap();
let res = super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "");
assert_eq!(res.unwrap(), true);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn test_should_scrub_parent_entries_after_check_pid_only_when_requested_and_not_alive() {
assert!(!super::should_scrub_parent_entries_after_check_pid(
false, false
));
assert!(!super::should_scrub_parent_entries_after_check_pid(
false, true
));
assert!(super::should_scrub_parent_entries_after_check_pid(
true, false
));
assert!(!super::should_scrub_parent_entries_after_check_pid(
true, true
));
}
}

View File

@@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "표시 이름"),
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
("Enable privacy mode", "개인정보 보호 모드 사용함"),
("Enable privacy mode", ""),
].iter().cloned().collect();
}

View File

@@ -29,12 +29,6 @@ use wallpaper;
pub const PA_SAMPLE_RATE: u32 = 48000;
static mut UNMODIFIED: bool = true;
#[derive(Clone, Debug)]
struct ActiveUserLookupCache {
uid: String,
username: String,
}
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
@@ -56,8 +50,6 @@ lazy_static::lazy_static! {
}
}
};
static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
std::sync::Mutex::new(None);
// https://github.com/rustdesk/rustdesk/issues/13705
// Check if `sudo -E` actually preserves environment.
//
@@ -90,27 +82,6 @@ lazy_static::lazy_static! {
};
}
#[inline]
fn update_active_user_lookup_cache(desktop: &Desktop) {
if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {
if desktop.uid.is_empty() || desktop.username.is_empty() {
*cache = None;
} else {
*cache = Some(ActiveUserLookupCache {
uid: desktop.uid.clone(),
username: desktop.username.clone(),
});
}
}
}
#[inline]
fn get_active_user_id_name_from_cache() -> Option<(String, String)> {
let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?;
let entry = cache.as_ref()?;
Some((entry.uid.clone(), entry.username.clone()))
}
thread_local! {
// XDO context - created via libxdo-sys (which uses dynamic loading stub).
// If libxdo is not available, xdo will be null and xdo-based functions become no-ops.
@@ -818,7 +789,6 @@ pub fn start_os_service() {
let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) {
desktop.refresh();
update_active_user_lookup_cache(&desktop);
// Duplicate logic here with should_start_server
// Login wayland will try to start a headless --server.
@@ -891,29 +861,13 @@ pub fn start_os_service() {
}
#[inline]
/// Returns the cached active `(uid, username)` snapshot when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_user_id_name() -> (String, String) {
if let Some(id_name) = get_active_user_id_name_from_cache() {
return id_name;
}
let vec_id_name = get_values_of_seat0(&[1, 2]);
(vec_id_name[0].clone(), vec_id_name[1].clone())
}
#[inline]
/// Returns the cached active uid when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_userid() -> String {
if let Some((uid, _)) = get_active_user_id_name_from_cache() {
return uid;
}
get_values_of_seat0(&[1])[0].clone()
}
#[inline]
/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache.
pub fn get_active_userid_fresh() -> String {
get_values_of_seat0(&[1])[0].clone()
}
@@ -968,12 +922,7 @@ fn _get_display_manager() -> String {
}
#[inline]
/// Returns the cached active username when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_username() -> String {
if let Some((_, username)) = get_active_user_id_name_from_cache() {
return username;
}
get_values_of_seat0(&[2])[0].clone()
}

View File

@@ -73,19 +73,10 @@ use winapi::{
};
use windows::Win32::{
Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE},
Security::{
GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser,
WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER,
},
System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
System::Threading::{
OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken,
QueryFullProcessImageNameW as WinQueryFullProcessImageNameW,
PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION,
},
};
use windows_service::{
define_windows_service,
@@ -97,14 +88,6 @@ use windows_service::{
};
use winreg::{enums::*, RegKey};
mod acl;
pub(crate) use acl::current_process_user_sid_string;
pub use acl::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
};
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
pub const EXPLORER_EXE: &'static str = "explorer.exe";
pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW";
@@ -582,55 +565,6 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD {
unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) }
}
#[inline]
fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option<u32> {
let share_rdp_enabled = is_share_rdp();
if get_available_sessions(false)
.iter()
.any(|e| e.sid == session_id)
{
return Some(session_id);
}
let current_active_session =
unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) };
if current_active_session == u32::MAX {
None
} else {
Some(current_active_session)
}
}
#[inline]
fn authorize_service_scoped_ipc_connection(
stream: &ipc::Connection,
expected_active_session_id: Option<u32>,
) -> bool {
let (authorized, peer_pid, peer_session_id, peer_is_system) =
stream.service_authorization_status_for_session(expected_active_session_id);
if !authorized {
ipc::log_rejected_windows_ipc_connection(
crate::POSTFIX_SERVICE,
peer_pid,
peer_session_id,
expected_active_session_id,
peer_is_system,
);
return false;
}
if let Err(err) =
ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE)
{
log::warn!(
"Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
crate::POSTFIX_SERVICE,
peer_pid,
err
);
return false;
}
true
}
extern "system" {
fn BlockInput(v: BOOL) -> BOOL;
}
@@ -697,15 +631,6 @@ async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
Ok(res) => match res {
Some(Ok(stream)) => {
let mut stream = ipc::Connection::new(stream);
// Keep IPC authorization consistent with the session we are currently serving.
// Recompute expected session right before authorization to avoid using a stale
// session_id after awaiting incoming.next().
let expected_active_session_id =
resolve_expected_active_session_id_for_service(session_id);
if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id)
{
continue;
}
if let Ok(Some(data)) = stream.next_timeout(1000).await {
match data {
ipc::Data::Close => {
@@ -1216,22 +1141,6 @@ pub fn get_active_user_home() -> Option<PathBuf> {
None
}
#[cfg(not(feature = "flutter"))]
#[inline]
pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> {
// Keep parity with history for now: derive LocalAppData from user profile path.
// If users report redirected/non-standard LocalAppData issues, switch to:
// `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution.
let user_dir = hbb_common::directories_next::UserDirs::new()?;
let dir = user_dir
.home_dir()
.join("AppData")
.join("Local")
.join("rustdesk-sciter");
let dst = dir.join("rustdesk.exe");
Some((dir, dst))
}
pub fn is_prelogin() -> bool {
let Some(username) = get_current_session_username() else {
return false;
@@ -2418,33 +2327,16 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
is_run_as_system,
crate::username(),
);
let mut arg_elevate = if is_setup {
let arg_elevate = if is_setup {
"--noinstall --elevate"
} else {
"--elevate"
}
.to_owned();
let mut arg_run_as_system = if is_setup {
};
let arg_run_as_system = if is_setup {
"--noinstall --run-as-system"
} else {
"--run-as-system"
}
.to_owned();
let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args();
if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() {
log::error!("Invalid portable service shared memory argument, aborting elevation flow");
// This is a malformed bootstrap argument in a privilege-sensitive path.
// Keep fail-closed process termination here to avoid continuing elevation
// with inconsistent shared-memory contract.
std::process::exit(1);
}
if let Some(shmem_name) = shmem_name_from_args {
let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name);
arg_elevate.push(' ');
arg_elevate.push_str(&shmem_arg);
arg_run_as_system.push(' ');
arg_run_as_system.push_str(&shmem_arg);
}
};
if is_root() {
if is_run_as_system {
log::info!("run portable service");
@@ -2455,7 +2347,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
Ok(elevated) => {
if elevated {
if !is_run_as_system {
if run_as_system(arg_run_as_system.as_str()).is_ok() {
if run_as_system(arg_run_as_system).is_ok() {
std::process::exit(0);
} else {
log::error!(
@@ -2466,7 +2358,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
}
} else {
if !is_elevate {
if let Ok(true) = elevate(arg_elevate.as_str()) {
if let Ok(true) = elevate(arg_elevate) {
std::process::exit(0);
} else {
log::error!("Failed to elevate, error {}", io::Error::last_os_error());
@@ -2524,115 +2416,6 @@ pub fn is_elevated(process_id: Option<DWORD>) -> ResultType<bool> {
}
}
#[inline]
unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType<Vec<u8>> {
let mut token_user_size = 0u32;
let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size);
match get_info_result {
Ok(()) => {
if token_user_size == 0 {
bail!(
"Failed to get {} token user size: unexpected zero buffer size",
subject
);
}
}
Err(e) => {
// Allow expected size-probe failures if Windows still returns required size.
let is_insufficient_buffer =
e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32);
let is_bad_length =
e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32);
if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 {
bail!("Failed to get {} token user size: {}", subject, e);
}
}
}
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token,
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?;
let min_size = std::mem::size_of::<TOKEN_USER>();
if buffer.len() < min_size {
bail!(
"Failed to parse {} token user: buffer too small (got {}, need >= {})",
subject,
buffer.len(),
min_size
);
}
Ok(buffer)
}
/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process.
///
/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18).
///
/// TODO: After a few releases of real-world validation, consider replacing
/// the legacy `is_local_system()` with this implementation.
pub fn is_process_running_as_system(process_id: DWORD) -> ResultType<bool> {
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let mut token = WinHANDLE::default();
let result = (|| -> ResultType<bool> {
WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?;
let token_subject = format!("process {}", process_id);
let buffer = read_token_user_buffer(token, token_subject.as_str())?;
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool())
})();
if !token.is_invalid() {
let _ = WinCloseHandle(token);
}
let _ = WinCloseHandle(process);
result
}
}
pub fn get_process_executable_path(process_id: DWORD) -> ResultType<PathBuf> {
const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024;
unsafe {
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
let result = (|| -> ResultType<PathBuf> {
let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN];
let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32;
WinQueryFullProcessImageNameW(
process,
windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0),
windows::core::PWSTR(buffer.as_mut_ptr()),
&mut length,
)
.map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?;
if length == 0 {
bail!(
"Failed to query process {} image path: empty result",
process_id
);
}
buffer.truncate(length as usize);
Ok(PathBuf::from(OsString::from_wide(&buffer)))
})();
let _ = WinCloseHandle(process);
result
}
}
pub fn is_foreground_window_elevated() -> ResultType<bool> {
unsafe {
let mut process_id: DWORD = 0;
@@ -2925,6 +2708,16 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
return Ok(());
}
pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> {
std::process::Command::new("icacls")
.arg(dir.as_os_str())
.arg("/grant")
.arg(format!("*S-1-1-0:(OI)(CI){}", permission))
.arg("/T")
.spawn()?;
Ok(())
}
#[inline]
fn str_to_device_name(name: &str) -> [u16; 32] {
let mut device_name: Vec<u16> = wide_string(name);
@@ -4488,87 +4281,6 @@ pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
#[cfg(test)]
mod tests {
use super::*;
// Test-only reusable Win32 HANDLE RAII helper.
// If a future non-test path needs the same pattern, move it out of this test module.
//
// This struct is similar to `hbb_common::platform::windows::RAIIHandle`,
// but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate.
struct HandleGuard(WinHANDLE);
impl HandleGuard {
#[inline]
fn new(handle: WinHANDLE) -> Self {
Self(handle)
}
#[inline]
fn get(&self) -> WinHANDLE {
self.0
}
}
impl Drop for HandleGuard {
fn drop(&mut self) {
unsafe {
if !self.0.is_invalid() {
let _ = WinCloseHandle(self.0);
}
}
}
}
#[test]
fn test_is_process_running_as_system_invalid_pid_errors() {
assert!(is_process_running_as_system(u32::MAX).is_err());
}
#[test]
fn test_is_process_running_as_system_matches_current_process_token_user() {
let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() };
let actual = is_process_running_as_system(pid).unwrap();
let expected = unsafe {
// Keep this test consistent: use only the `windows` crate APIs/types.
let process = HandleGuard::new(
WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
.expect("WinOpenProcess should succeed for current process"),
);
let mut token = WinHANDLE::default();
WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token)
.expect("WinOpenProcessToken should succeed for current process");
let token = HandleGuard::new(token);
let mut token_user_size = 0u32;
let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size);
assert_ne!(token_user_size, 0, "TokenUser size should be non-zero");
let mut buffer = vec![0u8; token_user_size as usize];
WinGetTokenInformation(
token.get(),
TokenUser,
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
token_user_size,
&mut token_user_size,
)
.expect("WinGetTokenInformation(TokenUser) should succeed for current process");
let min_size = std::mem::size_of::<TOKEN_USER>();
assert!(
buffer.len() >= min_size,
"TokenUser buffer too small (got {}, need >= {})",
buffer.len(),
min_size
);
let token_user: TOKEN_USER =
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool();
expected
};
assert_eq!(actual, expected);
}
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());

View File

@@ -1,903 +0,0 @@
// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary
use super::{read_token_user_buffer, wide_string, ResultType};
use hbb_common::{anyhow::anyhow, bail};
use std::{
fs, io,
os::windows::{ffi::OsStrExt, fs::MetadataExt},
path::Path,
};
use windows::{
core::{PCWSTR, PWSTR},
Win32::{
Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL},
Security::{
Authorization::{
ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW,
SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS,
SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
},
ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
TOKEN_QUERY, TOKEN_USER,
},
Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE},
System::Threading::{GetCurrentProcess, OpenProcessToken},
},
};
const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400;
#[inline]
fn is_reparse_point(metadata: &fs::Metadata) -> bool {
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0
}
fn apply_grant_sid_allow_ace_to_path(
path: &Path,
sid_ptr: *mut std::ffi::c_void,
access_mask: u32,
is_group: bool,
is_dir: bool,
) -> ResultType<()> {
// Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW.
// https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c--
let mut old_dacl: *mut ACL = std::ptr::null_mut();
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let get_named_result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut old_dacl),
None,
&mut security_descriptor,
)
};
if get_named_result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
get_named_result.0
);
}
let _sd_guard = LocalAllocGuard(security_descriptor.0);
let inherit_flags = if is_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let explicit_access = [make_sid_trustee_entry(
sid_ptr,
access_mask,
inherit_flags,
is_group,
)];
let old_acl_option = if old_dacl.is_null() {
None
} else {
Some(old_dacl as *const ACL)
};
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result = unsafe {
SetEntriesInAclW(
Some(explicit_access.as_slice()),
old_acl_option,
&mut new_acl,
)
};
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be
/// readable/executable across user contexts.
///
/// `access_mask` is the Win32 file access mask to grant recursively.
pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> {
let metadata = fs::symlink_metadata(dir).map_err(|e| {
anyhow!(
"Failed to inspect ACL target directory '{}': {}",
dir.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"ACL target directory is a reparse point and is rejected: '{}'",
dir.display()
);
}
if !metadata.file_type().is_dir() {
bail!("ACL target is not a directory: '{}'", dir.display());
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?;
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let metadata = fs::symlink_metadata(&path)
.map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?;
if is_reparse_point(&metadata) {
continue;
}
let is_dir = metadata.file_type().is_dir();
apply_grant_sid_allow_ace_to_path(
&path,
everyone_sid.as_sid_ptr(),
access_mask,
true,
is_dir,
)?;
if !is_dir {
continue;
}
for entry in fs::read_dir(&path)
.map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))?
{
let entry = entry.map_err(|e| {
anyhow!(
"Failed to read ACL target dir entry under '{}': {}",
path.display(),
e
)
})?;
stack.push(entry.path());
}
}
Ok(())
}
/// Returns the current process user SID as a standard SID string
/// (for example: `S-1-5-18`).
///
/// Source:
/// - Official SID-to-string API (`ConvertSidToStringSidW`):
/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw
pub(crate) fn current_process_user_sid_string() -> ResultType<String> {
let mut token = HANDLE::default();
let result = (|| -> ResultType<String> {
unsafe {
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| anyhow!("Failed to open current process token: {}", e))?;
}
let buffer = unsafe { read_token_user_buffer(token, "current process")? };
let token_user: TOKEN_USER =
unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) };
if token_user.User.Sid.0.is_null() {
bail!("Token SID is null");
}
let mut sid_string_ptr = PWSTR::null();
unsafe {
ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| {
anyhow!(
"ConvertSidToStringSidW failed for current process token SID: {}",
e
)
})?;
}
if sid_string_ptr.is_null() {
bail!("ConvertSidToStringSidW returned null SID string pointer");
}
let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void);
unsafe {
sid_string_ptr
.to_string()
.map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e))
}
})();
if !token.is_invalid() {
unsafe {
let _ = CloseHandle(token);
}
}
result
}
/// Hardens ACLs for portable-service shared-memory path (directory or file).
///
/// Why:
/// - Shared memory used by portable service carries runtime control/data and must not inherit
/// broad/default ACLs.
/// - We explicitly grant only trusted principals and remove broad groups to reduce local
/// privilege-boundary bypass risk.
///
/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`):
/// - common (directory + file):
/// - `S-1-5-18` (LocalSystem): full control
/// - `S-1-5-32-544` (Built-in Administrators): full control
/// - `current_process_user_sid_string()` result: full control
/// - directory (`portable_service_shmem` parent):
/// - keep `Authenticated Users` directory-level write so other local accounts can
/// create their own runtime shmem files after account switching
/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself;
/// it is intentionally not inherited by children.
/// Reference:
/// - File access rights:
/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
/// - ACE inheritance rules:
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules
/// - remove `Everyone` and `Users` grants
/// - file (`shared_memory*` flink):
/// - remove broad grants:
/// - `S-1-1-0` (Everyone)
/// - `S-1-5-11` (Authenticated Users)
/// - `S-1-5-32-545` (Users)
///
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, true)
}
#[inline]
pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
validate_portable_service_shmem_dir_target(path)
}
#[inline]
pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> {
set_path_permission_for_portable_service_shmem_impl(path, false)
}
#[derive(Debug)]
pub(super) struct LocalAllocGuard(*mut std::ffi::c_void);
impl LocalAllocGuard {
#[inline]
pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void {
self.0
}
}
impl Drop for LocalAllocGuard {
fn drop(&mut self) {
if self.0.is_null() {
return;
}
// Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW /
// ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed.
unsafe {
let _ = LocalFree(Some(HLOCAL(self.0)));
}
}
}
#[inline]
pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType<LocalAllocGuard> {
let sid_utf16 = wide_string(sid);
let mut sid_ptr = PSID::default();
unsafe {
ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr)
.map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?;
}
if sid_ptr.0.is_null() {
bail!("ConvertStringSidToSidW returned null SID for '{}'", sid);
}
Ok(LocalAllocGuard(sid_ptr.0))
}
#[inline]
fn make_sid_trustee_entry(
sid_ptr: *mut std::ffi::c_void,
access_permissions: u32,
inheritance: ACE_FLAGS,
is_group: bool,
) -> EXPLICIT_ACCESS_W {
// `is_group` is explicitly provided by the caller from the concrete SID semantic
// (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user).
EXPLICIT_ACCESS_W {
grfAccessPermissions: access_permissions,
grfAccessMode: SET_ACCESS,
grfInheritance: inheritance,
Trustee: TRUSTEE_W {
pMultipleTrustee: std::ptr::null_mut(),
MultipleTrusteeOperation: Default::default(),
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: if is_group {
TRUSTEE_IS_GROUP
} else {
TRUSTEE_IS_USER
},
// SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID.
ptstrName: PWSTR::from_raw(sid_ptr as *mut u16),
},
}
}
fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> {
let metadata = fs::symlink_metadata(path).map_err(|e| {
anyhow!(
"Failed to inspect portable service shared-memory ACL directory '{}': {}",
path.display(),
e
)
})?;
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'",
path.display()
);
}
if !metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is not a directory: '{}'",
path.display()
);
}
Ok(())
}
fn set_path_permission_for_portable_service_shmem_impl(
path: &Path,
expect_dir: bool,
) -> ResultType<()> {
if expect_dir {
validate_portable_service_shmem_dir_target(path)?;
} else {
let metadata_result = fs::symlink_metadata(path);
match metadata_result {
Ok(metadata) => {
if metadata.file_type().is_dir() {
bail!(
"Portable service shared-memory ACL target is a directory, expected file-like path: '{}'",
path.display()
);
}
if is_reparse_point(&metadata) {
bail!(
"Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'",
path.display()
);
}
}
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::PermissionDenied =>
{
// Keep going and let Win32 ACL APIs return the final OS error.
// `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into
// a false "not found" signal under restricted directory ACLs.
}
Err(e) => {
bail!(
"Failed to inspect portable service shared-memory ACL target '{}': {}",
path.display(),
e
);
}
}
}
let user_sid = current_process_user_sid_string()?;
let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?;
let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?;
let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?;
let authenticated_users_sid = if expect_dir {
Some(sid_string_to_local_alloc_guard("S-1-5-11")?)
} else {
None
};
let inherit_flags = if expect_dir {
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
} else {
NO_INHERITANCE
};
let mut entries = vec![
make_sid_trustee_entry(
local_system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
make_sid_trustee_entry(
administrators_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
true,
),
make_sid_trustee_entry(
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0,
inherit_flags,
false,
),
];
if let Some(auth_sid) = authenticated_users_sid.as_ref() {
// Keep the shared parent directory multi-user writable at directory level.
entries.push(make_sid_trustee_entry(
auth_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0,
NO_INHERITANCE,
true,
));
}
// Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected.
// This avoids carrying over broad legacy ACEs from inherited/default ACLs.
// Reference:
// - SetEntriesInAclW:
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw
// - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION):
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow
let mut new_acl: *mut ACL = std::ptr::null_mut();
let set_entries_result =
unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) };
if set_entries_result.0 != 0 {
bail!(
"SetEntriesInAclW failed for '{}': win32_error={}",
path.display(),
set_entries_result.0
);
}
if new_acl.is_null() {
bail!(
"SetEntriesInAclW returned null ACL for '{}'",
path.display()
);
}
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
let set_named_result = unsafe {
SetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
security_info,
None,
None,
Some(new_acl),
None,
)
};
if set_named_result.0 != 0 {
bail!(
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
set_named_result.0
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
current_process_user_sid_string, set_path_permission,
set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard,
LocalAllocGuard, ResultType,
};
use hbb_common::bail;
use std::{
fs,
os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file},
path::{Path, PathBuf},
};
use windows::{
core::PCWSTR,
Win32::{
Security::{
AclSizeInformation,
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl,
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION,
DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
},
Storage::FileSystem::{
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
},
},
};
const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0;
fn unique_acl_test_path(prefix: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"rustdesk_acl_{}_{}_{}",
prefix,
std::process::id(),
hbb_common::rand::random::<u32>()
))
}
fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_dir(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create directory reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
match symlink_file(target, link) {
Ok(()) => true,
Err(err) => {
eprintln!(
"skip {}: failed to create file reparse point (symlink): {}",
test_name, err
);
false
}
}
}
fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> {
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut sd = PSECURITY_DESCRIPTOR::default();
let path_utf16: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let result = unsafe {
GetNamedSecurityInfoW(
PCWSTR::from_raw(path_utf16.as_ptr()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&mut dacl),
None,
&mut sd,
)
};
if result.0 != 0 {
bail!(
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
path.display(),
result.0
);
}
if dacl.is_null() || sd.0.is_null() {
bail!("DACL/security descriptor missing for '{}'", path.display());
}
Ok((dacl, LocalAllocGuard(sd.0)))
}
fn has_allow_ace_with_mask(
dacl: *const ACL,
sid_ptr: *mut std::ffi::c_void,
mask: u32,
) -> bool {
let mut info = ACL_SIZE_INFORMATION::default();
if unsafe {
GetAclInformation(
dacl,
&mut info as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
}
.is_err()
{
return false;
}
for index in 0..info.AceCount {
let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() {
continue;
}
let header = unsafe { &*(ace_ptr as *const ACE_HEADER) };
if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 {
continue;
}
let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) };
let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void);
if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok()
&& (allowed.Mask & mask) == mask
{
return true;
}
}
false
}
fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool {
has_allow_ace_with_mask(dacl, sid_ptr, 0)
}
fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool {
let mut control: u16 = 0;
let mut revision: u32 = 0;
if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() {
return false;
}
(control & SE_DACL_PROTECTED.0) != 0
}
#[test]
fn test_portable_service_shmem_dir_acl_policy() {
let dir = unique_acl_test_path("dir");
fs::create_dir_all(&dir).unwrap();
set_path_permission_for_portable_service_shmem_dir(&dir).unwrap();
let (dacl, sd_guard) = get_file_dacl(&dir).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
auth_users_sid.as_sid_ptr(),
FILE_GENERIC_WRITE.0
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_policy() {
let dir = unique_acl_test_path("file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("shared_memory_portable_service_test");
fs::write(&file, b"x").unwrap();
set_path_permission_for_portable_service_shmem_file(&file).unwrap();
let (dacl, sd_guard) = get_file_dacl(&file).unwrap();
let current_user_sid =
sid_string_to_local_alloc_guard(&current_process_user_sid_string().unwrap()).unwrap();
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
assert!(has_allow_ace_with_mask(
dacl,
system_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
admin_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(has_allow_ace_with_mask(
dacl,
current_user_sid.as_sid_ptr(),
FILE_ALL_ACCESS.0
));
assert!(!has_any_allow_ace_for_sid(
dacl,
auth_users_sid.as_sid_ptr()
));
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
sd_guard.as_sid_ptr()
)));
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_set_path_permission_rx_applies_recursively() {
let root = unique_acl_test_path("set_path_permission");
let child_dir = root.join("child");
let child_file = child_dir.join("helper.exe");
fs::create_dir_all(&child_dir).unwrap();
fs::write(&child_file, b"x").unwrap();
if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) {
let text = err.to_string();
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
if text.contains("win32_error=5") || text.contains("Access is denied") {
eprintln!(
"skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}",
text
);
return;
}
panic!("set_path_permission failed unexpectedly: {}", text);
}
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0;
for target in [&root, &child_dir, &child_file] {
let (dacl, _sd_guard) = get_file_dacl(target).unwrap();
assert!(
has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask),
"Everyone RX grant missing on '{}'",
target.display()
);
}
let _ = fs::remove_file(&child_file);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_file_target() {
let dir = unique_acl_test_path("dir_target_file");
fs::create_dir_all(&dir).unwrap();
let file = dir.join("target.txt");
fs::write(&file, b"x").unwrap();
let result = set_path_permission_for_portable_service_shmem_dir(&file);
assert!(result.is_err());
let _ = fs::remove_file(&file);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_dir_target() {
let dir = unique_acl_test_path("file_target_dir");
fs::create_dir_all(&dir).unwrap();
let result = set_path_permission_for_portable_service_shmem_file(&dir);
assert!(result.is_err());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_missing_target() {
let path = unique_acl_test_path("missing").join("shared_memory_missing");
let result = set_path_permission_for_portable_service_shmem_file(&path);
assert!(result.is_err());
}
#[test]
fn test_set_path_permission_rejects_reparse_entrypoint() {
let root = unique_acl_test_path("reparse_entry");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_set_path_permission_rejects_reparse_entrypoint",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_dir_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_dir");
let real_dir = root.join("real");
let link_dir = root.join("link");
fs::create_dir_all(&real_dir).unwrap();
if !try_create_dir_reparse_point(
&real_dir,
&link_dir,
"test_portable_service_shmem_dir_acl_rejects_reparse_target",
) {
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_dir(&link_dir);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_dir(&link_dir);
let _ = fs::remove_dir_all(&real_dir);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn test_portable_service_shmem_file_acl_rejects_reparse_target() {
let root = unique_acl_test_path("reparse_shmem_file");
let real_file = root.join("real.txt");
let link_file = root.join("link.txt");
fs::create_dir_all(&root).unwrap();
fs::write(&real_file, b"x").unwrap();
if !try_create_file_reparse_point(
&real_file,
&link_file,
"test_portable_service_shmem_file_acl_rejects_reparse_target",
) {
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
return;
}
let result = set_path_permission_for_portable_service_shmem_file(&link_file);
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
assert!(
text.contains("reparse point"),
"expected reparse-point rejection, got '{}'",
text
);
let _ = fs::remove_file(&link_file);
let _ = fs::remove_file(&real_file);
let _ = fs::remove_dir_all(&root);
}
}

View File

@@ -731,7 +731,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
use hbb_common::sleep;
for i in 1..=tries {
sleep(i as f32 * CONFIG_SYNC_INTERVAL_SECS).await;
match crate::ipc::connect_service(1000).await {
match crate::ipc::connect(1000, "_service").await {
Ok(mut conn) => {
if !synced {
if conn.send(&Data::SyncConfig(None)).await.is_ok() {
@@ -772,12 +772,6 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
};
};
}
if !synced {
log::warn!(
"initial config sync from root failed, reconnecting to ipc_service"
);
continue;
}
}
loop {
@@ -794,7 +788,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Se
match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await {
Err(e) => {
log::error!("sync config to root failed: {}", e);
match crate::ipc::connect_service(1000).await {
match crate::ipc::connect(1000, "_service").await {
Ok(mut _conn) => {
conn = _conn;
log::info!("reconnected to ipc_service");

View File

@@ -22,6 +22,8 @@ use crate::{
#[cfg(any(target_os = "android", target_os = "ios"))]
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
use cidr_utils::cidr::IpCidr;
#[cfg(target_os = "linux")]
use hbb_common::platform::linux::run_cmds;
#[cfg(target_os = "android")]
use hbb_common::protobuf::EnumOrUnknown;
use hbb_common::{
@@ -4981,9 +4983,6 @@ pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool {
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
// IPC bootstrap summary:
// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux).
// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC.
async fn start_ipc(
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
@@ -4998,19 +4997,10 @@ async fn start_ipc(
}
sleep(1.).await;
}
#[cfg(target_os = "linux")]
let headless_cm = crate::is_server()
&& crate::platform::is_headless_allowed()
&& linux_desktop_manager::is_headless();
#[cfg(not(target_os = "linux"))]
let headless_cm = false;
let mut stream = None;
if !headless_cm {
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
}
}
if stream.is_none() {
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
} else {
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut args = vec!["--cm"];
@@ -5020,123 +5010,75 @@ async fn start_ipc(
// Cm run as user, wait until desktop session is ready.
#[cfg(target_os = "linux")]
if headless_cm {
if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() {
let mut username = linux_desktop_manager::get_username();
loop {
if !username.is_empty() {
break;
}
// `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes
// (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness.
// TODO:
// When `_rx_desktop_ready` is closed, `recv()` returns
// `None` immediately and this loop may spin if `username` remains empty.
// Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by
// breaking/returning to avoid hot-looping.
let _res = timeout(1_000, _rx_desktop_ready.recv()).await;
username = linux_desktop_manager::get_username();
}
let uid = {
let username_for_cmd = username.clone();
let mut uid_cmd = hbb_common::tokio::process::Command::new("id");
// TODO:
// Keep current behavior for now to minimize change risk.
// If usernames starting with '-' are observed in the field, prefer:
// `id -u -- <username>` to avoid option-parsing ambiguity.
// Already verified that `id -u -- <username>` works as expected on macOS and Ubuntu 24.04.
uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true);
let output = timeout(10_000, uid_cmd.output())
.await
.map_err(|_| anyhow!("Timed out querying uid for {}", username))?
.map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?;
if !output.status.success() {
bail!("Failed to query uid for {}", username);
}
let output = String::from_utf8_lossy(&output.stdout);
let output = run_cmds(&format!("id -u {}", &username))?;
let output = output.trim();
if output.parse::<u32>().is_err() {
bail!("Invalid uid {}", output);
if output.is_empty() || !output.parse::<i32>().is_ok() {
bail!("Invalid username {}", &username);
}
output.to_string()
};
user = Some((uid, username));
args = vec!["--cm-no-ui"];
}
#[cfg(target_os = "linux")]
let cm_uid: Option<u32> = match &user {
Some((uid, _)) => Some(
uid.parse::<u32>()
.map_err(|_| anyhow!("Invalid uid {}", uid))?,
),
None => None,
};
#[cfg(target_os = "linux")]
if let Some(uid) = cm_uid {
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
let run_done;
if crate::platform::is_root() {
let mut res = Ok(None);
for _ in 0..10 {
#[cfg(not(any(target_os = "linux")))]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(args.clone());
}
#[cfg(target_os = "linux")]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(
args.clone(),
user.clone(),
None::<(&str, &str)>,
);
}
if res.is_ok() {
break;
}
log::error!("Failed to run cm: {res:?}");
sleep(1.).await;
}
if let Some(task) = res? {
super::CHILD_PROCESS.lock().unwrap().push(task);
}
run_done = true;
} else {
run_done = false;
}
if !run_done {
log::debug!("Start cm");
super::CHILD_PROCESS
.lock()
.unwrap()
.push(crate::run_me(args)?);
}
for _ in 0..20 {
sleep(0.3).await;
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
break;
}
}
if stream.is_none() {
let run_done;
if crate::platform::is_root() {
let mut res = Ok(None);
for _ in 0..10 {
#[cfg(not(any(target_os = "linux")))]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(args.clone());
}
#[cfg(target_os = "linux")]
{
log::debug!("Start cm");
res = crate::platform::run_as_user(
args.clone(),
user.clone(),
None::<(&str, &str)>,
);
}
if res.is_ok() {
break;
}
log::error!("Failed to run cm: {res:?}");
sleep(1.).await;
}
if let Some(task) = res? {
super::CHILD_PROCESS.lock().unwrap().push(task);
}
run_done = true;
} else {
run_done = false;
}
if !run_done {
log::debug!("Start cm");
super::CHILD_PROCESS
.lock()
.unwrap()
.push(crate::run_me(args)?);
}
for _ in 0..20 {
sleep(0.3).await;
#[cfg(target_os = "linux")]
{
if let Some(uid) = cm_uid {
if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await {
stream = Some(s);
break;
}
continue;
}
}
if let Ok(s) = crate::ipc::connect(1000, "_cm").await {
stream = Some(s);
break;
}
}
bail!("Failed to connect to connection manager");
}
}
if stream.is_none() {
bail!("Failed to connect to connection manager");
}
let _res = tx_stream_ready.send(()).await;
let mut stream = stream.ok_or(anyhow!("none stream"))?;

View File

@@ -1,11 +1,3 @@
use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN},
platform::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
},
};
use core::slice;
use hbb_common::{
allow_err,
@@ -23,26 +15,26 @@ use shared_memory::*;
use std::{
mem::size_of,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex,
},
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use winapi::{
shared::minwindef::{BOOL, FALSE, TRUE},
um::winuser::{self, CURSORINFO, PCURSORINFO},
};
use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ};
use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService},
platform::set_path_permission,
};
use super::video_qos;
const SIZE_COUNTER: usize = size_of::<i32>() * 2;
const FRAME_ALIGN: usize = 64;
const ADDR_IPC_TOKEN: usize = 0;
const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN;
const ADDR_CURSOR_PARA: usize = 0;
const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::<CURSORINFO>();
const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER;
@@ -52,186 +44,12 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of::<i3
const ADDR_CAPTURE_FRAME: usize =
(ADDR_CAPTURE_FRAME_COUNTER + SIZE_COUNTER + FRAME_ALIGN - 1) / FRAME_ALIGN * FRAME_ALIGN;
const MIN_RUNTIME_SHMEM_LEN: usize = ADDR_CAPTURE_FRAME + FRAME_ALIGN;
const IPC_SUFFIX: &str = "_portable_service";
pub const SHMEM_NAME: &str = "_portable_service";
pub const SHMEM_ARG_PREFIX: &str = "--portable-service-shmem-name=";
const SHMEM_PARENT_DIR: &str = "portable_service_shmem";
const SHMEM_NAME_MAX_LEN: usize = 64;
const MAX_NACK: usize = 3;
const PORTABLE_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_DXGI_FAIL_TIME: usize = 5;
#[inline]
fn is_valid_portable_service_shmem_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= SHMEM_NAME_MAX_LEN
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}
#[inline]
pub fn portable_service_shmem_arg(name: &str) -> String {
format!("{SHMEM_ARG_PREFIX}{name}")
}
#[inline]
fn is_valid_portable_service_ipc_token(token: &str) -> bool {
token.len() == IPC_TOKEN_LEN
&& token
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}
#[inline]
fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option<String> {
if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN {
log::error!(
"Portable service shared memory too small: len={}, need>={}",
shmem.len(),
ADDR_IPC_TOKEN + IPC_TOKEN_LEN
);
return None;
}
unsafe {
let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN);
let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN);
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(IPC_TOKEN_LEN);
if end == 0 {
return None;
}
let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned();
if is_valid_portable_service_ipc_token(&token) {
Some(token)
} else {
None
}
}
}
#[inline]
fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> {
if shmem.len() < MIN_RUNTIME_SHMEM_LEN {
bail!(
"Portable service shared memory too small for runtime layout: len={}, need>={}",
shmem.len(),
MIN_RUNTIME_SHMEM_LEN
);
}
Ok(())
}
#[inline]
fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool {
let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME);
frame_len > 0 && frame_len <= frame_capacity
}
#[inline]
fn shared_memory_flink_path_by_name(name: &str) -> ResultType<PathBuf> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
Ok(dir.join(format!("shared_memory{}", name)))
}
#[inline]
fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool {
let flink = match shared_memory_flink_path_by_name(name) {
Ok(path) => path,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to resolve portable service shared-memory flink path for '{}': {}",
log_context,
name,
err
);
}
return false;
}
};
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"{} removed portable service shared-memory flink artifact: {:?}",
log_context,
flink
);
true
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to remove portable service shared-memory flink artifact {:?}: {}",
log_context,
flink,
err
);
}
false
}
}
}
#[inline]
fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> {
if !is_valid_portable_service_ipc_token(token) {
bail!("Invalid portable service ipc token");
}
shmem.write(ADDR_IPC_TOKEN, token.as_bytes());
Ok(())
}
#[inline]
fn clear_ipc_token_in_shmem(shmem: &SharedMemory) {
shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]);
}
#[inline]
fn portable_service_arg_value_candidate_from_arg<'a>(
arg: &'a str,
prefix: &str,
) -> Option<&'a str> {
let mut value = arg.strip_prefix(prefix)?;
value = value.trim_start();
value = value
.strip_prefix('"')
.or_else(|| value.strip_prefix('\''))
.unwrap_or(value);
value = value.split_whitespace().next().unwrap_or_default();
value = value.trim_matches(|c| c == '"' || c == '\'');
Some(value)
}
#[inline]
pub fn portable_service_shmem_name_from_args() -> Option<String> {
for arg in std::env::args() {
if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) {
if is_valid_portable_service_shmem_name(value) {
return Some(value.to_owned());
}
log::error!(
"Invalid portable service shared memory name argument: '{}'",
value
);
return None;
}
}
None
}
#[inline]
pub fn has_portable_service_shmem_arg() -> bool {
std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX))
}
pub struct SharedMemory {
inner: Shmem,
}
@@ -274,27 +92,7 @@ impl SharedMemory {
}
};
log::info!("Create shared memory, size: {}, flink: {}", size, flink);
if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) {
// Release shmem handle first so best-effort flink cleanup has a chance to succeed.
drop(shmem);
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"Create cleanup removed portable service shared-memory flink artifact: {}",
flink
);
}
Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {}
Err(remove_err) => {
log::warn!(
"Create cleanup failed to remove portable service shared-memory flink artifact {}: {}",
flink,
remove_err
);
}
}
return Err(err);
}
set_path_permission(Path::new(&flink), "F").ok();
Ok(SharedMemory { inner: shmem })
}
@@ -322,18 +120,9 @@ impl SharedMemory {
fn flink(name: String) -> ResultType<String> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
let parent_created = !dir.exists();
if parent_created {
std::fs::create_dir_all(&dir)?;
}
if parent_created || crate::platform::is_root() {
// Harden parent ACL on first provisioning and periodically on SYSTEM path.
set_path_permission_for_portable_service_shmem_dir(&dir)?;
} else {
// Existing parents still need type/reparse validation. Non-SYSTEM callers may lack
// WRITE_DAC on a valid parent, so avoid rebuilding the ACL here.
validate_path_for_portable_service_shmem_dir(&dir)?;
if !dir.exists() {
std::fs::create_dir(&dir)?;
set_path_permission(&dir, "F").ok();
}
Ok(dir
.join(format!("shared_memory{}", name))
@@ -443,45 +232,16 @@ pub mod server {
lazy_static::lazy_static! {
static ref EXIT: Arc<Mutex<bool>> = Default::default();
static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false);
}
pub fn run_portable_service() {
let shmem_name = match portable_service_shmem_name_from_args() {
Some(name) => name,
None => {
if has_portable_service_shmem_arg() {
log::error!(
"Invalid portable service shared memory argument, aborting startup"
);
} else {
log::error!(
"Missing portable service shared memory argument, aborting startup"
);
}
return;
}
};
let shmem = match SharedMemory::open_existing(&shmem_name) {
let shmem = match SharedMemory::open_existing(SHMEM_NAME) {
Ok(shmem) => Arc::new(shmem),
Err(e) => {
log::error!("Failed to open existing shared memory: {:?}", e);
return;
}
};
if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) {
log::error!("{}", e);
return;
}
let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) {
Some(token) => token,
None => {
log::error!(
"Missing portable service ipc token in shared memory, aborting startup"
);
return;
}
};
let shmem1 = shmem.clone();
let shmem2 = shmem.clone();
let mut threads = vec![];
@@ -491,24 +251,17 @@ pub mod server {
threads.push(std::thread::spawn(|| {
run_capture(shmem2);
}));
threads.push(std::thread::spawn(move || {
run_ipc_client(ipc_token);
threads.push(std::thread::spawn(|| {
run_ipc_client();
}));
// Detached shutdown watchdog:
// - gives graceful shutdown/cleanup a short window
// - force-exits the process if workers are still stuck
std::thread::spawn(|| {
threads.push(std::thread::spawn(|| {
run_exit_check();
});
}));
let record_pos_handle = crate::input_service::try_start_record_cursor_pos();
// Arm forced-exit watchdog only for worker join phase.
// Once join phase completes, cleanup should not be interrupted by forced exit.
FORCE_EXIT_ARMED.store(true, Ordering::SeqCst);
for th in threads.drain(..) {
th.join().ok();
log::info!("thread joined");
}
FORCE_EXIT_ARMED.store(false, Ordering::SeqCst);
crate::input_service::try_stop_record_cursor_pos();
if let Some(handle) = record_pos_handle {
@@ -517,47 +270,16 @@ pub mod server {
Err(e) => log::error!("record_pos_handle join error {:?}", &e),
}
}
drop(shmem);
remove_shared_memory_flink_with_retry(&shmem_name);
}
fn run_exit_check() {
const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3);
loop {
if EXIT.lock().unwrap().clone() {
break;
std::thread::sleep(Duration::from_millis(50));
std::process::exit(0);
}
std::thread::sleep(Duration::from_millis(50));
}
// Fallback only: normal shutdown path should complete and process should exit naturally.
// This forced exit is a last resort when worker threads are stuck and graceful teardown
// does not finish in time.
std::thread::sleep(FORCED_EXIT_DELAY);
if FORCE_EXIT_ARMED.load(Ordering::SeqCst) {
log::warn!(
"Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}",
FORCED_EXIT_DELAY
);
std::process::exit(0);
}
}
fn remove_shared_memory_flink_with_retry(name: &str) {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for attempt in 0..MAX_RETRY {
let is_last_attempt = attempt + 1 == MAX_RETRY;
if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") {
return;
}
if !is_last_attempt {
std::thread::sleep(RETRY_INTERVAL);
}
}
log::warn!(
"SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
}
fn run_get_cursor_info(shmem: Arc<SharedMemory>) {
@@ -664,17 +386,6 @@ pub mod server {
match c.as_mut().map(|f| f.frame(spf)) {
Some(Ok(f)) => match f {
Frame::PixelBuffer(f) => {
let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME);
if f.data().len() > frame_capacity {
log::error!(
"Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}",
f.data().len(),
frame_capacity,
shmem.len()
);
*EXIT.lock().unwrap() = true;
return;
}
utils::set_frame_info(
&shmem,
FrameInfo {
@@ -725,33 +436,17 @@ pub mod server {
}
#[tokio::main(flavor = "current_thread")]
async fn run_ipc_client(ipc_token: String) {
async fn run_ipc_client() {
use DataPortableService::*;
let postfix = IPC_SUFFIX;
match ipc::connect(1000, postfix).await {
Ok(mut stream) => {
if let Err(err) =
ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await
{
log::error!("portable service ipc handshake failed: {}", err);
*EXIT.lock().unwrap() = true;
return;
}
let mut timer =
crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
loop {
if *EXIT.lock().unwrap() {
log::info!("Portable service EXIT signaled, closing ipc client loop");
stream
.send(&Data::DataPortableService(WillClose))
.await
.ok();
break;
}
tokio::select! {
res = stream.next() => {
match res {
@@ -831,11 +526,7 @@ pub mod client {
lazy_static::lazy_static! {
static ref RUNNING: Arc<Mutex<bool>> = Default::default();
static ref STARTING: Arc<Mutex<bool>> = Default::default();
static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0);
static ref SHMEM: Arc<Mutex<Option<SharedMemory>>> = Default::default();
static ref SHMEM_RUNTIME_NAME: Arc<Mutex<Option<String>>> = Default::default();
static ref IPC_RUNTIME_TOKEN: Arc<Mutex<Option<String>>> = Default::default();
static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(client::start_ipc_server());
static ref QUICK_SUPPORT: Arc<Mutex<bool>> = Default::default();
}
@@ -845,176 +536,12 @@ pub mod client {
Logon(String, String),
}
fn has_running_portable_service_process() -> bool {
let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase());
!crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service")
.is_empty()
}
#[inline]
fn next_portable_service_shmem_name() -> String {
format!(
"{}_{}_{:08x}",
crate::portable_service::SHMEM_NAME,
std::process::id(),
hbb_common::rand::random::<u32>()
)
}
#[inline]
fn set_runtime_ipc_token(token: String) {
*IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token);
}
#[inline]
fn schedule_remove_runtime_shmem_flink_retry(name: String) {
std::thread::spawn(move || {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for _ in 0..MAX_RETRY {
std::thread::sleep(RETRY_INTERVAL);
if remove_shared_memory_flink_once(&name, false, "Client cleanup") {
return;
}
}
log::warn!(
"Failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
});
}
#[inline]
fn clear_runtime_shmem_state() {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
let mut shmem_lock = SHMEM.lock().unwrap();
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
*shmem_lock = None;
let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take();
*runtime_token = None;
drop(runtime_token);
drop(shmem_lock);
if let Some(name) = runtime_name.as_deref() {
if !remove_shared_memory_flink_once(name, true, "Client cleanup") {
schedule_remove_runtime_shmem_flink_retry(name.to_owned());
}
}
}
#[inline]
fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option<String>) {
let mut token = IPC_RUNTIME_TOKEN.lock().unwrap();
if !token
.as_deref()
.is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate))
{
return (false, None);
}
let mut shmem_lock = SHMEM.lock().unwrap();
let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
*token = None;
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
(true, matched_shmem_name)
}
#[inline]
fn restore_runtime_ipc_token_after_failed_handshake(
token: &str,
expected_shmem_name: Option<&str>,
) {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
if let Some(current) = runtime_token.as_deref() {
if current != token {
log::debug!(
"Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value"
);
return;
}
}
let mut shmem_lock = SHMEM.lock().unwrap();
let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
if current_shmem_name.as_deref() != expected_shmem_name {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::debug!(
"Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed"
);
return;
}
let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() {
write_ipc_token_to_shmem(shmem, token)
.err()
.map(|err| err.to_string())
} else {
Some("shared memory unavailable".to_owned())
};
if let Some(err) = shmem_write_error {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::warn!(
"Failed to restore portable service ipc token after handshake failure: {}",
err
);
return;
}
*runtime_token = Some(token.to_owned());
}
#[inline]
fn schedule_starting_timeout_reset(launch_token: u64) {
std::thread::spawn(move || {
std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT);
let should_reset = {
// Guard against stale watchdogs from previous launches:
// only the watchdog that matches the latest STARTING_TOKEN may reset STARTING.
let current_token = STARTING_TOKEN.load(Ordering::SeqCst);
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let starting = { *STARTING.lock().unwrap() };
let running = { *RUNNING.lock().unwrap() };
current_token == launch_token && starting && !running
};
if should_reset {
log::warn!(
"Portable service startup timeout before IPC ready, reset STARTING state"
);
*STARTING.lock().unwrap() = false;
}
});
}
// Launch flow summary:
// 1) Prepare/reset runtime shared memory + IPC token.
// 2) Start helper process (direct or logon) with shmem argument.
// 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it.
pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> {
log::info!("start portable service");
let launch_token = {
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let running = { *RUNNING.lock().unwrap() };
let mut starting = STARTING.lock().unwrap();
if *starting && !running && !has_running_portable_service_process() {
log::warn!(
"Detected stale portable service STARTING state without running process, reset it"
);
*starting = false;
}
if *starting || running {
bail!("already running");
}
*starting = true;
STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1
};
let start_result = (|| -> ResultType<()> {
clear_runtime_shmem_state();
let mut shmem_lock = SHMEM.lock().unwrap();
if RUNNING.lock().unwrap().clone() {
bail!("already running");
}
if SHMEM.lock().unwrap().is_none() {
let displays = scrap::Display::all()?;
if displays.is_empty() {
bail!("no display available!");
@@ -1031,153 +558,84 @@ pub mod client {
}
}
}
let shmem_size =
utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN);
let shmem_name = next_portable_service_shmem_name();
if !is_valid_portable_service_shmem_name(&shmem_name) {
bail!("Generated invalid portable service shared memory name");
}
let ipc_token = ipc::generate_one_time_ipc_token()?;
let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align);
// os error 112, no enough space
*shmem_lock = Some(crate::portable_service::SharedMemory::create(
&shmem_name,
*SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create(
crate::portable_service::SHMEM_NAME,
shmem_size,
)?);
*SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name);
shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory);
let shmem_name = SHMEM_RUNTIME_NAME
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?;
let init_token_result = if let Some(shmem) = shmem_lock.as_mut() {
unsafe {
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
write_ipc_token_to_shmem(shmem, &ipc_token)
} else {
Ok(())
};
if let Err(e) = init_token_result {
drop(shmem_lock);
clear_runtime_shmem_state();
bail!(
"Failed to initialize portable service ipc token in shared memory: {}",
e
);
};
drop(shmem_lock);
set_runtime_ipc_token(ipc_token.clone());
let portable_service_arg = format!(
"--portable-service {}",
crate::portable_service::portable_service_shmem_arg(&shmem_name)
);
{
let _sender = SENDER.lock().unwrap();
}
match para {
StartPara::Direct => {
match crate::platform::run_background(
&std::env::current_exe()?.to_string_lossy().to_string(),
&portable_service_arg,
) {
Ok(true) => {}
Ok(false) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process");
}
Err(e) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
StartPara::Logon(username, password) => {
#[allow(unused_mut)]
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
#[cfg(feature = "flutter")]
{
if let Some(dir) = Path::new(&exe).parent() {
if let Err(err) = set_path_permission(
Path::new(dir),
FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0,
) {
clear_runtime_shmem_state();
bail!("Failed to set permission of {:?}: {}", dir, err);
}
}
}
#[cfg(not(feature = "flutter"))]
if let Some((dir, dst)) =
crate::platform::windows::portable_service_logon_helper_paths()
{
let cleanup_helper_artifacts = || {
if Path::new(&exe) != dst {
std::fs::remove_file(&dst).ok();
}
std::fs::remove_dir(&dir).ok();
};
let mut use_logon_helper_exe = false;
if let Err(err) = std::fs::create_dir_all(&dir) {
log::warn!(
"Failed to create portable service logon helper dir {:?}: {}",
dir,
err
);
} else if let Err(err) = std::fs::copy(&exe, &dst) {
log::warn!(
"Failed to copy portable service logon helper binary from '{}' to {:?}: {}",
exe,
dst,
err
);
cleanup_helper_artifacts();
} else if !dst.exists() {
log::warn!(
"Portable service logon helper binary missing after copy: {:?}",
dst
);
cleanup_helper_artifacts();
} else if let Err(err) =
set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0)
{
log::warn!(
"Failed to set portable service logon helper path permission for {:?}: {}",
dir,
err
);
cleanup_helper_artifacts();
} else {
use_logon_helper_exe = true;
}
if use_logon_helper_exe {
exe = dst.to_string_lossy().to_string();
}
}
if let Err(e) = crate::platform::windows::create_process_with_logon(
username.as_str(),
password.as_str(),
&exe,
&portable_service_arg,
) {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
schedule_starting_timeout_reset(launch_token);
Ok(())
})();
if start_result.is_err() {
*STARTING.lock().unwrap() = false;
}
start_result
if let Some(shmem) = SHMEM.lock().unwrap().as_mut() {
unsafe {
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
}
match para {
StartPara::Direct => {
if let Err(e) = crate::platform::run_background(
&std::env::current_exe()?.to_string_lossy().to_string(),
"--portable-service",
) {
*SHMEM.lock().unwrap() = None;
bail!("Failed to run portable service process: {}", e);
}
}
StartPara::Logon(username, password) => {
#[allow(unused_mut)]
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
#[cfg(feature = "flutter")]
{
if let Some(dir) = Path::new(&exe).parent() {
if set_path_permission(Path::new(dir), "RX").is_err() {
*SHMEM.lock().unwrap() = None;
bail!("Failed to set permission of {:?}", dir);
}
}
}
#[cfg(not(feature = "flutter"))]
match hbb_common::directories_next::UserDirs::new() {
Some(user_dir) => {
let dir = user_dir
.home_dir()
.join("AppData")
.join("Local")
.join("rustdesk-sciter");
if std::fs::create_dir_all(&dir).is_ok() {
let dst = dir.join("rustdesk.exe");
if std::fs::copy(&exe, &dst).is_ok() {
if dst.exists() {
if set_path_permission(&dir, "RX").is_ok() {
exe = dst.to_string_lossy().to_string();
}
}
}
}
}
None => {}
}
if let Err(e) = crate::platform::windows::create_process_with_logon(
username.as_str(),
password.as_str(),
&exe,
"--portable-service",
) {
*SHMEM.lock().unwrap() = None;
bail!("Failed to run portable service process: {}", e);
}
}
}
let _sender = SENDER.lock().unwrap();
Ok(())
}
pub extern "C" fn drop_portable_service_shared_memory() {
// https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout
// Please make sure there is no print in the call stack
clear_runtime_shmem_state();
let mut lock = SHMEM.lock().unwrap();
if lock.is_some() {
*lock = None;
}
}
pub fn set_quick_support(v: bool) {
@@ -1197,11 +655,7 @@ pub mod client {
let mut option = SHMEM.lock().unwrap();
if let Some(shmem) = option.as_mut() {
unsafe {
libc::memset(
shmem.as_ptr().add(ADDR_CURSOR_PARA) as _,
0,
shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _,
);
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
utils::set_para(
shmem,
@@ -1248,19 +702,6 @@ pub mod client {
if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) {
let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO);
let frame_info = frame_info_ptr as *const FrameInfo;
let frame_len = (*frame_info).length;
if !is_valid_capture_frame_length(shmem.len(), frame_len) {
log::error!(
"Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}",
frame_len,
shmem.len(),
ADDR_CAPTURE_FRAME
);
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid portable service frame length".to_string(),
));
}
if (*frame_info).width != self.width || (*frame_info).height != self.height {
log::info!(
"skip frame, ({},{}) != ({},{})",
@@ -1275,7 +716,7 @@ pub mod client {
));
}
let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
let data = slice::from_raw_parts(frame_ptr, frame_len);
let data = slice::from_raw_parts(frame_ptr, (*frame_info).length);
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
data,
self.width,
@@ -1337,49 +778,10 @@ pub mod client {
Some(result) = incoming.next() => {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
if !ipc::authorize_windows_portable_service_ipc_connection(
&stream, postfix,
) {
continue;
}
let mut consumed_token: Option<String> = None;
let mut consumed_token_shmem_name: Option<String> = None;
let handshake_result =
ipc::portable_service_ipc_handshake_as_server(
&mut stream,
|token| {
let (matched, matched_shmem_name) =
consume_runtime_ipc_token_if_match(token);
if matched {
consumed_token = Some(token.to_owned());
consumed_token_shmem_name = matched_shmem_name;
true
} else {
false
}
},
)
.await;
if let Err(err) = handshake_result {
if let Some(token) = consumed_token.as_deref() {
restore_runtime_ipc_token_after_failed_handshake(
token,
consumed_token_shmem_name.as_deref(),
);
*STARTING.lock().unwrap() = false;
}
log::warn!(
"Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}",
postfix,
err
);
continue;
}
log::info!("Got portable service ipc connection");
let rx_clone = rx.clone();
tokio::spawn(async move {
let mut stream = stream;
let mut stream = Connection::new(stream);
let postfix = postfix.to_owned();
let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
@@ -1403,7 +805,6 @@ pub mod client {
Pong => {
nack = 0;
*RUNNING.lock().unwrap() = true;
*STARTING.lock().unwrap() = false;
},
ConnCount(None) => {
if !quick_support {
@@ -1440,7 +841,6 @@ pub mod client {
}
}
*RUNNING.lock().unwrap() = false;
*STARTING.lock().unwrap() = false;
});
}
Err(err) => {
@@ -1590,23 +990,3 @@ pub struct FrameInfo {
width: usize,
height: usize,
}
#[cfg(test)]
mod tests {
use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME};
#[test]
fn test_is_valid_capture_frame_length_rejects_zero_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0));
}
#[test]
fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17));
}
#[test]
fn test_is_valid_capture_frame_length_accepts_in_bounds_length() {
assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16));
}
}

View File

@@ -185,13 +185,9 @@ pub mod client {
pub mod service {
use super::*;
use hbb_common::lazy_static;
#[cfg(target_os = "linux")]
use parity_tokio_ipc::Connection as RawIpcConnection;
use scrap::wayland::{
pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop,
};
#[cfg(target_os = "linux")]
use std::os::unix::io::AsRawFd;
use std::{collections::HashMap, sync::Mutex};
lazy_static::lazy_static! {
@@ -606,10 +602,7 @@ pub mod service {
}
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
if *code < 8 {
log::error!(
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
code
);
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
allow_err!(keyboard.emit(&[down_event]));
@@ -617,10 +610,7 @@ pub mod service {
}
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
if *code < 8 {
log::error!(
"Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping",
code
);
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event]));
@@ -919,35 +909,6 @@ pub mod service {
});
}
#[cfg(target_os = "linux")]
fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool {
if !hbb_common::config::is_service_ipc_postfix(postfix) {
return true;
}
let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd());
let active_uid = crate::platform::linux::get_active_userid_fresh()
.trim()
.parse::<u32>()
.ok();
let authorized =
peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid));
if !authorized {
crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid);
return false;
}
if let Err(err) =
ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix)
{
log::warn!(
"Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}",
postfix,
err
);
return false;
}
true
}
/// Start uinput service.
async fn start_service<F: FnOnce(ipc::Connection) + Copy>(postfix: &str, handler: F) {
match new_listener(postfix).await {
@@ -955,10 +916,6 @@ pub mod service {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
#[cfg(target_os = "linux")]
if !authorize_uinput_peer(postfix, &stream) {
continue;
}
log::debug!("Got new connection of uinput ipc {}", postfix);
handler(Connection::new(stream));
}