redevplugin-runtime 0.7.7

Sandboxed WASM execution runtime for the ReDevPlugin platform
#[cfg(target_os = "linux")]
const PROCESS_CONTAINMENT_SCHEMA_VERSION: &str = "redevplugin.process_containment.v1";
#[cfg(target_os = "linux")]
const PROCESS_CONTAINMENT_PROFILE: &str = "linux-runtime-v1";
#[cfg(target_os = "linux")]
const SECCOMP_POLICY_SHA256: &str =
    "6305735925c1fbacaf4950df2e535d3a11cebec8ab7aa16ce37fca3c31745543";

#[cfg(target_os = "linux")]
mod linux {
    use super::{
        PROCESS_CONTAINMENT_PROFILE, PROCESS_CONTAINMENT_SCHEMA_VERSION, SECCOMP_POLICY_SHA256,
    };

    const BPF_LD_W_ABS: u16 = 0x20;
    const BPF_JMP_JEQ_K: u16 = 0x15;
    const BPF_ALU_AND_K: u16 = 0x54;
    const BPF_RET_K: u16 = 0x06;
    const SECCOMP_DATA_NR_OFFSET: u32 = 0;
    const SECCOMP_DATA_ARCH_OFFSET: u32 = 4;
    const SECCOMP_DATA_ARG0_OFFSET: u32 = 16;
    const SECCOMP_RET_KILL_PROCESS: u32 = 0x8000_0000;
    const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
    const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
    const SECCOMP_SET_MODE_FILTER: libc::c_uint = 1;
    const SECCOMP_FILTER_FLAG_TSYNC: libc::c_ulong = 1;

    #[cfg(target_arch = "x86_64")]
    const AUDIT_ARCH: u32 = 0xc000_003e;
    #[cfg(target_arch = "aarch64")]
    const AUDIT_ARCH: u32 = 0xc000_00b7;
    #[cfg(target_arch = "x86_64")]
    const SYS_EXECVE: u32 = 59;
    #[cfg(target_arch = "aarch64")]
    const SYS_EXECVE: u32 = 221;
    #[cfg(target_arch = "x86_64")]
    const SYS_EXECVEAT: u32 = 322;
    #[cfg(target_arch = "aarch64")]
    const SYS_EXECVEAT: u32 = 281;
    #[cfg(target_arch = "x86_64")]
    const SYS_CLONE: u32 = 56;
    #[cfg(target_arch = "aarch64")]
    const SYS_CLONE: u32 = 220;
    const SYS_CLONE3: u32 = 435;
    #[cfg(target_arch = "x86_64")]
    const SYS_UNSHARE: u32 = 272;
    #[cfg(target_arch = "aarch64")]
    const SYS_UNSHARE: u32 = 97;
    #[cfg(target_arch = "x86_64")]
    const SYS_SETNS: u32 = 308;
    #[cfg(target_arch = "aarch64")]
    const SYS_SETNS: u32 = 268;
    #[cfg(target_arch = "x86_64")]
    const SYS_FORK: u32 = 57;
    #[cfg(target_arch = "x86_64")]
    const SYS_VFORK: u32 = 58;

    #[repr(C)]
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    struct SockFilter {
        code: u16,
        jt: u8,
        jf: u8,
        k: u32,
    }

    #[repr(C)]
    struct SockFprog {
        len: u16,
        filter: *const SockFilter,
    }

    impl SockFilter {
        const fn statement(code: u16, k: u32) -> Self {
            Self {
                code,
                jt: 0,
                jf: 0,
                k,
            }
        }

        const fn jump(k: u32, jt: u8, jf: u8) -> Self {
            Self {
                code: BPF_JMP_JEQ_K,
                jt,
                jf,
                k,
            }
        }
    }

