1#![allow(unsafe_code)]
4
5#[cfg(target_os = "linux")]
6pub(crate) mod temporary {
7 #![allow(unsafe_code)]
9
10 use std::ffi::{CString, OsStr};
11 use std::fs::File;
12 use std::io;
13 use std::os::fd::{AsRawFd, FromRawFd};
14 use std::os::unix::ffi::OsStrExt as _;
15 use std::path::{Component, Path};
16
17 fn name(value: &OsStr) -> io::Result<CString> {
18 CString::new(value.as_bytes())
19 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in path"))
20 }
21
22 fn open_at(parent: &File, leaf: &CString, flags: i32) -> io::Result<File> {
23 let fd = unsafe {
25 libc::openat(
26 parent.as_raw_fd(),
27 leaf.as_ptr(),
28 flags | libc::O_CLOEXEC | libc::O_NOFOLLOW,
29 )
30 };
31 if fd < 0 {
32 return Err(io::Error::last_os_error());
33 }
34 Ok(unsafe { File::from_raw_fd(fd) })
36 }
37
38 pub(crate) fn directory(path: &Path) -> io::Result<File> {
39 if !path.is_absolute() {
40 return Err(io::Error::new(io::ErrorKind::InvalidInput, "absolute directory required"));
41 }
42 let mut dir = File::open("/")?;
43 for component in path.components() {
44 match component {
45 Component::RootDir => {}
46 Component::Normal(part) => {
47 dir = open_at(&dir, &name(part)?, libc::O_RDONLY | libc::O_DIRECTORY)?;
48 }
49 _ => {
50 return Err(io::Error::new(
51 io::ErrorKind::InvalidInput,
52 "noncanonical directory component",
53 ))
54 }
55 }
56 }
57 Ok(dir)
58 }
59
60 pub(crate) fn directory_entries(directory: &File) -> io::Result<std::fs::ReadDir> {
63 std::fs::read_dir(format!("/proc/self/fd/{}", directory.as_raw_fd()))
64 }
65
66 pub(crate) fn directory_child(directory: &File, child: &OsStr) -> io::Result<File> {
67 open_at(directory, &name(child)?, libc::O_RDONLY | libc::O_DIRECTORY)
68 }
69
70 pub(crate) struct Opened {
71 pub(crate) file: File,
72 parent: File,
73 leaf: CString,
74 }
75
76 pub(crate) fn open(path: &Path) -> io::Result<Opened> {
77 let parent = directory(
78 path.parent()
79 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "parent required"))?,
80 )?;
81 let leaf =
82 name(path.file_name().ok_or_else(|| {
83 io::Error::new(io::ErrorKind::InvalidInput, "filename required")
84 })?)?;
85 let file = open_at(&parent, &leaf, libc::O_RDONLY | libc::O_NONBLOCK)?;
86 if !file.metadata()?.is_file() {
87 return Err(io::Error::new(io::ErrorKind::InvalidInput, "regular file required"));
88 }
89 Ok(Opened { file, parent, leaf })
90 }
91
92 fn rename_noreplace(from: &File, old: &CString, to: &File, new: &CString) -> io::Result<()> {
93 if unsafe {
96 libc::renameat2(
97 from.as_raw_fd(),
98 old.as_ptr(),
99 to.as_raw_fd(),
100 new.as_ptr(),
101 libc::RENAME_NOREPLACE,
102 )
103 } == 0
104 {
105 Ok(())
106 } else {
107 Err(io::Error::last_os_error())
108 }
109 }
110
111 fn unlink(parent: &File, leaf: &CString, flags: i32) -> io::Result<()> {
112 if unsafe { libc::unlinkat(parent.as_raw_fd(), leaf.as_ptr(), flags) } == 0 {
114 Ok(())
115 } else {
116 Err(io::Error::last_os_error())
117 }
118 }
119
120 pub(crate) struct Claim {
121 parent: File,
122 original: CString,
123 quarantine: File,
124 quarantine_name: CString,
125 captured_name: CString,
126 captured: bool,
127 }
128
129 impl Opened {
130 pub(crate) fn claim(&self) -> io::Result<Claim> {
131 let parent = self.parent.try_clone()?;
132 let quarantine_name =
133 CString::new(format!(".winx-release-{:032x}", rand::random::<u128>()))
134 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
135 if unsafe { libc::mkdirat(self.parent.as_raw_fd(), quarantine_name.as_ptr(), 0o700) }
138 != 0
139 {
140 return Err(io::Error::last_os_error());
141 }
142 let quarantine =
143 match open_at(&self.parent, &quarantine_name, libc::O_RDONLY | libc::O_DIRECTORY) {
144 Ok(directory) => directory,
145 Err(error) => {
146 let _ = unlink(&self.parent, &quarantine_name, libc::AT_REMOVEDIR);
147 return Err(error);
148 }
149 };
150 let mut claim = Claim {
151 parent,
152 original: self.leaf.clone(),
153 quarantine,
154 quarantine_name,
155 captured_name: c"artifact".to_owned(),
156 captured: false,
157 };
158 rename_noreplace(&self.parent, &self.leaf, &claim.quarantine, &claim.captured_name)?;
159 claim.captured = true;
160 Ok(claim)
161 }
162 }
163
164 impl Claim {
165 pub(crate) fn open_captured(&self) -> io::Result<File> {
166 open_at(&self.quarantine, &self.captured_name, libc::O_RDONLY | libc::O_NONBLOCK)
167 }
168
169 pub(crate) fn recovery_name(&self) -> String {
170 format!("{}/artifact", self.quarantine_name.to_string_lossy())
171 }
172
173 pub(crate) fn remove(mut self) -> io::Result<()> {
174 unlink(&self.quarantine, &self.captured_name, 0)?;
175 self.captured = false;
176 Ok(())
177 }
178 }
179
180 impl Drop for Claim {
181 fn drop(&mut self) {
182 if self.captured {
183 if rename_noreplace(
186 &self.quarantine,
187 &self.captured_name,
188 &self.parent,
189 &self.original,
190 )
191 .is_err()
192 {
193 return;
194 }
195 }
196 let _ = unlink(&self.parent, &self.quarantine_name, libc::AT_REMOVEDIR);
197 }
198 }
199
200 #[cfg(test)]
201 mod tests {
202 use super::*;
203 #[test]
204 fn restoration_never_overwrites_a_concurrent_original_name() -> io::Result<()> {
205 let directory = tempfile::tempdir()?;
206 let root = directory.path().canonicalize()?;
207 let path = root.join("helper");
208 std::fs::write(&path, "captured")?;
209 let opened = open(&path)?;
210 let claim = opened.claim()?;
211 let recovery = root.join(claim.recovery_name());
212 std::fs::write(&path, "replacement")?;
213 drop(claim);
214 assert_eq!(std::fs::read_to_string(&path)?, "replacement");
215 assert_eq!(std::fs::read_to_string(recovery)?, "captured");
216 Ok(())
217 }
218 }
219}
220
221#[cfg(unix)]
222pub(crate) mod unix {
223 use std::io;
224
225 pub(crate) fn effective_uid() -> u32 {
226 unsafe { libc::geteuid() }
228 }
229
230 pub(crate) fn try_lock_exclusive(file: &std::fs::File) -> io::Result<bool> {
234 use std::os::unix::io::AsRawFd as _;
235 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0 {
237 return Ok(true);
238 }
239 let error = io::Error::last_os_error();
240 if error.kind() == io::ErrorKind::WouldBlock {
241 return Ok(false);
242 }
243 Err(error)
244 }
245
246 pub(crate) fn process_exists(pid: u32) -> bool {
249 let Ok(pid) = i32::try_from(pid) else { return false };
250 if pid <= 1 {
251 return false;
252 }
253 if unsafe { libc::kill(pid, 0) } == 0 {
256 return true;
257 }
258 io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
259 }
260
261 pub(crate) fn signal_process(pid: u32, signal: i32) -> io::Result<()> {
264 let pid = i32::try_from(pid)
265 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?;
266 signal_raw(pid, signal)
267 }
268
269 pub(crate) fn signal_raw(pid: i32, signal: i32) -> io::Result<()> {
270 if unsafe { libc::kill(pid, signal) } == 0 {
272 return Ok(());
273 }
274 let error = io::Error::last_os_error();
275 if error.raw_os_error() == Some(libc::ESRCH) {
276 Ok(())
277 } else {
278 Err(error)
279 }
280 }
281
282 pub(crate) fn owned_process_group(pid: u32) -> Option<i32> {
284 let pid = i32::try_from(pid).ok()?;
285 if pid <= 1 {
286 return None;
287 }
288 let group = unsafe { libc::getpgid(pid) };
291 (group == pid).then_some(group)
292 }
293
294 pub(crate) fn signal_group(group: i32, signal: i32) -> io::Result<()> {
295 if group <= 1 {
296 return Err(io::Error::new(
297 io::ErrorKind::InvalidInput,
298 "process group must be greater than 1",
299 ));
300 }
301 signal_raw(-group, signal)
302 }
303
304 pub(crate) fn configure_detached(command: &mut std::process::Command) {
307 use std::os::unix::process::CommandExt as _;
308 unsafe {
311 command.pre_exec(create_session);
312 }
313 }
314
315 fn create_session() -> io::Result<()> {
318 if unsafe { libc::setsid() } == -1 {
320 Err(io::Error::last_os_error())
321 } else {
322 Ok(())
323 }
324 }
325
326 #[cfg(test)]
327 mod tests {
328 #[test]
329 fn current_process_exists_and_reserved_ids_are_rejected() {
330 assert!(super::process_exists(std::process::id()));
331 assert!(!super::process_exists(0));
332 assert!(!super::process_exists(1));
333 }
334 }
335}
336
337pub fn harden_process() {
342 #[cfg(windows)]
343 windows::restrict_dll_search_path();
344}
345
346pub(crate) fn process_exists(pid: u32) -> bool {
347 #[cfg(unix)]
348 {
349 unix::process_exists(pid)
350 }
351 #[cfg(windows)]
352 {
353 windows::process_exists(pid)
354 }
355 #[cfg(not(any(unix, windows)))]
356 {
357 let _ = pid;
358 false
359 }
360}
361
362#[cfg(windows)]
363pub(crate) mod windows {
364 use std::ffi::c_void;
365 use std::io;
366
367 use windows_sys::Win32::Foundation::{
368 CloseHandle, LocalFree, SetHandleInformation, ERROR_ACCESS_DENIED, HANDLE,
369 HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, STILL_ACTIVE,
370 };
371 use windows_sys::Win32::Security::Authorization::{
372 ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
373 SDDL_REVISION_1,
374 };
375 use windows_sys::Win32::Security::{
376 GetTokenInformation, TokenElevation, TokenUser, SECURITY_ATTRIBUTES, TOKEN_ELEVATION,
377 TOKEN_QUERY, TOKEN_USER,
378 };
379 use windows_sys::Win32::System::Console::{
380 GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
381 };
382 use windows_sys::Win32::System::JobObjects::{
383 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
384 SetInformationJobObject, TerminateJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
385 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
386 };
387 use windows_sys::Win32::System::LibraryLoader::{
388 SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
389 };
390 use windows_sys::Win32::System::Threading::{
391 GetCurrentProcess, GetExitCodeProcess, OpenProcess, OpenProcessToken, TerminateProcess,
392 CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS,
393 PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_QUOTA, PROCESS_TERMINATE,
394 };
395
396 struct OwnedHandle(HANDLE);
398
399 unsafe impl Send for OwnedHandle {}
402 unsafe impl Sync for OwnedHandle {}
404
405 impl Drop for OwnedHandle {
406 fn drop(&mut self) {
407 unsafe {
410 CloseHandle(self.0);
411 }
412 }
413 }
414
415 fn open_process(pid: u32, access: u32) -> io::Result<OwnedHandle> {
416 let handle = unsafe { OpenProcess(access, 0, pid) };
419 if handle.is_null() {
420 Err(io::Error::last_os_error())
421 } else {
422 Ok(OwnedHandle(handle))
423 }
424 }
425
426 pub(crate) fn process_exists(pid: u32) -> bool {
430 if pid == 0 {
431 return false;
432 }
433 match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
434 Ok(handle) => {
435 let mut code = 0u32;
436 let queried =
438 unsafe { GetExitCodeProcess(handle.0, std::ptr::from_mut(&mut code)) } != 0;
439 queried && code == STILL_ACTIVE as u32
440 }
441 Err(error) => error.raw_os_error() == Some(ERROR_ACCESS_DENIED.cast_signed()),
442 }
443 }
444
445 pub(crate) fn terminate_process(pid: u32) -> io::Result<()> {
448 let handle = match open_process(pid, PROCESS_TERMINATE) {
449 Ok(handle) => handle,
450 Err(error) if !process_exists(pid) => {
451 let _ = error;
452 return Ok(());
453 }
454 Err(error) => return Err(error),
455 };
456 if unsafe { TerminateProcess(handle.0, 1) } == 0 {
458 let error = io::Error::last_os_error();
459 if process_exists(pid) {
460 return Err(error);
461 }
462 }
463 Ok(())
464 }
465
466 pub(crate) fn restrict_dll_search_path() {
477 if unsafe { SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) } == 0 {
480 tracing::warn!(
481 error = %io::Error::last_os_error(),
482 "could not restrict the DLL search path"
483 );
484 }
485 }
486
487 pub(crate) struct PipeSecurity {
497 descriptor: *mut c_void,
498 attributes: SECURITY_ATTRIBUTES,
499 }
500
501 unsafe impl Send for PipeSecurity {}
504 unsafe impl Sync for PipeSecurity {}
506
507 impl Drop for PipeSecurity {
508 fn drop(&mut self) {
509 unsafe {
513 LocalFree(self.descriptor);
514 }
515 }
516 }
517
518 impl PipeSecurity {
519 pub(crate) fn owner_only() -> io::Result<Self> {
520 let sddl = owner_only_sddl()?;
521 let descriptor = parse_sddl(&sddl)?;
522 let attributes = SECURITY_ATTRIBUTES {
523 nLength: u32::try_from(std::mem::size_of::<SECURITY_ATTRIBUTES>())
524 .map_err(|_| io::Error::other("security attributes too large"))?,
525 lpSecurityDescriptor: descriptor,
526 bInheritHandle: 0,
527 };
528 Ok(Self { descriptor, attributes })
529 }
530
531 pub(crate) fn create_pipe_instance(
533 &self,
534 options: &tokio::net::windows::named_pipe::ServerOptions,
535 name: &str,
536 ) -> io::Result<tokio::net::windows::named_pipe::NamedPipeServer> {
537 let attributes: *const SECURITY_ATTRIBUTES = &raw const self.attributes;
538 unsafe {
541 options.create_with_security_attributes_raw(name, attributes.cast_mut().cast())
542 }
543 }
544 }
545
546 fn owner_only_sddl() -> io::Result<String> {
548 let token = current_process_token()?;
549 let sid = token_user_sid(&token)?;
550 let integrity = if token_is_elevated(&token)? { "HI" } else { "ME" };
551 Ok(format!("D:P(A;;GA;;;SY)(A;;GRGW;;;{sid})S:(ML;;NRNW;;;{integrity})"))
552 }
553
554 fn current_process_token() -> io::Result<OwnedHandle> {
555 let mut token: HANDLE = std::ptr::null_mut();
556 if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) } == 0 {
559 return Err(io::Error::last_os_error());
560 }
561 Ok(OwnedHandle(token))
562 }
563
564 fn token_user_sid(token: &OwnedHandle) -> io::Result<String> {
565 let mut needed = 0u32;
566 unsafe {
569 GetTokenInformation(token.0, TokenUser, std::ptr::null_mut(), 0, &raw mut needed);
570 }
571 if needed == 0 {
572 return Err(io::Error::last_os_error());
573 }
574 let mut buffer = vec![0usize; (needed as usize).div_ceil(std::mem::size_of::<usize>())];
576 if unsafe {
578 GetTokenInformation(
579 token.0,
580 TokenUser,
581 buffer.as_mut_ptr().cast(),
582 needed,
583 &raw mut needed,
584 )
585 } == 0
586 {
587 return Err(io::Error::last_os_error());
588 }
589 let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
592 let mut sid_text: *mut u16 = std::ptr::null_mut();
593 if unsafe { ConvertSidToStringSidW(user.User.Sid, &raw mut sid_text) } == 0 {
596 return Err(io::Error::last_os_error());
597 }
598 let sid = wide_to_string(sid_text);
599 unsafe {
601 LocalFree(sid_text.cast());
602 }
603 Ok(sid)
604 }
605
606 fn token_is_elevated(token: &OwnedHandle) -> io::Result<bool> {
607 let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
608 let mut needed = 0u32;
609 let size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>())
610 .map_err(|_| io::Error::other("token elevation block too large"))?;
611 if unsafe {
613 GetTokenInformation(
614 token.0,
615 TokenElevation,
616 (&raw mut elevation).cast(),
617 size,
618 &raw mut needed,
619 )
620 } == 0
621 {
622 return Err(io::Error::last_os_error());
623 }
624 Ok(elevation.TokenIsElevated != 0)
625 }
626
627 fn parse_sddl(sddl: &str) -> io::Result<*mut c_void> {
630 let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
631 let mut descriptor: *mut c_void = std::ptr::null_mut();
632 if unsafe {
635 ConvertStringSecurityDescriptorToSecurityDescriptorW(
636 wide.as_ptr(),
637 SDDL_REVISION_1,
638 &raw mut descriptor,
639 std::ptr::null_mut(),
640 )
641 } == 0
642 {
643 return Err(io::Error::last_os_error());
644 }
645 Ok(descriptor)
646 }
647
648 fn wide_to_string(text: *const u16) -> String {
649 let mut len = 0usize;
650 while unsafe { *text.add(len) } != 0 {
653 len += 1;
654 }
655 String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(text, len) })
657 }
658
659 pub(crate) fn disable_std_handle_inheritance() {
669 for id in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
670 let handle = unsafe { GetStdHandle(id) };
673 if handle.is_null() || handle == INVALID_HANDLE_VALUE {
674 continue;
675 }
676 unsafe {
680 SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
681 }
682 }
683 }
684
685 pub(crate) fn configure_detached(command: &mut std::process::Command, breakaway: bool) {
690 use std::os::windows::process::CommandExt as _;
691 disable_std_handle_inheritance();
692 let mut flags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW;
693 if breakaway {
694 flags |= CREATE_BREAKAWAY_FROM_JOB;
695 }
696 command.creation_flags(flags);
697 }
698
699 pub(crate) struct ProcessTreeJob(OwnedHandle);
702
703 impl ProcessTreeJob {
704 pub(crate) fn new() -> io::Result<Self> {
705 let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
708 if job.is_null() {
709 return Err(io::Error::last_os_error());
710 }
711 let job = OwnedHandle(job);
712 let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
715 limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
716 let size = u32::try_from(std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
717 .map_err(|_| io::Error::other("job limit block too large"))?;
718 let configured = unsafe {
720 SetInformationJobObject(
721 job.0,
722 JobObjectExtendedLimitInformation,
723 std::ptr::from_ref(&limits).cast::<c_void>(),
724 size,
725 )
726 };
727 if configured == 0 {
728 return Err(io::Error::last_os_error());
729 }
730 Ok(Self(job))
731 }
732
733 pub(crate) fn assign(&self, pid: u32) -> io::Result<()> {
734 let process = open_process(pid, PROCESS_SET_QUOTA | PROCESS_TERMINATE)?;
735 if unsafe { AssignProcessToJobObject(self.0 .0, process.0) } == 0 {
737 return Err(io::Error::last_os_error());
738 }
739 Ok(())
740 }
741
742 pub(crate) fn terminate(&self) {
743 unsafe {
745 TerminateJobObject(self.0 .0, 1);
746 }
747 }
748 }
749
750 #[cfg(test)]
751 mod tests {
752 #![allow(clippy::expect_used)]
753
754 #[test]
755 fn current_process_exists_and_pid_zero_does_not() {
756 assert!(super::process_exists(std::process::id()));
757 assert!(!super::process_exists(0));
758 }
759
760 #[test]
761 fn job_object_can_be_created_and_owns_a_child() {
762 let job = super::ProcessTreeJob::new().expect("job object");
763 let mut child = std::process::Command::new("cmd.exe")
764 .args(["/c", "ping -n 30 127.0.0.1 >nul"])
765 .spawn()
766 .expect("spawn child");
767 job.assign(child.id()).expect("assign child to job");
768 job.terminate();
769 let status = child.wait().expect("child reaped after job termination");
770 assert!(!status.success());
771 assert!(!super::process_exists(child.id()));
772 }
773
774 #[test]
775 fn owner_only_sddl_names_the_current_user_and_an_integrity_label() {
776 let sddl = super::owner_only_sddl().expect("sddl");
777 assert!(sddl.starts_with("D:P(A;;GA;;;SY)(A;;GRGW;;;S-1-"), "{sddl}");
778 assert!(
779 sddl.ends_with("S:(ML;;NRNW;;;ME)") || sddl.ends_with("S:(ML;;NRNW;;;HI)"),
780 "{sddl}"
781 );
782 let security = super::PipeSecurity::owner_only().expect("descriptor");
783 assert!(!security.descriptor.is_null());
784 }
785
786 #[test]
787 fn dll_search_path_restriction_is_accepted() {
788 super::restrict_dll_search_path();
789 }
790
791 #[test]
792 fn std_handles_stop_being_inheritable() {
793 use windows_sys::Win32::Foundation::{
794 GetHandleInformation, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE,
795 };
796 use windows_sys::Win32::System::Console::{
797 GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
798 };
799
800 super::disable_std_handle_inheritance();
801 for id in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
802 let handle = unsafe { GetStdHandle(id) };
804 if handle.is_null() || handle == INVALID_HANDLE_VALUE {
805 continue;
806 }
807 let mut flags = 0u32;
808 let queried =
810 unsafe { GetHandleInformation(handle, std::ptr::from_mut(&mut flags)) };
811 if queried == 0 {
812 continue;
814 }
815 assert_eq!(flags & HANDLE_FLAG_INHERIT, 0, "std handle {id} still inheritable");
816 }
817 }
818 }
819}
820
821#[cfg(unix)]
824pub(crate) fn open_read_nofollow(path: &std::path::Path) -> std::io::Result<std::fs::File> {
825 use std::os::unix::fs::OpenOptionsExt as _;
826 let file = std::fs::OpenOptions::new()
827 .read(true)
828 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK)
829 .open(path)?;
830 if !file.metadata()?.is_file() {
831 return Err(std::io::Error::new(
832 std::io::ErrorKind::InvalidInput,
833 "expected a regular file",
834 ));
835 }
836 Ok(file)
837}
838
839#[cfg(windows)]
840pub(crate) fn open_read_nofollow(path: &std::path::Path) -> std::io::Result<std::fs::File> {
841 use std::os::windows::fs::{MetadataExt as _, OpenOptionsExt as _};
842 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
844 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
845 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0400;
846 let file = std::fs::OpenOptions::new()
847 .read(true)
848 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
849 .open(path)?;
850 let metadata = file.metadata()?;
851 if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
852 return Err(std::io::Error::new(
853 std::io::ErrorKind::InvalidInput,
854 "expected a regular file without a reparse point",
855 ));
856 }
857 Ok(file)
858}
859
860#[cfg(not(any(unix, windows)))]
861pub(crate) fn open_read_nofollow(_path: &std::path::Path) -> std::io::Result<std::fs::File> {
862 Err(std::io::Error::new(
863 std::io::ErrorKind::Unsupported,
864 "safe no-follow record reads are unsupported on this platform",
865 ))
866}