cloudfox-coreshift-core 2.18.0

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
use coreshift_core::inotify::decode_events;
use coreshift_core::spawn::{
    CancelPolicy, ExitStatus, ProcessGroup, SpawnBackend, SpawnFdPolicy, SpawnOptions, spawn_start,
};
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::time::Instant;

/// C1: the child keeps running after its pipes are drained (no capture at all
/// ⇒ `drain.is_done()` is immediately true). The old code called
/// `process.wait_blocking()` here and escaped the deadline entirely.
#[test]
fn test_spawn_timeout_no_capture_returns_quickly() {
    let start = Instant::now();
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "sleep 30".to_string(),
        ],
        SpawnBackend::PosixSpawn,
    )
    .timeout_ms(200)
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert!(
        output.timed_out,
        "a deadline must be reported as timed out, not block past it"
    );
    assert!(
        start.elapsed().as_millis() < 2000,
        "wait_loop escaped the deadline via wait_blocking (took {}ms)",
        start.elapsed().as_millis()
    );
    assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}

#[test]
fn test_spawn_echo_capture() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "echo hello world".to_string(),
        ],
        SpawnBackend::PosixSpawn,
    )
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "hello world"
    );
}

#[test]
fn test_spawn_large_stdout() {
    // Generate ~100KB of output
    let script = "for i in $(seq 1 10000); do echo \"line $i\"; done";
    let output = SpawnOptions::builder(
        vec!["/bin/sh".to_string(), "-c".to_string(), script.to_string()],
        SpawnBackend::PosixSpawn,
    )
    .capture_stdout()
    .max_output(200_000)
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert!(output.stdout.len() > 65536);
}

#[test]
fn test_inotify_decode_with_name() {
    let mut buf = Vec::new();
    let name = "test_file.txt";
    let name_bytes = name.as_bytes();
    let mut name_with_padding = name_bytes.to_vec();
    name_with_padding.push(0); // null terminator
    while !name_with_padding.len().is_multiple_of(8) {
        name_with_padding.push(0); // padding
    }
    let name_len = name_with_padding.len() as u32;

    // wd=1, mask=IN_MODIFY, cookie=0, len=name_len
    buf.extend_from_slice(&1i32.to_ne_bytes());
    buf.extend_from_slice(&libc::IN_MODIFY.to_ne_bytes());
    buf.extend_from_slice(&0u32.to_ne_bytes());
    buf.extend_from_slice(&name_len.to_ne_bytes());
    buf.extend_from_slice(&name_with_padding);

    let events = decode_events(&buf).unwrap();
    assert_eq!(events.len(), 1);
    assert_eq!(events[0].wd, 1);
    assert_eq!(events[0].name, Some(name.as_bytes().to_vec()));
}

#[test]
fn test_exec_context_many_args() {
    let mut args = vec![
        "/bin/sh".to_string(),
        "-c".to_string(),
        "echo $@".to_string(),
        "--".to_string(),
    ];
    for i in 0..100 {
        args.push(format!("arg{}", i));
    }

    let output = SpawnOptions::builder(args, SpawnBackend::PosixSpawn)
        .capture_stdout()
        .build()
        .unwrap()
        .run()
        .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("arg0"));
    assert!(stdout.contains("arg99"));
}

/// N4: the child exits but a background descendant keeps the stdout write end
/// open past the deadline ("wedged pipe"). The absolute deadline is
/// authoritative — spawn must return partial output instead of waiting for the
/// wedged pipe to close.
#[test]
fn test_spawn_timeout_wedged_pipe_partial_output() {
    let start = Instant::now();
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "(sleep 1; echo late) & echo immediate; exit 0".to_string(),
        ],
        SpawnBackend::PosixSpawn,
    )
    .capture_stdout()
    .timeout_ms(200)
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert!(
        output.timed_out,
        "wedged pipe with an elapsed deadline must report timed_out"
    );
    assert!(
        start.elapsed().as_millis() < 900,
        "wait_loop hung on the wedged pipe (took {}ms)",
        start.elapsed().as_millis()
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("immediate"),
        "partial output must contain the data drained before the kill"
    );
    assert!(
        !stdout.contains("late"),
        "must return partial output, not wait for the wedged pipe's late data"
    );
}

#[test]
fn test_vfork_echo_capture() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "echo vfork hello".to_string(),
        ],
        SpawnBackend::Vfork,
    )
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "vfork hello"
    );
}

