Skip to main content

proot/
launch.rs

1//! Starting the first tracee and wiring supervisor, listener and tracer.
2//!
3//! The child installs the seccomp filter and hands the listener back over a
4//! socketpair, then waits for one byte.  The parent seizes it with ptrace,
5//! starts the notification pool and sends the byte, so the child's first
6//! traced syscall already has a tracer and no `SIGSTOP` handshake is needed.
7//!
8//! Where the ptrace syscall is filtered out (a hardened container profile),
9//! or when `PROOT_NO_PTRACE` is set, there is no tracer: the filter sends the
10//! trace tier to the listener as well, the child's kernel working directory
11//! is pinned to the alias directory holding the exec shim, and the handlers
12//! in `syscall::direct` do the rest.  The process table is then lazy and a
13//! reaper thread watches pidfds for exits.
14//!
15//! Where the kernel's ptrace access check refuses the supervisor even
16//! towards its own child (Yama's `ptrace_scope` of 2), the first tracee
17//! enters a user namespace of its own before installing the filter; see
18//! `access`.  Where that is refused too, the guest-stub restricted mode
19//! (see `guest_stub`) traps the served syscalls into a stub inside each
20//! guest, and the first tracee execs the trampoline that arms it, with a
21//! plan the supervisor wrote before letting it go.
22
23use crate::access;
24use crate::cli::bwrap;
25use crate::config::Config;
26use crate::exec::shim;
27use crate::fs::translate::{detranslate_path, translate_path};
28use crate::guest_stub::{self, layout};
29use crate::note;
30use crate::procs::{ProcTable, TraceeInfo};
31use crate::seccomp::{filter, notify};
32use crate::state::Ctx;
33use crate::sys::{errno, strerror};
34use crate::syscall::{
35    DUMPABLE_SYSCALLS, NAMESPACE_FLAG_SYSCALLS, NO_PTRACE_SYSCALLS, NOTIFY_SYSCALLS, REFUSED_SYSCALLS,
36    TRACE_SYSCALLS, direct, nr,
37};
38use crate::tracee::ptrace;
39use crate::tracer;
40use std::ffi::CString;
41use std::os::unix::ffi::OsStringExt;
42use std::sync::Arc;
43
44unsafe extern "C" {
45    static environ: *const *const libc::c_char;
46}
47
48/// Runs the configured command under supervision and returns its exit code.
49///
50/// The child installs the seccomp filter and hands the listener back over a
51/// socketpair before stopping itself; the parent then becomes the tracer and
52/// starts the notification pool.
53///
54/// # Arguments
55/// * `config` - Fully parsed configuration.
56///
57/// # Errors
58/// A message describing why the command could not be started.
59pub fn run(config: Config) -> Result<i32, String> {
60    if config.command.is_empty() {
61        return Err("no command given".into());
62    }
63    let mut config = config;
64    resolve_command_in_path(&mut config);
65    let access = match access::probe(
66        !no_ptrace_forced() && direct::ptrace_usable(),
67        guest_stub_choice(),
68    ) {
69        Ok(a) => a,
70        Err(e) if config.bwrap_mode => exit_like_bwrap_without_namespaces(&e),
71        Err(e) => return Err(e),
72    };
73    let no_ptrace = !access.ptrace;
74    let mut ctx = Ctx::new(config, no_ptrace);
75    if access.trap_mode {
76        ctx.mailbox = Some(guest_stub::Mailbox::create()?.fd);
77    }
78    let ctx = Arc::new(ctx);
79    let trap_mode = ctx.trap_mode();
80    if no_ptrace {
81        note!(
82            ctx,
83            1,
84            "running without ptrace: the trace tier is served through notifications"
85        );
86    }
87    if trap_mode {
88        note!(
89            ctx,
90            1,
91            "running in the guest-stub mode: a stub in each guest stages syscall memory through a shared mailbox"
92        );
93    }
94    if access.userns.is_some() {
95        note!(
96            ctx,
97            1,
98            "the guest runs in a user namespace of its own: the ptrace access check refuses a plain child"
99        );
100    }
101    let mut notify_list: Vec<i64> = NOTIFY_SYSCALLS.to_vec();
102    notify_list.push(nr::CLONE3);
103    let trace_list: &[i64] = if no_ptrace {
104        notify_list.extend_from_slice(NO_PTRACE_SYSCALLS);
105        &[]
106    } else {
107        TRACE_SYSCALLS
108    };
109    if ctx.config.kernel_release.is_some() || ctx.config.hostname.is_some() {
110        notify_list.push(nr::UNAME);
111    }
112    if ctx.config.fake_root {
113        notify_list.extend_from_slice(crate::syscall::FAKE_ROOT_SYSCALLS);
114    }
115    let (native, compat) = (crate::abi::Abi::native(), crate::abi::Abi::compat());
116    let (notify64, trace64) = (native.numbers_for(&notify_list), native.numbers_for(trace_list));
117    let (notify32, trace32) = (compat.numbers_for(&notify_list), compat.numbers_for(trace_list));
118    let ns_list: Vec<i64> = NAMESPACE_FLAG_SYSCALLS
119        .iter()
120        .copied()
121        .filter(|nr| !notify_list.contains(nr))
122        .collect();
123    let (ns64, ns32) = (native.numbers_for(&ns_list), compat.numbers_for(&ns_list));
124    let (dumpable64, dumpable32) = (
125        native.numbers_for(DUMPABLE_SYSCALLS),
126        compat.numbers_for(DUMPABLE_SYSCALLS),
127    );
128    let (refused64, refused32) = (
129        native.numbers_for(REFUSED_SYSCALLS),
130        compat.numbers_for(REFUSED_SYSCALLS),
131    );
132    let program = if trap_mode {
133        let (mut trapped64, direct64): (Vec<i64>, Vec<i64>) =
134            notify64.iter().partition(|nr| layout::trapped64(**nr));
135        let (mut trapped32, direct32): (Vec<i64>, Vec<i64>) =
136            notify32.iter().partition(|nr| layout::trapped32(**nr));
137        trapped64.extend(layout::SIGNAL_CALLS64);
138        trapped32.extend(layout::SIGNAL_CALLS32);
139        filter::build_trapped(
140            &filter::Table {
141                abi: native,
142                notify: &direct64,
143                trace: &trapped64,
144                ns_flags: &ns64,
145                refused: &refused64,
146                dumpable: &dumpable64,
147            },
148            &filter::Table {
149                abi: compat,
150                notify: &direct32,
151                trace: &trapped32,
152                ns_flags: &ns32,
153                refused: &refused32,
154                dumpable: &dumpable32,
155            },
156        )
157    } else {
158        filter::build(
159            &filter::Table {
160                abi: native,
161                notify: &notify64,
162                trace: &trace64,
163                ns_flags: &ns64,
164                refused: &refused64,
165                dumpable: &dumpable64,
166            },
167            &filter::Table {
168                abi: compat,
169                notify: &notify32,
170                trace: &trace32,
171                ns_flags: &ns32,
172                refused: &refused32,
173                dumpable: &dumpable32,
174            },
175            no_ptrace,
176        )
177    };
178
179    // argv[0] may be overridden (bwrap --argv0); the program path is always command[0].
180    let program_path = ctx.config.command[0].as_ptr();
181    let argv: Vec<*const libc::c_char> = std::iter::once(
182        ctx.config
183            .argv0
184            .as_ref()
185            .unwrap_or(&ctx.config.command[0])
186            .as_ptr(),
187    )
188    .chain(ctx.config.command.iter().skip(1).map(|c| c.as_ptr()))
189    .chain(std::iter::once(std::ptr::null()))
190    .collect();
191    // The initial cwd is a guest path; the child's chdir(2) is a traced syscall
192    // so the tracer translates it like any other.
193    let is_dir = |guest: &[u8]| {
194        translate_path(&ctx.root.fs, std::process::id(), libc::AT_FDCWD, guest, true)
195            .map(|h| {
196                std::fs::metadata(std::ffi::OsStr::from_bytes_hack(&h))
197                    .map(|m| m.is_dir())
198                    .unwrap_or(false)
199            })
200            .unwrap_or(false)
201    };
202    let start = bwrap::start_dir(&ctx.config, is_dir);
203    let host_cwd_exists = start.is_some();
204    if !(host_cwd_exists || (ctx.config.bwrap_mode && ctx.config.cwd_default)) {
205        eprintln!(
206            "proot: can't chdir to {} in the guest rootfs, using /",
207            String::from_utf8_lossy(&ctx.config.cwd)
208        );
209    }
210    let start = start.unwrap_or_else(|| b"/".to_vec());
211    let cwd = CString::new(start.clone()).map_err(|_| "cwd contains NUL")?;
212    let env = ctx.config.env.as_ref().map(|e| {
213        if ctx.config.bwrap_mode {
214            bwrap::with_pwd(e, &start)
215        } else {
216            e.clone()
217        }
218    });
219    let env_storage: Option<Vec<*const libc::c_char>> = env.as_ref().map(|e| {
220        e.iter()
221            .map(|c| c.as_ptr())
222            .chain(std::iter::once(std::ptr::null()))
223            .collect()
224    });
225    let envp: *const *const libc::c_char = match &env_storage {
226        Some(v) => v.as_ptr(),
227        None => unsafe { environ },
228    };
229    let slash = CString::new("/").unwrap();
230    let mailbox = ctx.mailbox;
231    let pinned_cwd: Option<CString> = if no_ptrace {
232        let dir = shim::install()?;
233        become_subreaper();
234        Some(CString::new(dir.into_os_string().into_vec()).map_err(|_| "alias directory contains NUL")?)
235    } else {
236        let _ = shim::install();
237        None
238    };
239    let initial_cwd: Vec<u8> = if host_cwd_exists {
240        translate_path(&ctx.root.fs, std::process::id(), libc::AT_FDCWD, &start, true)
241            .map(|h| detranslate_path(&ctx.root.fs, &h))
242            .unwrap_or_else(|_| b"/".to_vec())
243    } else {
244        b"/".to_vec()
245    };
246    // Caller-supplied seccomp programs (bwrap --seccomp): converted before fork so the child does not allocate.
247    let extra_programs: Vec<Vec<filter::SockFilter>> = ctx
248        .config
249        .seccomp_programs
250        .iter()
251        .map(|bytes| filter::from_bytes(bytes))
252        .collect::<std::result::Result<_, _>>()
253        .map_err(|e| format!("--seccomp program: {e}"))?;
254
255    let mut sv = [0i32; 2];
256    if unsafe {
257        libc::socketpair(
258            libc::AF_UNIX,
259            libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
260            0,
261            sv.as_mut_ptr(),
262        )
263    } < 0
264    {
265        return Err(format!("socketpair: {}", strerror(errno())));
266    }
267    let pid = unsafe { libc::fork() };
268    if pid < 0 {
269        return Err(format!("fork: {}", strerror(errno())));
270    }
271    if pid == 0 {
272        // Child: nothing here may allocate.
273        unsafe {
274            libc::close(sv[0]);
275            if let Some(dir) = &pinned_cwd {
276                if libc::chdir(dir.as_ptr()) < 0 {
277                    libc::_exit(121);
278                }
279                libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL, 0, 0, 0);
280            }
281            if let Some(ns) = &access.userns
282                && ns.enter().is_err()
283            {
284                libc::_exit(118);
285            }
286            if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 {
287                libc::_exit(120);
288            }
289            let fd = match filter::install(&program) {
290                Ok(fd) => fd,
291                Err(libc::EBUSY) => libc::_exit(119),
292                Err(_) => libc::_exit(122),
293            };
294            if !send_fd(sv[1], fd) {
295                libc::_exit(123);
296            }
297            libc::close(fd);
298            if ctx.config.new_session {
299                libc::setsid();
300            }
301            for prog in &extra_programs {
302                if filter::install_plain(prog).is_err() {
303                    libc::_exit(124);
304                }
305            }
306            // Wait for the parent to seize us before the first traced syscall.
307            let mut go = 0u8;
308            if libc::read(sv[1], &mut go as *mut u8 as *mut _, 1) != 1 {
309                libc::_exit(125);
310            }
311            libc::close(sv[1]);
312            if go != 0 {
313                libc::_exit(go as i32);
314            }
315            // From here on every traced syscall is served by the parent.
316            if pinned_cwd.is_none() && (!host_cwd_exists || libc::chdir(cwd.as_ptr()) < 0) {
317                libc::chdir(slash.as_ptr());
318            }
319            if let Some(mail) = mailbox {
320                guest_stub::exec_trampoline(
321                    mail,
322                    guest_stub::layout::SHIM_PATH.as_ptr().cast(),
323                    argv.as_ptr(),
324                    envp,
325                );
326            }
327            libc::execve(program_path, argv.as_ptr(), envp);
328            let e = *libc::__errno_location();
329            libc::_exit(if e == libc::ENOENT { 127 } else { 126 });
330        }
331    }
332    unsafe { libc::close(sv[1]) };
333    let listener = match recv_fd(sv[0]) {
334        Some(fd) => fd,
335        None => {
336            unsafe { libc::close(sv[0]) };
337            let mut st = 0;
338            unsafe { libc::waitpid(pid, &mut st, libc::__WALL) };
339            return Err(match libc::WEXITSTATUS(st) {
340                119 if ctx.config.bwrap_mode => {
341                    exit_like_bwrap_without_namespaces("proot-next cannot run under another supervisor")
342                }
343                118 => "cannot enter the user namespace the probe entered".into(),
344                119 => "the seccomp filter chain already has a listener (a supervisor other than proot-next)"
345                    .into(),
346                120 => "PR_SET_NO_NEW_PRIVS refused".into(),
347                121 => "cannot enter the alias directory".into(),
348                124 => "the caller's --seccomp program was refused by the kernel".into(),
349                122 => "seccomp user notification is not available on this kernel or is filtered".into(),
350                _ => "child failed before handing over the seccomp listener".into(),
351            });
352        }
353    };
354    if no_ptrace {
355        ctx.procs.register_root(
356            pid,
357            TraceeInfo {
358                cwd: Some(initial_cwd),
359                ..Default::default()
360            },
361        );
362        if let Err(e) = ProcTable::start_reaper(&ctx.procs) {
363            eprintln!("proot: cannot watch process exits: {}", strerror(e));
364        }
365    } else if let Err(e) = ptrace::seize(pid, tracer::options(&ctx)) {
366        unsafe {
367            libc::kill(pid, libc::SIGKILL);
368            libc::close(sv[0]);
369            libc::close(listener);
370            let mut st = 0;
371            libc::waitpid(pid, &mut st, libc::__WALL);
372        }
373        if e == libc::EPERM && ctx.config.bwrap_mode {
374            exit_like_bwrap_without_namespaces("proot-next cannot run under another supervisor");
375        }
376        return Err(match e {
377            libc::EPERM => "ptrace(PTRACE_SEIZE) refused: this process is already traced (a debugger, or a supervisor other than proot-next) or ptrace is disallowed here".into(),
378            e => format!("ptrace(PTRACE_SEIZE): {}", strerror(e)),
379        });
380    }
381    report_status(&ctx.config.info_fds, &format!("{{\"child-pid\": {pid}}}\n"));
382    report_status(
383        &ctx.config.json_status_fds,
384        &format!("{{\"child-pid\": {pid}}}\n"),
385    );
386    let go = if trap_mode {
387        match plan_first_trapped(&ctx, pid, env.as_deref()) {
388            Ok(()) => 0u8,
389            Err(libc::ENOENT) => 127,
390            Err(_) => 126,
391        }
392    } else {
393        0
394    };
395    let pool = notify::start(listener, ctx.clone());
396    if unsafe { libc::write(sv[0], &go as *const u8 as *const _, 1) } != 1 {
397        return Err("could not release the first tracee".into());
398    }
399    unsafe { libc::close(sv[0]) };
400    let code = if no_ptrace {
401        let code = wait_root(pid);
402        pool.wait_closed();
403        code
404    } else {
405        tracer::run(&ctx, pid)
406    };
407    unsafe { libc::close(listener) };
408    report_status(
409        &ctx.config.json_status_fds,
410        &format!("{{\"exit-code\": {code}}}\n"),
411    );
412    // PROOT_BWRAP_KEEP=1 leaves the materialized root behind for inspection.
413    let keep = std::env::var_os("PROOT_BWRAP_KEEP").is_some();
414    ctx.root.remove_placeholders();
415    let _ = std::fs::remove_dir_all(crate::exec::alias_dir());
416    if no_ptrace {
417        direct::remove_staging();
418    }
419    if let Some(dir) = shim::path().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
420        let _ = std::fs::remove_dir_all(dir);
421    }
422    for dir in ctx.config.cleanup_dirs.iter().filter(|_| !keep) {
423        let _ = std::fs::remove_dir_all(std::ffi::OsStr::from_bytes_hack(dir));
424    }
425    Ok(code)
426}
427
428/// Plans the first tracee's exec in the guest-stub restricted mode: its
429/// command, argument vector and environment as the child will exec them.
430///
431/// # Arguments
432/// * `ctx` - Shared supervisor state, with the first tracee registered.
433/// * `pid` - The first tracee.
434/// * `env` - The environment the child execs with, `None` for the supervisor's.
435///
436/// # Errors
437/// The errno the exec would have failed with.
438fn plan_first_trapped(ctx: &Ctx, pid: i32, env: Option<&[CString]>) -> crate::sys::Result<()> {
439    let argv: Vec<Vec<u8>> = std::iter::once(ctx.config.argv0.as_ref().unwrap_or(&ctx.config.command[0]))
440        .chain(ctx.config.command.iter().skip(1))
441        .map(|c| c.as_bytes().to_vec())
442        .collect();
443    let split = |e: &[u8]| match e.iter().position(|&b| b == b'=') {
444        Some(i) => (e[..i].to_vec(), e[i + 1..].to_vec()),
445        None => (e.to_vec(), Vec::new()),
446    };
447    let env: Vec<(Vec<u8>, Vec<u8>)> = match env {
448        Some(e) => e.iter().map(|c| split(c.as_bytes())).collect(),
449        None => std::env::vars_os()
450            .map(|(k, v)| (k.into_vec(), v.into_vec()))
451            .collect(),
452    };
453    direct::plan_first_trapped(ctx, pid, ctx.config.command[0].as_bytes(), argv, env)
454}
455
456/// When the guest-stub restricted mode may be used, from `PROOT_GUEST_STUB`:
457/// as the fallback where nothing else reaches the guest's memory (unset, or
458/// any other value), anywhere (`force`), or never (`0`).
459///
460/// # Returns
461/// `Some(false)` for the fallback, `Some(true)` for `force`, `None` for `0`.
462fn guest_stub_choice() -> Option<bool> {
463    match std::env::var_os("PROOT_GUEST_STUB") {
464        Some(v) if v == "0" => None,
465        Some(v) => Some(v == "force"),
466        None => Some(false),
467    }
468}
469
470/// Whether `PROOT_NO_PTRACE` forces the mode without a ptrace tier (any
471/// value but `0`).
472///
473/// # Returns
474/// `true` when the mode is forced.
475fn no_ptrace_forced() -> bool {
476    std::env::var_os("PROOT_NO_PTRACE").is_some_and(|v| v != "0")
477}
478
479/// Exits the way bubblewrap exits when it cannot create its namespaces.
480///
481/// A supervisor cannot start under another tracer or another listener, nor
482/// on a host that refuses it access to its own child.  Inside proot-next
483/// the first never happens, since an exec of bwrap is served as a nested
484/// launch (see `exec::nested`); otherwise the caller is a program that
485/// started us as bubblewrap, and those programs know this message (GTK's
486/// glycin runs its image loaders unsandboxed on seeing it).
487///
488/// # Arguments
489/// * `reason` - Why proot-next cannot start, appended to the message.
490fn exit_like_bwrap_without_namespaces(reason: &str) -> ! {
491    eprintln!("bwrap: No permissions to create a new namespace: {reason}");
492    std::process::exit(1);
493}
494
495/// Makes the supervisor the parent of every orphan among its descendants.
496///
497/// Without ptrace the supervisor reads and writes tracee memory on its own
498/// authority, which Yama's default `ptrace_scope` of 1 grants only towards
499/// descendants.  A daemon that double-forks would be reparented to init and
500/// lose that standing; as a child subreaper the supervisor adopts it
501/// instead, and [`wait_root`] reaps it.
502fn become_subreaper() {
503    unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) };
504}
505
506/// Waits for the first tracee and returns the exit code to report, reaping
507/// every other child along the way.
508///
509/// Without ptrace the first tracee is the supervisor's own child and, with
510/// [`become_subreaper`], so is every orphaned descendant.  Once the first
511/// tracee is gone a thread keeps reaping until the process exits; the pool
512/// closes when no process uses the filter any more, the point at which the
513/// tracer loop would have run out of tracees.
514///
515/// # Arguments
516/// * `pid` - The first tracee.
517fn wait_root(pid: i32) -> i32 {
518    let code = loop {
519        let mut status = 0;
520        let got = unsafe { libc::waitpid(-1, &mut status, 0) };
521        if got < 0 {
522            match errno() {
523                libc::EINTR => continue,
524                _ => break 1,
525            }
526        }
527        if got != pid {
528            continue;
529        }
530        if libc::WIFEXITED(status) {
531            break libc::WEXITSTATUS(status);
532        }
533        if libc::WIFSIGNALED(status) {
534            break 128 + libc::WTERMSIG(status);
535        }
536    };
537    std::thread::Builder::new()
538        .name("subreaper".into())
539        .spawn(|| {
540            loop {
541                let mut status = 0;
542                if unsafe { libc::waitpid(-1, &mut status, 0) } < 0 && errno() == libc::ECHILD {
543                    std::thread::sleep(std::time::Duration::from_millis(50));
544                }
545            }
546        })
547        .expect("spawn subreaper thread");
548    code
549}
550
551/// Writes one status line to each of bubblewrap's `--info-fd`/`--json-status-fd` descriptors.
552///
553/// # Arguments
554/// * `fds` - Descriptor numbers inherited from the caller; write errors are ignored.
555/// * `line` - Text to write, newline included.
556fn report_status(fds: &[i32], line: &str) {
557    for &fd in fds {
558        unsafe { libc::write(fd, line.as_ptr() as *const _, line.len()) };
559    }
560}
561
562/// Resolves a bare command name through `PATH`, as seen from inside the guest.
563///
564/// bubblewrap and the shells exec with PATH search; the kernel does not.  The
565/// search uses the `PATH` of the environment the command will receive and
566/// checks each candidate on the host side of the translation.  An unresolved
567/// name is left alone so the kernel reports `ENOENT`.
568///
569/// # Arguments
570/// * `config` - Configuration whose `command[0]` may be a bare name.
571fn resolve_command_in_path(config: &mut Config) {
572    let Some(first) = config.command.first() else {
573        return;
574    };
575    let name = first.as_bytes().to_vec();
576    if name.is_empty() || name.contains(&b'/') {
577        return;
578    }
579    let path_var: Vec<u8> = match &config.env {
580        Some(env) => env
581            .iter()
582            .find_map(|e| e.as_bytes().strip_prefix(b"PATH=").map(|v| v.to_vec()))
583            .unwrap_or_default(),
584        None => std::env::var_os("PATH")
585            .map(|p| std::os::unix::ffi::OsStrExt::as_bytes(p.as_os_str()).to_vec())
586            .unwrap_or_default(),
587    };
588    let fs = crate::fs::Fs::new(config);
589    for dir in path_var.split(|&c| c == b':').filter(|d| !d.is_empty()) {
590        let mut guest = dir.to_vec();
591        if !guest.ends_with(b"/") {
592            guest.push(b'/');
593        }
594        guest.extend_from_slice(&name);
595        let Ok(host) = translate_path(&fs, std::process::id(), libc::AT_FDCWD, &guest, true) else {
596            continue;
597        };
598        let Ok(c) = CString::new(host) else { continue };
599        if unsafe { libc::access(c.as_ptr(), libc::X_OK) } == 0 {
600            if let Ok(g) = CString::new(guest) {
601                // execvp semantics: argv[0] stays the name as typed.
602                if config.argv0.is_none() {
603                    config.argv0 = Some(config.command[0].clone());
604                }
605                config.command[0] = g;
606            }
607            return;
608        }
609    }
610}
611
612/// Sends a descriptor over a unix socket with `SCM_RIGHTS`.  Allocation free.
613fn send_fd(sock: i32, fd: i32) -> bool {
614    let mut byte = 1u8;
615    let mut iov = libc::iovec {
616        iov_base: &mut byte as *mut u8 as *mut _,
617        iov_len: 1,
618    };
619    let mut cbuf = [0u8; 24];
620    cbuf[0..8].copy_from_slice(&20u64.to_ne_bytes());
621    cbuf[8..12].copy_from_slice(&libc::SOL_SOCKET.to_ne_bytes());
622    cbuf[12..16].copy_from_slice(&libc::SCM_RIGHTS.to_ne_bytes());
623    cbuf[16..20].copy_from_slice(&fd.to_ne_bytes());
624    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
625    msg.msg_iov = &mut iov;
626    msg.msg_iovlen = 1;
627    msg.msg_control = cbuf.as_mut_ptr() as *mut _;
628    msg.msg_controllen = 24;
629    unsafe { libc::sendmsg(sock, &msg, 0) == 1 }
630}
631
632/// Receives a descriptor sent by [`send_fd`].
633fn recv_fd(sock: i32) -> Option<i32> {
634    let mut byte = 0u8;
635    let mut iov = libc::iovec {
636        iov_base: &mut byte as *mut u8 as *mut _,
637        iov_len: 1,
638    };
639    let mut cbuf = [0u8; 24];
640    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
641    msg.msg_iov = &mut iov;
642    msg.msg_iovlen = 1;
643    msg.msg_control = cbuf.as_mut_ptr() as *mut _;
644    msg.msg_controllen = 24;
645    let n = unsafe { libc::recvmsg(sock, &mut msg, 0) };
646    if n != 1 || msg.msg_controllen < 20 {
647        return None;
648    }
649    Some(i32::from_ne_bytes(cbuf[16..20].try_into().unwrap()))
650}
651
652/// Byte-slice to `OsStr` helper kept local to avoid a trait import at call sites.
653trait FromBytesHack {
654    fn from_bytes_hack(b: &[u8]) -> &std::ffi::OsStr;
655}
656impl FromBytesHack for std::ffi::OsStr {
657    fn from_bytes_hack(b: &[u8]) -> &std::ffi::OsStr {
658        use std::os::unix::ffi::OsStrExt;
659        std::ffi::OsStr::from_bytes(b)
660    }
661}