From 990dddab553081c64c2c0b90c2f11ce0341cfe2d Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 7 Jul 2026 19:27:11 -0700 Subject: [PATCH] fix(native-sidecar): resolve js_bridge mount-root realpath without a bridge round-trip - Resolve `realpath("/")` of a js_bridge mount locally (mirroring host_dir): host-side drivers often cannot canonicalize their own root and returned ENOENT, which aborted kernel permission-subject resolution before the readDir bridge call was ever issued. - Regression test: readdir of a js_bridge mount root against a driver whose realpath always fails. --- .../native-sidecar/src/plugins/js_bridge.rs | 12 ++ crates/native-sidecar/tests/service.rs | 114 +++++++++++++++++- 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/crates/native-sidecar/src/plugins/js_bridge.rs b/crates/native-sidecar/src/plugins/js_bridge.rs index 06c98f6bb..1391b74b3 100644 --- a/crates/native-sidecar/src/plugins/js_bridge.rs +++ b/crates/native-sidecar/src/plugins/js_bridge.rs @@ -517,6 +517,18 @@ impl VirtualFileSystem for JsBridgeFilesystem { } fn realpath(&self, path: &str) -> VfsResult { + // The mount root always exists — it IS the mount point — so resolve it + // locally like `host_dir` does instead of round-tripping the bridge. + // Host-side drivers frequently cannot canonicalize their own root and + // return ENOENT for it, and because a js_bridge mount root is not a + // symlink leaf, `MountTable::realpath` propagates that ENOENT instead + // of falling back to the lexical path. The kernel permission wrapper + // resolves every operation's subject through `realpath` first, so that + // ENOENT aborts e.g. a readdir of the mount root before the readDir + // bridge call is ever issued. + if path.is_empty() || path == "/" { + return Ok(String::from("/")); + } self.parse_required( "realpath", path, diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index 6ea4daf03..cf0c6fbfb 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -992,6 +992,19 @@ ykAheWCsAteSEWVc0w==\n\ ) -> ( Arc>, Arc>>, + ) { + install_memory_js_bridge_handler_with_options(sidecar, false) + } + + /// `fail_realpath` simulates host-side drivers that cannot canonicalize + /// their own paths and answer every `realpath` bridge call with ENOENT + /// — the shape that used to break readdir of a js_bridge mount root. + fn install_memory_js_bridge_handler_with_options( + sidecar: &mut NativeSidecar, + fail_realpath: bool, + ) -> ( + Arc>, + Arc>>, ) { let filesystem = Arc::new(Mutex::new(MemoryFileSystem::new())); let calls = Arc::new(Mutex::new(Vec::::new())); @@ -1111,11 +1124,15 @@ ykAheWCsAteSEWVc0w==\n\ .map_err(|error| format!("{}: {error}", error.code())) } "realpath" => { - let path = call_args["path"].as_str().expect("realpath path"); - filesystem - .realpath(path) - .map(|resolved| Some(json!(resolved))) - .map_err(|error| format!("{}: {error}", error.code())) + if fail_realpath { + Err(String::from("ENOENT: no such file or directory")) + } else { + let path = call_args["path"].as_str().expect("realpath path"); + filesystem + .realpath(path) + .map(|resolved| Some(json!(resolved))) + .map_err(|error| format!("{}: {error}", error.code())) + } } "symlink" => { let target = call_args["target"].as_str().expect("symlink target"); @@ -7458,6 +7475,92 @@ ykAheWCsAteSEWVc0w==\n\ .expect_err("stat should fail"); assert_eq!(stat_error.code(), "EIO"); } + + fn configure_vm_js_bridge_mount_readdir_of_mount_root_survives_broken_driver_realpath() { + // Regression: readdir of a js_bridge mount root used to fail with + // ENOENT before any readDir bridge call was issued. The kernel + // permission wrapper resolves every subject through `realpath` + // first, and host-side drivers that cannot canonicalize their own + // root answer the mount-root realpath with ENOENT — which + // `MountTable::realpath` propagates for non-symlink-leaf mounts. + // The mount root must resolve locally, without a bridge realpath. + let mut sidecar = create_test_sidecar(); + let (filesystem, calls) = + install_memory_js_bridge_handler_with_options(&mut sidecar, true); + filesystem + .lock() + .expect("lock js bridge fs") + .write_file("/hello.txt", b"hi".to_vec()) + .expect("seed js bridge fs"); + + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + + sidecar + .dispatch_blocking(request( + 4, + OwnershipScope::vm(&connection_id, &session_id, &vm_id), + RequestPayload::ConfigureVm(ConfigureVmRequest { + mounts: vec![MountDescriptor { + guest_path: String::from("/workspace"), + read_only: false, + plugin: MountPluginDescriptor { + id: String::from("js_bridge"), + config: json!({ "mountId": "mount-root" }).to_string(), + }, + }], + software: Vec::new(), + permissions: None, + module_access_cwd: None, + instructions: Vec::new(), + projected_modules: Vec::new(), + command_permissions: std::collections::HashMap::new(), + loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), + bootstrap_commands: Vec::new(), + tool_shim_commands: Vec::new(), + }), + )) + .expect("configure js_bridge mount"); + + let vm = sidecar.vms.get_mut(&vm_id).expect("configured vm"); + let entries = vm + .kernel + .filesystem_mut() + .read_dir("/workspace") + .expect("readdir of js_bridge mount root"); + assert!( + entries.iter().any(|entry| entry == "hello.txt"), + "mount-root readdir should list seeded entries, got {entries:?}" + ); + + let calls = calls.lock().expect("lock js bridge calls"); + assert!( + calls + .iter() + .any(|call| call.operation == "readDir" + || call.operation == "readDirWithTypes"), + "readdir must reach the bridge; recorded operations: {:?}", + calls + .iter() + .map(|call| call.operation.clone()) + .collect::>() + ); + } + + #[test] + fn configure_vm_js_bridge_mount_root_readdir_without_bridge_realpath() { + configure_vm_js_bridge_mount_readdir_of_mount_root_survives_broken_driver_realpath(); + } + fn configure_vm_instantiates_sandbox_agent_mounts_through_the_plugin_registry() { let server = MockSandboxAgentServer::start("agentos-native-sidecar-sandbox", None); fs::write(server.root().join("hello.txt"), "hello from sandbox") @@ -21012,6 +21115,7 @@ console.log(JSON.stringify({ configure_vm_js_bridge_mount_rejects_oversized_read_payloads(); configure_vm_js_bridge_mount_rejects_pread_payloads_above_requested_length(); configure_vm_js_bridge_mount_maps_callback_errors_to_errno_codes(); + configure_vm_js_bridge_mount_readdir_of_mount_root_survives_broken_driver_realpath(); configure_vm_instantiates_sandbox_agent_mounts_through_the_plugin_registry(); configure_vm_instantiates_s3_mounts_through_the_plugin_registry(); configure_vm_instantiates_object_s3_mounts_through_the_plugin_registry();