From 280bd164a17d732d82d6c791a1a4028713b7602e Mon Sep 17 00:00:00 2001 From: WellDunDun <45949032+WellDunDun@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:48:57 +0300 Subject: [PATCH 1/2] fix: service queued control RPCs beneath WASM parents --- .../src/execution/child_process.rs | 28 ++++++- crates/native-sidecar/tests/service.rs | 82 +++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index 53a652c345..ade39e695f 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -2756,8 +2756,9 @@ where // The standalone WASM runner pulls descendant output through // child_process.poll while implementing waitpid. Keep stream - // and exit delivery single-owner; the parked kernel wait was - // already rechecked above without leasing either event lane. + // and exit delivery single-owner, but still claim control RPCs: + // the parent's poll requeues nested spawn/exec requests for this + // pump, so skipping the child entirely would starve them. let parent_is_pull_driven_wasm = self .vms .get(vm_id) @@ -2769,7 +2770,26 @@ where }) .unwrap_or(false); if parent_is_pull_driven_wasm { - continue; + let queued_control_request = self.vms.get(vm_id).is_some_and(|vm| { + vm.active_processes + .get(process_id) + .and_then(|root| Self::active_process_by_path(root, &parent_path)) + .and_then(|parent| parent.child_processes.get(&child_process_id)) + .and_then(|child| child.pending_execution_events.front()) + .is_some_and(|event| { + matches!(event, + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + if matches!(request.method.as_str(), + "child_process.spawn" | "child_process.spawn_sync" + | "child_process.poll" | "child_process.write_stdin" + | "child_process.close_stdin" | "child_process.kill" + | "process.exec_fd_image_commit" | "process.exec" + | "process.signal_state" | "process.kill")) + }) + }); + if !queued_control_request { + continue; + } } self.expire_child_process_sync_if_needed( vm_id, @@ -2783,7 +2803,7 @@ where process_id, &parent_path, &child_process_id, - false, + parent_is_pull_driven_wasm, javascript_services, python_services, python_socket_completions, diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index 03ef99a087..808bc42f88 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -25585,6 +25585,88 @@ console.log(JSON.stringify({ } } + #[test] + fn wasm_parent_child_spawn_is_claimed_without_consuming_output() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + + let root_kernel_handle = create_kernel_process_handle_for_tests(); + let mut root = active_process_for_tests( + root_kernel_handle.pid(), + root_kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::HostFunction(HostFunctionExecution::default()), + ); + let child_kernel_handle = create_kernel_process_handle_for_tests(); + let mut child = active_process_for_tests( + child_kernel_handle.pid(), + child_kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::HostFunction(HostFunctionExecution::default()), + ); + child + .queue_pending_execution_event(ActiveExecutionEvent::JavascriptSyncRpcRequest( + JavascriptSyncRpcRequest { + raw_bytes_args: std::collections::HashMap::new(), + id: 1, + method: String::from("child_process.spawn"), + args: vec![json!("/bin/ls"), json!([]), json!({})], + }, + )) + .expect("queue nested child spawn"); + child + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"pull-owned".to_vec())) + .expect("queue child output"); + root.child_processes.insert(String::from("child-1"), child); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .insert(String::from("wasm-root"), root); + + let mut javascript_services = Vec::new(); + sidecar + .pump_child_process_events_nowait( + &vm_id, + &mut javascript_services, + &mut Vec::new(), + &mut Vec::new(), + &mut Vec::new(), + 8, + ) + .expect("claim nested spawn"); + assert_eq!( + javascript_services.len(), + 1, + "WASM-owned children must still service spawn requests" + ); + assert_eq!(javascript_services[0].request.method, "child_process.spawn"); + assert_eq!( + javascript_services[0].child_path, + vec![String::from("child-1")] + ); + let vm = sidecar.vms.get(&vm_id).expect("test vm"); + let queued = vm + .active_processes + .get("wasm-root") + .and_then(|root| root.child_processes.get("child-1")) + .and_then(|child| child.pending_execution_events.front()) + .expect("WASM child output should remain available to child_process.poll"); + match queued { + ActiveExecutionEvent::Stdout(chunk) => assert_eq!(chunk, b"pull-owned"), + other => panic!("expected queued child stdout, got {other:?}"), + } + } + #[test] fn wasm_parent_child_write_deadline_wakes_after_parent_stops_polling() { assert_node_available(); From 52ecf88fdcfd2514e116c881143babf53dd4a8a3 Mon Sep 17 00:00:00 2001 From: WellDunDun <45949032+WellDunDun@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:04:47 +0300 Subject: [PATCH 2/2] refactor: centralize owned process control RPC classification --- .../src/execution/child_process.rs | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/crates/native-sidecar/src/execution/child_process.rs b/crates/native-sidecar/src/execution/child_process.rs index ade39e695f..247fd120c1 100644 --- a/crates/native-sidecar/src/execution/child_process.rs +++ b/crates/native-sidecar/src/execution/child_process.rs @@ -5,6 +5,22 @@ const SYNTHETIC_V8_TERMINATION_STDERR: &[u8] = b"Error: Execution terminated\n"; type OwnedChildExecutionStart = Pin> + 'static>>; +fn is_owned_process_control_rpc(method: &str) -> bool { + matches!( + method, + "child_process.spawn" + | "child_process.spawn_sync" + | "child_process.poll" + | "child_process.write_stdin" + | "child_process.close_stdin" + | "child_process.kill" + | "process.exec_fd_image_commit" + | "process.exec" + | "process.signal_state" + | "process.kill" + ) +} + fn settle_owned_javascript_process_event_target( target: OwnedJavascriptEventService, response: Result, @@ -2779,12 +2795,7 @@ where .is_some_and(|event| { matches!(event, ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - if matches!(request.method.as_str(), - "child_process.spawn" | "child_process.spawn_sync" - | "child_process.poll" | "child_process.write_stdin" - | "child_process.close_stdin" | "child_process.kill" - | "process.exec_fd_image_commit" | "process.exec" - | "process.signal_state" | "process.kill")) + if is_owned_process_control_rpc(&request.method)) }) }); if !queued_control_request { @@ -7028,18 +7039,7 @@ where _ => {} } - let special = matches!( - target.request.method.as_str(), - "child_process.poll" - | "child_process.write_stdin" - | "child_process.close_stdin" - | "child_process.kill" - | "process.exec_fd_image_commit" - | "process.exec" - | "process.signal_state" - | "process.kill" - ); - if !special { + if !is_owned_process_control_rpc(&target.request.method) { return Box::pin(async move { let result = service_owned_javascript_sync_rpc_request( &bridge, @@ -7385,19 +7385,7 @@ where drop(reservation); continue; } - if matches!( - request.method.as_str(), - "child_process.spawn" - | "child_process.spawn_sync" - | "child_process.poll" - | "child_process.write_stdin" - | "child_process.close_stdin" - | "child_process.kill" - | "process.exec_fd_image_commit" - | "process.exec" - | "process.signal_state" - | "process.kill" - ) { + if is_owned_process_control_rpc(&request.method) { vm.try_command("requeue nested special process RPC", |state| { let path = current_process_path .iter()