1use 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
288pub struct SpawnedProcess {
290 pid: ProcessId,
291 thread_id: ThreadId,
292 process: HANDLE,
293 thread: HANDLE,
294}
295
296impl SpawnedProcess {
297 pub fn pid(&self) -> ProcessId {
299 self.pid
300 }
301
302 pub fn thread_id(&self) -> ThreadId {
304 self.thread_id
305 }
306
307 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 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#[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 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 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 pub fn parent(mut self, pid: ProcessId) -> Self {
377 self.parent_pid = Some(pid);
378 self
379 }
380
381 pub fn as_user_of(mut self, pid: ProcessId) -> Self {
383 self.token_source_pid = Some(pid);
384 self
385 }
386
387 pub fn suspended(mut self) -> Self {
389 self.suspended = true;
390 self
391 }
392
393 pub fn desktop(mut self, desktop: &str) -> Self {
395 self.desktop = Some(desktop.to_string());
396 self
397 }
398
399 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}