#![allow(dead_code)]
#[path = "../../../bridge/tests/support.rs"]
mod bridge_support;
pub use bridge_support::RecordingBridge;
use nix::fcntl::{Flock, FlockArg};
use secure_exec_sidecar::protocol::{
DisposeReason, EventFrame, GuestRuntimeKind, OwnershipScope, RequestFrame, RequestId,
RequestPayload, ResponseFrame,
};
use secure_exec_sidecar::{DispatchResult, NativeSidecar, NativeSidecarConfig};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
pub const TEST_AUTH_TOKEN: &str = "sidecar-test-token";
const MAX_COLLECTED_PROCESS_STREAM_BYTES: usize = 1024 * 1024;
pub fn acquire_sidecar_runtime_test_lock() {
static LOCK_FILE: OnceLock<Flock<std::fs::File>> = OnceLock::new();
let _ = LOCK_FILE.get_or_init(|| {
let path = std::env::temp_dir().join("secure-exec-sidecar-runtime-tests.lock");
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.unwrap_or_else(|error| {
panic!("open sidecar test runtime lock {}: {error}", path.display())
});
Flock::lock(file, FlockArg::LockExclusive).unwrap_or_else(|(_, error)| {
panic!("lock sidecar test runtime {}: {error}", path.display())
})
});
}
pub fn assert_node_available() {
let output = Command::new("node")
.arg("--version")
.output()
.expect("spawn node --version");
assert!(
output.status.success(),
"node must be available for native sidecar execution tests"
);
}
pub fn temp_dir(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"secure-exec-sidecar-{name}-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos()
));
fs::create_dir_all(&root).expect("create temp dir");
root
}
pub fn new_sidecar(name: &str) -> NativeSidecar<RecordingBridge> {
new_sidecar_with_auth_token(name, TEST_AUTH_TOKEN)
}
pub fn new_sidecar_with_auth_token(
name: &str,
expected_auth_token: &str,
) -> NativeSidecar<RecordingBridge> {
acquire_sidecar_runtime_test_lock();
let root = temp_dir(name);
NativeSidecar::with_config(
RecordingBridge::default(),
NativeSidecarConfig {
sidecar_id: format!("sidecar-{name}"),
compile_cache_root: Some(root.join("cache")),
expected_auth_token: Some(expected_auth_token.to_owned()),
..NativeSidecarConfig::default()
},
)
.expect("create native sidecar")
}
pub fn request(id: RequestId, ownership: OwnershipScope, payload: RequestPayload) -> RequestFrame {
RequestFrame::new(id, ownership, payload)
}
pub fn wire_request(
id: secure_exec_sidecar::wire::RequestId,
ownership: secure_exec_sidecar::wire::OwnershipScope,
payload: secure_exec_sidecar::wire::RequestPayload,
) -> secure_exec_sidecar::wire::RequestFrame {
secure_exec_sidecar::wire::RequestFrame {
schema: secure_exec_sidecar::wire::protocol_schema(),
request_id: id,
ownership,
payload,
}
}
pub fn wire_connection(connection_id: &str) -> secure_exec_sidecar::wire::OwnershipScope {
secure_exec_sidecar::wire::OwnershipScope::ConnectionOwnership(
secure_exec_sidecar::wire::ConnectionOwnership {
connection_id: connection_id.to_owned(),
},
)
}
pub fn wire_session(
connection_id: &str,
session_id: &str,
) -> secure_exec_sidecar::wire::OwnershipScope {
secure_exec_sidecar::wire::OwnershipScope::SessionOwnership(
secure_exec_sidecar::wire::SessionOwnership {
connection_id: connection_id.to_owned(),
session_id: session_id.to_owned(),
},
)
}
pub fn wire_vm(
connection_id: &str,
session_id: &str,
vm_id: &str,
) -> secure_exec_sidecar::wire::OwnershipScope {
secure_exec_sidecar::wire::OwnershipScope::VmOwnership(secure_exec_sidecar::wire::VmOwnership {
connection_id: connection_id.to_owned(),
session_id: session_id.to_owned(),
vm_id: vm_id.to_owned(),
})
}
pub fn wire_permissions_allow_all() -> secure_exec_sidecar::wire::PermissionsPolicy {
secure_exec_sidecar::wire::PermissionsPolicy {
fs: Some(
secure_exec_sidecar::wire::FsPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
network: Some(
secure_exec_sidecar::wire::PatternPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
child_process: Some(
secure_exec_sidecar::wire::PatternPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
process: Some(
secure_exec_sidecar::wire::PatternPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
env: Some(
secure_exec_sidecar::wire::PatternPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
tool: Some(
secure_exec_sidecar::wire::PatternPermissionScope::PermissionMode(
secure_exec_sidecar::wire::PermissionMode::Allow,
),
),
}
}
pub fn authenticate_wire(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_hint: &str,
) -> String {
let result = authenticate_wire_with_token(sidecar, 1, connection_hint, TEST_AUTH_TOKEN);
match result.response.payload {
secure_exec_sidecar::wire::ResponsePayload::AuthenticatedResponse(response) => {
assert_eq!(
result.response.ownership,
wire_connection(&response.connection_id)
);
response.connection_id
}
other => panic!("unexpected wire auth response: {other:?}"),
}
}
pub fn authenticate_wire_with_token(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: secure_exec_sidecar::wire::RequestId,
connection_hint: &str,
auth_token: &str,
) -> secure_exec_sidecar::wire::WireDispatchResult {
sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_connection(connection_hint),
secure_exec_sidecar::wire::RequestPayload::AuthenticateRequest(
secure_exec_sidecar::wire::AuthenticateRequest {
client_name: String::from("sidecar-tests"),
auth_token: auth_token.to_owned(),
protocol_version: secure_exec_sidecar::wire::PROTOCOL_VERSION,
bridge_version: secure_exec_bridge::bridge_contract().version,
},
),
))
.expect("authenticate connection through wire")
}
pub fn open_session_wire(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: secure_exec_sidecar::wire::RequestId,
connection_id: &str,
) -> String {
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_connection(connection_id),
secure_exec_sidecar::wire::RequestPayload::OpenSessionRequest(
secure_exec_sidecar::wire::OpenSessionRequest {
placement: secure_exec_sidecar::wire::SidecarPlacement::SidecarPlacementShared(
secure_exec_sidecar::wire::SidecarPlacementShared { pool: None },
),
metadata: HashMap::new(),
},
),
))
.expect("open sidecar session through wire");
match result.response.payload {
secure_exec_sidecar::wire::ResponsePayload::SessionOpenedResponse(response) => {
response.session_id
}
other => panic!("unexpected wire session response: {other:?}"),
}
}
pub fn create_vm_wire(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: secure_exec_sidecar::wire::RequestId,
connection_id: &str,
session_id: &str,
runtime: secure_exec_sidecar::wire::GuestRuntimeKind,
cwd: &Path,
) -> (String, secure_exec_sidecar::wire::WireDispatchResult) {
create_vm_wire_with_metadata(
sidecar,
request_id,
connection_id,
session_id,
runtime,
cwd,
HashMap::new(),
)
}
pub fn create_vm_wire_with_metadata(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: secure_exec_sidecar::wire::RequestId,
connection_id: &str,
session_id: &str,
runtime: secure_exec_sidecar::wire::GuestRuntimeKind,
cwd: &Path,
mut metadata: HashMap<String, String>,
) -> (String, secure_exec_sidecar::wire::WireDispatchResult) {
metadata
.entry(String::from("cwd"))
.or_insert_with(|| cwd.to_string_lossy().into_owned());
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_session(connection_id, session_id),
secure_exec_sidecar::wire::RequestPayload::CreateVmRequest(
secure_exec_sidecar::wire::CreateVmRequest::legacy_test_config(
runtime,
metadata,
secure_exec_sidecar::wire::RootFilesystemDescriptor {
mode: secure_exec_sidecar::wire::RootFilesystemMode::Ephemeral,
disable_default_base_layer: false,
lowers: Vec::new(),
bootstrap_entries: Vec::new(),
},
Some(wire_permissions_allow_all()),
),
),
))
.expect("create sidecar VM through wire");
let vm_id = match &result.response.payload {
secure_exec_sidecar::wire::ResponsePayload::VmCreatedResponse(response) => {
response.vm_id.clone()
}
other => panic!("unexpected wire vm create response: {other:?}"),
};
(vm_id, result)
}
#[allow(clippy::too_many_arguments)]
pub fn execute_wire(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: secure_exec_sidecar::wire::RequestId,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
runtime: secure_exec_sidecar::wire::GuestRuntimeKind,
entrypoint: &Path,
args: Vec<String>,
) {
let result = sidecar
.dispatch_wire_blocking(wire_request(
request_id,
wire_vm(connection_id, session_id, vm_id),
secure_exec_sidecar::wire::RequestPayload::ExecuteRequest(
secure_exec_sidecar::wire::ExecuteRequest {
process_id: process_id.to_owned(),
command: None,
runtime: Some(runtime),
entrypoint: Some(entrypoint.to_string_lossy().into_owned()),
args,
env: HashMap::new(),
cwd: None,
wasm_permission_tier: None,
},
),
))
.expect("start sidecar execution through wire");
match result.response.payload {
secure_exec_sidecar::wire::ResponsePayload::ProcessStartedResponse(response) => {
assert_eq!(response.process_id, process_id);
}
other => panic!("unexpected wire execute response: {other:?}"),
}
}
pub fn collect_process_output_wire_with_timeout(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
timeout: Duration,
) -> (String, String, i32) {
let ownership = wire_session(connection_id, session_id);
let deadline = Instant::now() + timeout;
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit = None;
loop {
let event = sidecar
.poll_event_wire_blocking(&ownership, Duration::from_millis(100))
.expect("poll sidecar wire event");
if let Some(event) = event {
assert_eq!(event.ownership, wire_vm(connection_id, session_id, vm_id));
match event.payload {
secure_exec_sidecar::wire::EventPayload::ProcessOutputEvent(output) => {
if output.process_id == process_id {
match output.channel {
secure_exec_sidecar::wire::StreamChannel::Stdout => {
append_process_stream_chunk(
&mut stdout,
&output.chunk,
process_id,
"stdout",
);
}
secure_exec_sidecar::wire::StreamChannel::Stderr => {
append_process_stream_chunk(
&mut stderr,
&output.chunk,
process_id,
"stderr",
);
}
}
}
}
secure_exec_sidecar::wire::EventPayload::ProcessExitedEvent(exited)
if exited.process_id == process_id =>
{
exit = Some((exited.exit_code, Instant::now()));
}
secure_exec_sidecar::wire::EventPayload::ProcessExitedEvent(_)
| secure_exec_sidecar::wire::EventPayload::VmLifecycleEvent(_)
| secure_exec_sidecar::wire::EventPayload::StructuredEvent(_)
| secure_exec_sidecar::wire::EventPayload::ExtEnvelope(_) => {}
}
}
if let Some((exit_code, seen_at)) = exit {
if Instant::now().duration_since(seen_at) >= Duration::from_millis(200) {
return (
process_stream_to_string(&stdout),
process_stream_to_string(&stderr),
exit_code,
);
}
}
assert!(
Instant::now() < deadline,
"timed out waiting for wire process events; stdout bytes: {}; stderr bytes: {}",
stdout.len(),
stderr.len()
);
}
}
pub fn authenticate(sidecar: &mut NativeSidecar<RecordingBridge>, connection_hint: &str) -> String {
authenticate_wire(sidecar, connection_hint)
}
pub fn authenticate_with_token(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: RequestId,
connection_hint: &str,
auth_token: &str,
) -> DispatchResult {
let result = authenticate_wire_with_token(sidecar, request_id, connection_hint, auth_token);
dispatch_result_from_wire(result)
}
pub fn open_session(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: RequestId,
connection_id: &str,
) -> String {
open_session_wire(sidecar, request_id, connection_id)
}
pub fn create_vm(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: RequestId,
connection_id: &str,
session_id: &str,
runtime: GuestRuntimeKind,
cwd: &Path,
) -> (String, DispatchResult) {
create_vm_with_metadata(
sidecar,
request_id,
connection_id,
session_id,
runtime,
cwd,
BTreeMap::new(),
)
}
pub fn create_vm_with_metadata(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: RequestId,
connection_id: &str,
session_id: &str,
runtime: GuestRuntimeKind,
cwd: &Path,
metadata: BTreeMap<String, String>,
) -> (String, DispatchResult) {
let (vm_id, result) = create_vm_wire_with_metadata(
sidecar,
request_id,
connection_id,
session_id,
wire_runtime_kind(runtime),
cwd,
metadata.into_iter().collect(),
);
(vm_id, dispatch_result_from_wire(result))
}
#[allow(clippy::too_many_arguments)]
pub fn execute(
sidecar: &mut NativeSidecar<RecordingBridge>,
request_id: RequestId,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
runtime: GuestRuntimeKind,
entrypoint: &Path,
args: Vec<String>,
) {
execute_wire(
sidecar,
request_id,
connection_id,
session_id,
vm_id,
process_id,
wire_runtime_kind(runtime),
entrypoint,
args,
);
}
pub fn collect_process_output(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
) -> (String, String, i32) {
collect_process_output_with_timeout(
sidecar,
connection_id,
session_id,
vm_id,
process_id,
Duration::from_secs(10),
)
}
pub fn collect_process_output_with_timeout(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
process_id: &str,
timeout: Duration,
) -> (String, String, i32) {
collect_process_output_wire_with_timeout(
sidecar,
connection_id,
session_id,
vm_id,
process_id,
timeout,
)
}
fn dispatch_result_from_wire(
result: secure_exec_sidecar::wire::WireDispatchResult,
) -> DispatchResult {
DispatchResult {
response: response_frame_from_wire(result.response),
events: result
.events
.into_iter()
.map(event_frame_from_wire)
.collect(),
}
}
fn response_frame_from_wire(response: secure_exec_sidecar::wire::ResponseFrame) -> ResponseFrame {
match secure_exec_sidecar::protocol::from_generated_protocol_frame(
secure_exec_sidecar::wire::ProtocolFrame::ResponseFrame(response),
)
.expect("convert wire response frame to compatibility frame")
{
secure_exec_sidecar::protocol::ProtocolFrame::Response(response) => response,
other => panic!("unexpected compatibility response conversion: {other:?}"),
}
}
fn event_frame_from_wire(event: secure_exec_sidecar::wire::EventFrame) -> EventFrame {
match secure_exec_sidecar::protocol::from_generated_protocol_frame(
secure_exec_sidecar::wire::ProtocolFrame::EventFrame(event),
)
.expect("convert wire event frame to compatibility frame")
{
secure_exec_sidecar::protocol::ProtocolFrame::Event(event) => event,
other => panic!("unexpected compatibility event conversion: {other:?}"),
}
}
fn wire_runtime_kind(runtime: GuestRuntimeKind) -> secure_exec_sidecar::wire::GuestRuntimeKind {
match runtime {
GuestRuntimeKind::JavaScript => secure_exec_sidecar::wire::GuestRuntimeKind::JavaScript,
GuestRuntimeKind::Python => secure_exec_sidecar::wire::GuestRuntimeKind::Python,
GuestRuntimeKind::WebAssembly => secure_exec_sidecar::wire::GuestRuntimeKind::WebAssembly,
}
}
fn append_process_stream_chunk(
stream: &mut Vec<u8>,
chunk: &[u8],
process_id: &str,
stream_name: &str,
) {
assert!(
stream.len().saturating_add(chunk.len()) <= MAX_COLLECTED_PROCESS_STREAM_BYTES,
"process {process_id} {stream_name} exceeded {MAX_COLLECTED_PROCESS_STREAM_BYTES} bytes"
);
stream.extend_from_slice(chunk);
}
fn process_stream_to_string(stream: &[u8]) -> String {
String::from_utf8_lossy(stream).into_owned()
}
#[test]
fn collect_process_output_stream_append_is_bounded() {
let mut stream = Vec::new();
append_process_stream_chunk(&mut stream, &[b'a'; 16], "proc-limit", "stdout");
assert_eq!(stream.len(), 16);
let overflow = std::panic::catch_unwind(|| {
let mut stream = vec![b'a'; MAX_COLLECTED_PROCESS_STREAM_BYTES];
append_process_stream_chunk(&mut stream, b"!", "proc-limit", "stdout");
});
assert!(
overflow.is_err(),
"oversized process output should fail the shared test harness"
);
}
pub fn dispose_vm_and_close_session(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
) {
sidecar
.dispose_vm_internal_blocking(connection_id, session_id, vm_id, DisposeReason::Requested)
.expect("dispose sidecar VM");
sidecar
.close_session_blocking(connection_id, session_id)
.expect("close sidecar session");
sidecar
.remove_connection_blocking(connection_id)
.expect("remove sidecar connection");
}
pub fn dispose_vm_and_close_session_wire(
sidecar: &mut NativeSidecar<RecordingBridge>,
connection_id: &str,
session_id: &str,
vm_id: &str,
) {
let result = sidecar
.dispatch_wire_blocking(wire_request(
900,
wire_vm(connection_id, session_id, vm_id),
secure_exec_sidecar::wire::RequestPayload::DisposeVmRequest(
secure_exec_sidecar::wire::DisposeVmRequest {
reason: secure_exec_sidecar::wire::DisposeReason::Requested,
},
),
))
.expect("dispose sidecar VM through wire");
match result.response.payload {
secure_exec_sidecar::wire::ResponsePayload::VmDisposedResponse(response) => {
assert_eq!(response.vm_id, vm_id);
}
other => panic!("unexpected wire vm dispose response: {other:?}"),
}
sidecar
.close_session_blocking(connection_id, session_id)
.expect("close sidecar session");
sidecar
.remove_connection_blocking(connection_id)
.expect("remove sidecar connection");
}
pub fn write_fixture(path: &Path, contents: impl AsRef<[u8]>) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, contents).expect("write fixture");
}
pub fn wasm_stdout_module() -> Vec<u8> {
wat::parse_str(
r#"
(module
(type $fd_write_t (func (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t)))
(memory (export "memory") 1)
(data (i32.const 16) "wasm:ready\n")
(func $_start (export "_start")
(i32.store (i32.const 0) (i32.const 16))
(i32.store (i32.const 4) (i32.const 11))
(drop
(call $fd_write
(i32.const 1)
(i32.const 0)
(i32.const 1)
(i32.const 32)
)
)
)
)
"#,
)
.expect("compile wasm fixture")
}
pub fn wasm_signal_state_module() -> Vec<u8> {
wat::parse_str(
r#"
(module
(type $fd_write_t (func (param i32 i32 i32 i32) (result i32)))
(type $proc_sigaction_t (func (param i32 i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_t)))
(import "host_process" "proc_sigaction" (func $proc_sigaction (type $proc_sigaction_t)))
(memory (export "memory") 1)
(data (i32.const 32) "signal:ready\n")
(func $_start (export "_start")
(drop
(call $proc_sigaction
(i32.const 2)
(i32.const 2)
(i32.const 16384)
(i32.const 0)
(i32.const 4660)
)
)
(i32.store (i32.const 0) (i32.const 32))
(i32.store (i32.const 4) (i32.const 13))
(drop
(call $fd_write
(i32.const 1)
(i32.const 0)
(i32.const 1)
(i32.const 24)
)
)
)
)
"#,
)
.expect("compile signal-state wasm fixture")
}