mod support;
use secure_exec_sidecar::wire::{
ConfigureVmRequest, CreateVmRequest, FsPermissionRule, FsPermissionRuleSet, FsPermissionScope,
GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, PatternPermissionRule,
PatternPermissionRuleSet, PatternPermissionScope, PermissionMode, PermissionsPolicy,
RequestPayload, ResponsePayload, RootFilesystemDescriptor, RootFilesystemEntry,
RootFilesystemEntryKind, RootFilesystemMode,
};
use std::collections::HashMap;
use support::{
authenticate_wire, create_vm_wire, open_session_wire, temp_dir, wire_request, wire_session,
wire_vm,
};
fn expect_invalid_state(payload: ResponsePayload, expected_message: &str) {
match payload {
ResponsePayload::RejectedResponse(rejected) => {
assert_eq!(rejected.code, "invalid_state");
assert!(
rejected.message.contains(expected_message),
"unexpected rejection: {rejected:?}"
);
}
other => panic!("expected invalid_state rejection, got {other:?}"),
}
}
fn root_dir(path: &str, mode: u32) -> RootFilesystemEntry {
RootFilesystemEntry {
path: path.to_owned(),
kind: RootFilesystemEntryKind::Directory,
mode: Some(mode),
uid: None,
gid: None,
content: None,
encoding: None,
target: None,
executable: false,
}
}
fn create_vm_with_fs_permissions(
sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
connection_id: &str,
session_id: &str,
permissions: PermissionsPolicy,
) -> String {
let response = sidecar
.dispatch_wire_blocking(wire_request(
3,
wire_session(connection_id, session_id),
RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config(
GuestRuntimeKind::JavaScript,
HashMap::new(),
RootFilesystemDescriptor {
mode: RootFilesystemMode::Ephemeral,
disable_default_base_layer: false,
lowers: Vec::new(),
bootstrap_entries: vec![root_dir("/tmp", 0o755)],
},
Some(permissions),
)),
))
.expect("create vm with fs permissions");
match response.response.payload {
ResponsePayload::VmCreatedResponse(response) => response.vm_id,
other => panic!("expected vm create response, got {other:?}"),
}
}
fn mkdir_request(path: &str, recursive: bool) -> GuestFilesystemCallRequest {
GuestFilesystemCallRequest {
operation: GuestFilesystemOperation::Mkdir,
path: path.to_owned(),
destination_path: None,
target: None,
content: None,
encoding: None,
recursive,
mode: None,
uid: None,
gid: None,
atime_ms: None,
mtime_ms: None,
len: None,
offset: None,
}
}
#[test]
fn permission_flags_reject_empty_operations_and_accept_explicit_wildcards() {
let mut sidecar = support::new_sidecar("permission-flags-create");
let connection_id = authenticate_wire(&mut sidecar, "conn-1");
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let rejected = sidecar
.dispatch_wire_blocking(wire_request(
3,
wire_session(&connection_id, &session_id),
RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config(
GuestRuntimeKind::JavaScript,
HashMap::new(),
RootFilesystemDescriptor {
mode: RootFilesystemMode::Ephemeral,
disable_default_base_layer: false,
lowers: Vec::new(),
bootstrap_entries: Vec::new(),
},
Some(PermissionsPolicy {
fs: Some(FsPermissionScope::FsPermissionRuleSet(
FsPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![FsPermissionRule {
mode: PermissionMode::Allow,
operations: Vec::new(),
paths: vec![String::from("/**")],
}],
},
)),
network: None,
child_process: None,
process: None,
env: None,
binding: None,
}),
)),
))
.expect("dispatch create vm rejection");
expect_invalid_state(
rejected.response.payload,
"fs.rules[0].operations must not be empty",
);
let accepted = sidecar
.dispatch_wire_blocking(wire_request(
4,
wire_session(&connection_id, &session_id),
RequestPayload::CreateVmRequest(CreateVmRequest::legacy_test_config(
GuestRuntimeKind::JavaScript,
HashMap::new(),
RootFilesystemDescriptor {
mode: RootFilesystemMode::Ephemeral,
disable_default_base_layer: false,
lowers: Vec::new(),
bootstrap_entries: Vec::new(),
},
Some(PermissionsPolicy {
fs: Some(FsPermissionScope::FsPermissionRuleSet(
FsPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![String::from("*")],
paths: vec![String::from("/**")],
}],
},
)),
network: None,
child_process: None,
process: None,
env: None,
binding: None,
}),
)),
))
.expect("dispatch create vm with wildcard");
match accepted.response.payload {
ResponsePayload::VmCreatedResponse(_) => {}
other => panic!("expected vm creation with explicit wildcard, got {other:?}"),
}
}
#[test]
fn permission_flags_reject_empty_paths_and_patterns_on_configure() {
let mut sidecar = support::new_sidecar("permission-flags-configure");
let connection_id = authenticate_wire(&mut sidecar, "conn-1");
let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
let cwd = temp_dir("permission-flags-configure-cwd");
let (vm_id, _) = create_vm_wire(
&mut sidecar,
3,
&connection_id,
&session_id,
GuestRuntimeKind::JavaScript,
&cwd,
);
let empty_paths = sidecar
.dispatch_wire_blocking(wire_request(
4,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
mounts: Vec::new(),
software: Vec::new(),
permissions: Some(PermissionsPolicy {
fs: Some(FsPermissionScope::FsPermissionRuleSet(
FsPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![String::from("read")],
paths: Vec::new(),
}],
},
)),
network: None,
child_process: None,
process: None,
env: None,
binding: None,
}),
module_access_cwd: None,
instructions: Vec::new(),
projected_modules: Vec::new(),
command_permissions: Default::default(),
loopback_exempt_ports: Vec::new(),
}),
))
.expect("dispatch configure vm with empty fs paths");
expect_invalid_state(
empty_paths.response.payload,
"fs.rules[0].paths must not be empty",
);
let empty_patterns = sidecar
.dispatch_wire_blocking(wire_request(
5,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
mounts: Vec::new(),
software: Vec::new(),
permissions: Some(PermissionsPolicy {
fs: None,
network: Some(PatternPermissionScope::PatternPermissionRuleSet(
PatternPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![PatternPermissionRule {
mode: PermissionMode::Allow,
operations: vec![String::from("*")],
patterns: Vec::new(),
}],
},
)),
child_process: None,
process: None,
env: None,
binding: None,
}),
module_access_cwd: None,
instructions: Vec::new(),
projected_modules: Vec::new(),
command_permissions: Default::default(),
loopback_exempt_ports: Vec::new(),
}),
))
.expect("dispatch configure vm with empty network patterns");
expect_invalid_state(
empty_patterns.response.payload,
"network.rules[0].patterns must not be empty",
);
let empty_pattern_operations = sidecar
.dispatch_wire_blocking(wire_request(
6,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
mounts: Vec::new(),
software: Vec::new(),
permissions: Some(PermissionsPolicy {
fs: None,
network: Some(PatternPermissionScope::PatternPermissionRuleSet(
PatternPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![PatternPermissionRule {
mode: PermissionMode::Allow,
operations: Vec::new(),
patterns: vec![String::from("**")],
}],
},
)),
child_process: None,
process: None,
env: None,
binding: None,
}),
module_access_cwd: None,
instructions: Vec::new(),
projected_modules: Vec::new(),
command_permissions: Default::default(),
loopback_exempt_ports: Vec::new(),
}),
))
.expect("dispatch configure vm with empty network operations");
expect_invalid_state(
empty_pattern_operations.response.payload,
"network.rules[0].operations must not be empty",
);
}
#[test]
fn permission_flags_single_star_paths_do_not_cross_path_separators() {
let mut sidecar = support::new_sidecar("permission-flags-single-star");
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_with_fs_permissions(
&mut sidecar,
&connection_id,
&session_id,
PermissionsPolicy {
fs: Some(FsPermissionScope::FsPermissionRuleSet(
FsPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![
FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![String::from("read")],
paths: vec![String::from("/tmp")],
},
FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![
String::from("create_dir"),
String::from("read"),
String::from("stat"),
],
paths: vec![String::from("/tmp/*")],
},
],
},
)),
network: None,
child_process: None,
process: None,
env: None,
binding: None,
},
);
let allow_direct_child = sidecar
.dispatch_wire_blocking(wire_request(
4,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::GuestFilesystemCallRequest(mkdir_request("/tmp/a", false)),
))
.expect("create direct child directory");
match allow_direct_child.response.payload {
ResponsePayload::GuestFilesystemResultResponse(response) => {
assert_eq!(response.operation, GuestFilesystemOperation::Mkdir);
assert_eq!(response.path, "/tmp/a");
}
other => panic!("expected guest filesystem mkdir response, got {other:?}"),
}
let deny_nested_child = sidecar
.dispatch_wire_blocking(wire_request(
5,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::GuestFilesystemCallRequest(mkdir_request("/tmp/a/b", false)),
))
.expect("attempt nested child directory create");
match deny_nested_child.response.payload {
ResponsePayload::RejectedResponse(rejected) => {
assert_eq!(rejected.code, "kernel_error");
assert!(rejected.message.contains("EACCES"));
}
other => panic!("expected rejected nested mkdir response, got {other:?}"),
}
}
#[test]
fn permission_flags_double_star_paths_allow_nested_descendants() {
let mut sidecar = support::new_sidecar("permission-flags-double-star");
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_with_fs_permissions(
&mut sidecar,
&connection_id,
&session_id,
PermissionsPolicy {
fs: Some(FsPermissionScope::FsPermissionRuleSet(
FsPermissionRuleSet {
default: Some(PermissionMode::Deny),
rules: vec![
FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![String::from("read")],
paths: vec![String::from("/tmp")],
},
FsPermissionRule {
mode: PermissionMode::Allow,
operations: vec![
String::from("create_dir"),
String::from("read"),
String::from("stat"),
],
paths: vec![String::from("/tmp/**")],
},
],
},
)),
network: None,
child_process: None,
process: None,
env: None,
binding: None,
},
);
let allow_direct_child = sidecar
.dispatch_wire_blocking(wire_request(
4,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::GuestFilesystemCallRequest(mkdir_request("/tmp/a", false)),
))
.expect("create direct child directory");
match allow_direct_child.response.payload {
ResponsePayload::GuestFilesystemResultResponse(response) => {
assert_eq!(response.operation, GuestFilesystemOperation::Mkdir);
assert_eq!(response.path, "/tmp/a");
}
other => panic!("expected guest filesystem mkdir response, got {other:?}"),
}
let allow_nested_child = sidecar
.dispatch_wire_blocking(wire_request(
5,
wire_vm(&connection_id, &session_id, &vm_id),
RequestPayload::GuestFilesystemCallRequest(mkdir_request("/tmp/a/b/c", true)),
))
.expect("create nested child directory");
match allow_nested_child.response.payload {
ResponsePayload::GuestFilesystemResultResponse(response) => {
assert_eq!(response.operation, GuestFilesystemOperation::Mkdir);
assert_eq!(response.path, "/tmp/a/b/c");
}
other => panic!("expected guest filesystem mkdir response, got {other:?}"),
}
}