Skip to main content

rustpython_vm/stdlib/
posix.rs

1// spell-checker:disable
2
3use std::os::fd::BorrowedFd;
4
5pub(crate) use module::module_def;
6
7pub fn set_inheritable(fd: BorrowedFd<'_>, inheritable: bool) -> nix::Result<()> {
8    use nix::fcntl;
9    let flags = fcntl::FdFlag::from_bits_truncate(fcntl::fcntl(fd, fcntl::FcntlArg::F_GETFD)?);
10    let mut new_flags = flags;
11    new_flags.set(fcntl::FdFlag::FD_CLOEXEC, !inheritable);
12    if flags != new_flags {
13        fcntl::fcntl(fd, fcntl::FcntlArg::F_SETFD(new_flags))?;
14    }
15    Ok(())
16}
17
18#[pymodule(name = "posix", with(
19    super::os::_os,
20    #[cfg(any(
21        target_os = "linux",
22        target_os = "netbsd",
23        target_os = "freebsd",
24        target_os = "android"
25    ))]
26    posix_sched
27))]
28pub mod module {
29    use crate::{
30        AsObject, Py, PyObjectRef, PyResult, VirtualMachine,
31        builtins::{PyDictRef, PyInt, PyListRef, PyTupleRef, PyUtf8Str},
32        convert::{IntoPyException, ToPyObject, TryFromObject},
33        exceptions::OSErrorBuilder,
34        function::{ArgMapping, Either, KwArgs, OptionalArg},
35        ospath::{OsPath, OsPathOrFd},
36        stdlib::os::{
37            _os, DirFd, FollowSymlinks, SupportFunc, TargetIsDirectory, fs_metadata,
38            warn_if_bool_fd,
39        },
40    };
41    #[cfg(any(
42        target_os = "android",
43        target_os = "freebsd",
44        target_os = "linux",
45        target_os = "openbsd"
46    ))]
47    use crate::{builtins::PyUtf8StrRef, utils::ToCString};
48    use alloc::ffi::CString;
49    use bitflags::bitflags;
50    use core::ffi::CStr;
51    use nix::{
52        errno::Errno,
53        fcntl,
54        unistd::{self, Gid, Pid, Uid},
55    };
56    use rustpython_common::os::ffi::OsStringExt;
57    use std::{
58        env, fs, io,
59        os::fd::{AsFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd},
60    };
61    use strum::IntoEnumIterator;
62    use strum_macros::{EnumIter, EnumString};
63
64    #[cfg(target_os = "linux")]
65    #[pyattr]
66    use libc::PIDFD_NONBLOCK;
67
68    #[cfg(target_os = "macos")]
69    #[pyattr]
70    use libc::{
71        COPYFILE_DATA as _COPYFILE_DATA, O_EVTONLY, O_NOFOLLOW_ANY, PRIO_DARWIN_BG,
72        PRIO_DARWIN_NONUI, PRIO_DARWIN_PROCESS, PRIO_DARWIN_THREAD,
73    };
74
75    #[cfg(target_os = "freebsd")]
76    #[pyattr]
77    use libc::{SF_MNOWAIT, SF_NOCACHE, SF_NODISKIO, SF_SYNC};
78
79    #[cfg(any(target_os = "android", target_os = "linux"))]
80    #[pyattr]
81    use libc::{
82        CLONE_FILES, CLONE_FS, CLONE_NEWCGROUP, CLONE_NEWIPC, CLONE_NEWNET, CLONE_NEWNS,
83        CLONE_NEWPID, CLONE_NEWUSER, CLONE_NEWUTS, CLONE_SIGHAND, CLONE_SYSVSEM, CLONE_THREAD,
84        CLONE_VM, MFD_HUGE_SHIFT, O_NOATIME, O_TMPFILE, P_PIDFD, SCHED_BATCH, SCHED_DEADLINE,
85        SCHED_IDLE, SCHED_NORMAL, SCHED_RESET_ON_FORK, SPLICE_F_MORE, SPLICE_F_MOVE,
86        SPLICE_F_NONBLOCK,
87    };
88
89    #[cfg(any(target_os = "macos", target_os = "redox"))]
90    #[pyattr]
91    use libc::O_SYMLINK;
92
93    #[cfg(any(target_os = "android", target_os = "redox", unix))]
94    #[pyattr]
95    use libc::{O_NOFOLLOW, PRIO_PGRP, PRIO_PROCESS, PRIO_USER};
96
97    #[cfg(any(target_os = "linux", target_os = "macos", target_os = "netbsd"))]
98    #[pyattr]
99    use libc::{XATTR_CREATE, XATTR_REPLACE};
100
101    #[cfg(any(target_os = "android", target_os = "linux", target_os = "netbsd"))]
102    #[pyattr]
103    use libc::O_RSYNC;
104
105    #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux"))]
106    #[pyattr]
107    use libc::{
108        MFD_ALLOW_SEALING, MFD_CLOEXEC, MFD_HUGE_MASK, MFD_HUGETLB, POSIX_FADV_DONTNEED,
109        POSIX_FADV_NOREUSE, POSIX_FADV_NORMAL, POSIX_FADV_RANDOM, POSIX_FADV_SEQUENTIAL,
110        POSIX_FADV_WILLNEED,
111    };
112
113    #[cfg(any(target_os = "android", target_os = "linux", target_os = "redox", unix))]
114    #[pyattr]
115    use libc::{RTLD_LAZY, RTLD_NOW, WNOHANG};
116
117    #[cfg(any(target_os = "android", target_os = "macos", target_os = "redox", unix))]
118    #[pyattr]
119    use libc::RTLD_GLOBAL;
120
121    #[cfg(any(
122        target_os = "android",
123        target_os = "freebsd",
124        target_os = "linux",
125        target_os = "redox"
126    ))]
127    #[pyattr]
128    use libc::O_PATH;
129
130    #[cfg(any(
131        target_os = "android",
132        target_os = "freebsd",
133        target_os = "linux",
134        target_os = "netbsd"
135    ))]
136    #[pyattr]
137    use libc::{
138        EFD_CLOEXEC, EFD_NONBLOCK, EFD_SEMAPHORE, TFD_CLOEXEC, TFD_NONBLOCK, TFD_TIMER_ABSTIME,
139        TFD_TIMER_CANCEL_ON_SET,
140    };
141
142    #[cfg(any(
143        target_os = "android",
144        target_os = "dragonfly",
145        target_os = "linux",
146        target_os = "netbsd"
147    ))]
148    #[pyattr]
149    use libc::{GRND_NONBLOCK, GRND_RANDOM};
150
151    #[cfg(any(
152        target_os = "android",
153        target_os = "linux",
154        target_os = "macos",
155        target_os = "redox",
156        unix
157    ))]
158    #[pyattr]
159    use libc::{F_OK, R_OK, W_OK, X_OK};
160
161    #[cfg(any(
162        target_os = "android",
163        target_os = "freebsd",
164        target_os = "netbsd",
165        target_os = "redox",
166        unix
167    ))]
168    #[pyattr]
169    use libc::O_NONBLOCK;
170
171    #[cfg(any(
172        target_os = "android",
173        target_os = "freebsd",
174        target_os = "linux",
175        target_os = "macos",
176        target_os = "netbsd"
177    ))]
178    #[pyattr]
179    use libc::O_DSYNC;
180
181    #[cfg(any(
182        target_os = "dragonfly",
183        target_os = "freebsd",
184        target_os = "linux",
185        target_os = "macos",
186        target_os = "netbsd"
187    ))]
188    #[pyattr]
189    use libc::SCHED_OTHER;
190
191    #[cfg(any(
192        target_os = "android",
193        target_os = "dragonfly",
194        target_os = "freebsd",
195        target_os = "linux",
196        target_os = "macos"
197    ))]
198    #[pyattr]
199    use libc::{RTLD_NODELETE, SEEK_DATA, SEEK_HOLE};
200
201    #[cfg(any(
202        target_os = "android",
203        target_os = "dragonfly",
204        target_os = "freebsd",
205        target_os = "linux",
206        target_os = "netbsd"
207    ))]
208    #[pyattr]
209    use libc::O_DIRECT;
210
211    #[cfg(any(
212        target_os = "dragonfly",
213        target_os = "freebsd",
214        target_os = "macos",
215        target_os = "netbsd",
216        target_os = "redox"
217    ))]
218    #[pyattr]
219    use libc::{O_EXLOCK, O_FSYNC, O_SHLOCK};
220
221    #[cfg(any(
222        target_os = "android",
223        target_os = "linux",
224        target_os = "macos",
225        target_os = "netbsd",
226        target_os = "redox",
227        unix
228    ))]
229    #[pyattr]
230    use libc::RTLD_LOCAL;
231
232    #[cfg(any(
233        target_os = "android",
234        target_os = "freebsd",
235        target_os = "linux",
236        target_os = "netbsd",
237        target_os = "redox",
238        unix
239    ))]
240    #[pyattr]
241    use libc::WUNTRACED;
242
243    #[cfg(any(
244        target_os = "android",
245        target_os = "dragonfly",
246        target_os = "freebsd",
247        target_os = "linux",
248        target_os = "macos",
249        target_os = "netbsd"
250    ))]
251    #[pyattr]
252    use libc::{
253        CLD_CONTINUED, CLD_DUMPED, CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_TRAPPED, O_SYNC, P_ALL,
254        P_PGID, P_PID, RTLD_NOLOAD, SCHED_FIFO, SCHED_RR,
255    };
256
257    #[cfg(any(
258        target_os = "android",
259        target_os = "dragonfly",
260        target_os = "freebsd",
261        target_os = "macos",
262        target_os = "netbsd",
263        target_os = "redox",
264        unix
265    ))]
266    #[pyattr]
267    use libc::O_DIRECTORY;
268
269    #[cfg(any(
270        target_os = "android",
271        target_os = "dragonfly",
272        target_os = "freebsd",
273        target_os = "linux",
274        target_os = "macos",
275        target_os = "netbsd",
276        target_os = "redox"
277    ))]
278    #[pyattr]
279    use libc::{
280        F_LOCK, F_TEST, F_TLOCK, F_ULOCK, O_ASYNC, O_NDELAY, O_NOCTTY, WEXITED, WNOWAIT, WSTOPPED,
281    };
282
283    #[cfg(any(
284        target_os = "android",
285        target_os = "dragonfly",
286        target_os = "freebsd",
287        target_os = "linux",
288        target_os = "macos",
289        target_os = "netbsd",
290        target_os = "redox",
291        unix
292    ))]
293    #[pyattr]
294    use libc::{O_CLOEXEC, WCONTINUED};
295
296    #[pyattr]
297    const EX_OK: i8 = exitcode::OK as i8;
298
299    #[pyattr]
300    const EX_USAGE: i8 = exitcode::USAGE as i8;
301
302    #[pyattr]
303    const EX_DATAERR: i8 = exitcode::DATAERR as i8;
304
305    #[pyattr]
306    const EX_NOINPUT: i8 = exitcode::NOINPUT as i8;
307
308    #[pyattr]
309    const EX_NOUSER: i8 = exitcode::NOUSER as i8;
310
311    #[pyattr]
312    const EX_NOHOST: i8 = exitcode::NOHOST as i8;
313
314    #[pyattr]
315    const EX_UNAVAILABLE: i8 = exitcode::UNAVAILABLE as i8;
316
317    #[pyattr]
318    const EX_SOFTWARE: i8 = exitcode::SOFTWARE as i8;
319
320    #[pyattr]
321    const EX_OSERR: i8 = exitcode::OSERR as i8;
322
323    #[pyattr]
324    const EX_OSFILE: i8 = exitcode::OSFILE as i8;
325
326    #[pyattr]
327    const EX_CANTCREAT: i8 = exitcode::CANTCREAT as i8;
328
329    #[pyattr]
330    const EX_IOERR: i8 = exitcode::IOERR as i8;
331
332    #[pyattr]
333    const EX_TEMPFAIL: i8 = exitcode::TEMPFAIL as i8;
334
335    #[pyattr]
336    const EX_PROTOCOL: i8 = exitcode::PROTOCOL as i8;
337
338    #[pyattr]
339    const EX_NOPERM: i8 = exitcode::NOPERM as i8;
340
341    #[pyattr]
342    const EX_CONFIG: i8 = exitcode::CONFIG as i8;
343
344    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
345    #[pyattr]
346    const POSIX_SPAWN_OPEN: i32 = PosixSpawnFileActionIdentifier::Open as i32;
347
348    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
349    #[pyattr]
350    const POSIX_SPAWN_CLOSE: i32 = PosixSpawnFileActionIdentifier::Close as i32;
351
352    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
353    #[pyattr]
354    const POSIX_SPAWN_DUP2: i32 = PosixSpawnFileActionIdentifier::Dup2 as i32;
355
356    impl TryFromObject for BorrowedFd<'_> {
357        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
358            crate::stdlib::os::warn_if_bool_fd(&obj, vm)?;
359            let fd = i32::try_from_object(vm, obj)?;
360            if fd == -1 {
361                return Err(io::Error::from_raw_os_error(libc::EBADF).into_pyexception(vm));
362            }
363            // SAFETY: none, really. but, python's os api of passing around file descriptors
364            //         everywhere isn't really io-safe anyway, so, this is passed to the user.
365            Ok(unsafe { BorrowedFd::borrow_raw(fd) })
366        }
367    }
368
369    impl TryFromObject for OwnedFd {
370        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
371            let fd = i32::try_from_object(vm, obj)?;
372            if fd == -1 {
373                return Err(io::Error::from_raw_os_error(libc::EBADF).into_pyexception(vm));
374            }
375            // SAFETY: none, really. but, python's os api of passing around file descriptors
376            //         everywhere isn't really io-safe anyway, so, this is passed to the user.
377            Ok(unsafe { Self::from_raw_fd(fd) })
378        }
379    }
380
381    impl ToPyObject for OwnedFd {
382        fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
383            self.into_raw_fd().to_pyobject(vm)
384        }
385    }
386
387    // Flags for os_access
388    bitflags! {
389        #[derive(Copy, Clone, Debug, PartialEq)]
390        pub struct AccessFlags: u8 {
391            const F_OK = _os::F_OK;
392            const R_OK = _os::R_OK;
393            const W_OK = _os::W_OK;
394            const X_OK = _os::X_OK;
395        }
396    }
397
398    struct Permissions {
399        is_readable: bool,
400        is_writable: bool,
401        is_executable: bool,
402    }
403
404    const fn get_permissions(mode: u32) -> Permissions {
405        Permissions {
406            is_readable: mode & 4 != 0,
407            is_writable: mode & 2 != 0,
408            is_executable: mode & 1 != 0,
409        }
410    }
411
412    fn get_right_permission(
413        mode: u32,
414        file_owner: Uid,
415        file_group: Gid,
416    ) -> nix::Result<Permissions> {
417        let owner_mode = (mode & 0o700) >> 6;
418        let owner_permissions = get_permissions(owner_mode);
419
420        let group_mode = (mode & 0o070) >> 3;
421        let group_permissions = get_permissions(group_mode);
422
423        let others_mode = mode & 0o007;
424        let others_permissions = get_permissions(others_mode);
425
426        let user_id = nix::unistd::getuid();
427        let groups_ids = getgroups_impl()?;
428
429        if file_owner == user_id {
430            Ok(owner_permissions)
431        } else if groups_ids.contains(&file_group) {
432            Ok(group_permissions)
433        } else {
434            Ok(others_permissions)
435        }
436    }
437
438    #[cfg(any(target_os = "macos", target_os = "ios"))]
439    fn getgroups_impl() -> nix::Result<Vec<Gid>> {
440        use core::ptr;
441        use libc::{c_int, gid_t};
442
443        let ret = unsafe { libc::getgroups(0, ptr::null_mut()) };
444        let mut groups = Vec::<Gid>::with_capacity(Errno::result(ret)? as usize);
445        let ret = unsafe {
446            libc::getgroups(
447                groups.capacity() as c_int,
448                groups.as_mut_ptr() as *mut gid_t,
449            )
450        };
451
452        Errno::result(ret).map(|s| {
453            unsafe { groups.set_len(s as usize) };
454            groups
455        })
456    }
457
458    #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "redox")))]
459    use nix::unistd::getgroups as getgroups_impl;
460
461    #[cfg(target_os = "redox")]
462    fn getgroups_impl() -> nix::Result<Vec<Gid>> {
463        Err(nix::Error::EOPNOTSUPP)
464    }
465
466    #[pyfunction]
467    fn getgroups(vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
468        let group_ids = getgroups_impl().map_err(|e| e.into_pyexception(vm))?;
469        Ok(group_ids
470            .into_iter()
471            .map(|gid| vm.ctx.new_int(gid.as_raw()).into())
472            .collect())
473    }
474
475    #[pyfunction]
476    pub(super) fn access(path: OsPath, mode: u8, vm: &VirtualMachine) -> PyResult<bool> {
477        use std::os::unix::fs::MetadataExt;
478
479        let flags = AccessFlags::from_bits(mode).ok_or_else(|| {
480            vm.new_value_error(
481            "One of the flags is wrong, there are only 4 possibilities F_OK, R_OK, W_OK and X_OK",
482        )
483        })?;
484
485        let metadata = match fs::metadata(&path.path) {
486            Ok(m) => m,
487            // If the file doesn't exist, return False for any access check
488            Err(_) => return Ok(false),
489        };
490
491        // if it's only checking for F_OK
492        if flags == AccessFlags::F_OK {
493            return Ok(true); // File exists
494        }
495
496        let user_id = metadata.uid();
497        let group_id = metadata.gid();
498        let mode = metadata.mode();
499
500        let perm = get_right_permission(mode, Uid::from_raw(user_id), Gid::from_raw(group_id))
501            .map_err(|err| err.into_pyexception(vm))?;
502
503        let r_ok = !flags.contains(AccessFlags::R_OK) || perm.is_readable;
504        let w_ok = !flags.contains(AccessFlags::W_OK) || perm.is_writable;
505        let x_ok = !flags.contains(AccessFlags::X_OK) || perm.is_executable;
506
507        Ok(r_ok && w_ok && x_ok)
508    }
509
510    #[pyattr]
511    fn environ(vm: &VirtualMachine) -> PyDictRef {
512        let environ = vm.ctx.new_dict();
513        for (key, value) in env::vars_os() {
514            let key: PyObjectRef = vm.ctx.new_bytes(key.into_vec()).into();
515            let value: PyObjectRef = vm.ctx.new_bytes(value.into_vec()).into();
516            environ.set_item(&*key, value, vm).unwrap();
517        }
518
519        environ
520    }
521
522    #[pyfunction]
523    fn _create_environ(vm: &VirtualMachine) -> PyDictRef {
524        let environ = vm.ctx.new_dict();
525        for (key, value) in env::vars_os() {
526            let key: PyObjectRef = vm.ctx.new_bytes(key.into_vec()).into();
527            let value: PyObjectRef = vm.ctx.new_bytes(value.into_vec()).into();
528            environ.set_item(&*key, value, vm).unwrap();
529        }
530        environ
531    }
532
533    #[derive(FromArgs)]
534    pub(super) struct SymlinkArgs<'fd> {
535        src: OsPath,
536        dst: OsPath,
537        #[pyarg(flatten)]
538        _target_is_directory: TargetIsDirectory,
539        #[pyarg(flatten)]
540        dir_fd: DirFd<'fd, { _os::SYMLINK_DIR_FD as usize }>,
541    }
542
543    #[pyfunction]
544    pub(super) fn symlink(args: SymlinkArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
545        let src = args.src.into_cstring(vm)?;
546        let dst = args.dst.into_cstring(vm)?;
547        #[cfg(not(target_os = "redox"))]
548        {
549            nix::unistd::symlinkat(&*src, args.dir_fd.get(), &*dst)
550                .map_err(|err| err.into_pyexception(vm))
551        }
552        #[cfg(target_os = "redox")]
553        {
554            let [] = args.dir_fd.0;
555            let res = unsafe { libc::symlink(src.as_ptr(), dst.as_ptr()) };
556            if res < 0 {
557                Err(vm.new_last_errno_error())
558            } else {
559                Ok(())
560            }
561        }
562    }
563
564    #[pyfunction]
565    #[pyfunction(name = "unlink")]
566    fn remove(
567        path: OsPath,
568        dir_fd: DirFd<'_, { _os::UNLINK_DIR_FD as usize }>,
569        vm: &VirtualMachine,
570    ) -> PyResult<()> {
571        #[cfg(not(target_os = "redox"))]
572        if let Some(fd) = dir_fd.raw_opt() {
573            let c_path = path.clone().into_cstring(vm)?;
574            let res = unsafe { libc::unlinkat(fd, c_path.as_ptr(), 0) };
575            return if res < 0 {
576                let err = crate::common::os::errno_io_error();
577                Err(OSErrorBuilder::with_filename(&err, path, vm))
578            } else {
579                Ok(())
580            };
581        }
582        #[cfg(target_os = "redox")]
583        let [] = dir_fd.0;
584        fs::remove_file(&path).map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
585    }
586
587    #[cfg(not(target_os = "redox"))]
588    #[pyfunction]
589    fn fchdir(fd: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
590        warn_if_bool_fd(&fd, vm)?;
591        let fd = i32::try_from_object(vm, fd)?;
592        let ret = unsafe { libc::fchdir(fd) };
593        if ret == 0 {
594            Ok(())
595        } else {
596            Err(io::Error::last_os_error().into_pyexception(vm))
597        }
598    }
599
600    #[cfg(not(target_os = "redox"))]
601    #[pyfunction]
602    fn chroot(path: OsPath, vm: &VirtualMachine) -> PyResult<()> {
603        use crate::exceptions::OSErrorBuilder;
604
605        nix::unistd::chroot(&*path.path).map_err(|err| {
606            // Use `From<nix::Error> for io::Error` when it is available
607            let io_err: io::Error = err.into();
608            OSErrorBuilder::with_filename(&io_err, path, vm)
609        })
610    }
611
612    // As of now, redox does not seems to support chown command (cf. https://gitlab.redox-os.org/redox-os/coreutils , last checked on 05/07/2020)
613    #[cfg(not(target_os = "redox"))]
614    #[pyfunction]
615    fn chown(
616        path: OsPathOrFd<'_>,
617        uid: isize,
618        gid: isize,
619        dir_fd: DirFd<'_, 1>,
620        follow_symlinks: FollowSymlinks,
621        vm: &VirtualMachine,
622    ) -> PyResult<()> {
623        let uid = if uid >= 0 {
624            Some(nix::unistd::Uid::from_raw(uid as u32))
625        } else if uid == -1 {
626            None
627        } else {
628            return Err(vm.new_os_error("Specified uid is not valid."));
629        };
630
631        let gid = if gid >= 0 {
632            Some(nix::unistd::Gid::from_raw(gid as u32))
633        } else if gid == -1 {
634            None
635        } else {
636            return Err(vm.new_os_error("Specified gid is not valid."));
637        };
638
639        let flag = if follow_symlinks.0 {
640            nix::fcntl::AtFlags::empty()
641        } else {
642            nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW
643        };
644
645        match path {
646            OsPathOrFd::Path(ref p) => {
647                nix::unistd::fchownat(dir_fd.get(), p.path.as_os_str(), uid, gid, flag)
648            }
649            OsPathOrFd::Fd(fd) => nix::unistd::fchown(fd, uid, gid),
650        }
651        .map_err(|err| {
652            // Use `From<nix::Error> for io::Error` when it is available
653            let err = io::Error::from_raw_os_error(err as i32);
654            OSErrorBuilder::with_filename(&err, path, vm)
655        })
656    }
657
658    #[cfg(not(target_os = "redox"))]
659    #[pyfunction]
660    fn lchown(path: OsPath, uid: isize, gid: isize, vm: &VirtualMachine) -> PyResult<()> {
661        chown(
662            OsPathOrFd::Path(path),
663            uid,
664            gid,
665            DirFd::default(),
666            FollowSymlinks(false),
667            vm,
668        )
669    }
670
671    #[cfg(not(target_os = "redox"))]
672    #[pyfunction]
673    fn fchown(fd: BorrowedFd<'_>, uid: isize, gid: isize, vm: &VirtualMachine) -> PyResult<()> {
674        chown(
675            OsPathOrFd::Fd(fd.into()),
676            uid,
677            gid,
678            DirFd::default(),
679            FollowSymlinks(true),
680            vm,
681        )
682    }
683
684    #[derive(FromArgs)]
685    struct RegisterAtForkArgs {
686        #[pyarg(named, optional)]
687        before: OptionalArg<PyObjectRef>,
688        #[pyarg(named, optional)]
689        after_in_parent: OptionalArg<PyObjectRef>,
690        #[pyarg(named, optional)]
691        after_in_child: OptionalArg<PyObjectRef>,
692    }
693
694    impl RegisterAtForkArgs {
695        fn into_validated(
696            self,
697            vm: &VirtualMachine,
698        ) -> PyResult<(
699            Option<PyObjectRef>,
700            Option<PyObjectRef>,
701            Option<PyObjectRef>,
702        )> {
703            fn into_option(
704                arg: OptionalArg<PyObjectRef>,
705                vm: &VirtualMachine,
706            ) -> PyResult<Option<PyObjectRef>> {
707                match arg {
708                    OptionalArg::Present(obj) => {
709                        if !obj.is_callable() {
710                            return Err(vm.new_type_error("Args must be callable"));
711                        }
712                        Ok(Some(obj))
713                    }
714                    OptionalArg::Missing => Ok(None),
715                }
716            }
717            let before = into_option(self.before, vm)?;
718            let after_in_parent = into_option(self.after_in_parent, vm)?;
719            let after_in_child = into_option(self.after_in_child, vm)?;
720            if before.is_none() && after_in_parent.is_none() && after_in_child.is_none() {
721                return Err(vm.new_type_error("At least one arg must be present"));
722            }
723            Ok((before, after_in_parent, after_in_child))
724        }
725    }
726
727    #[pyfunction]
728    fn register_at_fork(
729        args: RegisterAtForkArgs,
730        _ignored: KwArgs,
731        vm: &VirtualMachine,
732    ) -> PyResult<()> {
733        let (before, after_in_parent, after_in_child) = args.into_validated(vm)?;
734
735        if let Some(before) = before {
736            vm.state.before_forkers.lock().push(before);
737        }
738        if let Some(after_in_parent) = after_in_parent {
739            vm.state.after_forkers_parent.lock().push(after_in_parent);
740        }
741        if let Some(after_in_child) = after_in_child {
742            vm.state.after_forkers_child.lock().push(after_in_child);
743        }
744        Ok(())
745    }
746
747    fn run_at_forkers(mut funcs: Vec<PyObjectRef>, reversed: bool, vm: &VirtualMachine) {
748        if !funcs.is_empty() {
749            if reversed {
750                funcs.reverse();
751            }
752            for func in funcs {
753                if let Err(e) = func.call((), vm) {
754                    let exit = e.fast_isinstance(vm.ctx.exceptions.system_exit);
755                    vm.run_unraisable(e, Some("Exception ignored in".to_owned()), func);
756                    if exit {
757                        // Do nothing!
758                    }
759                }
760            }
761        }
762    }
763
764    fn py_os_before_fork(vm: &VirtualMachine) {
765        let before_forkers: Vec<PyObjectRef> = vm.state.before_forkers.lock().clone();
766        // functions must be executed in reversed order as they are registered
767        // only for before_forkers, refer: test_register_at_fork in test_posix
768
769        run_at_forkers(before_forkers, true, vm);
770
771        #[cfg(feature = "threading")]
772        crate::stdlib::_imp::acquire_imp_lock_for_fork();
773
774        #[cfg(feature = "threading")]
775        vm.state.stop_the_world.stop_the_world(vm);
776    }
777
778    fn py_os_after_fork_child(vm: &VirtualMachine) {
779        #[cfg(feature = "threading")]
780        vm.state.stop_the_world.reset_after_fork();
781
782        // Phase 1: Reset all internal locks FIRST.
783        // After fork(), locks held by dead parent threads would deadlock
784        // if we try to acquire them. This must happen before anything else.
785        #[cfg(feature = "threading")]
786        reinit_locks_after_fork(vm);
787
788        // Reinit per-object IO buffer locks on std streams.
789        // BufferedReader/Writer/TextIOWrapper use PyThreadMutex which can be
790        // held by dead parent threads, causing deadlocks on any IO in the child.
791        #[cfg(feature = "threading")]
792        unsafe {
793            crate::stdlib::_io::reinit_std_streams_after_fork(vm)
794        };
795
796        // Phase 2: Reset low-level atomic state (no locks needed).
797        crate::signal::clear_after_fork();
798        crate::stdlib::_signal::_signal::clear_wakeup_fd_after_fork();
799
800        // Reset weakref stripe locks that may have been held during fork.
801        #[cfg(feature = "threading")]
802        crate::object::reset_weakref_locks_after_fork();
803
804        // Phase 3: Clean up thread state. Locks are now reinit'd so we can
805        // acquire them normally instead of using try_lock().
806        #[cfg(feature = "threading")]
807        crate::stdlib::_thread::after_fork_child(vm);
808
809        // CPython parity: reinit import lock ownership metadata in child
810        // and release the lock acquired by PyOS_BeforeFork().
811        #[cfg(feature = "threading")]
812        unsafe {
813            crate::stdlib::_imp::after_fork_child_imp_lock_release()
814        };
815
816        // Initialize signal handlers for the child's main thread.
817        // When forked from a worker thread, the OnceCell is empty.
818        vm.signal_handlers
819            .get_or_init(crate::signal::new_signal_handlers);
820
821        // Phase 4: Run Python-level at-fork callbacks.
822        let after_forkers_child: Vec<PyObjectRef> = vm.state.after_forkers_child.lock().clone();
823        run_at_forkers(after_forkers_child, false, vm);
824    }
825
826    /// Reset all parking_lot-based locks in the interpreter state after fork().
827    ///
828    /// After fork(), only the calling thread survives. Any locks held by other
829    /// (now-dead) threads would cause deadlocks. We unconditionally reset them
830    /// to unlocked by zeroing the raw lock bytes.
831    #[cfg(all(unix, feature = "threading"))]
832    fn reinit_locks_after_fork(vm: &VirtualMachine) {
833        use rustpython_common::lock::reinit_mutex_after_fork;
834
835        unsafe {
836            // PyGlobalState PyMutex locks
837            reinit_mutex_after_fork(&vm.state.before_forkers);
838            reinit_mutex_after_fork(&vm.state.after_forkers_child);
839            reinit_mutex_after_fork(&vm.state.after_forkers_parent);
840            reinit_mutex_after_fork(&vm.state.atexit_funcs);
841            reinit_mutex_after_fork(&vm.state.global_trace_func);
842            reinit_mutex_after_fork(&vm.state.global_profile_func);
843            reinit_mutex_after_fork(&vm.state.monitoring);
844
845            // PyGlobalState parking_lot::Mutex locks
846            reinit_mutex_after_fork(&vm.state.thread_frames);
847            reinit_mutex_after_fork(&vm.state.thread_handles);
848            reinit_mutex_after_fork(&vm.state.shutdown_handles);
849
850            // Context-level RwLock
851            vm.ctx.string_pool.reinit_after_fork();
852
853            // Codec registry RwLock
854            vm.state.codec_registry.reinit_after_fork();
855
856            // GC state (multiple Mutex + RwLock)
857            crate::gc_state::gc_state().reinit_after_fork();
858
859            // Import lock (RawReentrantMutex<RawMutex, RawThreadId>)
860            crate::stdlib::_imp::reinit_imp_lock_after_fork();
861        }
862    }
863
864    fn py_os_after_fork_parent(vm: &VirtualMachine) {
865        #[cfg(feature = "threading")]
866        vm.state.stop_the_world.start_the_world(vm);
867
868        #[cfg(feature = "threading")]
869        crate::stdlib::_imp::release_imp_lock_after_fork_parent();
870
871        let after_forkers_parent: Vec<PyObjectRef> = vm.state.after_forkers_parent.lock().clone();
872        run_at_forkers(after_forkers_parent, false, vm);
873    }
874
875    /// Best-effort number of OS threads in this process.
876    /// Returns <= 0 when unavailable.
877    fn get_number_of_os_threads() -> isize {
878        #[cfg(target_os = "macos")]
879        {
880            type MachPortT = libc::c_uint;
881            type KernReturnT = libc::c_int;
882            type MachMsgTypeNumberT = libc::c_uint;
883            type ThreadActArrayT = *mut MachPortT;
884            const KERN_SUCCESS: KernReturnT = 0;
885            unsafe extern "C" {
886                fn mach_task_self() -> MachPortT;
887                fn task_for_pid(
888                    task: MachPortT,
889                    pid: libc::c_int,
890                    target_task: *mut MachPortT,
891                ) -> KernReturnT;
892                fn task_threads(
893                    target_task: MachPortT,
894                    act_list: *mut ThreadActArrayT,
895                    act_list_cnt: *mut MachMsgTypeNumberT,
896                ) -> KernReturnT;
897                fn vm_deallocate(
898                    target_task: MachPortT,
899                    address: libc::uintptr_t,
900                    size: libc::uintptr_t,
901                ) -> KernReturnT;
902            }
903
904            let self_task = unsafe { mach_task_self() };
905            let mut proc_task: MachPortT = 0;
906            if unsafe { task_for_pid(self_task, libc::getpid(), &mut proc_task) } == KERN_SUCCESS {
907                let mut threads: ThreadActArrayT = core::ptr::null_mut();
908                let mut n_threads: MachMsgTypeNumberT = 0;
909                if unsafe { task_threads(proc_task, &mut threads, &mut n_threads) } == KERN_SUCCESS
910                {
911                    if !threads.is_null() {
912                        let _ = unsafe {
913                            vm_deallocate(
914                                self_task,
915                                threads as libc::uintptr_t,
916                                (n_threads as usize * core::mem::size_of::<MachPortT>())
917                                    as libc::uintptr_t,
918                            )
919                        };
920                    }
921                    return n_threads as isize;
922                }
923            }
924            0
925        }
926        #[cfg(target_os = "linux")]
927        {
928            use std::io::Read as _;
929            let mut file = match std::fs::File::open("/proc/self/stat") {
930                Ok(f) => f,
931                Err(_) => return 0,
932            };
933            let mut buf = [0u8; 160];
934            let n = match file.read(&mut buf) {
935                Ok(n) => n,
936                Err(_) => return 0,
937            };
938            let line = match core::str::from_utf8(&buf[..n]) {
939                Ok(s) => s,
940                Err(_) => return 0,
941            };
942            if let Some(field) = line.split_whitespace().nth(19) {
943                return field.parse::<isize>().unwrap_or(0);
944            }
945            0
946        }
947        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
948        {
949            0
950        }
951    }
952
953    /// Warn if forking from a multi-threaded process.
954    /// `num_os_threads` should be captured before parent after-fork hooks run.
955    fn warn_if_multi_threaded(name: &str, num_os_threads: isize, vm: &VirtualMachine) {
956        let num_threads = if num_os_threads > 0 {
957            num_os_threads as usize
958        } else {
959            // CPython fallback: if OS-level count isn't available, use the
960            // threading module's active+limbo view.
961            // Only check threading if it was already imported. Avoid vm.import()
962            // which can execute arbitrary Python code in the fork path.
963            let threading = match vm
964                .sys_module
965                .get_attr("modules", vm)
966                .and_then(|m| m.get_item("threading", vm))
967            {
968                Ok(m) => m,
969                Err(_) => return,
970            };
971            let active = threading.get_attr("_active", vm).ok();
972            let limbo = threading.get_attr("_limbo", vm).ok();
973
974            // Match threading module internals and avoid sequence overcounting:
975            // count only dict-backed _active/_limbo containers.
976            let count_dict = |obj: Option<crate::PyObjectRef>| -> usize {
977                obj.and_then(|o| {
978                    o.downcast_ref::<crate::builtins::PyDict>()
979                        .map(|d| d.__len__())
980                })
981                .unwrap_or(0)
982            };
983
984            count_dict(active) + count_dict(limbo)
985        };
986
987        if num_threads > 1 {
988            let pid = unsafe { libc::getpid() };
989            let msg = format!(
990                "This process (pid={}) is multi-threaded, use of {}() may lead to deadlocks in the child.",
991                pid, name
992            );
993
994            // Match PyErr_WarnFormat(..., stacklevel=1) in CPython.
995            // Best effort: ignore failures like CPython does in this path.
996            let _ =
997                crate::stdlib::_warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm);
998        }
999    }
1000
1001    #[pyfunction]
1002    fn fork(vm: &VirtualMachine) -> PyResult<i32> {
1003        if vm
1004            .state
1005            .finalizing
1006            .load(core::sync::atomic::Ordering::Acquire)
1007        {
1008            return Err(vm.new_exception_msg(
1009                vm.ctx.exceptions.python_finalization_error.to_owned(),
1010                "can't fork at interpreter shutdown".into(),
1011            ));
1012        }
1013
1014        // RustPython does not yet have C-level audit hooks; call sys.audit()
1015        // to preserve Python-visible behavior and failure semantics.
1016        vm.sys_module
1017            .get_attr("audit", vm)?
1018            .call(("os.fork",), vm)?;
1019
1020        py_os_before_fork(vm);
1021
1022        let pid = unsafe { libc::fork() };
1023        // Save errno immediately — AfterFork callbacks may clobber it.
1024        let saved_errno = nix::Error::last_raw();
1025        if pid == 0 {
1026            py_os_after_fork_child(vm);
1027        } else {
1028            // Match CPython timing: capture this before parent after-fork hooks
1029            // in case those hooks start threads.
1030            let num_os_threads = get_number_of_os_threads();
1031            py_os_after_fork_parent(vm);
1032            // Match CPython timing: warn only after parent callback path resumes world.
1033            warn_if_multi_threaded("fork", num_os_threads, vm);
1034        }
1035        if pid == -1 {
1036            Err(nix::Error::from_raw(saved_errno).into_pyexception(vm))
1037        } else {
1038            Ok(pid)
1039        }
1040    }
1041
1042    #[cfg(not(target_os = "redox"))]
1043    const MKNOD_DIR_FD: bool = cfg!(not(target_vendor = "apple"));
1044
1045    #[cfg(not(target_os = "redox"))]
1046    #[derive(FromArgs)]
1047    struct MknodArgs<'fd> {
1048        #[pyarg(any)]
1049        path: OsPath,
1050        #[pyarg(any)]
1051        mode: libc::mode_t,
1052        #[pyarg(any)]
1053        device: libc::dev_t,
1054        #[pyarg(flatten)]
1055        dir_fd: DirFd<'fd, { MKNOD_DIR_FD as usize }>,
1056    }
1057
1058    #[cfg(not(target_os = "redox"))]
1059    impl MknodArgs<'_> {
1060        fn _mknod(self, vm: &VirtualMachine) -> PyResult<i32> {
1061            Ok(unsafe {
1062                libc::mknod(
1063                    self.path.clone().into_cstring(vm)?.as_ptr(),
1064                    self.mode,
1065                    self.device,
1066                )
1067            })
1068        }
1069
1070        #[cfg(not(target_vendor = "apple"))]
1071        fn mknod(self, vm: &VirtualMachine) -> PyResult<()> {
1072            let ret = match self.dir_fd.raw_opt() {
1073                None => self._mknod(vm)?,
1074                Some(non_default_fd) => unsafe {
1075                    libc::mknodat(
1076                        non_default_fd,
1077                        self.path.clone().into_cstring(vm)?.as_ptr(),
1078                        self.mode,
1079                        self.device,
1080                    )
1081                },
1082            };
1083            if ret != 0 {
1084                Err(vm.new_last_errno_error())
1085            } else {
1086                Ok(())
1087            }
1088        }
1089
1090        #[cfg(target_vendor = "apple")]
1091        fn mknod(self, vm: &VirtualMachine) -> PyResult<()> {
1092            let [] = self.dir_fd.0;
1093            let ret = self._mknod(vm)?;
1094            if ret != 0 {
1095                Err(vm.new_last_errno_error())
1096            } else {
1097                Ok(())
1098            }
1099        }
1100    }
1101
1102    #[cfg(not(target_os = "redox"))]
1103    #[pyfunction]
1104    fn mknod(args: MknodArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
1105        args.mknod(vm)
1106    }
1107
1108    #[cfg(not(target_os = "redox"))]
1109    #[pyfunction]
1110    fn nice(increment: i32, vm: &VirtualMachine) -> PyResult<i32> {
1111        Errno::clear();
1112        let res = unsafe { libc::nice(increment) };
1113        if res == -1 && Errno::last_raw() != 0 {
1114            Err(vm.new_last_errno_error())
1115        } else {
1116            Ok(res)
1117        }
1118    }
1119
1120    #[cfg(not(target_os = "redox"))]
1121    #[pyfunction]
1122    fn sched_get_priority_max(policy: i32, vm: &VirtualMachine) -> PyResult<i32> {
1123        let max = unsafe { libc::sched_get_priority_max(policy) };
1124        if max == -1 {
1125            Err(vm.new_last_errno_error())
1126        } else {
1127            Ok(max)
1128        }
1129    }
1130
1131    #[cfg(not(target_os = "redox"))]
1132    #[pyfunction]
1133    fn sched_get_priority_min(policy: i32, vm: &VirtualMachine) -> PyResult<i32> {
1134        let min = unsafe { libc::sched_get_priority_min(policy) };
1135        if min == -1 {
1136            Err(vm.new_last_errno_error())
1137        } else {
1138            Ok(min)
1139        }
1140    }
1141
1142    #[pyfunction]
1143    fn sched_yield(vm: &VirtualMachine) -> PyResult<()> {
1144        nix::sched::sched_yield().map_err(|e| e.into_pyexception(vm))
1145    }
1146
1147    #[pyfunction]
1148    fn get_inheritable(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult<bool> {
1149        let flags = fcntl::fcntl(fd, fcntl::FcntlArg::F_GETFD);
1150        match flags {
1151            Ok(ret) => Ok((ret & libc::FD_CLOEXEC) == 0),
1152            Err(err) => Err(err.into_pyexception(vm)),
1153        }
1154    }
1155
1156    #[pyfunction]
1157    fn set_inheritable(fd: BorrowedFd<'_>, inheritable: bool, vm: &VirtualMachine) -> PyResult<()> {
1158        super::set_inheritable(fd, inheritable).map_err(|err| err.into_pyexception(vm))
1159    }
1160
1161    #[pyfunction]
1162    fn get_blocking(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult<bool> {
1163        let flags = fcntl::fcntl(fd, fcntl::FcntlArg::F_GETFL);
1164        match flags {
1165            Ok(ret) => Ok((ret & libc::O_NONBLOCK) == 0),
1166            Err(err) => Err(err.into_pyexception(vm)),
1167        }
1168    }
1169
1170    #[pyfunction]
1171    fn set_blocking(fd: BorrowedFd<'_>, blocking: bool, vm: &VirtualMachine) -> PyResult<()> {
1172        let _set_flag = || {
1173            use nix::fcntl::{FcntlArg, OFlag, fcntl};
1174
1175            let flags = OFlag::from_bits_truncate(fcntl(fd, FcntlArg::F_GETFL)?);
1176            let mut new_flags = flags;
1177            new_flags.set(OFlag::from_bits_truncate(libc::O_NONBLOCK), !blocking);
1178            if flags != new_flags {
1179                fcntl(fd, FcntlArg::F_SETFL(new_flags))?;
1180            }
1181            Ok(())
1182        };
1183        _set_flag().map_err(|err: nix::Error| err.into_pyexception(vm))
1184    }
1185
1186    #[pyfunction]
1187    fn pipe(vm: &VirtualMachine) -> PyResult<(OwnedFd, OwnedFd)> {
1188        use nix::unistd::pipe;
1189        let (rfd, wfd) = pipe().map_err(|err| err.into_pyexception(vm))?;
1190        set_inheritable(rfd.as_fd(), false, vm)?;
1191        set_inheritable(wfd.as_fd(), false, vm)?;
1192        Ok((rfd, wfd))
1193    }
1194
1195    // cfg from nix
1196    #[cfg(any(
1197        target_os = "android",
1198        target_os = "dragonfly",
1199        target_os = "emscripten",
1200        target_os = "freebsd",
1201        target_os = "linux",
1202        target_os = "netbsd",
1203        target_os = "openbsd"
1204    ))]
1205    #[pyfunction]
1206    fn pipe2(flags: libc::c_int, vm: &VirtualMachine) -> PyResult<(OwnedFd, OwnedFd)> {
1207        let oflags = fcntl::OFlag::from_bits_truncate(flags);
1208        nix::unistd::pipe2(oflags).map_err(|err| err.into_pyexception(vm))
1209    }
1210
1211    fn _chmod(
1212        path: OsPath,
1213        dir_fd: DirFd<'_, 0>,
1214        mode: u32,
1215        follow_symlinks: FollowSymlinks,
1216        vm: &VirtualMachine,
1217    ) -> PyResult<()> {
1218        let [] = dir_fd.0;
1219        let err_path = path.clone();
1220        let body = move || {
1221            use std::os::unix::fs::PermissionsExt;
1222            let meta = fs_metadata(&path, follow_symlinks.0)?;
1223            let mut permissions = meta.permissions();
1224            permissions.set_mode(mode);
1225            fs::set_permissions(&path, permissions)
1226        };
1227        body().map_err(|err| OSErrorBuilder::with_filename(&err, err_path, vm))
1228    }
1229
1230    #[cfg(not(target_os = "redox"))]
1231    fn _fchmod(fd: BorrowedFd<'_>, mode: u32, vm: &VirtualMachine) -> PyResult<()> {
1232        nix::sys::stat::fchmod(
1233            fd,
1234            nix::sys::stat::Mode::from_bits_truncate(mode as libc::mode_t),
1235        )
1236        .map_err(|err| err.into_pyexception(vm))
1237    }
1238
1239    #[cfg(not(target_os = "redox"))]
1240    #[pyfunction]
1241    fn chmod(
1242        path: OsPathOrFd<'_>,
1243        dir_fd: DirFd<'_, 0>,
1244        mode: u32,
1245        follow_symlinks: FollowSymlinks,
1246        vm: &VirtualMachine,
1247    ) -> PyResult<()> {
1248        match path {
1249            OsPathOrFd::Path(path) => {
1250                #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd",))]
1251                if !follow_symlinks.0 && dir_fd == Default::default() {
1252                    return lchmod(path, mode, vm);
1253                }
1254                _chmod(path, dir_fd, mode, follow_symlinks, vm)
1255            }
1256            OsPathOrFd::Fd(fd) => _fchmod(fd.into(), mode, vm),
1257        }
1258    }
1259
1260    #[cfg(target_os = "redox")]
1261    #[pyfunction]
1262    fn chmod(
1263        path: OsPath,
1264        dir_fd: DirFd<0>,
1265        mode: u32,
1266        follow_symlinks: FollowSymlinks,
1267        vm: &VirtualMachine,
1268    ) -> PyResult<()> {
1269        _chmod(path, dir_fd, mode, follow_symlinks, vm)
1270    }
1271
1272    #[cfg(not(target_os = "redox"))]
1273    #[pyfunction]
1274    fn fchmod(fd: BorrowedFd<'_>, mode: u32, vm: &VirtualMachine) -> PyResult<()> {
1275        _fchmod(fd, mode, vm)
1276    }
1277
1278    #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd",))]
1279    #[pyfunction]
1280    fn lchmod(path: OsPath, mode: u32, vm: &VirtualMachine) -> PyResult<()> {
1281        unsafe extern "C" {
1282            fn lchmod(path: *const libc::c_char, mode: libc::mode_t) -> libc::c_int;
1283        }
1284        let c_path = path.clone().into_cstring(vm)?;
1285        if unsafe { lchmod(c_path.as_ptr(), mode as libc::mode_t) } == 0 {
1286            Ok(())
1287        } else {
1288            let err = std::io::Error::last_os_error();
1289            Err(OSErrorBuilder::with_filename(&err, path, vm))
1290        }
1291    }
1292
1293    #[pyfunction]
1294    fn execv(
1295        path: OsPath,
1296        argv: Either<PyListRef, PyTupleRef>,
1297        vm: &VirtualMachine,
1298    ) -> PyResult<()> {
1299        let path = path.into_cstring(vm)?;
1300
1301        let argv = vm.extract_elements_with(argv.as_ref(), |obj| {
1302            OsPath::try_from_object(vm, obj)?.into_cstring(vm)
1303        })?;
1304        let argv: Vec<&CStr> = argv.iter().map(|entry| entry.as_c_str()).collect();
1305
1306        let first = argv
1307            .first()
1308            .ok_or_else(|| vm.new_value_error("execv() arg 2 must not be empty"))?;
1309        if first.to_bytes().is_empty() {
1310            return Err(vm.new_value_error("execv() arg 2 first element cannot be empty"));
1311        }
1312
1313        unistd::execv(&path, &argv)
1314            .map(|_ok| ())
1315            .map_err(|err| err.into_pyexception(vm))
1316    }
1317
1318    #[pyfunction]
1319    fn execve(
1320        path: OsPath,
1321        argv: Either<PyListRef, PyTupleRef>,
1322        env: ArgMapping,
1323        vm: &VirtualMachine,
1324    ) -> PyResult<()> {
1325        let path = path.into_cstring(vm)?;
1326
1327        let argv = vm.extract_elements_with(argv.as_ref(), |obj| {
1328            OsPath::try_from_object(vm, obj)?.into_cstring(vm)
1329        })?;
1330        let argv: Vec<&CStr> = argv.iter().map(|entry| entry.as_c_str()).collect();
1331
1332        let first = argv
1333            .first()
1334            .ok_or_else(|| vm.new_value_error("execve() arg 2 must not be empty"))?;
1335
1336        if first.to_bytes().is_empty() {
1337            return Err(vm.new_value_error("execve() arg 2 first element cannot be empty"));
1338        }
1339
1340        let env = crate::stdlib::os::envobj_to_dict(env, vm)?;
1341        let env = env
1342            .into_iter()
1343            .map(|(k, v)| -> PyResult<_> {
1344                let (key, value) = (
1345                    OsPath::try_from_object(vm, k)?.into_bytes(),
1346                    OsPath::try_from_object(vm, v)?.into_bytes(),
1347                );
1348
1349                if key.is_empty() || memchr::memchr(b'=', &key).is_some() {
1350                    return Err(vm.new_value_error("illegal environment variable name"));
1351                }
1352
1353                let mut entry = key;
1354                entry.push(b'=');
1355                entry.extend_from_slice(&value);
1356
1357                CString::new(entry).map_err(|err| err.into_pyexception(vm))
1358            })
1359            .collect::<Result<Vec<_>, _>>()?;
1360
1361        let env: Vec<&CStr> = env.iter().map(|entry| entry.as_c_str()).collect();
1362
1363        unistd::execve(&path, &argv, &env).map_err(|err| err.into_pyexception(vm))?;
1364        Ok(())
1365    }
1366
1367    #[pyfunction]
1368    fn getppid(vm: &VirtualMachine) -> PyObjectRef {
1369        let ppid = unistd::getppid().as_raw();
1370        vm.ctx.new_int(ppid).into()
1371    }
1372
1373    #[pyfunction]
1374    fn getgid(vm: &VirtualMachine) -> PyObjectRef {
1375        let gid = unistd::getgid().as_raw();
1376        vm.ctx.new_int(gid).into()
1377    }
1378
1379    #[pyfunction]
1380    fn getegid(vm: &VirtualMachine) -> PyObjectRef {
1381        let egid = unistd::getegid().as_raw();
1382        vm.ctx.new_int(egid).into()
1383    }
1384
1385    #[pyfunction]
1386    fn getpgid(pid: u32, vm: &VirtualMachine) -> PyResult {
1387        let pgid =
1388            unistd::getpgid(Some(Pid::from_raw(pid as i32))).map_err(|e| e.into_pyexception(vm))?;
1389        Ok(vm.new_pyobj(pgid.as_raw()))
1390    }
1391
1392    #[pyfunction]
1393    fn getpgrp(vm: &VirtualMachine) -> PyObjectRef {
1394        vm.ctx.new_int(unistd::getpgrp().as_raw()).into()
1395    }
1396
1397    #[cfg(not(target_os = "redox"))]
1398    #[pyfunction]
1399    fn getsid(pid: u32, vm: &VirtualMachine) -> PyResult {
1400        let sid =
1401            unistd::getsid(Some(Pid::from_raw(pid as i32))).map_err(|e| e.into_pyexception(vm))?;
1402        Ok(vm.new_pyobj(sid.as_raw()))
1403    }
1404
1405    #[pyfunction]
1406    fn getuid(vm: &VirtualMachine) -> PyObjectRef {
1407        let uid = unistd::getuid().as_raw();
1408        vm.ctx.new_int(uid).into()
1409    }
1410
1411    #[pyfunction]
1412    fn geteuid(vm: &VirtualMachine) -> PyObjectRef {
1413        let euid = unistd::geteuid().as_raw();
1414        vm.ctx.new_int(euid).into()
1415    }
1416
1417    #[cfg(not(any(target_os = "wasi", target_os = "android")))]
1418    #[pyfunction]
1419    fn setgid(gid: Gid, vm: &VirtualMachine) -> PyResult<()> {
1420        unistd::setgid(gid).map_err(|err| err.into_pyexception(vm))
1421    }
1422
1423    #[cfg(not(any(target_os = "wasi", target_os = "android", target_os = "redox")))]
1424    #[pyfunction]
1425    fn setegid(egid: Gid, vm: &VirtualMachine) -> PyResult<()> {
1426        unistd::setegid(egid).map_err(|err| err.into_pyexception(vm))
1427    }
1428
1429    #[pyfunction]
1430    fn setpgid(pid: u32, pgid: u32, vm: &VirtualMachine) -> PyResult<()> {
1431        unistd::setpgid(Pid::from_raw(pid as i32), Pid::from_raw(pgid as i32))
1432            .map_err(|err| err.into_pyexception(vm))
1433    }
1434
1435    #[pyfunction]
1436    fn setpgrp(vm: &VirtualMachine) -> PyResult<()> {
1437        // setpgrp() is equivalent to setpgid(0, 0)
1438        unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(|err| err.into_pyexception(vm))
1439    }
1440
1441    #[cfg(not(any(target_os = "wasi", target_os = "redox")))]
1442    #[pyfunction]
1443    fn setsid(vm: &VirtualMachine) -> PyResult<()> {
1444        unistd::setsid()
1445            .map(|_ok| ())
1446            .map_err(|err| err.into_pyexception(vm))
1447    }
1448
1449    #[cfg(not(any(target_os = "wasi", target_os = "redox")))]
1450    #[pyfunction]
1451    fn tcgetpgrp(fd: i32, vm: &VirtualMachine) -> PyResult<libc::pid_t> {
1452        use std::os::fd::BorrowedFd;
1453        let fd = unsafe { BorrowedFd::borrow_raw(fd) };
1454        unistd::tcgetpgrp(fd)
1455            .map(|pid| pid.as_raw())
1456            .map_err(|err| err.into_pyexception(vm))
1457    }
1458
1459    #[cfg(not(any(target_os = "wasi", target_os = "redox")))]
1460    #[pyfunction]
1461    fn tcsetpgrp(fd: i32, pgid: libc::pid_t, vm: &VirtualMachine) -> PyResult<()> {
1462        use std::os::fd::BorrowedFd;
1463        let fd = unsafe { BorrowedFd::borrow_raw(fd) };
1464        unistd::tcsetpgrp(fd, Pid::from_raw(pgid)).map_err(|err| err.into_pyexception(vm))
1465    }
1466
1467    fn try_from_id(vm: &VirtualMachine, obj: PyObjectRef, typ_name: &str) -> PyResult<u32> {
1468        use core::cmp::Ordering;
1469        let i = obj
1470            .try_to_ref::<PyInt>(vm)
1471            .map_err(|_| {
1472                vm.new_type_error(format!(
1473                    "an integer is required (got type {})",
1474                    obj.class().name()
1475                ))
1476            })?
1477            .try_to_primitive::<i64>(vm)?;
1478
1479        match i.cmp(&-1) {
1480            Ordering::Greater => Ok(i.try_into().map_err(|_| {
1481                vm.new_overflow_error(format!("{typ_name} is larger than maximum"))
1482            })?),
1483            Ordering::Less => {
1484                Err(vm.new_overflow_error(format!("{typ_name} is less than minimum")))
1485            }
1486            // -1 means does not change the value
1487            // In CPython, this is `(uid_t) -1`, rustc gets mad when we try to declare
1488            // a negative unsigned integer :).
1489            Ordering::Equal => Ok(-1i32 as u32),
1490        }
1491    }
1492
1493    impl TryFromObject for Uid {
1494        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
1495            try_from_id(vm, obj, "uid").map(Self::from_raw)
1496        }
1497    }
1498
1499    impl TryFromObject for Gid {
1500        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
1501            try_from_id(vm, obj, "gid").map(Self::from_raw)
1502        }
1503    }
1504
1505    #[cfg(not(any(target_os = "wasi", target_os = "android")))]
1506    #[pyfunction]
1507    fn setuid(uid: Uid) -> nix::Result<()> {
1508        unistd::setuid(uid)
1509    }
1510
1511    #[cfg(not(any(target_os = "wasi", target_os = "android", target_os = "redox")))]
1512    #[pyfunction]
1513    fn seteuid(euid: Uid) -> nix::Result<()> {
1514        unistd::seteuid(euid)
1515    }
1516
1517    #[cfg(not(any(target_os = "wasi", target_os = "android", target_os = "redox")))]
1518    #[pyfunction]
1519    fn setreuid(ruid: Uid, euid: Uid) -> nix::Result<()> {
1520        let ret = unsafe { libc::setreuid(ruid.as_raw(), euid.as_raw()) };
1521        nix::Error::result(ret).map(drop)
1522    }
1523
1524    // cfg from nix
1525    #[cfg(any(
1526        target_os = "android",
1527        target_os = "freebsd",
1528        target_os = "linux",
1529        target_os = "openbsd"
1530    ))]
1531    #[pyfunction]
1532    fn setresuid(ruid: Uid, euid: Uid, suid: Uid) -> nix::Result<()> {
1533        unistd::setresuid(ruid, euid, suid)
1534    }
1535
1536    #[cfg(not(target_os = "redox"))]
1537    #[pyfunction]
1538    fn openpty(vm: &VirtualMachine) -> PyResult<(OwnedFd, OwnedFd)> {
1539        let r = nix::pty::openpty(None, None).map_err(|err| err.into_pyexception(vm))?;
1540        for fd in [&r.master, &r.slave] {
1541            super::set_inheritable(fd.as_fd(), false).map_err(|e| e.into_pyexception(vm))?;
1542        }
1543        Ok((r.master, r.slave))
1544    }
1545
1546    #[pyfunction]
1547    fn ttyname(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult {
1548        let name = unistd::ttyname(fd).map_err(|e| e.into_pyexception(vm))?;
1549        let name = name.into_os_string().into_string().unwrap();
1550        Ok(vm.ctx.new_str(name).into())
1551    }
1552
1553    #[pyfunction]
1554    fn umask(mask: libc::mode_t) -> libc::mode_t {
1555        unsafe { libc::umask(mask) }
1556    }
1557
1558    #[pyfunction]
1559    fn uname(vm: &VirtualMachine) -> PyResult<_os::UnameResultData> {
1560        let info = uname::uname().map_err(|err| err.into_pyexception(vm))?;
1561        Ok(_os::UnameResultData {
1562            sysname: info.sysname,
1563            nodename: info.nodename,
1564            release: info.release,
1565            version: info.version,
1566            machine: info.machine,
1567        })
1568    }
1569
1570    #[pyfunction]
1571    fn sync() {
1572        #[cfg(not(any(target_os = "redox", target_os = "android")))]
1573        unsafe {
1574            libc::sync();
1575        }
1576    }
1577
1578    // cfg from nix
1579    #[cfg(any(target_os = "android", target_os = "linux", target_os = "openbsd"))]
1580    #[pyfunction]
1581    fn getresuid() -> nix::Result<(u32, u32, u32)> {
1582        let ret = unistd::getresuid()?;
1583        Ok((
1584            ret.real.as_raw(),
1585            ret.effective.as_raw(),
1586            ret.saved.as_raw(),
1587        ))
1588    }
1589
1590    // cfg from nix
1591    #[cfg(any(target_os = "android", target_os = "linux", target_os = "openbsd"))]
1592    #[pyfunction]
1593    fn getresgid() -> nix::Result<(u32, u32, u32)> {
1594        let ret = unistd::getresgid()?;
1595        Ok((
1596            ret.real.as_raw(),
1597            ret.effective.as_raw(),
1598            ret.saved.as_raw(),
1599        ))
1600    }
1601
1602    // cfg from nix
1603    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
1604    #[pyfunction]
1605    fn setresgid(rgid: Gid, egid: Gid, sgid: Gid, vm: &VirtualMachine) -> PyResult<()> {
1606        unistd::setresgid(rgid, egid, sgid).map_err(|err| err.into_pyexception(vm))
1607    }
1608
1609    #[cfg(not(any(target_os = "wasi", target_os = "android", target_os = "redox")))]
1610    #[pyfunction]
1611    fn setregid(rgid: Gid, egid: Gid) -> nix::Result<()> {
1612        let ret = unsafe { libc::setregid(rgid.as_raw(), egid.as_raw()) };
1613        nix::Error::result(ret).map(drop)
1614    }
1615
1616    // cfg from nix
1617    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
1618    #[pyfunction]
1619    fn initgroups(user_name: PyUtf8StrRef, gid: Gid, vm: &VirtualMachine) -> PyResult<()> {
1620        let user = user_name.to_cstring(vm)?;
1621        unistd::initgroups(&user, gid).map_err(|err| err.into_pyexception(vm))
1622    }
1623
1624    // cfg from nix
1625    #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "redox")))]
1626    #[pyfunction]
1627    fn setgroups(
1628        group_ids: crate::function::ArgIterable<Gid>,
1629        vm: &VirtualMachine,
1630    ) -> PyResult<()> {
1631        let gids = group_ids.iter(vm)?.collect::<Result<Vec<_>, _>>()?;
1632        unistd::setgroups(&gids).map_err(|err| err.into_pyexception(vm))
1633    }
1634
1635    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1636    fn envp_from_dict(
1637        env: crate::function::ArgMapping,
1638        vm: &VirtualMachine,
1639    ) -> PyResult<Vec<CString>> {
1640        let items = env.mapping().items(vm)?;
1641
1642        // Convert items to list if it isn't already
1643        let items = vm.ctx.new_list(
1644            items
1645                .get_iter(vm)?
1646                .iter(vm)?
1647                .collect::<PyResult<Vec<_>>>()?,
1648        );
1649
1650        items
1651            .borrow_vec()
1652            .iter()
1653            .map(|item| {
1654                let tuple = item
1655                    .downcast_ref::<crate::builtins::PyTuple>()
1656                    .ok_or_else(|| vm.new_type_error("items() should return tuples"))?;
1657                let tuple_items = tuple.as_slice();
1658                if tuple_items.len() != 2 {
1659                    return Err(vm.new_value_error("items() tuples should have exactly 2 elements"));
1660                }
1661                Ok((tuple_items[0].clone(), tuple_items[1].clone()))
1662            })
1663            .collect::<PyResult<Vec<_>>>()?
1664            .into_iter()
1665            .map(|(k, v)| {
1666                let k = OsPath::try_from_object(vm, k)?.into_bytes();
1667                let v = OsPath::try_from_object(vm, v)?.into_bytes();
1668                if k.contains(&0) {
1669                    return Err(vm.new_value_error("envp dict key cannot contain a nul byte"));
1670                }
1671                if k.contains(&b'=') {
1672                    return Err(vm.new_value_error("envp dict key cannot contain a '=' character"));
1673                }
1674                if v.contains(&0) {
1675                    return Err(vm.new_value_error("envp dict value cannot contain a nul byte"));
1676                }
1677                let mut env = k;
1678                env.push(b'=');
1679                env.extend(v);
1680                Ok(unsafe { CString::from_vec_unchecked(env) })
1681            })
1682            .collect()
1683    }
1684
1685    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1686    #[derive(FromArgs)]
1687    pub(super) struct PosixSpawnArgs {
1688        #[pyarg(positional)]
1689        path: OsPath,
1690        #[pyarg(positional)]
1691        args: crate::function::ArgIterable<OsPath>,
1692        #[pyarg(positional)]
1693        env: Option<crate::function::ArgMapping>,
1694        #[pyarg(named, default)]
1695        file_actions: Option<crate::function::ArgIterable<PyTupleRef>>,
1696        #[pyarg(named, default)]
1697        setsigdef: Option<crate::function::ArgIterable<i32>>,
1698        #[pyarg(named, default)]
1699        setpgroup: Option<libc::pid_t>,
1700        #[pyarg(named, default)]
1701        resetids: bool,
1702        #[pyarg(named, default)]
1703        setsid: bool,
1704        #[pyarg(named, default)]
1705        setsigmask: Option<crate::function::ArgIterable<i32>>,
1706        #[pyarg(named, default)]
1707        scheduler: Option<PyTupleRef>,
1708    }
1709
1710    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1711    #[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive)]
1712    #[repr(i32)]
1713    enum PosixSpawnFileActionIdentifier {
1714        Open,
1715        Close,
1716        Dup2,
1717    }
1718
1719    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1720    impl PosixSpawnArgs {
1721        fn spawn(self, spawnp: bool, vm: &VirtualMachine) -> PyResult<libc::pid_t> {
1722            use nix::sys::signal;
1723
1724            use crate::TryFromBorrowedObject;
1725
1726            let path = self
1727                .path
1728                .clone()
1729                .into_cstring(vm)
1730                .map_err(|_| vm.new_value_error("path should not have nul bytes"))?;
1731
1732            let mut file_actions =
1733                nix::spawn::PosixSpawnFileActions::init().map_err(|e| e.into_pyexception(vm))?;
1734            if let Some(it) = self.file_actions {
1735                for action in it.iter(vm)? {
1736                    let action = action?;
1737                    let (id, args) = action.split_first().ok_or_else(|| {
1738                        vm.new_type_error("Each file_actions element must be a non-empty tuple")
1739                    })?;
1740                    let id = i32::try_from_borrowed_object(vm, id)?;
1741                    let id = PosixSpawnFileActionIdentifier::try_from(id)
1742                        .map_err(|_| vm.new_type_error("Unknown file_actions identifier"))?;
1743                    let args: crate::function::FuncArgs = args.to_vec().into();
1744                    let ret = match id {
1745                        PosixSpawnFileActionIdentifier::Open => {
1746                            let (fd, path, oflag, mode): (_, OsPath, _, _) = args.bind(vm)?;
1747                            let path = CString::new(path.into_bytes()).map_err(|_| {
1748                                vm.new_value_error(
1749                                    "POSIX_SPAWN_OPEN path should not have nul bytes",
1750                                )
1751                            })?;
1752                            let oflag = nix::fcntl::OFlag::from_bits_retain(oflag);
1753                            let mode = nix::sys::stat::Mode::from_bits_retain(mode);
1754                            file_actions.add_open(fd, &*path, oflag, mode)
1755                        }
1756                        PosixSpawnFileActionIdentifier::Close => {
1757                            let (fd,) = args.bind(vm)?;
1758                            file_actions.add_close(fd)
1759                        }
1760                        PosixSpawnFileActionIdentifier::Dup2 => {
1761                            let (fd, newfd) = args.bind(vm)?;
1762                            file_actions.add_dup2(fd, newfd)
1763                        }
1764                    };
1765                    if let Err(err) = ret {
1766                        let err = err.into();
1767                        return Err(OSErrorBuilder::with_filename(&err, self.path, vm));
1768                    }
1769                }
1770            }
1771
1772            let mut attrp =
1773                nix::spawn::PosixSpawnAttr::init().map_err(|e| e.into_pyexception(vm))?;
1774            let mut flags = nix::spawn::PosixSpawnFlags::empty();
1775
1776            if let Some(sigs) = self.setsigdef {
1777                let mut set = signal::SigSet::empty();
1778                for sig in sigs.iter(vm)? {
1779                    let sig = sig?;
1780                    let sig = signal::Signal::try_from(sig).map_err(|_| {
1781                        vm.new_value_error(format!("signal number {sig} out of range"))
1782                    })?;
1783                    set.add(sig);
1784                }
1785                attrp
1786                    .set_sigdefault(&set)
1787                    .map_err(|e| e.into_pyexception(vm))?;
1788                flags.insert(nix::spawn::PosixSpawnFlags::POSIX_SPAWN_SETSIGDEF);
1789            }
1790
1791            if let Some(pgid) = self.setpgroup {
1792                attrp
1793                    .set_pgroup(nix::unistd::Pid::from_raw(pgid))
1794                    .map_err(|e| e.into_pyexception(vm))?;
1795                flags.insert(nix::spawn::PosixSpawnFlags::POSIX_SPAWN_SETPGROUP);
1796            }
1797
1798            if self.resetids {
1799                flags.insert(nix::spawn::PosixSpawnFlags::POSIX_SPAWN_RESETIDS);
1800            }
1801
1802            if self.setsid {
1803                // Note: POSIX_SPAWN_SETSID may not be available on all platforms
1804                cfg_if::cfg_if! {
1805                    if #[cfg(any(
1806                        target_os = "linux",
1807                        target_os = "haiku",
1808                        target_os = "solaris",
1809                        target_os = "illumos",
1810                        target_os = "hurd",
1811                    ))] {
1812                        flags.insert(nix::spawn::PosixSpawnFlags::from_bits_retain(libc::POSIX_SPAWN_SETSID));
1813                    } else {
1814                        return Err(vm.new_not_implemented_error(
1815                            "setsid parameter is not supported on this platform",
1816                        ));
1817                    }
1818                }
1819            }
1820
1821            if let Some(sigs) = self.setsigmask {
1822                let mut set = signal::SigSet::empty();
1823                for sig in sigs.iter(vm)? {
1824                    let sig = sig?;
1825                    let sig = signal::Signal::try_from(sig).map_err(|_| {
1826                        vm.new_value_error(format!("signal number {sig} out of range"))
1827                    })?;
1828                    set.add(sig);
1829                }
1830                attrp
1831                    .set_sigmask(&set)
1832                    .map_err(|e| e.into_pyexception(vm))?;
1833                flags.insert(nix::spawn::PosixSpawnFlags::POSIX_SPAWN_SETSIGMASK);
1834            }
1835
1836            if let Some(_scheduler) = self.scheduler {
1837                // TODO: Implement scheduler parameter handling
1838                // This requires platform-specific sched_param struct handling
1839                return Err(
1840                    vm.new_not_implemented_error("scheduler parameter is not yet implemented")
1841                );
1842            }
1843
1844            if !flags.is_empty() {
1845                attrp.set_flags(flags).map_err(|e| e.into_pyexception(vm))?;
1846            }
1847
1848            let args: Vec<CString> = self
1849                .args
1850                .iter(vm)?
1851                .map(|res| {
1852                    CString::new(res?.into_bytes())
1853                        .map_err(|_| vm.new_value_error("path should not have nul bytes"))
1854                })
1855                .collect::<Result<_, _>>()?;
1856            let env = if let Some(env_dict) = self.env {
1857                envp_from_dict(env_dict, vm)?
1858            } else {
1859                // env=None means use the current environment
1860
1861                env::vars_os()
1862                    .map(|(k, v)| {
1863                        let mut entry = k.into_vec();
1864                        entry.push(b'=');
1865                        entry.extend(v.into_vec());
1866                        CString::new(entry).map_err(|_| {
1867                            vm.new_value_error("environment string contains null byte")
1868                        })
1869                    })
1870                    .collect::<PyResult<Vec<_>>>()?
1871            };
1872
1873            let ret = if spawnp {
1874                nix::spawn::posix_spawnp(&path, &file_actions, &attrp, &args, &env)
1875            } else {
1876                nix::spawn::posix_spawn(&*path, &file_actions, &attrp, &args, &env)
1877            };
1878            ret.map(Into::into)
1879                .map_err(|err| OSErrorBuilder::with_filename(&err.into(), self.path, vm))
1880        }
1881    }
1882
1883    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1884    #[pyfunction]
1885    fn posix_spawn(args: PosixSpawnArgs, vm: &VirtualMachine) -> PyResult<libc::pid_t> {
1886        args.spawn(false, vm)
1887    }
1888
1889    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
1890    #[pyfunction]
1891    fn posix_spawnp(args: PosixSpawnArgs, vm: &VirtualMachine) -> PyResult<libc::pid_t> {
1892        args.spawn(true, vm)
1893    }
1894
1895    #[pyfunction(name = "WCOREDUMP")]
1896    fn wcoredump(status: i32) -> bool {
1897        libc::WCOREDUMP(status)
1898    }
1899
1900    #[pyfunction(name = "WIFCONTINUED")]
1901    fn wifcontinued(status: i32) -> bool {
1902        libc::WIFCONTINUED(status)
1903    }
1904
1905    #[pyfunction(name = "WIFSTOPPED")]
1906    fn wifstopped(status: i32) -> bool {
1907        libc::WIFSTOPPED(status)
1908    }
1909
1910    #[pyfunction(name = "WIFSIGNALED")]
1911    fn wifsignaled(status: i32) -> bool {
1912        libc::WIFSIGNALED(status)
1913    }
1914
1915    #[pyfunction(name = "WIFEXITED")]
1916    fn wifexited(status: i32) -> bool {
1917        libc::WIFEXITED(status)
1918    }
1919
1920    #[pyfunction(name = "WEXITSTATUS")]
1921    fn wexitstatus(status: i32) -> i32 {
1922        libc::WEXITSTATUS(status)
1923    }
1924
1925    #[pyfunction(name = "WSTOPSIG")]
1926    fn wstopsig(status: i32) -> i32 {
1927        libc::WSTOPSIG(status)
1928    }
1929
1930    #[pyfunction(name = "WTERMSIG")]
1931    fn wtermsig(status: i32) -> i32 {
1932        libc::WTERMSIG(status)
1933    }
1934
1935    #[cfg(target_os = "linux")]
1936    #[pyfunction]
1937    fn pidfd_open(
1938        pid: libc::pid_t,
1939        flags: OptionalArg<u32>,
1940        vm: &VirtualMachine,
1941    ) -> PyResult<OwnedFd> {
1942        let flags = flags.unwrap_or(0);
1943        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, flags) as libc::c_long };
1944        if fd == -1 {
1945            Err(vm.new_last_errno_error())
1946        } else {
1947            // Safety: syscall returns a new owned file descriptor.
1948            Ok(unsafe { OwnedFd::from_raw_fd(fd as libc::c_int) })
1949        }
1950    }
1951
1952    #[pyfunction]
1953    fn waitpid(pid: libc::pid_t, opt: i32, vm: &VirtualMachine) -> PyResult<(libc::pid_t, i32)> {
1954        let mut status = 0;
1955        loop {
1956            // Capture errno inside the closure: attach_thread (called by
1957            // allow_threads on return) can clobber errno via syscalls.
1958            let (res, err) = vm.allow_threads(|| {
1959                let r = unsafe { libc::waitpid(pid, &mut status, opt) };
1960                (r, nix::Error::last_raw())
1961            });
1962            if res == -1 {
1963                if err == libc::EINTR {
1964                    vm.check_signals()?;
1965                    continue;
1966                }
1967                return Err(nix::Error::from_raw(err).into_pyexception(vm));
1968            }
1969            return Ok((res, status));
1970        }
1971    }
1972
1973    #[pyfunction]
1974    fn wait(vm: &VirtualMachine) -> PyResult<(libc::pid_t, i32)> {
1975        waitpid(-1, 0, vm)
1976    }
1977
1978    #[pyfunction]
1979    fn kill(pid: i32, sig: isize, vm: &VirtualMachine) -> PyResult<()> {
1980        {
1981            let ret = unsafe { libc::kill(pid, sig as i32) };
1982            if ret == -1 {
1983                Err(vm.new_last_errno_error())
1984            } else {
1985                Ok(())
1986            }
1987        }
1988    }
1989
1990    #[pyfunction]
1991    fn get_terminal_size(
1992        fd: OptionalArg<i32>,
1993        vm: &VirtualMachine,
1994    ) -> PyResult<_os::TerminalSizeData> {
1995        let (columns, lines) = {
1996            nix::ioctl_read_bad!(winsz, libc::TIOCGWINSZ, libc::winsize);
1997            let mut w = libc::winsize {
1998                ws_row: 0,
1999                ws_col: 0,
2000                ws_xpixel: 0,
2001                ws_ypixel: 0,
2002            };
2003            unsafe { winsz(fd.unwrap_or(libc::STDOUT_FILENO), &mut w) }
2004                .map_err(|err| err.into_pyexception(vm))?;
2005            (w.ws_col.into(), w.ws_row.into())
2006        };
2007        Ok(_os::TerminalSizeData { columns, lines })
2008    }
2009
2010    // from libstd:
2011    // https://github.com/rust-lang/rust/blob/daecab3a784f28082df90cebb204998051f3557d/src/libstd/sys/unix/fs.rs#L1251
2012    #[cfg(target_os = "macos")]
2013    unsafe extern "C" {
2014        fn fcopyfile(
2015            in_fd: libc::c_int,
2016            out_fd: libc::c_int,
2017            state: *mut libc::c_void, // copyfile_state_t (unused)
2018            flags: u32,               // copyfile_flags_t
2019        ) -> libc::c_int;
2020    }
2021
2022    #[cfg(target_os = "macos")]
2023    #[pyfunction]
2024    fn _fcopyfile(in_fd: i32, out_fd: i32, flags: i32, vm: &VirtualMachine) -> PyResult<()> {
2025        let ret = unsafe { fcopyfile(in_fd, out_fd, core::ptr::null_mut(), flags as u32) };
2026        if ret < 0 {
2027            Err(vm.new_last_errno_error())
2028        } else {
2029            Ok(())
2030        }
2031    }
2032
2033    #[pyfunction]
2034    fn dup(fd: BorrowedFd<'_>, vm: &VirtualMachine) -> PyResult<OwnedFd> {
2035        let fd = nix::unistd::dup(fd).map_err(|e| e.into_pyexception(vm))?;
2036        super::set_inheritable(fd.as_fd(), false)
2037            .map(|()| fd)
2038            .map_err(|e| e.into_pyexception(vm))
2039    }
2040
2041    #[derive(FromArgs)]
2042    struct Dup2Args<'fd> {
2043        #[pyarg(positional)]
2044        fd: BorrowedFd<'fd>,
2045        #[pyarg(positional)]
2046        fd2: OwnedFd,
2047        #[pyarg(any, default = true)]
2048        inheritable: bool,
2049    }
2050
2051    #[pyfunction]
2052    fn dup2(args: Dup2Args<'_>, vm: &VirtualMachine) -> PyResult<OwnedFd> {
2053        let mut fd2 = core::mem::ManuallyDrop::new(args.fd2);
2054        nix::unistd::dup2(args.fd, &mut fd2).map_err(|e| e.into_pyexception(vm))?;
2055        let fd2 = core::mem::ManuallyDrop::into_inner(fd2);
2056        if !args.inheritable {
2057            super::set_inheritable(fd2.as_fd(), false).map_err(|e| e.into_pyexception(vm))?
2058        }
2059        Ok(fd2)
2060    }
2061
2062    pub(crate) fn support_funcs() -> Vec<SupportFunc> {
2063        vec![
2064            SupportFunc::new(
2065                "chmod",
2066                Some(false),
2067                Some(false),
2068                Some(cfg!(any(
2069                    target_os = "macos",
2070                    target_os = "freebsd",
2071                    target_os = "netbsd"
2072                ))),
2073            ),
2074            #[cfg(not(target_os = "redox"))]
2075            SupportFunc::new("chroot", Some(false), None, None),
2076            #[cfg(not(target_os = "redox"))]
2077            SupportFunc::new("chown", Some(true), Some(true), Some(true)),
2078            #[cfg(not(target_os = "redox"))]
2079            SupportFunc::new("lchown", None, None, None),
2080            #[cfg(not(target_os = "redox"))]
2081            SupportFunc::new("fchown", Some(true), None, Some(true)),
2082            #[cfg(not(target_os = "redox"))]
2083            SupportFunc::new("mknod", Some(true), Some(MKNOD_DIR_FD), Some(false)),
2084            SupportFunc::new("umask", Some(false), Some(false), Some(false)),
2085            SupportFunc::new("execv", None, None, None),
2086            SupportFunc::new("pathconf", Some(true), None, None),
2087            SupportFunc::new("fpathconf", Some(true), None, None),
2088            SupportFunc::new("fchdir", Some(true), None, None),
2089        ]
2090    }
2091
2092    #[pyfunction]
2093    fn getlogin(vm: &VirtualMachine) -> PyResult<String> {
2094        // Get a pointer to the login name string. The string is statically
2095        // allocated and might be overwritten on subsequent calls to this
2096        // function or to `cuserid()`. See man getlogin(3) for more information.
2097        let ptr = unsafe { libc::getlogin() };
2098        if ptr.is_null() {
2099            return Err(vm.new_os_error("unable to determine login name"));
2100        }
2101        let slice = unsafe { CStr::from_ptr(ptr) };
2102        slice
2103            .to_str()
2104            .map(|s| s.to_owned())
2105            .map_err(|e| vm.new_unicode_decode_error(format!("unable to decode login name: {e}")))
2106    }
2107
2108    // cfg from nix
2109    #[cfg(any(
2110        target_os = "android",
2111        target_os = "freebsd",
2112        target_os = "linux",
2113        target_os = "openbsd"
2114    ))]
2115    #[pyfunction]
2116    fn getgrouplist(
2117        user: PyUtf8StrRef,
2118        group: u32,
2119        vm: &VirtualMachine,
2120    ) -> PyResult<Vec<PyObjectRef>> {
2121        let user = user.to_cstring(vm)?;
2122        let gid = Gid::from_raw(group);
2123        let group_ids = unistd::getgrouplist(&user, gid).map_err(|err| err.into_pyexception(vm))?;
2124        Ok(group_ids
2125            .into_iter()
2126            .map(|gid| vm.new_pyobj(gid.as_raw()))
2127            .collect())
2128    }
2129
2130    #[cfg(not(target_os = "redox"))]
2131    cfg_if::cfg_if! {
2132        if #[cfg(all(target_os = "linux", target_env = "gnu"))] {
2133            type PriorityWhichType = libc::__priority_which_t;
2134        } else {
2135            type PriorityWhichType = libc::c_int;
2136        }
2137    }
2138    #[cfg(not(target_os = "redox"))]
2139    cfg_if::cfg_if! {
2140        if #[cfg(target_os = "freebsd")] {
2141            type PriorityWhoType = i32;
2142        } else {
2143            type PriorityWhoType = u32;
2144        }
2145    }
2146
2147    #[cfg(not(target_os = "redox"))]
2148    #[pyfunction]
2149    fn getpriority(
2150        which: PriorityWhichType,
2151        who: PriorityWhoType,
2152        vm: &VirtualMachine,
2153    ) -> PyResult {
2154        Errno::clear();
2155        let retval = unsafe { libc::getpriority(which, who) };
2156        if Errno::last_raw() != 0 {
2157            Err(vm.new_last_errno_error())
2158        } else {
2159            Ok(vm.ctx.new_int(retval).into())
2160        }
2161    }
2162
2163    #[cfg(not(target_os = "redox"))]
2164    #[pyfunction]
2165    fn setpriority(
2166        which: PriorityWhichType,
2167        who: PriorityWhoType,
2168        priority: i32,
2169        vm: &VirtualMachine,
2170    ) -> PyResult<()> {
2171        let retval = unsafe { libc::setpriority(which, who, priority) };
2172        if retval == -1 {
2173            Err(vm.new_last_errno_error())
2174        } else {
2175            Ok(())
2176        }
2177    }
2178
2179    struct PathconfName(i32);
2180
2181    impl TryFromObject for PathconfName {
2182        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
2183            let i = match obj.downcast::<PyInt>() {
2184                Ok(int) => int.try_to_primitive(vm)?,
2185                Err(obj) => {
2186                    let s = obj.downcast::<PyUtf8Str>().map_err(|_| {
2187                        vm.new_type_error("configuration names must be strings or integers")
2188                    })?;
2189                    s.as_str()
2190                        .parse::<PathconfVar>()
2191                        .map_err(|_| vm.new_value_error("unrecognized configuration name"))?
2192                        as i32
2193                }
2194            };
2195            Ok(Self(i))
2196        }
2197    }
2198
2199    // Copy from [nix::unistd::PathconfVar](https://docs.rs/nix/0.21.0/nix/unistd/enum.PathconfVar.html)
2200    // Change enum name to fit python doc
2201    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, EnumIter, EnumString)]
2202    #[repr(i32)]
2203    #[allow(non_camel_case_types)]
2204    pub enum PathconfVar {
2205        #[cfg(any(
2206            target_os = "dragonfly",
2207            target_os = "freebsd",
2208            target_os = "linux",
2209            target_os = "netbsd",
2210            target_os = "openbsd",
2211            target_os = "redox"
2212        ))]
2213        /// Minimum number of bits needed to represent, as a signed integer value,
2214        /// the maximum size of a regular file allowed in the specified directory.
2215        PC_FILESIZEBITS = libc::_PC_FILESIZEBITS,
2216        /// Maximum number of links to a single file.
2217        PC_LINK_MAX = libc::_PC_LINK_MAX,
2218        /// Maximum number of bytes in a terminal canonical input line.
2219        PC_MAX_CANON = libc::_PC_MAX_CANON,
2220        /// Minimum number of bytes for which space is available in a terminal input
2221        /// queue; therefore, the maximum number of bytes a conforming application
2222        /// may require to be typed as input before reading them.
2223        PC_MAX_INPUT = libc::_PC_MAX_INPUT,
2224        /// Maximum number of bytes in a filename (not including the terminating
2225        /// null of a filename string).
2226        PC_NAME_MAX = libc::_PC_NAME_MAX,
2227        /// Maximum number of bytes the implementation will store as a pathname in a
2228        /// user-supplied buffer of unspecified size, including the terminating null
2229        /// character. Minimum number the implementation will accept as the maximum
2230        /// number of bytes in a pathname.
2231        PC_PATH_MAX = libc::_PC_PATH_MAX,
2232        /// Maximum number of bytes that is guaranteed to be atomic when writing to
2233        /// a pipe.
2234        PC_PIPE_BUF = libc::_PC_PIPE_BUF,
2235        #[cfg(any(
2236            target_os = "android",
2237            target_os = "dragonfly",
2238            target_os = "illumos",
2239            target_os = "linux",
2240            target_os = "netbsd",
2241            target_os = "openbsd",
2242            target_os = "redox",
2243            target_os = "solaris"
2244        ))]
2245        /// Symbolic links can be created.
2246        PC_2_SYMLINKS = libc::_PC_2_SYMLINKS,
2247        #[cfg(any(
2248            target_os = "android",
2249            target_os = "dragonfly",
2250            target_os = "freebsd",
2251            target_os = "linux",
2252            target_os = "openbsd",
2253            target_os = "redox"
2254        ))]
2255        /// Minimum number of bytes of storage actually allocated for any portion of
2256        /// a file.
2257        PC_ALLOC_SIZE_MIN = libc::_PC_ALLOC_SIZE_MIN,
2258        #[cfg(any(
2259            target_os = "android",
2260            target_os = "dragonfly",
2261            target_os = "freebsd",
2262            target_os = "linux",
2263            target_os = "openbsd"
2264        ))]
2265        /// Recommended increment for file transfer sizes between the
2266        /// `POSIX_REC_MIN_XFER_SIZE` and `POSIX_REC_MAX_XFER_SIZE` values.
2267        PC_REC_INCR_XFER_SIZE = libc::_PC_REC_INCR_XFER_SIZE,
2268        #[cfg(any(
2269            target_os = "android",
2270            target_os = "dragonfly",
2271            target_os = "freebsd",
2272            target_os = "linux",
2273            target_os = "openbsd",
2274            target_os = "redox"
2275        ))]
2276        /// Maximum recommended file transfer size.
2277        PC_REC_MAX_XFER_SIZE = libc::_PC_REC_MAX_XFER_SIZE,
2278        #[cfg(any(
2279            target_os = "android",
2280            target_os = "dragonfly",
2281            target_os = "freebsd",
2282            target_os = "linux",
2283            target_os = "openbsd",
2284            target_os = "redox"
2285        ))]
2286        /// Minimum recommended file transfer size.
2287        PC_REC_MIN_XFER_SIZE = libc::_PC_REC_MIN_XFER_SIZE,
2288        #[cfg(any(
2289            target_os = "android",
2290            target_os = "dragonfly",
2291            target_os = "freebsd",
2292            target_os = "linux",
2293            target_os = "openbsd",
2294            target_os = "redox"
2295        ))]
2296        ///  Recommended file transfer buffer alignment.
2297        PC_REC_XFER_ALIGN = libc::_PC_REC_XFER_ALIGN,
2298        #[cfg(any(
2299            target_os = "android",
2300            target_os = "dragonfly",
2301            target_os = "freebsd",
2302            target_os = "illumos",
2303            target_os = "linux",
2304            target_os = "netbsd",
2305            target_os = "openbsd",
2306            target_os = "redox",
2307            target_os = "solaris"
2308        ))]
2309        /// Maximum number of bytes in a symbolic link.
2310        PC_SYMLINK_MAX = libc::_PC_SYMLINK_MAX,
2311        /// The use of `chown` and `fchown` is restricted to a process with
2312        /// appropriate privileges, and to changing the group ID of a file only to
2313        /// the effective group ID of the process or to one of its supplementary
2314        /// group IDs.
2315        PC_CHOWN_RESTRICTED = libc::_PC_CHOWN_RESTRICTED,
2316        /// Pathname components longer than {NAME_MAX} generate an error.
2317        PC_NO_TRUNC = libc::_PC_NO_TRUNC,
2318        /// This symbol shall be defined to be the value of a character that shall
2319        /// disable terminal special character handling.
2320        PC_VDISABLE = libc::_PC_VDISABLE,
2321        #[cfg(any(
2322            target_os = "android",
2323            target_os = "dragonfly",
2324            target_os = "freebsd",
2325            target_os = "illumos",
2326            target_os = "linux",
2327            target_os = "openbsd",
2328            target_os = "redox",
2329            target_os = "solaris"
2330        ))]
2331        /// Asynchronous input or output operations may be performed for the
2332        /// associated file.
2333        PC_ASYNC_IO = libc::_PC_ASYNC_IO,
2334        #[cfg(any(
2335            target_os = "android",
2336            target_os = "dragonfly",
2337            target_os = "freebsd",
2338            target_os = "illumos",
2339            target_os = "linux",
2340            target_os = "openbsd",
2341            target_os = "redox",
2342            target_os = "solaris"
2343        ))]
2344        /// Prioritized input or output operations may be performed for the
2345        /// associated file.
2346        PC_PRIO_IO = libc::_PC_PRIO_IO,
2347        #[cfg(any(
2348            target_os = "android",
2349            target_os = "dragonfly",
2350            target_os = "freebsd",
2351            target_os = "illumos",
2352            target_os = "linux",
2353            target_os = "netbsd",
2354            target_os = "openbsd",
2355            target_os = "redox",
2356            target_os = "solaris"
2357        ))]
2358        /// Synchronized input or output operations may be performed for the
2359        /// associated file.
2360        PC_SYNC_IO = libc::_PC_SYNC_IO,
2361        #[cfg(any(target_os = "dragonfly", target_os = "openbsd"))]
2362        /// The resolution in nanoseconds for all file timestamps.
2363        PC_TIMESTAMP_RESOLUTION = libc::_PC_TIMESTAMP_RESOLUTION,
2364    }
2365
2366    #[cfg(unix)]
2367    #[pyfunction]
2368    fn pathconf(
2369        path: OsPathOrFd<'_>,
2370        PathconfName(name): PathconfName,
2371        vm: &VirtualMachine,
2372    ) -> PyResult<Option<libc::c_long>> {
2373        Errno::clear();
2374        debug_assert_eq!(Errno::last_raw(), 0);
2375        let raw = match &path {
2376            OsPathOrFd::Path(path) => {
2377                let path = path.clone().into_cstring(vm)?;
2378                unsafe { libc::pathconf(path.as_ptr(), name) }
2379            }
2380            OsPathOrFd::Fd(fd) => unsafe { libc::fpathconf(fd.as_raw(), name) },
2381        };
2382
2383        if raw == -1 {
2384            if Errno::last_raw() == 0 {
2385                Ok(None)
2386            } else {
2387                Err(OSErrorBuilder::with_filename(
2388                    &io::Error::from(Errno::last()),
2389                    path,
2390                    vm,
2391                ))
2392            }
2393        } else {
2394            Ok(Some(raw))
2395        }
2396    }
2397
2398    #[pyfunction]
2399    fn fpathconf(
2400        fd: BorrowedFd<'_>,
2401        name: PathconfName,
2402        vm: &VirtualMachine,
2403    ) -> PyResult<Option<libc::c_long>> {
2404        pathconf(OsPathOrFd::Fd(fd.into()), name, vm)
2405    }
2406
2407    #[pyattr]
2408    fn pathconf_names(vm: &VirtualMachine) -> PyDictRef {
2409        let pathname = vm.ctx.new_dict();
2410        for variant in PathconfVar::iter() {
2411            // get the name of variant as a string to use as the dictionary key
2412            let key = vm.ctx.new_str(format!("{variant:?}"));
2413            // get the enum from the string and convert it to an integer for the dictionary value
2414            let value = vm.ctx.new_int(variant as u8);
2415            pathname
2416                .set_item(&*key, value.into(), vm)
2417                .expect("dict set_item unexpectedly failed");
2418        }
2419        pathname
2420    }
2421
2422    #[cfg(not(target_os = "redox"))]
2423    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, EnumIter, EnumString)]
2424    #[repr(i32)]
2425    #[allow(non_camel_case_types)]
2426    pub enum SysconfVar {
2427        SC_2_CHAR_TERM = libc::_SC_2_CHAR_TERM,
2428        SC_2_C_BIND = libc::_SC_2_C_BIND,
2429        SC_2_C_DEV = libc::_SC_2_C_DEV,
2430        SC_2_FORT_DEV = libc::_SC_2_FORT_DEV,
2431        SC_2_FORT_RUN = libc::_SC_2_FORT_RUN,
2432        SC_2_LOCALEDEF = libc::_SC_2_LOCALEDEF,
2433        SC_2_SW_DEV = libc::_SC_2_SW_DEV,
2434        SC_2_UPE = libc::_SC_2_UPE,
2435        SC_2_VERSION = libc::_SC_2_VERSION,
2436        SC_AIO_LISTIO_MAX = libc::_SC_AIO_LISTIO_MAX,
2437        SC_AIO_MAX = libc::_SC_AIO_MAX,
2438        SC_AIO_PRIO_DELTA_MAX = libc::_SC_AIO_PRIO_DELTA_MAX,
2439        SC_ARG_MAX = libc::_SC_ARG_MAX,
2440        SC_ASYNCHRONOUS_IO = libc::_SC_ASYNCHRONOUS_IO,
2441        SC_ATEXIT_MAX = libc::_SC_ATEXIT_MAX,
2442        SC_BC_BASE_MAX = libc::_SC_BC_BASE_MAX,
2443        SC_BC_DIM_MAX = libc::_SC_BC_DIM_MAX,
2444        SC_BC_SCALE_MAX = libc::_SC_BC_SCALE_MAX,
2445        SC_BC_STRING_MAX = libc::_SC_BC_STRING_MAX,
2446        SC_CHILD_MAX = libc::_SC_CHILD_MAX,
2447        SC_CLK_TCK = libc::_SC_CLK_TCK,
2448        SC_COLL_WEIGHTS_MAX = libc::_SC_COLL_WEIGHTS_MAX,
2449        SC_DELAYTIMER_MAX = libc::_SC_DELAYTIMER_MAX,
2450        SC_EXPR_NEST_MAX = libc::_SC_EXPR_NEST_MAX,
2451        SC_FSYNC = libc::_SC_FSYNC,
2452        SC_GETGR_R_SIZE_MAX = libc::_SC_GETGR_R_SIZE_MAX,
2453        SC_GETPW_R_SIZE_MAX = libc::_SC_GETPW_R_SIZE_MAX,
2454        SC_IOV_MAX = libc::_SC_IOV_MAX,
2455        SC_JOB_CONTROL = libc::_SC_JOB_CONTROL,
2456        SC_LINE_MAX = libc::_SC_LINE_MAX,
2457        SC_LOGIN_NAME_MAX = libc::_SC_LOGIN_NAME_MAX,
2458        SC_MAPPED_FILES = libc::_SC_MAPPED_FILES,
2459        SC_MEMLOCK = libc::_SC_MEMLOCK,
2460        SC_MEMLOCK_RANGE = libc::_SC_MEMLOCK_RANGE,
2461        SC_MEMORY_PROTECTION = libc::_SC_MEMORY_PROTECTION,
2462        SC_MESSAGE_PASSING = libc::_SC_MESSAGE_PASSING,
2463        SC_MQ_OPEN_MAX = libc::_SC_MQ_OPEN_MAX,
2464        SC_MQ_PRIO_MAX = libc::_SC_MQ_PRIO_MAX,
2465        SC_NGROUPS_MAX = libc::_SC_NGROUPS_MAX,
2466        SC_NPROCESSORS_CONF = libc::_SC_NPROCESSORS_CONF,
2467        SC_NPROCESSORS_ONLN = libc::_SC_NPROCESSORS_ONLN,
2468        SC_OPEN_MAX = libc::_SC_OPEN_MAX,
2469        SC_PAGE_SIZE = libc::_SC_PAGE_SIZE,
2470        #[cfg(any(
2471            target_os = "linux",
2472            target_vendor = "apple",
2473            target_os = "netbsd",
2474            target_os = "fuchsia"
2475        ))]
2476        SC_PASS_MAX = libc::_SC_PASS_MAX,
2477        SC_PHYS_PAGES = libc::_SC_PHYS_PAGES,
2478        SC_PRIORITIZED_IO = libc::_SC_PRIORITIZED_IO,
2479        SC_PRIORITY_SCHEDULING = libc::_SC_PRIORITY_SCHEDULING,
2480        SC_REALTIME_SIGNALS = libc::_SC_REALTIME_SIGNALS,
2481        SC_RE_DUP_MAX = libc::_SC_RE_DUP_MAX,
2482        SC_RTSIG_MAX = libc::_SC_RTSIG_MAX,
2483        SC_SAVED_IDS = libc::_SC_SAVED_IDS,
2484        SC_SEMAPHORES = libc::_SC_SEMAPHORES,
2485        SC_SEM_NSEMS_MAX = libc::_SC_SEM_NSEMS_MAX,
2486        SC_SEM_VALUE_MAX = libc::_SC_SEM_VALUE_MAX,
2487        SC_SHARED_MEMORY_OBJECTS = libc::_SC_SHARED_MEMORY_OBJECTS,
2488        SC_SIGQUEUE_MAX = libc::_SC_SIGQUEUE_MAX,
2489        SC_STREAM_MAX = libc::_SC_STREAM_MAX,
2490        SC_SYNCHRONIZED_IO = libc::_SC_SYNCHRONIZED_IO,
2491        SC_THREADS = libc::_SC_THREADS,
2492        SC_THREAD_ATTR_STACKADDR = libc::_SC_THREAD_ATTR_STACKADDR,
2493        SC_THREAD_ATTR_STACKSIZE = libc::_SC_THREAD_ATTR_STACKSIZE,
2494        SC_THREAD_DESTRUCTOR_ITERATIONS = libc::_SC_THREAD_DESTRUCTOR_ITERATIONS,
2495        SC_THREAD_KEYS_MAX = libc::_SC_THREAD_KEYS_MAX,
2496        SC_THREAD_PRIORITY_SCHEDULING = libc::_SC_THREAD_PRIORITY_SCHEDULING,
2497        SC_THREAD_PRIO_INHERIT = libc::_SC_THREAD_PRIO_INHERIT,
2498        SC_THREAD_PRIO_PROTECT = libc::_SC_THREAD_PRIO_PROTECT,
2499        SC_THREAD_PROCESS_SHARED = libc::_SC_THREAD_PROCESS_SHARED,
2500        SC_THREAD_SAFE_FUNCTIONS = libc::_SC_THREAD_SAFE_FUNCTIONS,
2501        SC_THREAD_STACK_MIN = libc::_SC_THREAD_STACK_MIN,
2502        SC_THREAD_THREADS_MAX = libc::_SC_THREAD_THREADS_MAX,
2503        SC_TIMERS = libc::_SC_TIMERS,
2504        SC_TIMER_MAX = libc::_SC_TIMER_MAX,
2505        SC_TTY_NAME_MAX = libc::_SC_TTY_NAME_MAX,
2506        SC_TZNAME_MAX = libc::_SC_TZNAME_MAX,
2507        SC_VERSION = libc::_SC_VERSION,
2508        SC_XOPEN_CRYPT = libc::_SC_XOPEN_CRYPT,
2509        SC_XOPEN_ENH_I18N = libc::_SC_XOPEN_ENH_I18N,
2510        SC_XOPEN_LEGACY = libc::_SC_XOPEN_LEGACY,
2511        SC_XOPEN_REALTIME = libc::_SC_XOPEN_REALTIME,
2512        SC_XOPEN_REALTIME_THREADS = libc::_SC_XOPEN_REALTIME_THREADS,
2513        SC_XOPEN_SHM = libc::_SC_XOPEN_SHM,
2514        SC_XOPEN_UNIX = libc::_SC_XOPEN_UNIX,
2515        SC_XOPEN_VERSION = libc::_SC_XOPEN_VERSION,
2516        SC_XOPEN_XCU_VERSION = libc::_SC_XOPEN_XCU_VERSION,
2517        #[cfg(any(
2518            target_os = "linux",
2519            target_vendor = "apple",
2520            target_os = "netbsd",
2521            target_os = "fuchsia"
2522        ))]
2523        SC_XBS5_ILP32_OFF32 = libc::_SC_XBS5_ILP32_OFF32,
2524        #[cfg(any(
2525            target_os = "linux",
2526            target_vendor = "apple",
2527            target_os = "netbsd",
2528            target_os = "fuchsia"
2529        ))]
2530        SC_XBS5_ILP32_OFFBIG = libc::_SC_XBS5_ILP32_OFFBIG,
2531        #[cfg(any(
2532            target_os = "linux",
2533            target_vendor = "apple",
2534            target_os = "netbsd",
2535            target_os = "fuchsia"
2536        ))]
2537        SC_XBS5_LP64_OFF64 = libc::_SC_XBS5_LP64_OFF64,
2538        #[cfg(any(
2539            target_os = "linux",
2540            target_vendor = "apple",
2541            target_os = "netbsd",
2542            target_os = "fuchsia"
2543        ))]
2544        SC_XBS5_LPBIG_OFFBIG = libc::_SC_XBS5_LPBIG_OFFBIG,
2545    }
2546
2547    #[cfg(target_os = "redox")]
2548    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, EnumIter, EnumString)]
2549    #[repr(i32)]
2550    #[allow(non_camel_case_types)]
2551    pub enum SysconfVar {
2552        SC_ARG_MAX = libc::_SC_ARG_MAX,
2553        SC_CHILD_MAX = libc::_SC_CHILD_MAX,
2554        SC_CLK_TCK = libc::_SC_CLK_TCK,
2555        SC_NGROUPS_MAX = libc::_SC_NGROUPS_MAX,
2556        SC_OPEN_MAX = libc::_SC_OPEN_MAX,
2557        SC_STREAM_MAX = libc::_SC_STREAM_MAX,
2558        SC_TZNAME_MAX = libc::_SC_TZNAME_MAX,
2559        SC_VERSION = libc::_SC_VERSION,
2560        SC_PAGE_SIZE = libc::_SC_PAGE_SIZE,
2561        SC_RE_DUP_MAX = libc::_SC_RE_DUP_MAX,
2562        SC_LOGIN_NAME_MAX = libc::_SC_LOGIN_NAME_MAX,
2563        SC_TTY_NAME_MAX = libc::_SC_TTY_NAME_MAX,
2564        SC_SYMLOOP_MAX = libc::_SC_SYMLOOP_MAX,
2565        SC_HOST_NAME_MAX = libc::_SC_HOST_NAME_MAX,
2566    }
2567
2568    impl SysconfVar {
2569        pub const SC_PAGESIZE: Self = Self::SC_PAGE_SIZE;
2570    }
2571
2572    struct SysconfName(i32);
2573
2574    impl TryFromObject for SysconfName {
2575        fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
2576            let i = match obj.downcast::<PyInt>() {
2577                Ok(int) => int.try_to_primitive(vm)?,
2578                Err(obj) => {
2579                    let s = obj.downcast::<PyUtf8Str>().map_err(|_| {
2580                        vm.new_type_error("configuration names must be strings or integers")
2581                    })?;
2582                    {
2583                        let name = s.as_str();
2584                        name.parse::<SysconfVar>().or_else(|_| {
2585                            if name == "SC_PAGESIZE" {
2586                                Ok(SysconfVar::SC_PAGESIZE)
2587                            } else {
2588                                Err(vm.new_value_error("unrecognized configuration name"))
2589                            }
2590                        })? as i32
2591                    }
2592                }
2593            };
2594            Ok(Self(i))
2595        }
2596    }
2597
2598    #[pyfunction]
2599    fn sysconf(name: SysconfName, vm: &VirtualMachine) -> PyResult<libc::c_long> {
2600        crate::common::os::set_errno(0);
2601        let r = unsafe { libc::sysconf(name.0) };
2602        if r == -1 && crate::common::os::get_errno() != 0 {
2603            return Err(vm.new_last_errno_error());
2604        }
2605        Ok(r)
2606    }
2607
2608    #[pyattr]
2609    fn sysconf_names(vm: &VirtualMachine) -> PyDictRef {
2610        let names = vm.ctx.new_dict();
2611        for variant in SysconfVar::iter() {
2612            // get the name of variant as a string to use as the dictionary key
2613            let key = vm.ctx.new_str(format!("{variant:?}"));
2614            // get the enum from the string and convert it to an integer for the dictionary value
2615            let value = vm.ctx.new_int(variant as u8);
2616            names
2617                .set_item(&*key, value.into(), vm)
2618                .expect("dict set_item unexpectedly failed");
2619        }
2620        names
2621    }
2622
2623    #[cfg(any(target_os = "linux", target_os = "macos"))]
2624    #[derive(FromArgs)]
2625    struct SendFileArgs<'fd> {
2626        out_fd: BorrowedFd<'fd>,
2627        in_fd: BorrowedFd<'fd>,
2628        offset: crate::common::crt_fd::Offset,
2629        count: i64,
2630        #[cfg(target_os = "macos")]
2631        #[pyarg(any, optional)]
2632        headers: OptionalArg<PyObjectRef>,
2633        #[cfg(target_os = "macos")]
2634        #[pyarg(any, optional)]
2635        trailers: OptionalArg<PyObjectRef>,
2636        #[cfg(target_os = "macos")]
2637        #[allow(dead_code)]
2638        #[pyarg(any, default)]
2639        // TODO: not implemented
2640        flags: OptionalArg<i32>,
2641    }
2642
2643    #[cfg(target_os = "linux")]
2644    #[pyfunction]
2645    fn sendfile(args: SendFileArgs<'_>, vm: &VirtualMachine) -> PyResult {
2646        let mut file_offset = args.offset;
2647
2648        let res = nix::sys::sendfile::sendfile(
2649            args.out_fd,
2650            args.in_fd,
2651            Some(&mut file_offset),
2652            args.count as usize,
2653        )
2654        .map_err(|err| err.into_pyexception(vm))?;
2655        Ok(vm.ctx.new_int(res as u64).into())
2656    }
2657
2658    #[cfg(target_os = "macos")]
2659    fn _extract_vec_bytes(
2660        x: OptionalArg,
2661        vm: &VirtualMachine,
2662    ) -> PyResult<Option<Vec<crate::function::ArgBytesLike>>> {
2663        x.into_option()
2664            .map(|x| {
2665                let v: Vec<crate::function::ArgBytesLike> = x.try_to_value(vm)?;
2666                Ok(if v.is_empty() { None } else { Some(v) })
2667            })
2668            .transpose()
2669            .map(Option::flatten)
2670    }
2671
2672    #[cfg(target_os = "macos")]
2673    #[pyfunction]
2674    fn sendfile(args: SendFileArgs<'_>, vm: &VirtualMachine) -> PyResult {
2675        let headers = _extract_vec_bytes(args.headers, vm)?;
2676        let count = headers
2677            .as_ref()
2678            .map(|v| v.iter().map(|s| s.len()).sum())
2679            .unwrap_or(0) as i64
2680            + args.count;
2681
2682        let headers = headers
2683            .as_ref()
2684            .map(|v| v.iter().map(|b| b.borrow_buf()).collect::<Vec<_>>());
2685        let headers = headers
2686            .as_ref()
2687            .map(|v| v.iter().map(|borrowed| &**borrowed).collect::<Vec<_>>());
2688        let headers = headers.as_deref();
2689
2690        let trailers = _extract_vec_bytes(args.trailers, vm)?;
2691        let trailers = trailers
2692            .as_ref()
2693            .map(|v| v.iter().map(|b| b.borrow_buf()).collect::<Vec<_>>());
2694        let trailers = trailers
2695            .as_ref()
2696            .map(|v| v.iter().map(|borrowed| &**borrowed).collect::<Vec<_>>());
2697        let trailers = trailers.as_deref();
2698
2699        let (res, written) = nix::sys::sendfile::sendfile(
2700            args.in_fd,
2701            args.out_fd,
2702            args.offset,
2703            Some(count),
2704            headers,
2705            trailers,
2706        );
2707        // On macOS, sendfile can return EAGAIN even when some bytes were written.
2708        // In that case, we should return the number of bytes written rather than
2709        // raising an exception. Only raise an error if no bytes were written.
2710        if let Err(err) = res
2711            && written == 0
2712        {
2713            return Err(err.into_pyexception(vm));
2714        }
2715        Ok(vm.ctx.new_int(written as u64).into())
2716    }
2717
2718    #[cfg(target_os = "linux")]
2719    unsafe fn sys_getrandom(buf: *mut libc::c_void, buflen: usize, flags: u32) -> isize {
2720        unsafe { libc::syscall(libc::SYS_getrandom, buf, buflen, flags as usize) as _ }
2721    }
2722
2723    #[cfg(target_os = "linux")]
2724    #[pyfunction]
2725    fn getrandom(size: isize, flags: OptionalArg<u32>, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
2726        let size = usize::try_from(size)
2727            .map_err(|_| vm.new_os_error(format!("Invalid argument for size: {size}")))?;
2728        let mut buf = Vec::with_capacity(size);
2729        unsafe {
2730            let len = sys_getrandom(
2731                buf.as_mut_ptr() as *mut libc::c_void,
2732                size,
2733                flags.unwrap_or(0),
2734            )
2735            .try_into()
2736            .map_err(|_| vm.new_last_os_error())?;
2737            buf.set_len(len);
2738        }
2739        Ok(buf)
2740    }
2741
2742    pub(crate) fn module_exec(
2743        vm: &VirtualMachine,
2744        module: &Py<crate::builtins::PyModule>,
2745    ) -> PyResult<()> {
2746        __module_exec(vm, module);
2747        super::super::os::module_exec(vm, module)?;
2748        Ok(())
2749    }
2750}
2751
2752#[cfg(any(
2753    target_os = "linux",
2754    target_os = "netbsd",
2755    target_os = "freebsd",
2756    target_os = "android"
2757))]
2758#[pymodule(sub)]
2759mod posix_sched {
2760    use crate::{
2761        AsObject, Py, PyObjectRef, PyResult, VirtualMachine, builtins::PyTupleRef,
2762        convert::ToPyObject, function::FuncArgs, types::PyStructSequence,
2763    };
2764
2765    #[derive(FromArgs)]
2766    struct SchedParamArgs {
2767        #[pyarg(any)]
2768        sched_priority: PyObjectRef,
2769    }
2770
2771    #[pystruct_sequence_data]
2772    struct SchedParamData {
2773        pub sched_priority: PyObjectRef,
2774    }
2775
2776    #[pyattr]
2777    #[pystruct_sequence(name = "sched_param", module = "posix", data = "SchedParamData")]
2778    struct PySchedParam;
2779
2780    #[pyclass(with(PyStructSequence))]
2781    impl PySchedParam {
2782        #[pyslot]
2783        fn slot_new(
2784            cls: crate::builtins::PyTypeRef,
2785            args: FuncArgs,
2786            vm: &VirtualMachine,
2787        ) -> PyResult {
2788            use crate::PyPayload;
2789            let SchedParamArgs { sched_priority } = args.bind(vm)?;
2790            let items = vec![sched_priority];
2791            crate::builtins::PyTuple::new_unchecked(items.into_boxed_slice())
2792                .into_ref_with_type(vm, cls)
2793                .map(Into::into)
2794        }
2795
2796        #[extend_class]
2797        fn extend_pyclass(ctx: &crate::vm::Context, class: &'static Py<crate::builtins::PyType>) {
2798            // Override __reduce__ to return (type, (sched_priority,))
2799            // instead of the generic structseq (type, ((sched_priority,),)).
2800            // The trait's extend_class checks contains_key before setting default.
2801            const SCHED_PARAM_REDUCE: crate::function::PyMethodDef =
2802                crate::function::PyMethodDef::new_const(
2803                    "__reduce__",
2804                    |zelf: crate::PyRef<crate::builtins::PyTuple>,
2805                     vm: &VirtualMachine|
2806                     -> PyTupleRef {
2807                        vm.new_tuple((zelf.class().to_owned(), (zelf[0].clone(),)))
2808                    },
2809                    crate::function::PyMethodFlags::METHOD,
2810                    None,
2811                );
2812            class.set_attr(
2813                ctx.intern_str("__reduce__"),
2814                SCHED_PARAM_REDUCE.to_proper_method(class, ctx),
2815            );
2816        }
2817    }
2818
2819    #[cfg(not(target_env = "musl"))]
2820    fn convert_sched_param(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<libc::sched_param> {
2821        use crate::{
2822            builtins::{PyInt, PyTuple},
2823            class::StaticType,
2824        };
2825        if !obj.fast_isinstance(PySchedParam::static_type()) {
2826            return Err(vm.new_type_error("must have a sched_param object"));
2827        }
2828        let tuple = obj.downcast_ref::<PyTuple>().unwrap();
2829        let priority = tuple[0].clone();
2830        let priority_type = priority.class().name().to_string();
2831        let value = priority.downcast::<PyInt>().map_err(|_| {
2832            vm.new_type_error(format!("an integer is required (got type {priority_type})"))
2833        })?;
2834        let sched_priority = value.try_to_primitive(vm)?;
2835        Ok(libc::sched_param { sched_priority })
2836    }
2837
2838    #[pyfunction]
2839    fn sched_getscheduler(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult<i32> {
2840        let policy = unsafe { libc::sched_getscheduler(pid) };
2841        if policy == -1 {
2842            Err(vm.new_last_errno_error())
2843        } else {
2844            Ok(policy)
2845        }
2846    }
2847
2848    #[cfg(not(target_env = "musl"))]
2849    #[derive(FromArgs)]
2850    struct SchedSetschedulerArgs {
2851        #[pyarg(positional)]
2852        pid: i32,
2853        #[pyarg(positional)]
2854        policy: i32,
2855        #[pyarg(positional)]
2856        sched_param: PyObjectRef,
2857    }
2858
2859    #[cfg(not(target_env = "musl"))]
2860    #[pyfunction]
2861    fn sched_setscheduler(args: SchedSetschedulerArgs, vm: &VirtualMachine) -> PyResult<i32> {
2862        let libc_sched_param = convert_sched_param(&args.sched_param, vm)?;
2863        let policy = unsafe { libc::sched_setscheduler(args.pid, args.policy, &libc_sched_param) };
2864        if policy == -1 {
2865            Err(vm.new_last_errno_error())
2866        } else {
2867            Ok(policy)
2868        }
2869    }
2870
2871    #[pyfunction]
2872    fn sched_getparam(pid: libc::pid_t, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
2873        let param = unsafe {
2874            let mut param = core::mem::MaybeUninit::uninit();
2875            if -1 == libc::sched_getparam(pid, param.as_mut_ptr()) {
2876                return Err(vm.new_last_errno_error());
2877            }
2878            param.assume_init()
2879        };
2880        Ok(PySchedParam::from_data(
2881            SchedParamData {
2882                sched_priority: param.sched_priority.to_pyobject(vm),
2883            },
2884            vm,
2885        ))
2886    }
2887
2888    #[cfg(not(target_env = "musl"))]
2889    #[derive(FromArgs)]
2890    struct SchedSetParamArgs {
2891        #[pyarg(positional)]
2892        pid: i32,
2893        #[pyarg(positional)]
2894        sched_param: PyObjectRef,
2895    }
2896
2897    #[cfg(not(target_env = "musl"))]
2898    #[pyfunction]
2899    fn sched_setparam(args: SchedSetParamArgs, vm: &VirtualMachine) -> PyResult<i32> {
2900        let libc_sched_param = convert_sched_param(&args.sched_param, vm)?;
2901        let ret = unsafe { libc::sched_setparam(args.pid, &libc_sched_param) };
2902        if ret == -1 {
2903            Err(vm.new_last_errno_error())
2904        } else {
2905            Ok(ret)
2906        }
2907    }
2908}