返回 CodeWhale
windows.rs
根目录 / crates / tui / src / extension_host / windows.rs
1 //! Native-only Windows launch boundary. LPAC with only the `registryRead`
2 //! capability (Winsock cannot initialize without it) supplies
3 //! filesystem/network denial; the existing Job supplies lifetime and memory.
4 //! A fresh profile never inherits ACLs from a retired host. Profiles/assets
5 //! created here are disposed by their exact owner after the process ends;
6 //! crash leftovers are not guessed at or swept. Windows may also provide its
7 //! private AppContainer scratch/registry, separate from Core-owned state.
8
9 use std::collections::BTreeMap;
10 use std::ffi::{OsStr, OsString};
11 use std::fs::{self, File};
12 use std::io::{self, Read};
13 use std::mem::{size_of, zeroed};
14 use std::os::windows::ffi::OsStrExt;
15 use std::os::windows::fs::OpenOptionsExt;
16 use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
17 use std::path::{Path, PathBuf};
18 use std::ptr::{null, null_mut};
19 use std::sync::{Arc, Mutex};
20 use std::time::Duration;
21
22 use windows_sys::Win32::Foundation::{
23 ERROR_PIPE_CONNECTED, GetLastError, INVALID_HANDLE_VALUE, LocalFree, WAIT_OBJECT_0,
24 WAIT_TIMEOUT,
25 };
26 use windows_sys::Win32::Security::Authorization::{GRANT_ACCESS, REVOKE_ACCESS};
27 use windows_sys::Win32::Security::Isolation::{
28 CreateAppContainerProfile, DeleteAppContainerProfile,
29 };
30 use windows_sys::Win32::Security::{
31 ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, EqualSid, FreeSid, GetTokenInformation,
32 OBJECT_INHERIT_ACE, PSID, SECURITY_ATTRIBUTES, SECURITY_CAPABILITIES, SID_AND_ATTRIBUTES,
33 TOKEN_APPCONTAINER_INFORMATION, TOKEN_GROUPS, TOKEN_QUERY, TokenAppContainerSid,
34 TokenCapabilities, TokenIsAppContainer, TokenIsLessPrivilegedAppContainer,
35 };
36 use windows_sys::Win32::Storage::FileSystem::{
37 CreateFileW, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED,
38 FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES,
39 OPEN_EXISTING, PIPE_ACCESS_INBOUND, PIPE_ACCESS_OUTBOUND, READ_CONTROL, WRITE_DAC,
40 };
41 use windows_sys::Win32::System::Pipes::{ConnectNamedPipe, CreateNamedPipeW, PIPE_WAIT};
42 use windows_sys::Win32::System::Threading::{
43 CREATE_NO_WINDOW, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT, CreateProcessW,
44 DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, GetExitCodeProcess,
45 InitializeProcThreadAttributeList, OpenProcessToken,
46 PROC_THREAD_ATTRIBUTE_ALL_APPLICATION_PACKAGES_POLICY,
47 PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
48 PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, PROCESS_INFORMATION, ResumeThread,
49 STARTF_USESTDHANDLES, STARTUPINFOEXW, UpdateProcThreadAttribute, WaitForSingleObject,
50 };
51 use windows_sys::Win32::System::WindowsProgramming::PROCESS_CREATION_CHILD_PROCESS_OVERRIDE;
52
53 use crate::dependencies::HostRuntime;
54 use crate::fleet::files::WindowsDirectory;
55 use crate::process_tree::ProcessTree;
56
57 const PROBE_SOURCE: &str = include_str!("../../extension-host/src/windows-sandbox-probe.mjs");
58 const PROBE_DEADLINE: Duration = Duration::from_secs(15);
59 // Windows SDK winnt.h; the documented LPAC startup attribute opts out of the
60 // ambient ALL APPLICATION PACKAGES group. Not a fabricated token status.
61 const PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT: u32 = 0x1;
62 const MAX_GRANT_ENTRIES: usize = 65_536;
63 const MAX_GRANT_SCOPES: usize = 1024;
64 /// Empty Bun config in the granted runtime-copy directory (see sandbox_args).
65 const EMPTY_BUN_CONFIG: &str = "empty-bunfig.toml";
66 // Serialize Core's read/merge/write ACL operations across old-profile cleanup
67 // and a new host admission. Never overwrite a concurrently admitted profile.
68 static ACL_EDITS: Mutex<()> = Mutex::new(());
69
70 #[derive(Clone)]
71 pub(crate) struct NativeSandbox {
72 profile: Arc<Profile>,
73 _assets: Arc<tempfile::TempDir>,
74 pub program: PathBuf,
75 data: PathBuf,
76 }
77
78 impl std::fmt::Debug for NativeSandbox {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("NativeSandbox")
81 .field("program", &self.program)
82 .finish_non_exhaustive()
83 }
84 }
85
86 struct Profile {
87 name: Vec<u16>,
88 sid: PSID,
89 grants: Mutex<BTreeMap<PathBuf, GrantScope>>,
90 cleanup_runtime: Option<tokio::runtime::Handle>,
91 }
92 struct GrantScope {
93 identity: (u32, u64),
94 tree: bool,
95 }
96 // A retained exact profile SID, never a parsed/guessed orphan identity.
97 struct RetiredProfile {
98 name: Vec<u16>,
99 sid: PSID,
100 grants: BTreeMap<PathBuf, GrantScope>,
101 }
102 // SAFETY: retired state is exclusively owned; the SID is freed only after the
103 // existing runtime's blocking cleanup worker has finished its exact grants.
104 unsafe impl Send for RetiredProfile {}
105 // SAFETY: SID/name are immutable after successful creation. Win32 consumes
106 // borrowed SID memory synchronously; only the final Arc drop frees it.
107 unsafe impl Send for Profile {}
108 unsafe impl Sync for Profile {}
109
110 impl Profile {
111 fn create() -> io::Result<Self> {
112 let name = wide(OsStr::new(&format!(
113 "Codewhale.Native.{}",
114 uuid::Uuid::new_v4()
115 )))?;
116 let mut sid = null_mut();
117 // SAFETY: nul-terminated owned strings and an initialized out pointer.
118 let result = unsafe {
119 CreateAppContainerProfile(
120 name.as_ptr(),
121 name.as_ptr(),
122 name.as_ptr(),
123 null(),
124 0,
125 &mut sid,
126 )
127 };
128 if result < 0 {
129 return Err(io::Error::other(format!(
130 "CreateAppContainerProfile HRESULT {result:#x}"
131 )));
132 }
133 if sid.is_null() {
134 // No existing/user profile is adopted or removed on a collision.
135 unsafe {
136 DeleteAppContainerProfile(name.as_ptr());
137 }
138 return Err(io::Error::other("AppContainer profile has no SID"));
139 }
140 Ok(Self {
141 name,
142 sid,
143 grants: Mutex::new(BTreeMap::new()),
144 cleanup_runtime: tokio::runtime::Handle::try_current().ok(),
145 })
146 }
147 }
148
149 impl Profile {
150 fn remember(&self, path: &Path, file: &File, tree: bool) -> io::Result<()> {
151 let value = crate::plugins::windows_file_identity(file)?;
152 let mut grants = self
153 .grants
154 .lock()
155 .map_err(|_| io::Error::other("profile grant accounting poisoned"))?;
156 if let Some(old) = grants.get(path) {
157 if old.identity != (value.volume, value.index) || old.tree != tree {
158 return Err(io::Error::other(
159 "recorded profile grant identity changed; refusing overwrite",
160 ));
161 }
162 return Ok(());
163 }
164 if grants.len() == MAX_GRANT_SCOPES {
165 return Err(io::Error::other("profile exceeds 1024 exact grant scopes"));
166 }
167 grants.insert(
168 path.to_path_buf(),
169 GrantScope {
170 identity: (value.volume, value.index),
171 tree,
172 },
173 );
174 Ok(())
175 }
176 }
177 impl Drop for Profile {
178 fn drop(&mut self) {
179 let grants = std::mem::take(
180 self.grants
181 .get_mut()
182 .unwrap_or_else(|error| error.into_inner()),
183 );
184 let retired = RetiredProfile {
185 name: std::mem::take(&mut self.name),
186 sid: self.sid,
187 grants,
188 };
189 // Same existing Tokio scheduler; no new runtime/authority. The worker
190 // owns the SID until retirement completes, even during cancellation.
191 if let Some(runtime) = self.cleanup_runtime.take() {
192 runtime.spawn_blocking(move || retired.dispose());
193 } else {
194 // Synchronous platform callers have no runtime; work remains
195 // bounded to the exact recorded roots, never a profile sweep.
196 retired.dispose();
197 }
198 }
199 }
200 impl RetiredProfile {
201 fn dispose(self) {
202 drop(self);
203 }
204 }
205 // Drop also retires a task cancelled before its blocking worker starts. Once
206 // started, spawn_blocking cannot abandon this exclusively owned cleanup.
207 impl Drop for RetiredProfile {
208 fn drop(&mut self) {
209 for (path, scope) in &self.grants {
210 if let Err(error) = retire_scope(path, scope, self.sid) {
211 tracing::warn!(path = %path.display(), "Native exact-profile ACL retirement failed: {error}");
212 }
213 }
214 // The OS profile and SID stay alive until exact grant retirement ends.
215 // Crash remnants or refused/replaced roots are not guessed at/swept.
216 unsafe {
217 let result = DeleteAppContainerProfile(self.name.as_ptr());
218 if result < 0 {
219 tracing::warn!("Native AppContainer profile disposal failed ({result:#x})");
220 }
221 FreeSid(self.sid);
222 }
223 }
224 }
225
226 fn retire_scope(root: &Path, scope: &GrantScope, sid: PSID) -> io::Result<()> {
227 let _serial = ACL_EDITS.lock().unwrap_or_else(|error| error.into_inner());
228 if !root.try_exists()? {
229 return Ok(());
230 }
231 let root_pin = if scope.tree {
232 WindowsDirectory::open_acl(root)?
233 } else {
234 WindowsDirectory::open(
235 root.parent()
236 .ok_or_else(|| io::Error::other("grant has no parent"))?,
237 )?
238 };
239 let root_file;
240 let file = if scope.tree {
241 root_pin.acl_handle()?
242 } else {
243 root_file = acl_file_with_share(root, 1 | 2 | 4)?;
244 &root_file
245 };
246 let value = crate::plugins::windows_file_identity(file)?;
247 if (value.volume, value.index) != scope.identity {
248 return Err(io::Error::other(
249 "recorded grant object was replaced; refusing cleanup",
250 ));
251 }
252 // Remove the parent's inheritable grant before walking, so newly created
253 // children cannot inherit this retired SID. The kernel write never
254 // propagates; every child is independently fenced and updated.
255 edit_acl(file, sid, 0, 0, REVOKE_ACCESS)?;
256 if !scope.tree {
257 return Ok(());
258 }
259 let mut pending = Vec::new();
260 let mut count = 1;
261 enqueue_pinned_children(root, &root_pin, &mut pending, count)?;
262 let mut failure = None;
263 while let Some((path, parent_pin)) = pending.pop() {
264 count += 1;
265 if count > MAX_GRANT_ENTRIES {
266 return Err(io::Error::other(
267 "profile retirement exceeds 65536 entries in a recorded root",
268 ));
269 }
270 let mut work = || -> io::Result<()> {
271 let metadata = fs::symlink_metadata(&path)?;
272 if crate::plugins::metadata_is_link_or_reparse(&metadata) {
273 return Err(io::Error::other(
274 "profile retirement refuses links/reparse points",
275 ));
276 }
277 if metadata.is_dir() {
278 let directory = parent_pin.open_acl_child(
279 path.file_name()
280 .ok_or_else(|| io::Error::other("grant has no filename"))?,
281 )?;
282 edit_acl(directory.acl_handle()?, sid, 0, 0, REVOKE_ACCESS)?;
283 enqueue_pinned_children(&path, &directory, &mut pending, count)
284 } else if metadata.is_file() {
285 if parent_pin.child_path(
286 path.file_name()
287 .ok_or_else(|| io::Error::other("grant has no filename"))?,
288 )? != path
289 {
290 return Err(io::Error::other("grant left its pinned parent"));
291 }
292 // Retiring this exact SID may overlap a new host's data
293 // writes. The handle edits this exact object while the pinned
294 // parent chain fences its path, so allow those legitimate
295 // writers/renames; never restore an old whole ACL.
296 let file = acl_file_with_share(&path, 1 | 2 | 4)?;
297 edit_acl(&file, sid, 0, 0, REVOKE_ACCESS)?;
298 Ok(())
299 } else {
300 Err(io::Error::other(
301 "profile retirement refuses a nonregular entry",
302 ))
303 }
304 };
305 match work() {
306 Ok(()) => {}
307 Err(error) => {
308 failure = Some(error);
309 }
310 }
311 }
312 match failure {
313 Some(error) => Err(error),
314 None => Ok(()),
315 }
316 }
317 fn enqueue_children(path: &Path, pending: &mut Vec<PathBuf>, visited: usize) -> io::Result<()> {
318 for entry in fs::read_dir(path)? {
319 // Bound materialized pending entries, not only already visited paths.
320 if visited + pending.len() >= MAX_GRANT_ENTRIES {
321 return Err(io::Error::other(
322 "profile directory traversal exceeds 65536 entries",
323 ));
324 }
325 pending.push(entry?.path());
326 }
327 Ok(())
328 }
329 fn enqueue_pinned_children(
330 path: &Path,
331 pin: &WindowsDirectory,
332 pending: &mut Vec<(PathBuf, WindowsDirectory)>,
333 visited: usize,
334 ) -> io::Result<()> {
335 let mut children = Vec::new();
336 // Reuse the exact admission/retirement bound, including already pending
337 // entries. Each child keeps its actual direct parent pinned until visited.
338 enqueue_children(path, &mut children, visited + pending.len())?;
339 pending.extend(children.into_iter().map(|child| (child, pin.clone())));
340 Ok(())
341 }
342 /// Name the admission step in an error, so a refusal says where it stopped.
343 fn in_step<T>(step: &str, result: io::Result<T>) -> io::Result<T> {
344 result.map_err(|error| io::Error::new(error.kind(), format!("{step}: {error}")))
345 }
346 fn acl_file(path: &Path) -> io::Result<File> {
347 acl_file_with_share(path, 1)
348 }
349 fn acl_file_with_share(path: &Path, share: u32) -> io::Result<File> {
350 let file = fs::OpenOptions::new()
351 .access_mode(READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES)
352 .share_mode(share)
353 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
354 .open(path)?;
355 let metadata = file.metadata()?;
356 if !metadata.is_file()
357 || crate::plugins::metadata_is_link_or_reparse(&metadata)
358 || crate::plugins::windows_file_identity(&file)?.links != 1
359 {
360 return Err(io::Error::other(
361 "profile ACL operation refuses a linked/nonregular file",
362 ));
363 }
364 Ok(file)
365 }
366
367 impl NativeSandbox {
368 /// Blocking, invoked by the existing manager's bounded launch worker.
369 pub(crate) fn prepare(
370 runtime: &HostRuntime,
371 bundle: &Path,
372 home: &Path,
373 data: &Path,
374 memory_cap: u64,
375 ) -> Result<Self, String> {
376 let work = || -> io::Result<Self> {
377 let profile = Arc::new(Profile::create()?);
378 let parent = home.join("extension-host").join("native-launch");
379 fs::create_dir_all(&parent)?;
380 let _parent = in_step("pin launch parent", WindowsDirectory::open(&parent))?;
381 let assets = Arc::new(
382 tempfile::Builder::new()
383 .prefix("host-")
384 .tempdir_in(&parent)?,
385 );
386 let program = assets.path().join("runtime.exe");
387 fs::write(assets.path().join(EMPTY_BUN_CONFIG), b"")?;
388 // The selected runtime is copied, never granted access in an
389 // installation/user directory. Pin its opened bytes while copying.
390 let original = runtime.path.canonicalize()?;
391 let source_pin = in_step(
392 "pin runtime directory",
393 WindowsDirectory::open(
394 original
395 .parent()
396 .ok_or_else(|| io::Error::other("runtime has no parent"))?,
397 ),
398 )?;
399 let mut source = in_step(
400 "open runtime",
401 crate::plugins::manifest::open_bundle_file(&original),
402 )?;
403 if source.metadata()?.len() > 512 * 1024 * 1024 {
404 return Err(io::Error::other(
405 "selected runtime exceeds 512 MiB launch limit",
406 ));
407 }
408 let mut target = fs::OpenOptions::new()
409 .write(true)
410 .create_new(true)
411 .open(&program)?;
412 io::copy(&mut source, &mut target)?;
413 target.sync_all()?;
414 drop(target);
415 drop(source_pin);
416 let sandbox = Self {
417 profile,
418 _assets: assets,
419 program,
420 data: data.to_path_buf(),
421 };
422 in_step(
423 "grant runtime copy",
424 sandbox.grant_tree(sandbox._assets.path(), false),
425 )?;
426 // Exact canonical bundle file only: never recursively grant its
427 // parent, which also contains the Builtin's private data.
428 in_step(
429 "grant host bundle",
430 sandbox.grant_file(bundle, FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, true),
431 )?;
432 fs::create_dir_all(data.join("tmp"))?;
433 in_step("grant data directory", sandbox.grant_tree(data, true))?;
434 in_step("isolation probe", sandbox.probe(runtime, memory_cap))?;
435 Ok(sandbox)
436 };
437 work().map_err(|error| format!("Windows Native isolation could not be verified: {error}"))
438 }
439
440 /// Only call after the existing Rust Native receipt/hash check. The root
441 /// is a reviewed staged snapshot, never the mutable source or a link.
442 pub(crate) fn admit_root(&self, root: &Path) -> Result<(), String> {
443 self.grant_tree(root, false)
444 .map_err(|error| format!("cannot admit reviewed Windows bundle: {error}"))
445 }
446
447 fn grant_tree(&self, root: &Path, writable: bool) -> io::Result<()> {
448 let _serial = ACL_EDITS.lock().unwrap_or_else(|error| error.into_inner());
449 let root_pin = WindowsDirectory::open_acl(root)?;
450 self.profile.remember(root, root_pin.acl_handle()?, true)?;
451 let access = FILE_GENERIC_READ
452 | FILE_GENERIC_EXECUTE
453 | if writable {
454 FILE_GENERIC_WRITE | windows_sys::Win32::Storage::FileSystem::DELETE
455 } else {
456 0
457 };
458 set_acl(
459 root_pin.acl_handle()?,
460 self.profile.sid,
461 access,
462 OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
463 )?;
464 let mut pending = Vec::new();
465 let mut count = 1;
466 enqueue_pinned_children(root, &root_pin, &mut pending, count)?;
467 while let Some((path, parent_pin)) = pending.pop() {
468 count += 1;
469 if count > MAX_GRANT_ENTRIES {
470 return Err(io::Error::other("sandbox grant tree exceeds 65536 entries"));
471 }
472 let metadata = fs::symlink_metadata(&path)?;
473 if crate::plugins::metadata_is_link_or_reparse(&metadata) {
474 return Err(io::Error::other(
475 "sandbox grant refuses a link/reparse point",
476 ));
477 }
478 if metadata.is_dir() {
479 let pin = parent_pin.open_acl_child(
480 path.file_name()
481 .ok_or_else(|| io::Error::other("grant has no filename"))?,
482 )?;
483 set_acl(
484 pin.acl_handle()?,
485 self.profile.sid,
486 access,
487 OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
488 )?;
489 enqueue_pinned_children(&path, &pin, &mut pending, count)?;
490 } else if metadata.is_file() {
491 self.grant_pinned_file(&parent_pin, &path, access, false)?;
492 } else {
493 return Err(io::Error::other(
494 "sandbox grant refuses a non-regular entry",
495 ));
496 }
497 }
498 Ok(())
499 }
500
501 fn grant_file(&self, path: &Path, access: u32, remember: bool) -> io::Result<()> {
502 let _serial = ACL_EDITS.lock().unwrap_or_else(|error| error.into_inner());
503 let pin = WindowsDirectory::open(
504 path.parent()
505 .ok_or_else(|| io::Error::other("file has no parent"))?,
506 )?;
507 self.grant_pinned_file(&pin, path, access, remember)
508 }
509
510 // The owning grant entrypoint holds ACL_EDITS. Tree files reuse the parent
511 // chain rather than reopen pinned directory objects. The
512 // child_path comparison is only a lexical invariant; the protection is the
513 // held no-write/no-delete parent chain plus acl_file's no-follow,
514 // regular, single-link checks.
515 fn grant_pinned_file(
516 &self,
517 parent_pin: &WindowsDirectory,
518 path: &Path,
519 access: u32,
520 remember: bool,
521 ) -> io::Result<()> {
522 if parent_pin.child_path(
523 path.file_name()
524 .ok_or_else(|| io::Error::other("file has no filename"))?,
525 )? != path
526 {
527 return Err(io::Error::other("grant left its pinned parent"));
528 }
529 let file = acl_file(path)?;
530 if remember {
531 self.profile.remember(path, &file, false)?;
532 }
533 set_acl(&file, self.profile.sid, access, 0)
534 }
535
536 fn probe(&self, runtime: &HostRuntime, memory_cap: u64) -> io::Result<()> {
537 let outside = tempfile::tempdir()?;
538 let mut reads = Vec::new();
539 for name in [
540 "codex-auth.json",
541 "dsh-credentials.yaml",
542 "builtin-private-state.json",
543 ] {
544 let path = outside.path().join(name);
545 fs::write(&path, b"non-secret-denial-control")?;
546 reads.push(path);
547 }
548 let listener = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))?;
549 listener.set_nonblocking(true)?;
550 let overrides = [
551 (
552 "CODEWHALE_WINDOWS_PROBE_INSIDE".to_string(),
553 self.data
554 .join(format!(".probe-{}", uuid::Uuid::new_v4()))
555 .to_string_lossy()
556 .into_owned(),
557 ),
558 (
559 "CODEWHALE_WINDOWS_PROBE_OUTSIDE".to_string(),
560 outside
561 .path()
562 .join("forbidden-write")
563 .to_string_lossy()
564 .into_owned(),
565 ),
566 (
567 "CODEWHALE_WINDOWS_PROBE_READS".to_string(),
568 serde_json::to_string(&reads)?,
569 ),
570 (
571 "CODEWHALE_WINDOWS_PROBE_PORT".to_string(),
572 listener.local_addr()?.port().to_string(),
573 ),
574 ];
575 let runtime_env = super::supervisor::runtime_env(runtime.kind);
576 let mut args = super::supervisor::runtime_args(runtime);
577 if runtime.compiled {
578 args.extend(["--tier=plugin".into(), "--windows-sandbox-probe".into()]);
579 } else {
580 args.extend([
581 "--input-type=module".into(),
582 "-e".into(),
583 format!("{PROBE_SOURCE}\nconsole.log(JSON.stringify(await windowsSandboxProbe()))"),
584 ]);
585 }
586 // Serialize the same sandbox-safe argv used to launch the parent.
587 let args = sandbox_args(&args, self._assets.path());
588 let child_args = (
589 "CODEWHALE_WINDOWS_PROBE_CHILD_ARGS".to_string(),
590 serde_json::to_string(&args)?,
591 );
592 let env = crate::child_env::sanitized_plugin_mcp_env_from(
593 std::env::vars_os(),
594 runtime_env
595 .iter()
596 .chain(&overrides)
597 .chain(std::iter::once(&child_args))
598 .map(|(k, v)| (k.as_str(), v.as_str())),
599 );
600 let mut probe = self.spawn_inner(&args, &env, memory_cap, false)?;
601 // Close input immediately. Fixed output is under 512 bytes; a runtime
602 // substitution that floods a pipe cannot evade the wait deadline.
603 drop(probe.stdin);
604 let status = match probe.child.wait_timeout(PROBE_DEADLINE) {
605 Ok(status) => status,
606 Err(error) => {
607 let _ = probe.child.tree.kill();
608 let _ = probe.child.wait_timeout(Duration::from_secs(2));
609 return Err(error);
610 }
611 };
612 let mut stdout = Vec::new();
613 File::from(probe.stdout)
614 .take(4097)
615 .read_to_end(&mut stdout)?;
616 let mut stderr = Vec::new();
617 File::from(probe.stderr)
618 .take(4097)
619 .read_to_end(&mut stderr)?;
620 if !status.success() || stdout.len() > 4096 {
621 return Err(io::Error::other(format!(
622 "isolation probe failed ({status}): {}",
623 String::from_utf8_lossy(&stderr)
624 )));
625 }
626 let expected = serde_json::json!({"version":1,"data_roundtrip":true,"outside_read_denied":true,"outside_write_denied":true,"network_denied":true,"descendant_denied":true});
627 if serde_json::from_slice::<serde_json::Value>(&stdout)? != expected
628 || listener.accept().is_ok()
629 {
630 return Err(io::Error::other(
631 "sandbox probe returned no exact allow/deny receipt",
632 ));
633 }
634 Ok(())
635 }
636
637 pub(crate) fn spawn(
638 &self,
639 args: &[String],
640 env: &[(OsString, OsString)],
641 memory_cap: u64,
642 ) -> io::Result<Spawned> {
643 let args = sandbox_args(args, self._assets.path());
644 self.spawn_inner(&args, env, memory_cap, true)
645 }
646
647 fn spawn_inner(
648 &self,
649 args: &[String],
650 env: &[(OsString, OsString)],
651 memory_cap: u64,
652 overlapped: bool,
653 ) -> io::Result<Spawned> {
654 let (stdin, child_stdin) = pipe(false, overlapped)?;
655 let (stdout, child_stdout) = pipe(true, overlapped)?;
656 let (stderr, child_stderr) = pipe(true, overlapped)?;
657 let mut attrs = Attributes::new(4)?;
658 let registry_read = CapabilitySid::registry_read()?;
659 let capability_sid = registry_read.sid();
660 let mut capability = SID_AND_ATTRIBUTES {
661 Sid: capability_sid,
662 Attributes: windows_sys::Win32::System::SystemServices::SE_GROUP_ENABLED as u32,
663 };
664 let capabilities = SECURITY_CAPABILITIES {
665 AppContainerSid: self.profile.sid,
666 Capabilities: &mut capability,
667 CapabilityCount: 1,
668 Reserved: 0,
669 };
670 let lpac = PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT;
671 // Core creates this LPAC from an unrestricted process, so it can opt
672 // the runtime into spawning descendants. Windows gives those children
673 // the parent's AppContainer token and, by default, the same Job; the
674 // startup probe verifies that they keep the same file/network limits.
675 let child_process_policy = PROCESS_CREATION_CHILD_PROCESS_OVERRIDE;
676 let handles = [
677 child_stdin.as_raw_handle(),
678 child_stdout.as_raw_handle(),
679 child_stderr.as_raw_handle(),
680 ];
681 attrs.set(PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &capabilities)?;
682 attrs.set(PROC_THREAD_ATTRIBUTE_ALL_APPLICATION_PACKAGES_POLICY, &lpac)?;
683 attrs.set(
684 PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY,
685 &child_process_policy,
686 )?;
687 attrs.set_slice(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles)?;
688 let mut startup: STARTUPINFOEXW = unsafe { zeroed() };
689 startup.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
690 startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
691 startup.StartupInfo.hStdInput = child_stdin.as_raw_handle();
692 startup.StartupInfo.hStdOutput = child_stdout.as_raw_handle();
693 startup.StartupInfo.hStdError = child_stderr.as_raw_handle();
694 startup.lpAttributeList = attrs.ptr();
695 let application = wide(self.program.as_os_str())?;
696 let mut command = command_line(self.program.as_os_str(), args)?;
697 let directory = wide(self.data.as_os_str())?;
698 let mut env = env.to_vec();
699 let temp = self.data.join("tmp").into_os_string();
700 for key in ["TEMP", "TMP", "TMPDIR"] {
701 env.retain(|(name, _)| !name.to_string_lossy().eq_ignore_ascii_case(key));
702 env.push((key.into(), temp.clone()));
703 }
704 let environment = environment_block(&env)?;
705 let mut info: PROCESS_INFORMATION = unsafe { zeroed() };
706 // SAFETY: owned attribute backing/storage/argv/environment outlive
707 // this call; HANDLE_LIST is precisely the three inheritable pipe ends.
708 let created = unsafe {
709 CreateProcessW(
710 application.as_ptr(),
711 command.as_mut_ptr(),
712 null(),
713 null(),
714 1,
715 CREATE_SUSPENDED
716 | CREATE_NO_WINDOW
717 | CREATE_UNICODE_ENVIRONMENT
718 | EXTENDED_STARTUPINFO_PRESENT,
719 environment.as_ptr().cast(),
720 directory.as_ptr(),
721 &startup.StartupInfo,
722 &mut info,
723 )
724 };
725 if created == 0 {
726 return Err(io::Error::last_os_error());
727 }
728 let process = unsafe { OwnedHandle::from_raw_handle(info.hProcess) };
729 let thread = unsafe { OwnedHandle::from_raw_handle(info.hThread) };
730 let setup = || -> io::Result<Arc<ProcessTree>> {
731 let tree = Arc::new(ProcessTree::attach_windows_handle(
732 info.dwProcessId,
733 &process,
734 )?);
735 tree.limit_process_memory(memory_cap)?;
736 verify_token(&process, self.profile.sid, capability_sid)?;
737 // No Native byte executes until Job, memory and actual LPAC token
738 // identity/capabilities have all been checked by Rust.
739 if unsafe { ResumeThread(thread.as_raw_handle()) } != 1 {
740 return Err(io::Error::other(
741 "Native main thread did not resume from its exact suspended state",
742 ));
743 }
744 Ok(tree)
745 };
746 let tree = match setup() {
747 Ok(tree) => tree,
748 Err(error) => {
749 unsafe {
750 windows_sys::Win32::System::Threading::TerminateProcess(
751 process.as_raw_handle(),
752 70,
753 );
754 WaitForSingleObject(process.as_raw_handle(), 2000);
755 }
756 return Err(error);
757 }
758 };
759 drop((child_stdin, child_stdout, child_stderr, thread));
760 Ok(Spawned {
761 child: Child {
762 process: Arc::new(process),
763 tree,
764 pid: info.dwProcessId,
765 _sandbox: self.clone(),
766 reaped: false,
767 },
768 stdin,
769 stdout,
770 stderr,
771 })
772 }
773 }
774
775 pub(crate) struct Spawned {
776 pub child: Child,
777 pub stdin: OwnedHandle,
778 pub stdout: OwnedHandle,
779 pub stderr: OwnedHandle,
780 }
781
782 pub(crate) struct Child {
783 process: Arc<OwnedHandle>,
784 pub tree: Arc<ProcessTree>,
785 pub pid: u32,
786 _sandbox: NativeSandbox,
787 reaped: bool,
788 }
789
790 impl Child {
791 pub(crate) async fn wait(&mut self) -> io::Result<std::process::ExitStatus> {
792 let process = Arc::clone(&self.process);
793 let sandbox = self._sandbox.clone();
794 let status = tokio::task::spawn_blocking(move || {
795 let _sandbox = sandbox;
796 wait(&process, u32::MAX)
797 })
798 .await
799 .map_err(io::Error::other)??;
800 self.reaped = true;
801 Ok(status)
802 }
803 pub(crate) async fn kill(&mut self) -> io::Result<()> {
804 self.tree.kill()?;
805 self.wait().await.map(|_| ())
806 }
807 fn wait_timeout(&mut self, after: Duration) -> io::Result<std::process::ExitStatus> {
808 let status = wait(
809 &self.process,
810 after.as_millis().min(u32::MAX as u128 - 1) as u32,
811 )?;
812 self.reaped = true;
813 // The fixed probe must not retain background descendants/pipe handles.
814 let _ = self.tree.kill();
815 Ok(status)
816 }
817 }
818
819 impl Drop for Child {
820 fn drop(&mut self) {
821 if !self.reaped {
822 let _ = self.tree.kill();
823 }
824 }
825 }
826
827 fn wait(process: &OwnedHandle, timeout: u32) -> io::Result<std::process::ExitStatus> {
828 use std::os::windows::process::ExitStatusExt;
829 match unsafe { WaitForSingleObject(process.as_raw_handle(), timeout) } {
830 WAIT_OBJECT_0 => {
831 let mut status = 0;
832 if unsafe { GetExitCodeProcess(process.as_raw_handle(), &mut status) } == 0 {
833 return Err(io::Error::last_os_error());
834 }
835 Ok(std::process::ExitStatus::from_raw(status))
836 }
837 WAIT_TIMEOUT => Err(io::Error::new(
838 io::ErrorKind::TimedOut,
839 "Windows sandbox probe exceeded launch deadline",
840 )),
841 _ => Err(io::Error::last_os_error()),
842 }
843 }
844
845 fn verify_token(process: &OwnedHandle, sid: PSID, capability: PSID) -> io::Result<()> {
846 let mut token = null_mut();
847 if unsafe { OpenProcessToken(process.as_raw_handle(), TOKEN_QUERY, &mut token) } == 0 {
848 return Err(io::Error::last_os_error());
849 }
850 let token = unsafe { OwnedHandle::from_raw_handle(token) };
851 // AppContainer must be confirmed, and LPAC through the token flag or, when
852 // that query fails (hosted Windows Server returned ERROR_INVALID_PARAMETER;
853 // the cause is unknown), through the `WIN://NOALLAPPPKG` security
854 // attribute the LPAC opt-out adds (ntdoc; NtObjectManager's
855 // LowPrivilegeAppContainer). Report each observation, so a refusal says
856 // which property the kernel did not confirm.
857 let query = |class| -> io::Result<u32> {
858 let mut value = 0_u32;
859 let mut read = 0;
860 if unsafe {
861 GetTokenInformation(
862 token.as_raw_handle(),
863 class,
864 (&mut value as *mut u32).cast(),
865 size_of::<u32>() as u32,
866 &mut read,
867 )
868 } == 0
869 {
870 return Err(io::Error::last_os_error());
871 }
872 Ok(value)
873 };
874 let mut observed = Vec::new();
875 let app_container = match query(TokenIsAppContainer) {
876 Ok(value) => {
877 observed.push(format!("TokenIsAppContainer={value}"));
878 value == 1
879 }
880 Err(error) => {
881 observed.push(format!("TokenIsAppContainer query failed: {error}"));
882 false
883 }
884 };
885 let less_privileged = match query(TokenIsLessPrivilegedAppContainer) {
886 Ok(value) => {
887 observed.push(format!("TokenIsLessPrivilegedAppContainer={value}"));
888 value == 1
889 }
890 Err(error) => {
891 observed.push(format!(
892 "TokenIsLessPrivilegedAppContainer query failed: {error}"
893 ));
894 match token_has_lpac_attribute(&token) {
895 Ok(true) => {
896 observed.push("WIN://NOALLAPPPKG=1".to_string());
897 true
898 }
899 Ok(false) => {
900 observed.push("WIN://NOALLAPPPKG absent".to_string());
901 false
902 }
903 Err(error) => {
904 observed.push(format!("WIN://NOALLAPPPKG unreadable: {error}"));
905 false
906 }
907 }
908 }
909 };
910 if !(app_container && less_privileged) {
911 return Err(io::Error::other(format!(
912 "Windows refused the required LPAC token ({})",
913 observed.join(", ")
914 )));
915 }
916 // Variable-size token buffers are aligned for their SDK structs.
917 for class in [TokenAppContainerSid, TokenCapabilities] {
918 let mut size = 0;
919 unsafe {
920 GetTokenInformation(token.as_raw_handle(), class, null_mut(), 0, &mut size);
921 }
922 if size == 0 || size > 64 * 1024 {
923 return Err(io::Error::other("invalid sandbox token size"));
924 }
925 let mut buffer = vec![0_usize; (size as usize).div_ceil(size_of::<usize>())];
926 if unsafe {
927 GetTokenInformation(
928 token.as_raw_handle(),
929 class,
930 buffer.as_mut_ptr().cast(),
931 size,
932 &mut size,
933 )
934 } == 0
935 {
936 return Err(io::Error::last_os_error());
937 }
938 let valid = unsafe {
939 if class == TokenAppContainerSid {
940 let value = &*buffer.as_ptr().cast::<TOKEN_APPCONTAINER_INFORMATION>();
941 !value.TokenAppContainer.is_null() && EqualSid(value.TokenAppContainer, sid) != 0
942 } else {
943 // Exactly the one granted capability, and nothing else.
944 let groups = &*buffer.as_ptr().cast::<TOKEN_GROUPS>();
945 groups.GroupCount == 1 && EqualSid(groups.Groups[0].Sid, capability) != 0
946 }
947 };
948 if !valid {
949 return Err(io::Error::other(
950 "sandbox token has unexpected identity/capabilities",
951 ));
952 }
953 }
954 Ok(())
955 }
956
957 /// Whether the token carries the LPAC opt-out attribute `WIN://NOALLAPPPKG`
958 /// with a nonzero integer value. `TokenSecurityAttributes` (Windows 8+)
959 /// returns TOKEN_SECURITY_ATTRIBUTES_INFORMATION whose pointers are absolute
960 /// addresses into the returned buffer; each is checked to be aligned and to
961 /// stay inside it before it is read.
962 fn token_has_lpac_attribute(token: &OwnedHandle) -> io::Result<bool> {
963 use windows_sys::Win32::Security::TokenSecurityAttributes;
964 #[repr(C)]
965 struct UnicodeString {
966 length: u16,
967 maximum_length: u16,
968 buffer: *const u16,
969 }
970 #[repr(C)]
971 struct AttributeV1 {
972 name: UnicodeString,
973 value_type: u16,
974 reserved: u16,
975 flags: u32,
976 value_count: u32,
977 values: *const u64,
978 }
979 #[repr(C)]
980 struct AttributesInformation {
981 version: u16,
982 reserved: u16,
983 attribute_count: u32,
984 attributes: *const AttributeV1,
985 }
986 const VERSION_V1: u16 = 1;
987 const TYPE_INT64: u16 = 1;
988 const TYPE_UINT64: u16 = 2;
989 let mut size = 0;
990 unsafe {
991 GetTokenInformation(
992 token.as_raw_handle(),
993 TokenSecurityAttributes,
994 null_mut(),
995 0,
996 &mut size,
997 );
998 }
999 if (size as usize) < size_of::<AttributesInformation>() || size > 64 * 1024 {
1000 return Err(io::Error::other("invalid token security attribute size"));
1001 }
1002 let mut buffer = vec![0_usize; (size as usize).div_ceil(size_of::<usize>())];
1003 if unsafe {
1004 GetTokenInformation(
1005 token.as_raw_handle(),
1006 TokenSecurityAttributes,
1007 buffer.as_mut_ptr().cast(),
1008 size,
1009 &mut size,
1010 )
1011 } == 0
1012 {
1013 return Err(io::Error::last_os_error());
1014 }
1015 let start = buffer.as_ptr() as usize;
1016 let end = start + buffer.len() * size_of::<usize>();
1017 let inside = |pointer: usize, bytes: usize, align: usize| {
1018 pointer % align == 0
1019 && pointer >= start
1020 && pointer.checked_add(bytes).is_some_and(|last| last <= end)
1021 };
1022 let information = unsafe { &*buffer.as_ptr().cast::<AttributesInformation>() };
1023 if information.version != VERSION_V1 {
1024 return Err(io::Error::other("unknown token security attribute version"));
1025 }
1026 let count = information.attribute_count as usize;
1027 if count == 0 {
1028 return Ok(false);
1029 }
1030 if !count
1031 .checked_mul(size_of::<AttributeV1>())
1032 .is_some_and(|bytes| {
1033 inside(
1034 information.attributes as usize,
1035 bytes,
1036 align_of::<AttributeV1>(),
1037 )
1038 })
1039 {
1040 return Err(io::Error::other(
1041 "token security attributes exceed their buffer",
1042 ));
1043 }
1044 for index in 0..count {
1045 let attribute = unsafe { &*information.attributes.add(index) };
1046 let name_bytes = usize::from(attribute.name.length);
1047 if name_bytes % 2 != 0
1048 || !inside(
1049 attribute.name.buffer as usize,
1050 name_bytes,
1051 align_of::<u16>(),
1052 )
1053 {
1054 return Err(io::Error::other(
1055 "token security attribute name exceeds its buffer",
1056 ));
1057 }
1058 let name = unsafe { std::slice::from_raw_parts(attribute.name.buffer, name_bytes / 2) };
1059 if !String::from_utf16_lossy(name).eq_ignore_ascii_case("WIN://NOALLAPPPKG") {
1060 continue;
1061 }
1062 let values = attribute.value_count as usize;
1063 if !matches!(attribute.value_type, TYPE_INT64 | TYPE_UINT64)
1064 || values == 0
1065 || !values
1066 .checked_mul(size_of::<u64>())
1067 .is_some_and(|bytes| inside(attribute.values as usize, bytes, align_of::<u64>()))
1068 {
1069 return Ok(false);
1070 }
1071 return Ok(unsafe { *attribute.values } != 0);
1072 }
1073 Ok(false)
1074 }
1075
1076 /// The only capability a Native LPAC host receives: `registryRead`. Without
1077 /// it an LPAC cannot open any registry key, so Winsock initialization, which
1078 /// reads its catalog under HKLM, fails and Node aborts at startup. It grants
1079 /// no file, network or COM access; socket creation stays denied, which the
1080 /// isolation probe checks.
1081 struct CapabilitySid {
1082 sid: PSID,
1083 sids: *mut PSID,
1084 count: u32,
1085 groups: *mut PSID,
1086 group_count: u32,
1087 }
1088
1089 impl CapabilitySid {
1090 fn registry_read() -> io::Result<Self> {
1091 use windows_sys::Win32::Security::DeriveCapabilitySidsFromName;
1092 let name = wide(OsStr::new("registryRead"))?;
1093 let mut value = Self {
1094 sid: null_mut(),
1095 sids: null_mut(),
1096 count: 0,
1097 groups: null_mut(),
1098 group_count: 0,
1099 };
1100 // SAFETY: nul-terminated name and initialized out pointers; the
1101 // returned arrays and SIDs are freed by Drop with LocalFree.
1102 if unsafe {
1103 DeriveCapabilitySidsFromName(
1104 name.as_ptr(),
1105 &mut value.groups,
1106 &mut value.group_count,
1107 &mut value.sids,
1108 &mut value.count,
1109 )
1110 } == 0
1111 {
1112 return Err(io::Error::last_os_error());
1113 }
1114 if value.count != 1 || value.sids.is_null() {
1115 return Err(io::Error::other(
1116 "registryRead did not derive exactly one capability SID",
1117 ));
1118 }
1119 // SAFETY: DeriveCapabilitySidsFromName succeeded, and the checked
1120 // count says its owned array contains exactly one PSID. The array and
1121 // SID stay alive in `value` until its Drop implementation frees them.
1122 value.sid = unsafe { std::slice::from_raw_parts(value.sids, 1)[0] };
1123 Ok(value)
1124 }
1125
1126 fn sid(&self) -> PSID {
1127 self.sid
1128 }
1129 }
1130
1131 impl Drop for CapabilitySid {
1132 fn drop(&mut self) {
1133 // SAFETY: each SID and each array came from DeriveCapabilitySidsFromName.
1134 unsafe {
1135 for (array, count) in [(self.sids, self.count), (self.groups, self.group_count)] {
1136 if array.is_null() {
1137 continue;
1138 }
1139 for index in 0..count as usize {
1140 LocalFree(*array.add(index));
1141 }
1142 LocalFree(array.cast());
1143 }
1144 }
1145 }
1146 }
1147
1148 fn set_acl(file: &File, sid: PSID, access: u32, inheritance: u32) -> io::Result<()> {
1149 edit_acl(file, sid, access, inheritance, GRANT_ACCESS)
1150 }
1151 /// Edit exactly one SID's allow entries on this pinned handle. Read the stored
1152 /// descriptor with GetKernelObjectSecurity: GetSecurityInfo can normalize a
1153 /// child's inherited ACEs/control while its parent has a grant. Writing that
1154 /// view back freezes the normalized state. The exact kernel reader/writer pair
1155 /// preserves every other ACE and never propagates changes to child objects.
1156 fn edit_acl(
1157 file: &File,
1158 sid: PSID,
1159 access: u32,
1160 inheritance: u32,
1161 mode: windows_sys::Win32::Security::Authorization::ACCESS_MODE,
1162 ) -> io::Result<()> {
1163 use windows_sys::Win32::Security::{
1164 ACCESS_ALLOWED_ACE, ACE_HEADER, AddAccessAllowedAceEx, AddAce, GetAce, GetLengthSid,
1165 INHERITED_ACE, InitializeAcl, InitializeSecurityDescriptor, SE_DACL_AUTO_INHERIT_REQ,
1166 SE_DACL_AUTO_INHERITED, SE_DACL_PROTECTED, SECURITY_DESCRIPTOR, SetKernelObjectSecurity,
1167 SetSecurityDescriptorControl, SetSecurityDescriptorDacl,
1168 };
1169 use windows_sys::Win32::System::SystemServices::{
1170 ACCESS_ALLOWED_ACE_TYPE, SECURITY_DESCRIPTOR_REVISION,
1171 };
1172 // The owning grant/retirement entrypoint holds ACL_EDITS while opening
1173 // and editing its exact objects, including all shared pinned ancestors.
1174 let grant = mode == GRANT_ACCESS;
1175 if !grant && mode != REVOKE_ACCESS {
1176 return Err(io::Error::other("unsupported ACL edit mode"));
1177 }
1178 let descriptor = RawDacl::read(file)?;
1179 let old_acl = descriptor.acl();
1180 let preserved = descriptor.control & (SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED);
1181 let old = std::ptr::NonNull::new(old_acl)
1182 .ok_or_else(|| io::Error::other("refusing to replace an unrestricted DACL"))?;
1183 let (acl_revision, acl_size, ace_count) = {
1184 // SAFETY: RawDacl validated this DACL inside its owned descriptor.
1185 let acl = unsafe { old.as_ref() };
1186 (
1187 u32::from(acl.AclRevision),
1188 usize::from(acl.AclSize),
1189 acl.AceCount,
1190 )
1191 };
1192 let added = if grant {
1193 size_of::<ACCESS_ALLOWED_ACE>() - size_of::<u32>() + unsafe { GetLengthSid(sid) } as usize
1194 } else {
1195 0
1196 };
1197 let size = acl_size + added;
1198 if size > 0xFFFC {
1199 return Err(io::Error::other("edited DACL exceeds the ACL size limit"));
1200 }
1201 // ACLs must be DWORD aligned.
1202 let mut storage = vec![0_u32; size.div_ceil(size_of::<u32>())];
1203 let new_acl = storage.as_mut_ptr().cast::<ACL>();
1204 if unsafe {
1205 InitializeAcl(
1206 new_acl,
1207 (storage.len() * size_of::<u32>()) as u32,
1208 acl_revision,
1209 )
1210 } == 0
1211 {
1212 return Err(io::Error::last_os_error());
1213 }
1214 let mut merged = access;
1215 let mut inserted = !grant;
1216 for index in 0..u32::from(ace_count) {
1217 let mut ace = null_mut();
1218 if unsafe { GetAce(old_acl, index, &mut ace) } == 0 {
1219 return Err(io::Error::last_os_error());
1220 }
1221 let ace = std::ptr::NonNull::new(ace)
1222 .ok_or_else(|| io::Error::other("GetAce returned no entry"))?;
1223 // SAFETY: GetAce returned a pointer to an entry inside the live DACL.
1224 let header = unsafe { ace.cast::<ACE_HEADER>().as_ref() };
1225 let flags = u32::from(header.AceFlags);
1226 let inherited = flags & INHERITED_ACE != 0;
1227 // Only ACCESS_ALLOWED_ACE carries its SID at SidStart.
1228 let ours = u32::from(header.AceType) == ACCESS_ALLOWED_ACE_TYPE
1229 && usize::from(header.AceSize) >= size_of::<ACCESS_ALLOWED_ACE>()
1230 && unsafe {
1231 EqualSid(
1232 std::ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>().as_ptr()).SidStart)
1233 .cast(),
1234 sid,
1235 )
1236 } != 0;
1237 if ours && grant && !inherited && flags == inheritance {
1238 // GRANT_ACCESS semantics: combine with the existing explicit grant.
1239 merged |= unsafe { ace.cast::<ACCESS_ALLOWED_ACE>().as_ref().Mask };
1240 continue;
1241 }
1242 if ours && !grant {
1243 // Retirement removes explicit entries and the stale inherited
1244 // copies left after the parent's grant was revoked first.
1245 continue;
1246 }
1247 if !inserted && inherited {
1248 // Explicit entries precede inherited ones.
1249 if unsafe { AddAccessAllowedAceEx(new_acl, acl_revision, inheritance, merged, sid) }
1250 == 0
1251 {
1252 return Err(io::Error::last_os_error());
1253 }
1254 inserted = true;
1255 }
1256 if unsafe {
1257 AddAce(
1258 new_acl,
1259 acl_revision,
1260 u32::MAX,
1261 ace.as_ptr(),
1262 u32::from(header.AceSize),
1263 )
1264 } == 0
1265 {
1266 return Err(io::Error::last_os_error());
1267 }
1268 }
1269 if !inserted
1270 && unsafe { AddAccessAllowedAceEx(new_acl, acl_revision, inheritance, merged, sid) } == 0
1271 {
1272 return Err(io::Error::last_os_error());
1273 }
1274 let mut security: SECURITY_DESCRIPTOR = unsafe { zeroed() };
1275 let security_ptr = (&mut security as *mut SECURITY_DESCRIPTOR).cast();
1276 // NTFS stores this descriptor as given, with no inheritance merge. The
1277 // kernel keeps SE_DACL_AUTO_INHERITED only when SE_DACL_AUTO_INHERIT_REQ
1278 // accompanies it, so request it exactly when the object already had it;
1279 // otherwise the object would silently become a legacy DACL.
1280 let requested = preserved
1281 | if preserved & SE_DACL_AUTO_INHERITED != 0 {
1282 SE_DACL_AUTO_INHERIT_REQ
1283 } else {
1284 0
1285 };
1286 if unsafe { InitializeSecurityDescriptor(security_ptr, SECURITY_DESCRIPTOR_REVISION) } == 0
1287 || unsafe { SetSecurityDescriptorDacl(security_ptr, 1, new_acl, 0) } == 0
1288 || unsafe {
1289 SetSecurityDescriptorControl(
1290 security_ptr,
1291 SE_DACL_AUTO_INHERIT_REQ | SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED,
1292 requested,
1293 )
1294 } == 0
1295 || unsafe {
1296 SetKernelObjectSecurity(
1297 file.as_raw_handle(),
1298 DACL_SECURITY_INFORMATION,
1299 security_ptr,
1300 )
1301 } == 0
1302 {
1303 return Err(io::Error::last_os_error());
1304 }
1305 drop(descriptor);
1306 let (still_granted, control) = read_back_dacl(file, sid)?;
1307 // The raw descriptor and exact kernel write preserve both bits on files
1308 // and directories, without re-deriving or propagating inherited entries.
1309 let checked = SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED;
1310 if control & checked != preserved & checked {
1311 return Err(io::Error::other(
1312 "edited DACL changed its inheritance control bits",
1313 ));
1314 }
1315 if !grant && still_granted {
1316 return Err(io::Error::other(
1317 "retired profile SID is still present after ACL retirement",
1318 ));
1319 }
1320 Ok(())
1321 }
1322
1323 /// Read back the object's DACL: whether any allow entry names `sid`, and the
1324 /// descriptor's control bits.
1325 fn read_back_dacl(file: &File, sid: PSID) -> io::Result<(bool, u16)> {
1326 use windows_sys::Win32::Security::{ACCESS_ALLOWED_ACE, ACE_HEADER, GetAce};
1327 use windows_sys::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
1328 let descriptor = RawDacl::read(file)?;
1329 let acl = descriptor.acl();
1330 let control = descriptor.control;
1331 let dacl = std::ptr::NonNull::new(acl)
1332 .ok_or_else(|| io::Error::other("refusing to inspect an unrestricted DACL"))?;
1333 // SAFETY: RawDacl validated this DACL inside its owned descriptor.
1334 for index in 0..u32::from(unsafe { dacl.as_ref().AceCount }) {
1335 let mut ace = null_mut();
1336 if unsafe { GetAce(acl, index, &mut ace) } == 0 {
1337 return Err(io::Error::last_os_error());
1338 }
1339 let ace = std::ptr::NonNull::new(ace)
1340 .ok_or_else(|| io::Error::other("GetAce returned no entry"))?;
1341 // SAFETY: GetAce returned a pointer to an entry inside the live DACL.
1342 let header = unsafe { ace.cast::<ACE_HEADER>().as_ref() };
1343 if u32::from(header.AceType) == ACCESS_ALLOWED_ACE_TYPE
1344 && usize::from(header.AceSize) >= size_of::<ACCESS_ALLOWED_ACE>()
1345 && unsafe {
1346 EqualSid(
1347 std::ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>().as_ptr()).SidStart)
1348 .cast(),
1349 sid,
1350 )
1351 } != 0
1352 {
1353 return Ok((true, control));
1354 }
1355 }
1356 Ok((false, control))
1357 }
1358
1359 /// Owned DWORD-aligned storage for a bounded self-relative kernel descriptor.
1360 /// The ACL offset is validated once and never outlives this allocation.
1361 struct RawDacl {
1362 storage: Vec<u32>,
1363 acl_offset: usize,
1364 control: u16,
1365 }
1366 impl RawDacl {
1367 fn read(file: &File) -> io::Result<Self> {
1368 use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
1369 use windows_sys::Win32::Security::{GetKernelObjectSecurity, SECURITY_DESCRIPTOR_RELATIVE};
1370 let mut required = 0_u32;
1371 // SAFETY: a zero-length size query writes only `required`.
1372 if unsafe {
1373 GetKernelObjectSecurity(
1374 file.as_raw_handle(),
1375 DACL_SECURITY_INFORMATION,
1376 null_mut(),
1377 0,
1378 &mut required,
1379 )
1380 } == 0
1381 {
1382 let error = io::Error::last_os_error();
1383 if error.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) {
1384 return Err(error);
1385 }
1386 }
1387 // An external ACL writer can change the required size between calls.
1388 // Bound both the allocation and the number of attempts; fail closed.
1389 for _ in 0..3 {
1390 if !(size_of::<SECURITY_DESCRIPTOR_RELATIVE>()..=1024 * 1024)
1391 .contains(&(required as usize))
1392 {
1393 return Err(io::Error::other(
1394 "kernel security descriptor exceeds size bounds",
1395 ));
1396 }
1397 let capacity = required;
1398 let mut storage = vec![0_u32; (capacity as usize).div_ceil(size_of::<u32>())];
1399 // SAFETY: storage is aligned, owned, and at least capacity bytes.
1400 if unsafe {
1401 GetKernelObjectSecurity(
1402 file.as_raw_handle(),
1403 DACL_SECURITY_INFORMATION,
1404 storage.as_mut_ptr().cast(),
1405 capacity,
1406 &mut required,
1407 )
1408 } == 0
1409 {
1410 let error = io::Error::last_os_error();
1411 if error.raw_os_error() == Some(ERROR_INSUFFICIENT_BUFFER as i32)
1412 && required > capacity
1413 {
1414 continue;
1415 }
1416 return Err(error);
1417 }
1418 if required > capacity {
1419 return Err(io::Error::other(
1420 "kernel security descriptor exceeds its buffer",
1421 ));
1422 }
1423 return Self::from_storage(storage, required as usize);
1424 }
1425 Err(io::Error::other(
1426 "kernel security descriptor repeatedly changed size",
1427 ))
1428 }
1429
1430 fn from_storage(mut storage: Vec<u32>, length: usize) -> io::Result<Self> {
1431 use windows_sys::Win32::Security::{
1432 ACCESS_ALLOWED_ACE, ACE_HEADER, GetAce, GetSecurityDescriptorControl,
1433 GetSecurityDescriptorDacl, IsValidAcl, IsValidSid, SE_SELF_RELATIVE,
1434 SECURITY_DESCRIPTOR_RELATIVE, SID,
1435 };
1436 use windows_sys::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
1437 if length < size_of::<SECURITY_DESCRIPTOR_RELATIVE>()
1438 || length > storage.len() * size_of::<u32>()
1439 {
1440 return Err(io::Error::other("kernel security descriptor is truncated"));
1441 }
1442 let descriptor = storage.as_mut_ptr().cast();
1443 let mut control = 0_u16;
1444 let mut revision = 0_u32;
1445 // SAFETY: the aligned allocation contains the complete descriptor header.
1446 if unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) } == 0 {
1447 return Err(io::Error::last_os_error());
1448 }
1449 if control & SE_SELF_RELATIVE == 0 {
1450 return Err(io::Error::other(
1451 "kernel security descriptor is not self-relative",
1452 ));
1453 }
1454 let mut present = 0;
1455 let mut defaulted = 0;
1456 let mut acl: *mut ACL = null_mut();
1457 // SAFETY: only the validated self-relative header is read here. Bound
1458 // the returned DACL pointer before dereferencing any of its bytes.
1459 if unsafe { GetSecurityDescriptorDacl(descriptor, &mut present, &mut acl, &mut defaulted) }
1460 == 0
1461 {
1462 return Err(io::Error::last_os_error());
1463 }
1464 if present == 0 || acl.is_null() {
1465 return Err(io::Error::other("refusing an absent or unrestricted DACL"));
1466 }
1467 let acl_offset = (acl as usize)
1468 .checked_sub(descriptor as usize)
1469 .filter(|offset| {
1470 *offset >= size_of::<SECURITY_DESCRIPTOR_RELATIVE>()
1471 && *offset % size_of::<u32>() == 0
1472 && offset
1473 .checked_add(size_of::<ACL>())
1474 .is_some_and(|end| end <= length)
1475 })
1476 .ok_or_else(|| io::Error::other("DACL header is outside its descriptor"))?;
1477 // Read through owned storage at the checked offset, not through the
1478 // out-pointer Windows returned: same address, owned provenance.
1479 // SAFETY: acl_offset + size_of::<ACL>() <= length <= storage bytes.
1480 let acl = unsafe { storage.as_ptr().cast::<u8>().add(acl_offset) }
1481 .cast_mut()
1482 .cast::<ACL>();
1483 // SAFETY: the aligned DACL header lies completely inside storage.
1484 let acl_size = usize::from(unsafe { (*acl).AclSize });
1485 if acl_size < size_of::<ACL>()
1486 || acl_offset
1487 .checked_add(acl_size)
1488 .is_none_or(|end| end > length)
1489 {
1490 return Err(io::Error::other("DACL is outside its descriptor"));
1491 }
1492 // SAFETY: the entire claimed ACL extent is bounded by owned storage.
1493 // IsValidAcl checks revision and whether its ACEs fit that extent.
1494 if unsafe { IsValidAcl(acl) } == 0 {
1495 return Err(io::Error::other(
1496 "kernel descriptor contains an invalid ACL",
1497 ));
1498 }
1499 for index in 0..u32::from(unsafe { (*acl).AceCount }) {
1500 let mut entry = null_mut();
1501 if unsafe { GetAce(acl, index, &mut entry) } == 0 {
1502 return Err(io::Error::last_os_error());
1503 }
1504 let entry_offset = (entry as usize)
1505 .checked_sub(acl as usize)
1506 .filter(|offset| {
1507 *offset >= size_of::<ACL>()
1508 && *offset % size_of::<u32>() == 0
1509 && offset
1510 .checked_add(size_of::<ACE_HEADER>())
1511 .is_some_and(|end| end <= acl_size)
1512 })
1513 .ok_or_else(|| io::Error::other("ACE header is outside its DACL"))?;
1514 // Same rule: address the ACE from the storage-derived ACL.
1515 // SAFETY: entry_offset + size_of::<ACE_HEADER>() <= acl_size.
1516 let entry = unsafe { acl.cast::<u8>().add(entry_offset) };
1517 // SAFETY: the complete, aligned ACE header is inside the DACL.
1518 let header = unsafe { &*entry.cast::<ACE_HEADER>() };
1519 let entry_size = usize::from(header.AceSize);
1520 if entry_size < size_of::<ACE_HEADER>() || entry_offset + entry_size > acl_size {
1521 return Err(io::Error::other("ACE is outside its DACL"));
1522 }
1523 if u32::from(header.AceType) == ACCESS_ALLOWED_ACE_TYPE {
1524 // IsValidAcl does not validate SIDs. Bound the whole SID before
1525 // IsValidSid or either caller's EqualSid can inspect it.
1526 let sid_offset = std::mem::offset_of!(ACCESS_ALLOWED_ACE, SidStart);
1527 let sid_header = std::mem::offset_of!(SID, SubAuthority);
1528 if entry_size < sid_offset + sid_header {
1529 return Err(io::Error::other("allow ACE has a truncated SID header"));
1530 }
1531 // SAFETY: the complete SID header is inside this ACE.
1532 let sid = unsafe { entry.cast::<u8>().add(sid_offset) };
1533 let count = usize::from(unsafe { *sid.add(1) });
1534 if sid_header + count * size_of::<u32>() > entry_size - sid_offset
1535 || unsafe { IsValidSid(sid.cast()) } == 0
1536 {
1537 return Err(io::Error::other(
1538 "allow ACE has an invalid or truncated SID",
1539 ));
1540 }
1541 }
1542 }
1543 Ok(Self {
1544 storage,
1545 acl_offset,
1546 control,
1547 })
1548 }
1549
1550 fn acl(&self) -> *mut ACL {
1551 // SAFETY: from_storage validated the offset, alignment, and ACL extent;
1552 // storage stays owned and unchanged while the returned pointer is used.
1553 unsafe {
1554 self.storage
1555 .as_ptr()
1556 .cast::<u8>()
1557 .add(self.acl_offset)
1558 .cast_mut()
1559 .cast()
1560 }
1561 }
1562 }
1563
1564 #[cfg(test)]
1565 struct LocalAllocation(*mut core::ffi::c_void);
1566 #[cfg(test)]
1567 impl Drop for LocalAllocation {
1568 fn drop(&mut self) {
1569 unsafe {
1570 LocalFree(self.0);
1571 }
1572 }
1573 }
1574
1575 struct Attributes {
1576 bytes: Vec<usize>,
1577 initialized: bool,
1578 }
1579 impl Attributes {
1580 fn new(count: u32) -> io::Result<Self> {
1581 let mut size = 0;
1582 unsafe {
1583 InitializeProcThreadAttributeList(null_mut(), count, 0, &mut size);
1584 }
1585 if size == 0 {
1586 return Err(io::Error::last_os_error());
1587 }
1588 let mut value = Self {
1589 bytes: vec![0; size.div_ceil(size_of::<usize>())],
1590 initialized: false,
1591 };
1592 if unsafe { InitializeProcThreadAttributeList(value.ptr(), count, 0, &mut size) } == 0 {
1593 return Err(io::Error::last_os_error());
1594 }
1595 value.initialized = true;
1596 Ok(value)
1597 }
1598 fn ptr(&mut self) -> *mut core::ffi::c_void {
1599 self.bytes.as_mut_ptr().cast()
1600 }
1601 fn set<T>(&mut self, attribute: u32, value: &T) -> io::Result<()> {
1602 self.set_raw(attribute, (value as *const T).cast(), size_of::<T>())
1603 }
1604 fn set_slice<T>(&mut self, attribute: u32, values: &[T]) -> io::Result<()> {
1605 self.set_raw(
1606 attribute,
1607 values.as_ptr().cast(),
1608 std::mem::size_of_val(values),
1609 )
1610 }
1611 fn set_raw(
1612 &mut self,
1613 attribute: u32,
1614 value: *const core::ffi::c_void,
1615 size: usize,
1616 ) -> io::Result<()> {
1617 if unsafe {
1618 UpdateProcThreadAttribute(
1619 self.ptr(),
1620 0,
1621 attribute as usize,
1622 value,
1623 size,
1624 null_mut(),
1625 null(),
1626 )
1627 } == 0
1628 {
1629 return Err(io::Error::last_os_error());
1630 }
1631 Ok(())
1632 }
1633 }
1634 impl Drop for Attributes {
1635 fn drop(&mut self) {
1636 if self.initialized {
1637 unsafe {
1638 DeleteProcThreadAttributeList(self.ptr());
1639 }
1640 }
1641 }
1642 }
1643
1644 fn pipe(output: bool, overlapped: bool) -> io::Result<(OwnedHandle, OwnedHandle)> {
1645 let name = wide(OsStr::new(&format!(
1646 r"\\.\pipe\Codewhale.Native.{}",
1647 uuid::Uuid::new_v4()
1648 )))?;
1649 // Parent endpoints are overlapped/noninheritable; exactly the synchronous
1650 // child endpoints are included in the process attribute HANDLE_LIST.
1651 let handle = unsafe {
1652 CreateNamedPipeW(
1653 name.as_ptr(),
1654 (if output {
1655 PIPE_ACCESS_INBOUND
1656 } else {
1657 PIPE_ACCESS_OUTBOUND
1658 }) | if overlapped { FILE_FLAG_OVERLAPPED } else { 0 }
1659 | FILE_FLAG_FIRST_PIPE_INSTANCE,
1660 PIPE_WAIT,
1661 1,
1662 8192,
1663 8192,
1664 0,
1665 null(),
1666 )
1667 };
1668 if handle == INVALID_HANDLE_VALUE {
1669 return Err(io::Error::last_os_error());
1670 }
1671 let parent = unsafe { OwnedHandle::from_raw_handle(handle) };
1672 let security = SECURITY_ATTRIBUTES {
1673 nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
1674 lpSecurityDescriptor: null_mut(),
1675 bInheritHandle: 1,
1676 };
1677 let handle = unsafe {
1678 CreateFileW(
1679 name.as_ptr(),
1680 if output {
1681 FILE_GENERIC_WRITE
1682 } else {
1683 FILE_GENERIC_READ
1684 },
1685 0,
1686 &security,
1687 OPEN_EXISTING,
1688 0,
1689 null_mut(),
1690 )
1691 };
1692 if handle == INVALID_HANDLE_VALUE {
1693 return Err(io::Error::last_os_error());
1694 }
1695 let child = unsafe { OwnedHandle::from_raw_handle(handle) };
1696 if unsafe { ConnectNamedPipe(parent.as_raw_handle(), null_mut()) } == 0
1697 && unsafe { GetLastError() } != ERROR_PIPE_CONNECTED
1698 {
1699 return Err(io::Error::last_os_error());
1700 }
1701 Ok((parent, child))
1702 }
1703
1704 fn wide(value: &OsStr) -> io::Result<Vec<u16>> {
1705 let mut value: Vec<_> = value.encode_wide().collect();
1706 if value.contains(&0) {
1707 return Err(io::Error::new(
1708 io::ErrorKind::InvalidInput,
1709 "Windows argv/env contains NUL",
1710 ));
1711 }
1712 value.push(0);
1713 Ok(value)
1714 }
1715
1716 fn sandbox_args(args: &[String], runtime_dir: &Path) -> Vec<String> {
1717 // An LPAC cannot open the NUL device. Both the host and its probe
1718 // descendant read the empty config in the already-granted runtime copy.
1719 let empty_config = format!("--config={}", runtime_dir.join(EMPTY_BUN_CONFIG).display());
1720 args.iter()
1721 .map(|arg| {
1722 if arg == "--config=NUL" {
1723 empty_config.clone()
1724 } else {
1725 arg.clone()
1726 }
1727 })
1728 .collect()
1729 }
1730
1731 fn command_line(program: &OsStr, args: &[String]) -> io::Result<Vec<u16>> {
1732 // The documented CommandLineToArgvW/MS CRT quote+backslash rules. An
1733 // explicit application path means PATH/first-token parsing is never authority.
1734 let mut result = Vec::new();
1735 for arg in std::iter::once(program).chain(args.iter().map(OsStr::new)) {
1736 let units = wide(arg)?;
1737 if !result.is_empty() {
1738 result.push(b' ' as u16);
1739 }
1740 result.push(b'"' as u16);
1741 let mut slashes = 0;
1742 for unit in units.into_iter().take_while(|unit| *unit != 0) {
1743 if unit == b'\\' as u16 {
1744 slashes += 1;
1745 continue;
1746 }
1747 if unit == b'"' as u16 {
1748 result.extend(std::iter::repeat_n(b'\\' as u16, slashes * 2 + 1));
1749 } else {
1750 result.extend(std::iter::repeat_n(b'\\' as u16, slashes));
1751 }
1752 slashes = 0;
1753 result.push(unit);
1754 }
1755 result.extend(std::iter::repeat_n(b'\\' as u16, slashes * 2));
1756 result.push(b'"' as u16);
1757 }
1758 result.push(0);
1759 Ok(result)
1760 }
1761
1762 fn environment_block(env: &[(OsString, OsString)]) -> io::Result<Vec<u16>> {
1763 let mut entries = env.iter().collect::<Vec<_>>();
1764 entries.sort_by_key(|(key, _)| key.to_string_lossy().to_uppercase());
1765 let mut result = Vec::new();
1766 for (key, value) in entries {
1767 let key = wide(key)?;
1768 if key.len() == 1 || key.contains(&(b'=' as u16)) {
1769 return Err(io::Error::other("invalid Windows environment key"));
1770 }
1771 result.extend(&key[..key.len() - 1]);
1772 result.push(b'=' as u16);
1773 result.extend(wide(value)?);
1774 }
1775 if result.is_empty() {
1776 result.push(0);
1777 }
1778 result.push(0);
1779 Ok(result)
1780 }
1781
1782 #[cfg(test)]
1783 #[path = "windows_tests.rs"]
1784 mod tests;
1785
1785 lines RUST