secure-exec-sidecar 0.3.1

Native Secure Exec sidecar runtime
Documentation
mod support;

use secure_exec_sidecar::wire::{
    BootstrapRootFilesystemRequest, ConfigureVmRequest, DisposeReason, DisposeVmRequest,
    GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, MountDescriptor,
    MountPluginDescriptor, RequestPayload, ResponsePayload, RootFilesystemEntry,
    RootFilesystemEntryEncoding, RootFilesystemEntryKind,
};
use std::collections::HashMap;
use std::fs;
use std::time::Duration;
use support::{
    assert_node_available, authenticate_wire, collect_process_output_wire_with_timeout,
    create_vm_wire, execute_wire, new_sidecar, open_session_wire, temp_dir, wire_request, wire_vm,
};

/// Regression test for GitHub issue #109: an external package living in a host
/// `node_modules` projected into the VM via the `host_dir` mount plugin must
/// resolve from guest `import`. This mirrors `NodeRuntime.create({ nodeModules })`,
/// which mounts the host `node_modules` at guest `/tmp/node_modules` and runs
/// programs under `/tmp`, so the ancestor-`node_modules` resolution walk from
/// `/tmp` reaches the mount. When this regressed, the guest reported
/// `_resolveModule returned non-string for '<pkg>'`.
///
/// This test runs a guest V8 isolate, so it lives in its own integration-test
/// binary (one isolate per process) to avoid the multi-isolate process-teardown
/// segfault that surfaces when several guest-executing tests share a binary.
#[test]
fn host_mounted_node_modules_package_resolves_from_guest_import() {
    assert_node_available();

    let mut sidecar = new_sidecar("node-modules-host-mount");

    // Seed a host `node_modules` tree with a single package, exactly as a caller
    // would prepare libraries before execution.
    let host_root = temp_dir("node-modules-host-mount-host");
    let host_node_modules = host_root.join("node_modules");
    let pkg_dir = host_node_modules.join("mypkg");
    fs::create_dir_all(&pkg_dir).expect("create host package dir");
    fs::write(
        pkg_dir.join("package.json"),
        r#"{"name":"mypkg","version":"1.0.0","type":"module","main":"index.js"}"#,
    )
    .expect("write host package.json");
    fs::write(
        pkg_dir.join("index.js"),
        "export default () => \"resolved-from-host-mount\";\n",
    )
    .expect("write host package entry");

    let cwd = temp_dir("node-modules-host-mount-cwd");

    let connection_id = authenticate_wire(&mut sidecar, "conn-1");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);

    let (vm_id, _) = create_vm_wire(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
    );

    // Provide the mountpoint, then project the host `node_modules` at guest
    // `/tmp/node_modules` (read-only), the default `nodeModules` projection.
    sidecar
        .dispatch_wire_blocking(wire_request(
            4,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::BootstrapRootFilesystemRequest(BootstrapRootFilesystemRequest {
                entries: vec![RootFilesystemEntry {
                    path: String::from("/tmp/node_modules"),
                    kind: RootFilesystemEntryKind::Directory,
                    mode: None,
                    uid: None,
                    gid: None,
                    content: None,
                    encoding: None,
                    target: None,
                    executable: false,
                }],
            }),
        ))
        .expect("bootstrap mountpoint");

    sidecar
        .dispatch_wire_blocking(wire_request(
            5,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
                mounts: vec![MountDescriptor {
                    guest_path: String::from("/tmp/node_modules"),
                    read_only: true,
                    plugin: MountPluginDescriptor {
                        id: String::from("host_dir"),
                        config: serde_json::to_string(&serde_json::json!({
                            "hostPath": host_node_modules.to_string_lossy(),
                            "readOnly": true,
                        }))
                        .expect("serialize host_dir mount config"),
                    },
                }],
                software: Vec::new(),
                permissions: None,
                module_access_cwd: None,
                instructions: Vec::new(),
                projected_modules: Vec::new(),
                command_permissions: HashMap::new(),
                loopback_exempt_ports: Vec::new(),
            }),
        ))
        .expect("mount host node_modules");

    // Write the guest program under `/tmp` (like `NodeRuntime.exec`), so its
    // module-resolution context is `/tmp` and the bare specifier `mypkg`
    // resolves through `/tmp/node_modules`.
    let entry_source = r#"
import greet from "mypkg";
console.log(greet());
"#;
    let write = sidecar
        .dispatch_wire_blocking(wire_request(
            6,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::GuestFilesystemCallRequest(GuestFilesystemCallRequest {
                operation: GuestFilesystemOperation::WriteFile,
                path: String::from("/tmp/entry.mjs"),
                destination_path: None,
                target: None,
                content: Some(String::from(entry_source)),
                encoding: Some(RootFilesystemEntryEncoding::Utf8),
                recursive: false,
                mode: None,
                uid: None,
                gid: None,
                atime_ms: None,
                mtime_ms: None,
                len: None,
                offset: None,
            }),
        ))
        .expect("write guest entry");
    match write.response.payload {
        ResponsePayload::GuestFilesystemResultResponse(_) => {}
        other => panic!("unexpected guest write response: {other:?}"),
    }

    execute_wire(
        &mut sidecar,
        7,
        &connection_id,
        &session_id,
        &vm_id,
        "proc-resolve",
        GuestRuntimeKind::JavaScript,
        std::path::Path::new("/tmp/entry.mjs"),
        Vec::new(),
    );
    let (stdout, stderr, exit) = collect_process_output_wire_with_timeout(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "proc-resolve",
        Duration::from_secs(10),
    );

    assert_eq!(
        stdout.trim(),
        "resolved-from-host-mount",
        "guest import of a host-mounted package should resolve; stderr: {stderr}"
    );
    assert_eq!(exit, 0, "stderr: {stderr}");

    sidecar
        .dispatch_wire_blocking(wire_request(
            8,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::DisposeVmRequest(DisposeVmRequest {
                reason: DisposeReason::Requested,
            }),
        ))
        .expect("dispose vm");
}