#[test]
fn test_clone3_echo_capture() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "echo clone3 hello".to_string(),
        ],
        SpawnBackend::Clone3,
    )
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "clone3 hello"
    );
}

#[test]
fn test_clone3_pidfd_echo_capture() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "echo pidfd hello".to_string(),
        ],
        SpawnBackend::Clone3Pidfd,
    )
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "pidfd hello"
    );
}

#[test]
fn test_clone3_pidfd_exposes_pidfd() {
    let running = spawn_start(
        SpawnOptions::builder(vec!["/bin/true".to_string()], SpawnBackend::Clone3Pidfd)
            .build()
            .unwrap(),
    )
    .unwrap();

    assert!(
        running.process.pidfd().is_some(),
        "Clone3Pidfd must attach a pidfd to the Process handle"
    );
    assert_eq!(
        running.process.wait_blocking().unwrap(),
        ExitStatus::Exited(0)
    );
}

#[test]
fn test_vfork_stdin_capture() {
    let output = SpawnOptions::builder(
        vec!["/bin/sh".to_string(), "-c".to_string(), "cat".to_string()],
        SpawnBackend::Vfork,
    )
    .stdin(b"vfork stdin data".to_vec())
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(output.status, Some(ExitStatus::Exited(0)));
    assert_eq!(String::from_utf8_lossy(&output.stdout), "vfork stdin data");
}

/// The vfork child must not corrupt the parent's shared `Pipes`/fd state:
/// isolated pgroup (`setsid`), `CloseFrom3` fd scan, and group kill all run in
/// the child, and stdin/stdout capture must survive on the parent side.
#[test]
fn test_vfork_isolated_pgroup_closefrom3_timeout_kills_group() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "trap '' TERM; while true; do sleep 1; done".to_string(),
        ],
        SpawnBackend::Vfork,
    )
    .pgroup(ProcessGroup::new(None, true))
    .fd_policy(SpawnFdPolicy::CloseFrom3)
    .cancel(CancelPolicy::Graceful)
    .kill_grace_ms(100)
    .timeout_ms(200)
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert!(output.timed_out, "vfork child must be timed out");
    // TERM is ignored by the shell; the grace elapses and the isolated group
    // is escalated to SIGKILL.
    assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}

#[test]
fn test_vfork_close_from3_closes_inherited_fd() {
    let file = File::open("/dev/null").unwrap();
    let fd = file.as_raw_fd();
    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
    assert!(flags >= 0);
    let ret = unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) };
    assert_eq!(ret, 0);

    let script = format!("if [ -e /proc/$$/fd/{fd} ]; then echo open; else echo closed; fi");
    let output = SpawnOptions::builder(
        vec!["/bin/sh".to_string(), "-c".to_string(), script],
        SpawnBackend::Vfork,
    )
    .fd_policy(SpawnFdPolicy::CloseFrom3)
    .capture_stdout()
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "closed");
}

#[test]
fn test_vfork_exec_failure_reports_error() {
    let err = SpawnOptions::builder(
        vec!["/definitely/missing/coreshift-core-bin".to_string()],
        SpawnBackend::Vfork,
    )
    .build()
    .unwrap()
    .run()
    .unwrap_err();

    assert_eq!(err.raw_os_error(), Some(libc::ENOENT));
}

#[test]
fn test_clone3_exec_failure_reports_error() {
    let err = SpawnOptions::builder(
        vec!["/definitely/missing/coreshift-core-bin".to_string()],
        SpawnBackend::Clone3,
    )
    .build()
    .unwrap()
    .run()
    .unwrap_err();

    assert_eq!(err.raw_os_error(), Some(libc::ENOENT));
}

/// The pidfd path: TERM is ignored, so the grace elapses and the pidfd-based
/// signal escalates to SIGKILL (targeting the pid, not a group).
#[test]
fn test_clone3_pidfd_timeout_graceful_kills() {
    let output = SpawnOptions::builder(
        vec![
            "/bin/sh".to_string(),
            "-c".to_string(),
            "trap '' TERM; while true; do sleep 1; done".to_string(),
        ],
        SpawnBackend::Clone3Pidfd,
    )
    .cancel(CancelPolicy::Graceful)
    .kill_grace_ms(100)
    .timeout_ms(200)
    .build()
    .unwrap()
    .run()
    .unwrap();

    assert!(output.timed_out, "pidfd child must be timed out");
    assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}