Skip to content

Commit e02be12

Browse files
authored
Merge pull request #5 from scryner/feat/hardening-driver
Harden CrossOver driver startup and install flow
2 parents 5953a47 + 8976589 commit e02be12

13 files changed

Lines changed: 279 additions & 231 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ members = [
88
resolver = "2"
99

1010
[workspace.package]
11-
version = "0.3.0"
11+
version = "0.3.1-rc0"
1212
edition = "2021"
1313
license = "Apache-2.0"
1414
authors = ["Seonghwan Jeong <scryner@gmail.com>"]

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,18 @@ install/update/uninstall automatically, and install/update/repair import the
188188
Wine loader override registry file as described above. The shell installer
189189
below is mainly for manual development and smoke testing.
190190

191-
Install the driver next to `Steam.exe` in the target bottle and import the Wine
192-
loader override:
191+
Copy the driver next to `Steam.exe` in the target bottle:
193192

194193
```sh
195194
tools/install-driver.sh --bottle Steam
196195
```
197196

197+
To also import Wine's `hid=native,builtin` loader override:
198+
199+
```sh
200+
tools/install-driver.sh --bottle Steam --override-dll
201+
```
202+
198203
Useful options:
199204

200205
```sh
@@ -205,11 +210,11 @@ tools/install-driver.sh \
205210
```
206211

207212
The script copies `hid.dll`, backs up any existing local `hid.dll`, and
208-
initializes `crosspuck-driver.log`. It also writes
209-
`crosspuck-wine-override.reg` into the bottle and imports it with the matching
210-
CrossOver app, using CrossOver Preview first for preview-marked bottles and
211-
CrossOver first for regular bottles. It does not set guest runtime options or
212-
rewrite `user.reg` directly.
213+
initializes `crosspuck-driver.log`. By default it does not call CrossOver
214+
`regedit`. With `--override-dll`, it writes `crosspuck-wine-override.reg` into
215+
the bottle and imports it with the matching CrossOver app, using CrossOver
216+
Preview first for preview-marked bottles and CrossOver first for regular
217+
bottles. It does not set guest runtime options or rewrite `user.reg` directly.
213218

214219
Do not install this DLL into `drive_c/windows/system32`. It is designed to live
215220
next to Steam and forward non-virtual HID calls to the real system HID DLL.

crates/crosspuck-app/src/hid_backend.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use crosspuck_core::hid::{
33
HidFilter, HidSnapshotError, PuckSnapshot,
44
};
55
use crosspuck_core::protocol::{
6-
session_trace_label, CollectionRole, FeatureResult, GetFeature, IdentityPayload, SetFeature,
7-
SetFeatureResult, SetOutput, SetOutputResult, StatusCode, WriteReport, WriteResult,
6+
session_trace_label, CollectionRole, FeatureResult, GetFeature, SetFeature, SetFeatureResult,
7+
SetOutput, SetOutputResult, StatusCode, WriteReport, WriteResult,
88
};
99
use std::collections::BTreeMap;
1010
use std::fmt;
@@ -51,7 +51,6 @@ fn log_host_guest_warning(args: std::fmt::Arguments<'_>) {
5151
}
5252

5353
pub(crate) trait HostBackend: Send + Sync {
54-
fn identity(&self) -> &IdentityPayload;
5554
fn open_input_reader(&self) -> Result<Box<dyn InputReportReader>, HostHidError>;
5655
fn get_feature(&self, request: &GetFeature) -> FeatureResult;
5756
fn set_feature(&self, request: &SetFeature) -> SetFeatureResult;
@@ -79,7 +78,6 @@ pub(crate) struct HostInputReport {
7978
#[derive(Clone, Debug)]
8079
pub(crate) struct RealHostBackend {
8180
snapshot: Arc<Mutex<PuckSnapshot>>,
82-
identity: IdentityPayload,
8381
main: SharedMainDevice,
8482
interface_devices: Arc<Mutex<BTreeMap<u8, CachedInterfaceDevice>>>,
8583
}
@@ -98,11 +96,10 @@ struct CachedInterfaceDevice {
9896
}
9997

10098
impl RealHostBackend {
101-
pub fn new(snapshot: PuckSnapshot, identity: IdentityPayload) -> Result<Self, HostHidError> {
99+
pub fn new(snapshot: PuckSnapshot) -> Result<Self, HostHidError> {
102100
let main = Self::open_main_device(&snapshot)?;
103101
Ok(Self {
104102
snapshot: Arc::new(Mutex::new(snapshot)),
105-
identity,
106103
main,
107104
interface_devices: Arc::new(Mutex::new(BTreeMap::new())),
108105
})
@@ -427,10 +424,6 @@ impl RealHostBackend {
427424
}
428425

429426
impl HostBackend for RealHostBackend {
430-
fn identity(&self) -> &IdentityPayload {
431-
&self.identity
432-
}
433-
434427
fn open_input_reader(&self) -> Result<Box<dyn InputReportReader>, HostHidError> {
435428
let readers = self.open_input_devices();
436429
if readers.is_empty() {
@@ -1006,11 +999,19 @@ impl fmt::Display for HostHidError {
1006999
write!(f, "invalid HID interface number: {interface_number}")
10071000
}
10081001
Self::DeviceLockPoisoned => write!(f, "HID device lock poisoned"),
1002+
Self::Hid(error) if is_macos_hid_permission_error(error) => write!(
1003+
f,
1004+
"{error}; macOS denied HID access. Run CrossPuck as an app bundle and enable Privacy & Security > Input Monitoring for CrossPuck."
1005+
),
10091006
Self::Hid(error) => write!(f, "{error}"),
10101007
}
10111008
}
10121009
}
10131010

1011+
fn is_macos_hid_permission_error(error: &HidSnapshotError) -> bool {
1012+
matches!(error, HidSnapshotError::Hid(_)) && error.to_string().contains("0xE00002E2")
1013+
}
1014+
10141015
impl std::error::Error for HostHidError {
10151016
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
10161017
match self {

crates/crosspuck-app/src/runtime.rs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -350,23 +350,33 @@ fn handle_session(
350350
}
351351
};
352352
let identity = IdentityPayload::try_from(&snapshot)?;
353-
let backend = Arc::new(with_worker_autorelease_pool(|| {
354-
RealHostBackend::new(snapshot, identity)
355-
})?);
356-
handle_session_with_backend(
357-
listeners,
358-
control,
359-
SessionStart {
360-
session_id,
361-
session_trace_id,
362-
guest_runtime_overrides,
363-
hello_request_id: hello_frame.header.id,
364-
guest_pid: hello.guest_pid,
365-
},
366-
app_state,
367-
backend,
368-
stop,
369-
)
353+
let session = SessionStart {
354+
session_id,
355+
session_trace_id,
356+
guest_runtime_overrides,
357+
hello_request_id: hello_frame.header.id,
358+
guest_pid: hello.guest_pid,
359+
};
360+
app_state.set(HostRuntimeState::PuckConnected {
361+
serial: identity.serial.clone(),
362+
});
363+
let backend = match with_worker_autorelease_pool(|| RealHostBackend::new(snapshot)) {
364+
Ok(backend) => Arc::new(backend),
365+
Err(error) => {
366+
let hello_ok = hello_ok_with_status(
367+
StatusCode::DeviceDisconnected,
368+
session_id,
369+
session_trace_id,
370+
identity.default_input_report_len(),
371+
);
372+
write_payload(control, hello_frame.header.id, &hello_ok)?;
373+
return Err(error.into());
374+
}
375+
};
376+
write_session_preamble(control, &session, &identity)?;
377+
let input =
378+
attach_input_for_session(listeners, stop, INPUT_ATTACH_TIMEOUT, &session, &identity)?;
379+
run_attached_session(control, input, session, app_state, backend, stop, identity)
370380
}
371381

372382
#[derive(Clone, Debug)]
@@ -378,11 +388,13 @@ struct SessionStart {
378388
guest_pid: u32,
379389
}
380390

391+
#[cfg(test)]
381392
fn handle_session_with_backend(
382393
listeners: &TransportListeners,
383394
control: &mut ChannelStream,
384395
session: SessionStart,
385396
app_state: &AppState,
397+
identity: IdentityPayload,
386398
backend: Arc<dyn HostBackend>,
387399
stop: &Arc<AtomicBool>,
388400
) -> Result<(), RuntimeError> {
@@ -391,26 +403,39 @@ fn handle_session_with_backend(
391403
control,
392404
session,
393405
app_state,
406+
identity,
394407
backend,
395408
stop,
396409
INPUT_ATTACH_TIMEOUT,
397410
)
398411
}
399412

413+
#[cfg(test)]
400414
fn handle_session_with_backend_timeout(
401415
listeners: &TransportListeners,
402416
control: &mut ChannelStream,
403417
session: SessionStart,
404418
app_state: &AppState,
419+
identity: IdentityPayload,
405420
backend: Arc<dyn HostBackend>,
406421
stop: &Arc<AtomicBool>,
407422
input_attach_timeout: Duration,
408423
) -> Result<(), RuntimeError> {
409-
let identity = backend.identity().clone();
410424
app_state.set(HostRuntimeState::PuckConnected {
411425
serial: identity.serial.clone(),
412426
});
413427

428+
write_session_preamble(control, &session, &identity)?;
429+
let input =
430+
attach_input_for_session(listeners, stop, input_attach_timeout, &session, &identity)?;
431+
run_attached_session(control, input, session, app_state, backend, stop, identity)
432+
}
433+
434+
fn write_session_preamble(
435+
control: &mut ChannelStream,
436+
session: &SessionStart,
437+
identity: &IdentityPayload,
438+
) -> Result<(), RuntimeError> {
414439
write_payload(
415440
control,
416441
session.hello_request_id,
@@ -428,8 +453,17 @@ fn handle_session_with_backend_timeout(
428453
log_level.as_str()
429454
);
430455
}
431-
write_payload(control, 0, &identity)?;
456+
write_payload(control, 0, identity)?;
457+
Ok(())
458+
}
432459

460+
fn attach_input_for_session(
461+
listeners: &TransportListeners,
462+
stop: &Arc<AtomicBool>,
463+
input_attach_timeout: Duration,
464+
session: &SessionStart,
465+
identity: &IdentityPayload,
466+
) -> Result<ChannelStream, RuntimeError> {
433467
let mut input = accept_input_for_session(listeners, stop, input_attach_timeout)?;
434468
input.set_write_timeout(Some(Duration::from_millis(250)))?;
435469
input.set_read_timeout(Some(Duration::from_millis(250)))?;
@@ -461,7 +495,18 @@ fn handle_session_with_backend_timeout(
461495
actual: attach.session_id,
462496
});
463497
}
498+
Ok(input)
499+
}
464500

501+
fn run_attached_session(
502+
control: &mut ChannelStream,
503+
input: ChannelStream,
504+
session: SessionStart,
505+
app_state: &AppState,
506+
backend: Arc<dyn HostBackend>,
507+
stop: &Arc<AtomicBool>,
508+
identity: IdentityPayload,
509+
) -> Result<(), RuntimeError> {
465510
app_state.set(HostRuntimeState::GuestConnected {
466511
session_id: session.session_id,
467512
session_trace_id: session.session_trace_id,
@@ -812,7 +857,6 @@ mod tests {
812857

813858
#[derive(Clone)]
814859
struct FakeBackend {
815-
identity: IdentityPayload,
816860
reports: Arc<Mutex<VecDeque<Vec<u8>>>>,
817861
operations: Arc<Mutex<Vec<String>>>,
818862
cleanup_called: Arc<AtomicBool>,
@@ -821,7 +865,6 @@ mod tests {
821865
impl FakeBackend {
822866
fn new(reports: Vec<Vec<u8>>) -> Self {
823867
Self {
824-
identity: test_identity(),
825868
reports: Arc::new(Mutex::new(reports.into())),
826869
operations: Arc::new(Mutex::new(Vec::new())),
827870
cleanup_called: Arc::new(AtomicBool::new(false)),
@@ -842,10 +885,6 @@ mod tests {
842885
}
843886

844887
impl HostBackend for FakeBackend {
845-
fn identity(&self) -> &IdentityPayload {
846-
&self.identity
847-
}
848-
849888
fn open_input_reader(&self) -> Result<Box<dyn InputReportReader>, HostHidError> {
850889
Ok(Box::new(FakeInputReader {
851890
reports: Arc::clone(&self.reports),
@@ -949,6 +988,7 @@ mod tests {
949988
guest_pid: 1234,
950989
},
951990
&AppState::new(),
991+
test_identity(),
952992
Arc::new(backend_for_server),
953993
&server_stop,
954994
)
@@ -1034,6 +1074,7 @@ mod tests {
10341074
guest_pid: 1234,
10351075
},
10361076
&AppState::new(),
1077+
test_identity(),
10371078
Arc::new(backend),
10381079
&server_stop,
10391080
)
@@ -1081,6 +1122,7 @@ mod tests {
10811122
guest_pid: 1234,
10821123
},
10831124
&AppState::new(),
1125+
test_identity(),
10841126
Arc::new(backend),
10851127
&server_stop,
10861128
);
@@ -1142,6 +1184,7 @@ mod tests {
11421184
guest_pid: 1234,
11431185
},
11441186
&AppState::new(),
1187+
test_identity(),
11451188
Arc::new(backend),
11461189
&server_stop,
11471190
Duration::from_millis(100),

0 commit comments

Comments
 (0)