    pub(super) fn activate() -> Result<redevplugin_ipc::ProcessContainmentEvidence, String> {
        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
        return Err("runtime process containment is unsupported on this architecture".to_string());

        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
        {
            install_signal_handlers()?;
            set_no_new_privileges()?;
            install_seccomp_filter()?;
            restore_runtime_signal_mask()?;
            Ok(redevplugin_ipc::ProcessContainmentEvidence {
                schema_version: PROCESS_CONTAINMENT_SCHEMA_VERSION.to_string(),
                profile: PROCESS_CONTAINMENT_PROFILE.to_string(),
                seccomp_policy_sha256: SECCOMP_POLICY_SHA256.to_string(),
                no_new_privs: true,
                seccomp_tsync: true,
                process_creation_denied: true,
                reexec_denied: true,
                active: true,
            })
        }
    }

    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
    fn install_signal_handlers() -> Result<(), String> {
        let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
        action.sa_sigaction = libc::SIG_IGN;
        if unsafe { libc::sigemptyset(&mut action.sa_mask) } != 0 {
            return Err("initialize runtime signal handler mask".to_string());
        }
        if unsafe { libc::sigaction(libc::SIGPIPE, &action, std::ptr::null_mut()) } != 0 {
            return Err(format!(
                "install runtime SIGPIPE handler: {}",
                std::io::Error::last_os_error()
            ));
        }
        Ok(())
    }

    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
    fn set_no_new_privileges() -> Result<(), String> {
        if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
            return Err(format!(
                "set no_new_privs: {}",
                std::io::Error::last_os_error()
            ));
        }
        let active = unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) };
        if active != 1 {
            return Err("no_new_privs did not become active".to_string());
        }
        Ok(())
    }

    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
    fn install_seccomp_filter() -> Result<(), String> {
        let filter = seccomp_filter()?;
        let program = SockFprog {
            len: u16::try_from(filter.len())
                .map_err(|_| "seccomp filter is too large".to_string())?,
            filter: filter.as_ptr(),
        };
        let result = unsafe {
            libc::syscall(
                libc::SYS_seccomp,
                SECCOMP_SET_MODE_FILTER,
                SECCOMP_FILTER_FLAG_TSYNC,
                &program as *const SockFprog,
            )
        };
        if result != 0 {
            return Err(format!(
                "install seccomp TSYNC policy: {}",
                std::io::Error::last_os_error()
            ));
        }
        Ok(())
    }

    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
    fn restore_runtime_signal_mask() -> Result<(), String> {
        let mut empty: libc::sigset_t = unsafe { std::mem::zeroed() };
        if unsafe { libc::sigemptyset(&mut empty) } != 0 {
            return Err("initialize runtime signal mask".to_string());
        }
        let result =
            unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &empty, std::ptr::null_mut()) };
        if result != 0 {
            return Err(format!(
                "restore runtime signal mask: {}",
                std::io::Error::from_raw_os_error(result)
            ));
        }
        Ok(())
    }

    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
    fn seccomp_filter() -> Result<Vec<SockFilter>, String> {
        let mut filter = vec![
            SockFilter::statement(BPF_LD_W_ABS, SECCOMP_DATA_ARCH_OFFSET),
            SockFilter::jump(AUDIT_ARCH, 1, 0),
            SockFilter::statement(BPF_RET_K, SECCOMP_RET_KILL_PROCESS),
            SockFilter::statement(BPF_LD_W_ABS, SECCOMP_DATA_NR_OFFSET),
        ];
        let denied_syscalls: &[u32] = &[
            SYS_EXECVE,
            SYS_EXECVEAT,
            SYS_UNSHARE,
            SYS_SETNS,
            #[cfg(target_arch = "x86_64")]
            SYS_FORK,
            #[cfg(target_arch = "x86_64")]
            SYS_VFORK,
        ];
        for syscall in denied_syscalls {
            filter.push(SockFilter::jump(*syscall, 0, 1));
            filter.push(SockFilter::statement(
                BPF_RET_K,
                SECCOMP_RET_ERRNO | u32::try_from(libc::EPERM).unwrap_or(1),
            ));
        }
        filter.push(SockFilter::jump(SYS_CLONE3, 0, 1));
        filter.push(SockFilter::statement(
            BPF_RET_K,
            SECCOMP_RET_ERRNO | u32::try_from(libc::ENOSYS).unwrap_or(38),
        ));
        filter.push(SockFilter::jump(SYS_CLONE, 1, 0));
        filter.push(SockFilter::statement(BPF_RET_K, SECCOMP_RET_ALLOW));
        filter.push(SockFilter::statement(
            BPF_LD_W_ABS,
            SECCOMP_DATA_ARG0_OFFSET,
        ));
        let required_thread_flags = u32::try_from(
            libc::CLONE_VM
                | libc::CLONE_FS
                | libc::CLONE_FILES
                | libc::CLONE_SIGHAND
                | libc::CLONE_THREAD,
        )
        .map_err(|_| "clone thread flags exceed seccomp word size".to_string())?;
        filter.push(SockFilter::statement(BPF_ALU_AND_K, required_thread_flags));
        filter.push(SockFilter::jump(required_thread_flags, 0, 1));
        filter.push(SockFilter::statement(BPF_RET_K, SECCOMP_RET_ALLOW));
        filter.push(SockFilter::statement(
            BPF_RET_K,
            SECCOMP_RET_ERRNO | u32::try_from(libc::EPERM).unwrap_or(1),
        ));
        Ok(filter)
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn containment_policy_identity_is_closed() {
            assert_eq!(PROCESS_CONTAINMENT_PROFILE, "linux-runtime-v1");
            assert_eq!(SECCOMP_POLICY_SHA256.len(), 64);
            assert!(
                SECCOMP_POLICY_SHA256
                    .bytes()
                    .all(|byte| byte.is_ascii_hexdigit())
            );
        }

        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
        #[test]
        fn seccomp_filter_arch_locks_and_denies_reexec() {
            let filter = seccomp_filter().expect("seccomp filter");
            assert_eq!(filter[0], SockFilter::statement(BPF_LD_W_ABS, 4));
            assert!(
                filter
                    .iter()
                    .any(|instruction| { *instruction == SockFilter::jump(SYS_EXECVEAT, 0, 1,) })
            );
            assert_eq!(
                filter.last().copied(),
                Some(SockFilter::statement(
                    BPF_RET_K,
                    SECCOMP_RET_ERRNO | u32::try_from(libc::EPERM).unwrap_or(1)
                ))
            );
        }

        #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
        #[test]
        fn containment_enforces_reexec_and_process_creation_denial() {
            const CHILD_ENV: &str = "REDEVPLUGIN_CONTAINMENT_TEST_CHILD";
            if std::env::var_os(CHILD_ENV).is_none() {
                let executable = std::env::current_exe().expect("current test executable");
                let status = std::process::Command::new(executable)
                    .args([
                        "--exact",
                        "process_containment::linux::tests::containment_enforces_reexec_and_process_creation_denial",
                        "--nocapture",
                    ])
                    .env(CHILD_ENV, "1")
                    .status()
                    .expect("start containment child test");
                assert!(status.success(), "containment child failed: {status}");
                return;
            }

            let evidence = activate().expect("activate process containment");
            evidence.validate().expect("containment evidence");
            std::thread::spawn(|| 42)
                .join()
                .expect("thread creation remains available");

            let fork_result = unsafe { libc::fork() };
            assert_eq!(fork_result, -1);
            assert_eq!(
                std::io::Error::last_os_error().raw_os_error(),
                Some(libc::EPERM)
            );

            let clone3_result = unsafe {
                libc::syscall(libc::SYS_clone3, std::ptr::null::<libc::c_void>(), 0usize)
            };
            assert_eq!(clone3_result, -1);
            assert_eq!(
                std::io::Error::last_os_error().raw_os_error(),
                Some(libc::ENOSYS)
            );

            let exec_result = unsafe {
                libc::syscall(
                    libc::SYS_execve,
                    std::ptr::null::<libc::c_char>(),
                    std::ptr::null::<*const libc::c_char>(),
                    std::ptr::null::<*const libc::c_char>(),
                )
            };
            assert_eq!(exec_result, -1);
            assert_eq!(
                std::io::Error::last_os_error().raw_os_error(),
                Some(libc::EPERM)
            );
        }
    }
}

#[cfg(not(target_os = "linux"))]
pub(crate) fn activate() -> Result<redevplugin_ipc::ProcessContainmentEvidence, String> {
    Err("runtime process containment is supported only on Linux".to_string())
}

#[cfg(target_os = "linux")]
pub(crate) fn activate() -> Result<redevplugin_ipc::ProcessContainmentEvidence, String> {
    linux::activate()
}