Skip to main content

windows_erg/process/
spawn.rs

1//! Process spawning with optional parent reparenting and token-based launch.
2
3use std::borrow::Cow;
4use std::ffi::c_void;
5
6use windows::Win32::Foundation::{
7    CloseHandle, ERROR_NOT_ALL_ASSIGNED, GetLastError, HANDLE, WIN32_ERROR,
8};
9use windows::Win32::Security::{
10    AdjustTokenPrivileges, DuplicateTokenEx, LookupPrivilegeValueW, SE_PRIVILEGE_ENABLED,
11    SecurityImpersonation, TOKEN_ADJUST_PRIVILEGES, TOKEN_ALL_ACCESS, TOKEN_ASSIGN_PRIMARY,
12    TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY, TokenPrimary,
13};
14use windows::Win32::System::Environment::{CreateEnvironmentBlock, DestroyEnvironmentBlock};
15use windows::Win32::System::Threading::{
16    CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT,
17    CreateProcessAsUserW, CreateProcessW, DeleteProcThreadAttributeList,
18    EXTENDED_STARTUPINFO_PRESENT, GetCurrentProcess, InitializeProcThreadAttributeList,
19    LPPROC_THREAD_ATTRIBUTE_LIST, OpenProcess, OpenProcessToken,
20    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, PROCESS_CREATE_PROCESS, PROCESS_INFORMATION,
21    PROCESS_QUERY_LIMITED_INFORMATION, ResumeThread, STARTUPINFOEXW, UpdateProcThreadAttribute,
22};
23use windows::core::PCWSTR;
24
25use super::processes::Process;
26use super::types::{ProcessAccess, ProcessId, ThreadId};
27use crate::error::{Error, InvalidParameterError, ProcessError, ProcessSpawnError, Result};
28use crate::utils::{OwnedHandle, to_utf16_nul};
29
30const DEFAULT_DESKTOP: &str = "winsta0\\default";
31
32fn format_command_line(exe_path: &str, args: &[String]) -> String {
33    if args.is_empty() {
34        format!("\"{exe_path}\"")
35    } else {
36        format!("\"{exe_path}\" {}", args.join(" "))
37    }
38}
39
40fn map_spawn_windows_error(
41    command: &str,
42    reason: impl Into<Cow<'static, str>>,
43    error: &windows::core::Error,
44) -> Error {
45    Error::Process(ProcessError::SpawnFailed(ProcessSpawnError::with_code(
46        Cow::Owned(command.to_string()),
47        reason,
48        error.code().0,
49    )))
50}
51
52fn spawn_error(command: &str, reason: impl Into<Cow<'static, str>>, error_code: i32) -> Error {
53    Error::Process(ProcessError::SpawnFailed(ProcessSpawnError::with_code(
54        Cow::Owned(command.to_string()),
55        reason,
56        error_code,
57    )))
58}
59
60fn close_handle_if_valid(handle: HANDLE) {
61    if !handle.0.is_null() {
62        unsafe {
63            let _ = CloseHandle(handle);
64        }
65    }
66}
67
68struct EnvBlock(*mut c_void);
69
70impl EnvBlock {
71    fn as_ptr(&self) -> *mut c_void {
72        self.0
73    }
74
75    fn is_null(&self) -> bool {
76        self.0.is_null()
77    }
78}
79
80impl Drop for EnvBlock {
81    fn drop(&mut self) {
82        if !self.0.is_null() {
83            unsafe {
84                let _ = DestroyEnvironmentBlock(self.0);
85            }
86        }
87    }
88}
89
90struct AttributeList {
91    buffer: Vec<u8>,
92    ptr: LPPROC_THREAD_ATTRIBUTE_LIST,
93    initialized: bool,
94}
95
96impl AttributeList {
97    fn with_parent(parent_handle: &HANDLE) -> Result<Self> {
98        let mut size = 0;
99        unsafe {
100            let _ = InitializeProcThreadAttributeList(
101                LPPROC_THREAD_ATTRIBUTE_LIST(std::ptr::null_mut()),
102                1,
103                0,
104                &mut size,
105            );
106        }
107
108        let mut buffer = vec![0u8; size];
109        let ptr = LPPROC_THREAD_ATTRIBUTE_LIST(buffer.as_mut_ptr() as *mut _);
110
111        unsafe { InitializeProcThreadAttributeList(ptr, 1, 0, &mut size) }.map_err(|e| {
112            Error::Process(ProcessError::SpawnFailed(ProcessSpawnError::with_code(
113                "<attribute_list>",
114                "Failed to initialize process attribute list",
115                e.code().0,
116            )))
117        })?;
118
119        let parent_ptr = std::ptr::addr_of!(parent_handle.0);
120
121        if let Err(e) = unsafe {
122            UpdateProcThreadAttribute(
123                ptr,
124                0,
125                PROC_THREAD_ATTRIBUTE_PARENT_PROCESS as usize,
126                Some(parent_ptr as *const _ as *mut _),
127                std::mem::size_of::<HANDLE>(),
128                None,
129                None,
130            )
131        } {
132            unsafe {
133                DeleteProcThreadAttributeList(ptr);
134            }
135            return Err(Error::Process(ProcessError::SpawnFailed(
136                ProcessSpawnError::with_code(
137                    "<attribute_list>",
138                    "Failed to set parent process attribute",
139                    e.code().0,
140                ),
141            )));
142        }
143
144        Ok(Self {
145            buffer,
146            ptr,
147            initialized: true,
148        })
149    }
150
151    fn ptr(&self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
152        self.ptr
153    }
154}
155
156impl Drop for AttributeList {
157    fn drop(&mut self) {
158        if self.initialized {
159            unsafe {
160                DeleteProcThreadAttributeList(self.ptr);
161            }
162        }
163        self.buffer.clear();
164    }
165}
166
167fn get_user_token_from_pid(pid: ProcessId, command: &str) -> Result<OwnedHandle> {
168    let process_handle =
169        unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid.as_u32()) }.map_err(
170            |e| map_spawn_windows_error(command, "Failed to open token source process", &e),
171        )?;
172
173    let process_handle = OwnedHandle::new(process_handle);
174
175    let mut token = HANDLE(std::ptr::null_mut());
176    unsafe {
177        OpenProcessToken(
178            process_handle.raw(),
179            TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
180            &mut token,
181        )
182    }
183    .map_err(|e| map_spawn_windows_error(command, "Failed to open process token", &e))?;
184
185    let token = OwnedHandle::new(token);
186
187    let mut duplicated = HANDLE(std::ptr::null_mut());
188    unsafe {
189        DuplicateTokenEx(
190            token.raw(),
191            TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS | TOKEN_QUERY | TOKEN_DUPLICATE,
192            None,
193            SecurityImpersonation,
194            TokenPrimary,
195            &mut duplicated,
196        )
197    }
198    .map_err(|e| map_spawn_windows_error(command, "Failed to duplicate process token", &e))?;
199
200    Ok(OwnedHandle::new(duplicated))
201}
202
203fn enable_privilege(privilege_name: &str, command: &str) -> Result<()> {
204    let mut token = HANDLE(std::ptr::null_mut());
205    unsafe {
206        OpenProcessToken(
207            GetCurrentProcess(),
208            TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
209            &mut token,
210        )
211    }
212    .map_err(|e| map_spawn_windows_error(command, "Failed to open current process token", &e))?;
213
214    let token = OwnedHandle::new(token);
215    let mut token_privileges = TOKEN_PRIVILEGES {
216        PrivilegeCount: 1,
217        Privileges: Default::default(),
218    };
219
220    let privilege_wide = to_utf16_nul(privilege_name);
221    unsafe {
222        LookupPrivilegeValueW(
223            None,
224            PCWSTR(privilege_wide.as_ptr()),
225            &mut token_privileges.Privileges[0].Luid,
226        )
227    }
228    .map_err(|e| map_spawn_windows_error(command, "Failed to lookup privilege LUID", &e))?;
229
230    token_privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
231
232    unsafe { AdjustTokenPrivileges(token.raw(), false, Some(&token_privileges), 0, None, None) }
233        .map_err(|e| map_spawn_windows_error(command, "Failed to adjust token privileges", &e))?;
234
235    let last_error: WIN32_ERROR = unsafe { GetLastError() };
236    if last_error == ERROR_NOT_ALL_ASSIGNED {
237        return Err(spawn_error(
238            command,
239            Cow::Owned(format!(
240                "Privilege '{privilege_name}' was not assigned to current token"
241            )),
242            last_error.0 as i32,
243        ));
244    }
245
246    Ok(())
247}
248
249fn create_env_block(token: HANDLE, command: &str) -> Result<EnvBlock> {
250    let mut env_block: *mut c_void = std::ptr::null_mut();
251    unsafe { CreateEnvironmentBlock(&mut env_block, token, false) }
252        .map_err(|e| map_spawn_windows_error(command, "Failed to create environment block", &e))?;
253    Ok(EnvBlock(env_block))
254}
255
256fn get_userprofile_from_env_block(env_block: *mut c_void) -> Option<Vec<u16>> {
257    if env_block.is_null() {
258        return None;
259    }
260
261    unsafe {
262        let mut ptr = env_block as *const u16;
263        while *ptr != 0 {
264            let mut len = 0usize;
265            while *ptr.add(len) != 0 {
266                len += 1;
267            }
268
269            let env_var = std::slice::from_raw_parts(ptr, len);
270            let env_var_string = String::from_utf16_lossy(env_var);
271
272            if let Some(eq_pos) = env_var_string.find('=')
273                && env_var_string.starts_with("USERPROFILE=")
274            {
275                let value_len = env_var_string[eq_pos + 1..].encode_utf16().count();
276                let mut out = std::slice::from_raw_parts(ptr.add(eq_pos + 1), value_len).to_vec();
277                out.push(0);
278                return Some(out);
279            }
280
281            ptr = ptr.add(len + 1);
282        }
283    }
284
285    None
286}
287
288/// A spawned process with owned process/thread handles.
289pub struct SpawnedProcess {
290    pid: ProcessId,
291    thread_id: ThreadId,
292    process: HANDLE,
293    thread: HANDLE,
294}
295
296impl SpawnedProcess {
297    /// Process ID for the newly spawned process.
298    pub fn pid(&self) -> ProcessId {
299        self.pid
300    }
301
302    /// Primary thread ID for the newly spawned process.
303    pub fn thread_id(&self) -> ThreadId {
304        self.thread_id
305    }
306
307    /// Resume the primary thread. Useful when spawned in suspended state.
308    pub fn resume(&self) -> Result<()> {
309        let result = unsafe { ResumeThread(self.thread) };
310        if result == u32::MAX {
311            let err = windows::core::Error::from_win32();
312            return Err(Error::Process(ProcessError::SpawnFailed(
313                ProcessSpawnError::with_code(
314                    "<resume-thread>",
315                    "Failed to resume spawned process thread",
316                    err.code().0,
317                ),
318            )));
319        }
320        Ok(())
321    }
322
323    /// Open a managed `Process` handle for the spawned process.
324    pub fn open_process(&self, access: ProcessAccess) -> Result<Process> {
325        Process::open_with_access(self.pid, access)
326    }
327}
328
329impl Drop for SpawnedProcess {
330    fn drop(&mut self) {
331        close_handle_if_valid(self.process);
332        close_handle_if_valid(self.thread);
333    }
334}
335
336unsafe impl Send for SpawnedProcess {}
337
338/// Builder for spawning processes with optional parent reparenting and token source.
339#[derive(Debug, Clone)]
340pub struct ProcessSpawner {
341    exe_path: String,
342    args: Vec<String>,
343    parent_pid: Option<ProcessId>,
344    token_source_pid: Option<ProcessId>,
345    suspended: bool,
346    desktop: Option<String>,
347}
348
349impl ProcessSpawner {
350    /// Create a new process spawner for an executable path.
351    pub fn new(exe_path: &str) -> Self {
352        Self {
353            exe_path: exe_path.to_string(),
354            args: Vec::new(),
355            parent_pid: None,
356            token_source_pid: None,
357            suspended: false,
358            desktop: None,
359        }
360    }
361
362    /// Set command-line arguments for the spawned process.
363    pub fn args<I, S>(mut self, args: I) -> Self
364    where
365        I: IntoIterator<Item = S>,
366        S: AsRef<str>,
367    {
368        self.args = args
369            .into_iter()
370            .map(|arg| arg.as_ref().to_string())
371            .collect();
372        self
373    }
374
375    /// Set the parent process for reparenting via process attributes.
376    pub fn parent(mut self, pid: ProcessId) -> Self {
377        self.parent_pid = Some(pid);
378        self
379    }
380
381    /// Launch under a duplicated primary token from another process ID.
382    pub fn as_user_of(mut self, pid: ProcessId) -> Self {
383        self.token_source_pid = Some(pid);
384        self
385    }
386
387    /// Spawn the process in suspended state.
388    pub fn suspended(mut self) -> Self {
389        self.suspended = true;
390        self
391    }
392
393    /// Set an explicit desktop for process creation (e.g. "winsta0\\default").
394    pub fn desktop(mut self, desktop: &str) -> Self {
395        self.desktop = Some(desktop.to_string());
396        self
397    }
398
399    /// Spawn the process using the configured options.
400    pub fn spawn(self) -> Result<SpawnedProcess> {
401        if self.exe_path.trim().is_empty() {
402            return Err(Error::InvalidParameter(InvalidParameterError::new(
403                "exe_path",
404                "Executable path cannot be empty",
405            )));
406        }
407
408        let command = format_command_line(&self.exe_path, &self.args);
409        let mut command_wide = to_utf16_nul(&command);
410
411        let mut creation_flags =
412            EXTENDED_STARTUPINFO_PRESENT | CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE;
413
414        if self.suspended {
415            creation_flags |= CREATE_SUSPENDED;
416        }
417
418        let mut process_info = PROCESS_INFORMATION::default();
419        let mut startup_info = STARTUPINFOEXW::default();
420        startup_info.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
421
422        let mut desktop_wide = self.desktop.as_deref().map(to_utf16_nul).or_else(|| {
423            if self.token_source_pid.is_some() {
424                Some(to_utf16_nul(DEFAULT_DESKTOP))
425            } else {
426                None
427            }
428        });
429
430        if let Some(desktop) = desktop_wide.as_mut() {
431            startup_info.StartupInfo.lpDesktop = windows::core::PWSTR(desktop.as_mut_ptr());
432        }
433
434        let parent_handle = if let Some(parent_pid) = self.parent_pid {
435            let handle = unsafe {
436                OpenProcess(
437                    PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_CREATE_PROCESS,
438                    false,
439                    parent_pid.as_u32(),
440                )
441            }
442            .map_err(|e| map_spawn_windows_error(&command, "Failed to open parent process", &e))?;
443            Some(OwnedHandle::new(handle))
444        } else {
445            None
446        };
447
448        let attribute_list = if let Some(parent) = parent_handle.as_ref() {
449            let attrs = AttributeList::with_parent(&parent.raw())?;
450            startup_info.lpAttributeList = attrs.ptr();
451            Some(attrs)
452        } else {
453            None
454        };
455
456        let mut env_block: Option<EnvBlock> = None;
457        let mut current_directory_wide: Option<Vec<u16>> = None;
458
459        let process_token = if let Some(token_source_pid) = self.token_source_pid {
460            enable_privilege("SeAssignPrimaryTokenPrivilege", &command)?;
461            enable_privilege("SeIncreaseQuotaPrivilege", &command)?;
462
463            let token = get_user_token_from_pid(token_source_pid, &command)?;
464            let block = create_env_block(token.raw(), &command)?;
465            if !block.is_null() {
466                current_directory_wide = get_userprofile_from_env_block(block.as_ptr());
467            }
468            env_block = Some(block);
469            creation_flags |= CREATE_UNICODE_ENVIRONMENT;
470            Some(token)
471        } else {
472            None
473        };
474
475        let command_ptr = windows::core::PWSTR(command_wide.as_mut_ptr());
476        let current_dir = current_directory_wide
477            .as_ref()
478            .map(|v| PCWSTR(v.as_ptr()))
479            .unwrap_or(PCWSTR::null());
480
481        let result = if let Some(token) = process_token.as_ref() {
482            unsafe {
483                CreateProcessAsUserW(
484                    token.raw(),
485                    PCWSTR::null(),
486                    command_ptr,
487                    None,
488                    None,
489                    false,
490                    creation_flags,
491                    env_block.as_ref().map(|b| b.as_ptr() as *const c_void),
492                    current_dir,
493                    &startup_info.StartupInfo as *const _ as *const _,
494                    &mut process_info,
495                )
496            }
497        } else {
498            unsafe {
499                CreateProcessW(
500                    PCWSTR::null(),
501                    command_ptr,
502                    None,
503                    None,
504                    false,
505                    creation_flags,
506                    None,
507                    current_dir,
508                    &startup_info.StartupInfo,
509                    &mut process_info,
510                )
511            }
512        };
513
514        let _keep_attr_alive = attribute_list;
515
516        result.map_err(|e| map_spawn_windows_error(&command, "Failed to spawn process", &e))?;
517
518        Ok(SpawnedProcess {
519            pid: ProcessId::new(process_info.dwProcessId),
520            thread_id: ThreadId::new(process_info.dwThreadId),
521            process: process_info.hProcess,
522            thread: process_info.hThread,
523        })
524    }
525}