Skip to main content

running_process/broker/server/handoff/
windows.rs

1//! Windows `DuplicateHandle` handoff transport model.
2//!
3//! This module owns the broker-side `DuplicateHandle` call used to pass an
4//! already-accepted client pipe into a backend process. The backend still has
5//! to verify the one-time token before adopting the connection; failures map
6//! into the existing silent reconnect fallback policy.
7
8use super::{
9    HandoffAttemptDecision, HandoffAttemptFailure, HandoffFallbackDecision, HandoffFallbackReason,
10    HandoffToken,
11};
12
13/// Whether this build target can eventually use the Windows handoff transport.
14pub const DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool = cfg!(windows);
15
16/// Opaque raw Windows handle value held by the broker or duplicated into a backend.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub struct WindowsHandleValue(usize);
19
20impl WindowsHandleValue {
21    /// Build an opaque handle value for transport bookkeeping.
22    pub fn new(value: usize) -> Self {
23        Self(value)
24    }
25
26    /// Return the raw opaque handle value.
27    pub fn get(self) -> usize {
28        self.0
29    }
30
31    #[cfg(windows)]
32    fn from_handle(handle: windows_sys::Win32::Foundation::HANDLE) -> Self {
33        Self(handle as usize)
34    }
35
36    #[cfg(windows)]
37    fn as_handle(self) -> windows_sys::Win32::Foundation::HANDLE {
38        self.0 as windows_sys::Win32::Foundation::HANDLE
39    }
40}
41
42/// Inputs for one future `DuplicateHandle` attempt.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct DuplicateHandleAttempt {
45    /// Broker-owned pipe handle to duplicate.
46    pub pipe_handle: WindowsHandleValue,
47    /// Backend process ID that should receive the duplicated handle.
48    pub backend_pid: u32,
49    /// One-time token associated with this handoff attempt.
50    pub handoff_token: HandoffToken,
51}
52
53impl DuplicateHandleAttempt {
54    /// Build typed inputs for one `DuplicateHandle` attempt.
55    pub fn new(
56        pipe_handle: WindowsHandleValue,
57        backend_pid: u32,
58        handoff_token: HandoffToken,
59    ) -> Self {
60        Self {
61            pipe_handle,
62            backend_pid,
63            handoff_token,
64        }
65    }
66}
67
68/// Successful `DuplicateHandle` outcome once real handle passing is wired.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct DuplicateHandleSuccess {
71    /// Handle value duplicated into the backend process.
72    pub duplicated_handle: WindowsHandleValue,
73    /// Backend process ID that received the duplicated handle.
74    pub backend_pid: u32,
75    /// One-time token paired with the duplicated handle.
76    pub handoff_token: HandoffToken,
77}
78
79impl DuplicateHandleSuccess {
80    /// Build a typed successful handoff result.
81    pub fn new(
82        duplicated_handle: WindowsHandleValue,
83        backend_pid: u32,
84        handoff_token: HandoffToken,
85    ) -> Self {
86        Self {
87            duplicated_handle,
88            backend_pid,
89            handoff_token,
90        }
91    }
92}
93
94/// Result returned by the future Windows transport.
95pub type DuplicateHandleResult = Result<DuplicateHandleSuccess, DuplicateHandleError>;
96
97/// Try to duplicate the broker-held pipe handle into the backend process.
98///
99/// The returned handle value is valid in the backend process handle table.
100/// Callers must still deliver the paired [`HandoffToken`] over the
101/// broker-to-backend control channel and wait for backend acknowledgement
102/// before reporting handoff success to the client.
103pub fn try_duplicate_handle(attempt: &DuplicateHandleAttempt) -> DuplicateHandleResult {
104    platform_try_duplicate_handle(attempt)
105}
106
107/// Failure from a future `DuplicateHandle` handoff attempt.
108#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
109pub enum DuplicateHandleError {
110    /// The current target cannot use the Windows handoff transport.
111    #[error("DuplicateHandle handoff transport is unsupported on this platform")]
112    UnsupportedPlatform,
113    /// Opening the backend process for `PROCESS_DUP_HANDLE` failed.
114    #[error("cannot open backend process {backend_pid} for DuplicateHandle")]
115    CannotOpenBackend {
116        /// Backend process ID that could not be opened.
117        backend_pid: u32,
118    },
119    /// The platform denied handle duplication.
120    #[error("permission denied duplicating handle into backend process {backend_pid}")]
121    PermissionDenied {
122        /// Backend process ID targeted by the handoff.
123        backend_pid: u32,
124    },
125    /// `DuplicateHandle` failed after the backend process was opened.
126    #[error("DuplicateHandle failed for backend process {backend_pid}")]
127    DuplicateFailed {
128        /// Backend process ID targeted by the handoff.
129        backend_pid: u32,
130        /// Raw Windows error code returned by the platform, when available.
131        raw_os_error: Option<i32>,
132    },
133    /// The broker and backend trust or integrity levels are incompatible.
134    #[error("integrity mismatch duplicating handle into backend process {backend_pid}")]
135    IntegrityMismatch {
136        /// Backend process ID targeted by the handoff.
137        backend_pid: u32,
138    },
139    /// The backend did not acknowledge the duplicated handle before the deadline.
140    #[error("backend process {backend_pid} did not acknowledge duplicated handle")]
141    BackendAckTimeout {
142        /// Backend process ID targeted by the handoff.
143        backend_pid: u32,
144    },
145}
146
147#[cfg(windows)]
148fn platform_try_duplicate_handle(attempt: &DuplicateHandleAttempt) -> DuplicateHandleResult {
149    use windows_sys::Win32::Foundation::{
150        CloseHandle, DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE,
151    };
152    use windows_sys::Win32::System::Threading::{
153        GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE,
154    };
155
156    let backend_process = unsafe { OpenProcess(PROCESS_DUP_HANDLE, 0, attempt.backend_pid) };
157    if is_invalid_handle(backend_process) {
158        return Err(open_process_error(attempt.backend_pid));
159    }
160
161    let mut duplicated: HANDLE = std::ptr::null_mut();
162    let ok = unsafe {
163        DuplicateHandle(
164            GetCurrentProcess(),
165            attempt.pipe_handle.as_handle(),
166            backend_process,
167            &mut duplicated,
168            0,
169            0,
170            DUPLICATE_SAME_ACCESS,
171        )
172    };
173    let duplicate_error = std::io::Error::last_os_error();
174    unsafe {
175        CloseHandle(backend_process);
176    }
177
178    if ok == 0 || is_invalid_handle(duplicated) {
179        return Err(duplicate_handle_error(
180            attempt.backend_pid,
181            duplicate_error.raw_os_error(),
182        ));
183    }
184
185    Ok(DuplicateHandleSuccess::new(
186        WindowsHandleValue::from_handle(duplicated),
187        attempt.backend_pid,
188        attempt.handoff_token,
189    ))
190}
191
192#[cfg(not(windows))]
193fn platform_try_duplicate_handle(_attempt: &DuplicateHandleAttempt) -> DuplicateHandleResult {
194    Err(DuplicateHandleError::UnsupportedPlatform)
195}
196
197#[cfg(windows)]
198fn is_invalid_handle(handle: windows_sys::Win32::Foundation::HANDLE) -> bool {
199    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
200
201    handle.is_null() || handle == INVALID_HANDLE_VALUE
202}
203
204#[cfg(windows)]
205fn open_process_error(backend_pid: u32) -> DuplicateHandleError {
206    use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED;
207
208    if std::io::Error::last_os_error().raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) {
209        DuplicateHandleError::PermissionDenied { backend_pid }
210    } else {
211        DuplicateHandleError::CannotOpenBackend { backend_pid }
212    }
213}
214
215#[cfg(windows)]
216fn duplicate_handle_error(backend_pid: u32, raw_os_error: Option<i32>) -> DuplicateHandleError {
217    use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED;
218
219    if raw_os_error == Some(ERROR_ACCESS_DENIED as i32) {
220        DuplicateHandleError::PermissionDenied { backend_pid }
221    } else {
222        DuplicateHandleError::DuplicateFailed {
223            backend_pid,
224            raw_os_error,
225        }
226    }
227}
228
229#[cfg(all(test, windows))]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn duplicate_handle_into_current_process_returns_backend_owned_handle() {
235        use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
236        use windows_sys::Win32::System::Threading::GetCurrentProcess;
237
238        let token = HandoffToken::from_bytes([7; 16]);
239        let attempt = DuplicateHandleAttempt::new(
240            WindowsHandleValue::new(unsafe { GetCurrentProcess() } as usize),
241            std::process::id(),
242            token,
243        );
244
245        let success = try_duplicate_handle(&attempt).unwrap();
246
247        assert_eq!(success.backend_pid, std::process::id());
248        assert_eq!(success.handoff_token, token);
249        assert_ne!(success.duplicated_handle.get(), 0);
250        assert_ne!(
251            success.duplicated_handle.get(),
252            INVALID_HANDLE_VALUE as usize
253        );
254
255        unsafe {
256            CloseHandle(success.duplicated_handle.get() as HANDLE);
257        }
258    }
259
260    #[test]
261    fn missing_backend_pid_maps_to_fallback_safe_error() {
262        let attempt = DuplicateHandleAttempt::new(
263            WindowsHandleValue::new(unsafe {
264                windows_sys::Win32::System::Threading::GetCurrentProcess()
265            } as usize),
266            u32::MAX,
267            HandoffToken::from_bytes([9; 16]),
268        );
269
270        let err = try_duplicate_handle(&attempt).unwrap_err();
271
272        assert!(matches!(
273            err,
274            DuplicateHandleError::CannotOpenBackend { .. }
275                | DuplicateHandleError::PermissionDenied { .. }
276        ));
277        assert!(err.is_fallback_safe());
278    }
279}
280
281impl DuplicateHandleError {
282    /// Return the existing attempt-failure classification, when this was a real attempt.
283    pub fn attempt_failure(&self) -> Option<HandoffAttemptFailure> {
284        match self {
285            Self::UnsupportedPlatform => None,
286            Self::CannotOpenBackend { .. }
287            | Self::PermissionDenied { .. }
288            | Self::DuplicateFailed { .. } => Some(HandoffAttemptFailure::PermissionDenied),
289            Self::IntegrityMismatch { .. } => Some(HandoffAttemptFailure::IntegrityMismatch),
290            Self::BackendAckTimeout { .. } => Some(HandoffAttemptFailure::BackendAckTimeout),
291        }
292    }
293
294    /// Map this transport failure into the existing fallback reason vocabulary.
295    pub fn fallback_reason(&self) -> HandoffFallbackReason {
296        match self.attempt_failure() {
297            Some(failure) => failure.into(),
298            None => HandoffFallbackReason::ServicePolicyDisabled,
299        }
300    }
301
302    /// Return the silent reconnect fallback for this transport failure.
303    pub fn fallback_decision(&self) -> HandoffFallbackDecision {
304        HandoffFallbackDecision::new(self.fallback_reason())
305    }
306
307    /// Return the full attempt decision for callers that operate on broker decisions.
308    pub fn fallback_attempt_decision(&self) -> HandoffAttemptDecision {
309        HandoffAttemptDecision::FallbackToReconnect(self.fallback_decision())
310    }
311
312    /// Return true when this error is safe to hide behind reconnect fallback.
313    pub fn is_fallback_safe(&self) -> bool {
314        let fallback = self.fallback_decision();
315        fallback.uses_backend_reconnect() && !fallback.sends_client_error()
316    }
317}