Skip to content

Commit 71d7e35

Browse files
committed
fix(native-sidecar): preserve live host mounts during teardown
1 parent 1db3302 commit 71d7e35

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

crates/native-sidecar/src/execution/launch.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,13 @@ fn sync_host_directory_tree_to_kernel(
10591059
) -> Result<(), SidecarError> {
10601060
let normalized_host_root = normalize_host_path(host_root);
10611061
let normalized_guest_root = normalize_path(guest_root);
1062+
if guest_sync_root_targets_live_mount(vm, &normalized_guest_root) {
1063+
// A host_dir/module_access mount is the kernel's live backing store.
1064+
// Importing shadow output through a guest alias into that mount is
1065+
// destructive: symlink reconciliation can unlink or rewrite entries
1066+
// in the host workspace while a process is exiting.
1067+
return Ok(());
1068+
}
10621069
if host_sync_root_is_filesystem_root(host_root) {
10631070
// A process tracked with host cwd "/" would pull the entire host
10641071
// filesystem into the kernel VFS (until the size/inode caps fire).
@@ -1079,6 +1086,15 @@ fn sync_host_directory_tree_to_kernel(
10791086
)
10801087
}
10811088

1089+
fn guest_sync_root_targets_live_mount(vm: &mut VmState, guest_root: &str) -> bool {
1090+
host_mount_path_for_guest_path(vm, guest_root).is_some()
1091+
|| vm
1092+
.kernel
1093+
.realpath(guest_root)
1094+
.ok()
1095+
.is_some_and(|resolved| host_mount_path_for_guest_path(vm, &resolved).is_some())
1096+
}
1097+
10821098
fn sync_host_directory_tree_to_kernel_inner(
10831099
vm: &mut VmState,
10841100
host_root: &Path,

crates/native-sidecar/tests/service.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8777,6 +8777,107 @@ console.log(JSON.stringify({ status: "ok", summary }));
87778777
fs::remove_dir_all(host_dir).expect("remove temp dir");
87788778
}
87798779

8780+
fn disposing_dirty_process_does_not_reconcile_live_host_mount_into_itself() {
8781+
let host_dir = temp_dir("agentos-native-sidecar-live-host-dir-dispose");
8782+
let generated_dir = host_dir.join("generated");
8783+
fs::create_dir(&generated_dir).expect("create generated workspace directory");
8784+
let generated_file = generated_dir.join("generated.txt");
8785+
fs::write(&generated_file, "keep me").expect("seed generated host file");
8786+
let stale_process_dir = temp_dir("agentos-native-sidecar-stale-process-output");
8787+
std::os::unix::fs::symlink("stale-target", stale_process_dir.join("generated.txt"))
8788+
.expect("seed stale process symlink");
8789+
8790+
let mut sidecar = create_test_sidecar();
8791+
let (connection_id, session_id) =
8792+
authenticate_and_open_session(&mut sidecar).expect("authenticate and open session");
8793+
let vm_id = create_vm(
8794+
&mut sidecar,
8795+
&connection_id,
8796+
&session_id,
8797+
PermissionsPolicy::allow_all(),
8798+
)
8799+
.expect("create vm");
8800+
sidecar
8801+
.vms
8802+
.get_mut(&vm_id)
8803+
.expect("created vm")
8804+
.kernel
8805+
.symlink("/workspace/generated", "/current")
8806+
.expect("create guest alias before mounting its target");
8807+
sidecar
8808+
.dispatch_blocking(request(
8809+
4,
8810+
OwnershipScope::vm(&connection_id, &session_id, &vm_id),
8811+
RequestPayload::ConfigureVm(ConfigureVmRequest {
8812+
mounts: vec![MountDescriptor {
8813+
guest_path: String::from("/workspace"),
8814+
guest_source: String::from("host_dir"),
8815+
guest_fstype: String::from("host_dir"),
8816+
read_only: false,
8817+
plugin: MountPluginDescriptor {
8818+
id: String::from("host_dir"),
8819+
config: json!({
8820+
"hostPath": host_dir,
8821+
"readOnly": false,
8822+
})
8823+
.to_string(),
8824+
},
8825+
}],
8826+
software: Vec::new(),
8827+
permissions: None,
8828+
module_access_cwd: None,
8829+
instructions: Vec::new(),
8830+
projected_modules: Vec::new(),
8831+
command_permissions: std::collections::HashMap::new(),
8832+
loopback_exempt_ports: Vec::new(),
8833+
packages: Vec::new(),
8834+
packages_mount_at: String::new(),
8835+
bootstrap_commands: Vec::new(),
8836+
binding_shim_commands: Vec::new(),
8837+
}),
8838+
))
8839+
.expect("configure live host mount");
8840+
8841+
insert_fake_javascript_parent_process(
8842+
&mut sidecar,
8843+
&vm_id,
8844+
&stale_process_dir,
8845+
"dirty-host-mount-process",
8846+
);
8847+
{
8848+
let mut vm = sidecar.vms.get_mut(&vm_id).expect("configured vm");
8849+
let process = vm
8850+
.active_processes
8851+
.get_mut("dirty-host-mount-process")
8852+
.expect("inserted dirty process");
8853+
process.guest_cwd = String::from("/current");
8854+
process.host_write_dirty = true;
8855+
}
8856+
8857+
sidecar
8858+
.dispose_vm_internal_blocking(
8859+
&connection_id,
8860+
&session_id,
8861+
&vm_id,
8862+
DisposeReason::Requested,
8863+
)
8864+
.expect("dispose VM with live host mount");
8865+
8866+
assert_eq!(
8867+
fs::read_to_string(&generated_file).expect("generated host file must survive"),
8868+
"keep me"
8869+
);
8870+
8871+
sidecar
8872+
.close_session_blocking(&connection_id, &session_id)
8873+
.expect("close session");
8874+
sidecar
8875+
.remove_connection_blocking(&connection_id)
8876+
.expect("remove connection");
8877+
fs::remove_dir_all(host_dir).expect("remove host dir");
8878+
fs::remove_dir_all(stale_process_dir).expect("remove stale process dir");
8879+
}
8880+
87808881
fn configure_vm_passes_resource_read_limits_to_host_dir_mounts() {
87818882
let host_dir = temp_dir("agentos-native-sidecar-host-dir-read-limit");
87828883
fs::write(host_dir.join("hello.txt"), "hello from host").expect("seed host dir");
@@ -25687,6 +25788,16 @@ try {
2568725788
dirty_host_shadow_sync_precedes_top_level_and_nested_spawn_actions();
2568825789
}
2568925790

25791+
#[test]
25792+
fn service_dispose_preserves_live_host_mount_symlinks() {
25793+
disposing_dirty_process_does_not_reconcile_live_host_mount_into_itself();
25794+
}
25795+
25796+
#[test]
25797+
fn service_node_input_type_module_eval_uses_esm_parser() {
25798+
command_resolution_executes_node_module_eval_command();
25799+
}
25800+
2569025801
#[test]
2569125802
fn service_posix_spawnp_path_and_recursive_shebang_match_linux() {
2569225803
posix_spawnp_path_and_recursive_shebang_match_linux();

0 commit comments

Comments
 (0)