Skip to content

Commit c811392

Browse files
authored
fix(native-sidecar): propagate guest wasm deletions and renames to the VM shadow root (#1645)
1 parent c81d201 commit c811392

2 files changed

Lines changed: 79 additions & 10 deletions

File tree

crates/native-sidecar/src/filesystem.rs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,8 +1964,13 @@ pub(crate) fn service_javascript_fs_sync_rpc(
19641964
}
19651965
kernel
19661966
.rename(source, destination)
1967-
.map(|()| Value::Null)
1968-
.map_err(kernel_error)
1967+
.map_err(kernel_error)?;
1968+
// Mirror the rename into the process shadow tree, otherwise the
1969+
// exit-time shadow->kernel sync resurrects the stale source path
1970+
// (the shadow walk only copies entries in, it cannot express
1971+
// deletions).
1972+
rename_process_shadow_path(process, source, destination)?;
1973+
Ok(Value::Null)
19691974
}
19701975
"fs.rmdirSync" | "fs.promises.rmdir" => {
19711976
let path =
@@ -1997,10 +2002,11 @@ pub(crate) fn service_javascript_fs_sync_rpc(
19972002
}
19982003
None => {}
19992004
}
2000-
kernel
2001-
.remove_dir(path)
2002-
.map(|()| Value::Null)
2003-
.map_err(kernel_error)
2005+
kernel.remove_dir(path).map_err(kernel_error)?;
2006+
// Mirror the removal into the process shadow tree, otherwise the
2007+
// exit-time shadow->kernel sync resurrects the deleted directory.
2008+
remove_process_shadow_path(process, path)?;
2009+
Ok(Value::Null)
20042010
}
20052011
"fs.unlinkSync" | "fs.promises.unlink" => {
20062012
let path =
@@ -2034,10 +2040,13 @@ pub(crate) fn service_javascript_fs_sync_rpc(
20342040
}
20352041
None => {}
20362042
}
2037-
kernel
2038-
.remove_file(path)
2039-
.map(|()| Value::Null)
2040-
.map_err(kernel_error)
2043+
kernel.remove_file(path).map_err(kernel_error)?;
2044+
// Mirror the deletion into the process shadow tree: wasm guest
2045+
// deletions route kernel-direct, and without removing the shadow
2046+
// copy the exit-time shadow->kernel sync resurrects the file for
2047+
// later builtins in the same shell and for subsequent execs.
2048+
remove_process_shadow_path(process, path)?;
2049+
Ok(Value::Null)
20412050
}
20422051
"fs.chmodSync" | "fs.promises.chmod" => {
20432052
let path =
@@ -4349,6 +4358,52 @@ fn resolve_process_guest_path_to_host(
43494358
))
43504359
}
43514360

4361+
/// Removes the host shadow copy of `guest_path` after a kernel-direct guest
4362+
/// deletion so the exit-time shadow->kernel sync cannot resurrect it.
4363+
fn remove_process_shadow_path(
4364+
process: &ActiveProcess,
4365+
guest_path: &str,
4366+
) -> Result<(), SidecarError> {
4367+
let Some(shadow_path) = resolve_process_guest_path_to_host(process, guest_path) else {
4368+
return Ok(());
4369+
};
4370+
remove_shadow_path_if_exists(&shadow_path, guest_path)
4371+
}
4372+
4373+
/// Mirrors a kernel-direct guest rename into the host shadow tree. If the
4374+
/// source shadow entry is missing the stale destination copy is still removed
4375+
/// so the shadow walk cannot resurrect pre-rename content.
4376+
fn rename_process_shadow_path(
4377+
process: &ActiveProcess,
4378+
source: &str,
4379+
destination: &str,
4380+
) -> Result<(), SidecarError> {
4381+
let Some(source_shadow) = resolve_process_guest_path_to_host(process, source) else {
4382+
return Ok(());
4383+
};
4384+
let Some(destination_shadow) = resolve_process_guest_path_to_host(process, destination) else {
4385+
return Ok(());
4386+
};
4387+
4388+
if fs::symlink_metadata(&source_shadow).is_err() {
4389+
return remove_shadow_path_if_exists(&destination_shadow, destination);
4390+
}
4391+
4392+
if let Some(parent) = destination_shadow.parent() {
4393+
fs::create_dir_all(parent).map_err(|error| {
4394+
SidecarError::Io(format!(
4395+
"failed to create shadow parent for rename {source} -> {destination}: {error}"
4396+
))
4397+
})?;
4398+
}
4399+
remove_shadow_path_if_exists(&destination_shadow, destination)?;
4400+
fs::rename(&source_shadow, &destination_shadow).map_err(|error| {
4401+
SidecarError::Io(format!(
4402+
"failed to mirror guest rename {source} -> {destination} into shadow root: {error}"
4403+
))
4404+
})
4405+
}
4406+
43524407
fn sync_host_directory_to_kernel(
43534408
vm: &mut VmState,
43544409
guest_path: &str,

packages/core/tests/wasm-commands.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ EOF`);
152152
expect(r.stdout.trim()).toBe("ok");
153153
});
154154

155+
test("rm deletion is visible across execs and to the host", async () => {
156+
await vm.writeFile("/tmp/rm-cross-exec.txt", "x");
157+
const rm = await vm.exec("rm /tmp/rm-cross-exec.txt");
158+
expect(rm.exitCode).toBe(0);
159+
// The deletion must survive the exit-time shadow sync: the host
160+
// filesystem API and a later exec both observe the file as gone.
161+
expect(await vm.exists("/tmp/rm-cross-exec.txt")).toBe(false);
162+
const probe = await vm.exec(
163+
"sh -c 'test ! -f /tmp/rm-cross-exec.txt && echo gone'",
164+
);
165+
expect(probe.exitCode).toBe(0);
166+
expect(probe.stdout.trim()).toBe("gone");
167+
});
168+
155169
test("mkdir -p and rmdir", async () => {
156170
const r = await vm.exec(
157171
"mkdir -p /tmp/a/b/c && test -d /tmp/a/b/c && echo ok",

0 commit comments

Comments
 (0)