diff --git a/crates/native-sidecar/src/execution.rs b/crates/native-sidecar/src/execution.rs index a23d0cff08..6448bb7583 100644 --- a/crates/native-sidecar/src/execution.rs +++ b/crates/native-sidecar/src/execution.rs @@ -41,7 +41,8 @@ use crate::state::{ JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent, JavascriptUnixListenerEvent, KernelSocketReadinessEvent, KernelSocketReadinessRegistry, KernelSocketReadinessTarget, LoopbackTlsPendingWriteHandle, LoopbackTlsPendingWriteState, - NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope, + NetworkResourceCounts, PendingKernelStdin, PendingTcpSocket, PendingUnixSocket, ProcNetEntry, + ProcessEventEnvelope, PythonHostSocket, ResolvedChildProcessExecution, ResolvedTcpConnectAddr, SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution, VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, @@ -471,6 +472,7 @@ impl ActiveProcess { kernel_pid, kernel_handle, kernel_stdin_writer_fd: None, + pending_kernel_stdin: PendingKernelStdin::default(), tty_master_fd: None, runtime, detached: false, @@ -7885,6 +7887,11 @@ where { return Ok(false); } + // Top off the child's stdin pipe from any host-side backlog before + // probing readiness: the child draining the pipe is exactly what frees + // capacity for the next queued stdin bytes (and, once the backlog is + // empty, executes a deferred stdin close so EOF arrives in order). + flush_pending_kernel_stdin(kernel, child)?; let now = Instant::now(); let requested_timeout_ms = match request.method.as_str() { "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1, @@ -19509,6 +19516,20 @@ fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result MAX_PENDING_KERNEL_STDIN_BYTES { + return Err(SidecarError::InvalidState(format!( + "child process stdin backlog limit exceeded: {pending_total} pending + {} new bytes \ + > {MAX_PENDING_KERNEL_STDIN_BYTES} (MAX_PENDING_KERNEL_STDIN_BYTES); the child is \ + not draining stdin — write smaller chunks after the child consumes them, or raise \ + this bound together with the kernel resource.max_fd_write_bytes limit", + chunk.len(), + ))); + } + process.pending_kernel_stdin.push(chunk); + flush_pending_kernel_stdin(kernel, process) +} + +/// Write as much queued stdin as the child's kernel pipe can take right now. +/// Called on every stdin write and from the child kernel-wait servicing path +/// (each parked `__kernel_stdin_read` / `__kernel_poll` re-check), so the +/// backlog drains in lockstep with the child's reads. Executes the deferred +/// stdin close once the backlog is empty. +pub(crate) fn flush_pending_kernel_stdin( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, +) -> Result<(), SidecarError> { + if process.tty_master_fd.is_some() { + return Ok(()); + } + let Some(writer_fd) = process.kernel_stdin_writer_fd else { + process.pending_kernel_stdin.clear(); + process.pending_kernel_stdin.close_requested = false; + return Ok(()); + }; + while let Some(front) = process.pending_kernel_stdin.chunks.pop_front() { + let offset = process.pending_kernel_stdin.front_offset; + let slice = &front[offset..]; + match kernel.fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, slice) { + Ok(written) if written >= slice.len() => { + process.pending_kernel_stdin.total = + process.pending_kernel_stdin.total.saturating_sub(slice.len()); + process.pending_kernel_stdin.front_offset = 0; + } + Ok(written) => { + // Pipe is full mid-chunk; keep the remainder queued. + process.pending_kernel_stdin.total = + process.pending_kernel_stdin.total.saturating_sub(written); + process.pending_kernel_stdin.front_offset = offset + written; + process.pending_kernel_stdin.chunks.push_front(front); + break; + } + Err(error) if error.code() == "EAGAIN" => { + process.pending_kernel_stdin.chunks.push_front(front); + break; + } + Err(error) if error.code() == "EPIPE" => { + // Reader side is gone (child exited or closed stdin): drop the + // backlog and release the writer, mirroring a SIGPIPE'd POSIX + // writer. + process.pending_kernel_stdin.clear(); + process.pending_kernel_stdin.close_requested = false; + process.kernel_stdin_writer_fd = None; + let _ = kernel.fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd); + return Ok(()); + } + Err(error) => { + process.pending_kernel_stdin.chunks.push_front(front); + return Err(kernel_error(error)); + } + } + } + if process.pending_kernel_stdin.is_empty() && process.pending_kernel_stdin.close_requested { + process.pending_kernel_stdin.close_requested = false; + if let Some(writer_fd) = process.kernel_stdin_writer_fd.take() { + kernel + .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd) + .map_err(kernel_error)?; + } + } Ok(()) } @@ -19605,6 +19719,13 @@ pub(crate) fn close_kernel_process_stdin( kernel: &mut SidecarKernel, process: &mut ActiveProcess, ) -> Result<(), SidecarError> { + if !process.pending_kernel_stdin.is_empty() && process.kernel_stdin_writer_fd.is_some() { + // Queued stdin has not reached the pipe yet; closing now would hand + // the child a premature EOF. `flush_pending_kernel_stdin` performs the + // close once the backlog drains. + process.pending_kernel_stdin.close_requested = true; + return Ok(()); + } let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else { return Ok(()); }; diff --git a/crates/native-sidecar/src/state.rs b/crates/native-sidecar/src/state.rs index 3004ec7b36..973d8534df 100644 --- a/crates/native-sidecar/src/state.rs +++ b/crates/native-sidecar/src/state.rs @@ -449,11 +449,49 @@ impl Default for VmListenPolicy { // Active process state // --------------------------------------------------------------------------- +/// Stdin bytes accepted from a parent's `child_process.write_stdin` but not +/// yet written into the child's kernel stdin pipe. The kernel pipe holds at +/// most `MAX_PIPE_BUFFER_BYTES` (64 KiB) and `fd_write` reports partial +/// writes with POSIX pipe semantics, so multi-buffer stdin payloads (for +/// example git's spooled pack fed to `index-pack --stdin`) must be queued +/// host-side and flushed as the child drains its pipe. `close_requested` +/// defers the writer-fd close until the backlog fully drains so the child +/// never observes an early EOF. +#[derive(Default)] +pub(crate) struct PendingKernelStdin { + pub(crate) chunks: VecDeque>, + /// Bytes of the front chunk already written into the pipe. + pub(crate) front_offset: usize, + /// Total unwritten bytes across all queued chunks. + pub(crate) total: usize, + pub(crate) close_requested: bool, +} + +impl PendingKernelStdin { + pub(crate) fn is_empty(&self) -> bool { + self.chunks.is_empty() + } + + pub(crate) fn push(&mut self, chunk: &[u8]) { + self.total += chunk.len(); + self.chunks.push_back(chunk.to_vec()); + } + + pub(crate) fn clear(&mut self) { + self.chunks.clear(); + self.front_offset = 0; + self.total = 0; + } +} + #[allow(dead_code)] pub(crate) struct ActiveProcess { pub(crate) kernel_pid: u32, pub(crate) kernel_handle: KernelProcessHandle, pub(crate) kernel_stdin_writer_fd: Option, + /// Backlog for pipe-backed kernel stdin awaiting pipe capacity; see + /// [`PendingKernelStdin`]. + pub(crate) pending_kernel_stdin: PendingKernelStdin, /// For a TTY (PTY-backed) process, the master-end fd whose output buffer /// carries cooked-mode echo plus ONLCR-processed guest output. When set, /// this master output is the single ordered output stream surfaced to the diff --git a/crates/native-sidecar/tests/fixtures/limits-inventory.json b/crates/native-sidecar/tests/fixtures/limits-inventory.json index 65f5098b8a..08e26cf1a1 100644 --- a/crates/native-sidecar/tests/fixtures/limits-inventory.json +++ b/crates/native-sidecar/tests/fixtures/limits-inventory.json @@ -1114,5 +1114,11 @@ "path": "crates/execution/src/wasm.rs", "class": "invariant", "rationale": "Host-side LRU entry cap for cached wasm module bytes; process-wide cache sizing, not per-VM policy." + }, + { + "name": "MAX_PENDING_KERNEL_STDIN_BYTES", + "path": "crates/native-sidecar/src/execution.rs", + "class": "policy-deferred", + "rationale": "Host-side backlog cap for child stdin awaiting kernel pipe capacity; mirrors resource.max_fd_write_bytes, not yet wired to VmLimits." } ] diff --git a/packages/runtime-core/commands/git b/packages/runtime-core/commands/git index f2dc7a774c..0a577332dc 100644 Binary files a/packages/runtime-core/commands/git and b/packages/runtime-core/commands/git differ diff --git a/packages/runtime-core/commands/git-receive-pack b/packages/runtime-core/commands/git-receive-pack index f2dc7a774c..0a577332dc 100644 Binary files a/packages/runtime-core/commands/git-receive-pack and b/packages/runtime-core/commands/git-receive-pack differ diff --git a/packages/runtime-core/commands/git-remote-http b/packages/runtime-core/commands/git-remote-http index 124c54b4e9..8c78d028ca 100644 Binary files a/packages/runtime-core/commands/git-remote-http and b/packages/runtime-core/commands/git-remote-http differ diff --git a/packages/runtime-core/commands/git-remote-https b/packages/runtime-core/commands/git-remote-https index 124c54b4e9..8c78d028ca 100644 Binary files a/packages/runtime-core/commands/git-remote-https and b/packages/runtime-core/commands/git-remote-https differ diff --git a/packages/runtime-core/commands/git-upload-archive b/packages/runtime-core/commands/git-upload-archive index f2dc7a774c..0a577332dc 100644 Binary files a/packages/runtime-core/commands/git-upload-archive and b/packages/runtime-core/commands/git-upload-archive differ diff --git a/packages/runtime-core/commands/git-upload-pack b/packages/runtime-core/commands/git-upload-pack index f2dc7a774c..0a577332dc 100644 Binary files a/packages/runtime-core/commands/git-upload-pack and b/packages/runtime-core/commands/git-upload-pack differ diff --git a/toolchain/c/scripts/build-git-upstream.sh b/toolchain/c/scripts/build-git-upstream.sh index 304e004647..40d343a530 100755 --- a/toolchain/c/scripts/build-git-upstream.sh +++ b/toolchain/c/scripts/build-git-upstream.sh @@ -121,6 +121,15 @@ CURL_CFLAGS="-I$CURL_PREFIX/include" CURL_LDFLAGS="-L$CURL_PREFIX/lib -lcurl -L$MBEDTLS_LIBDIR -lmbedtls -lmbedx509 -lmbedcrypto -L$ZLIB_BUILD_DIR -lz -L$BROTLI_LIBDIR -lbrotlidec -lbrotlicommon -L$ZSTD_LIBDIR -lzstd" echo "Building upstream Git ${VERSION} for wasm32-wasip1 (with in-process libcurl)..." +# Stack layout: wasm-ld defaults to a 64 KiB stack placed *above* the data +# segment. Git assumes native-sized stacks (recv_sideband alone puts a +# LARGE_PACKET_MAX (~64 KiB) buffer on the stack, reached several frames deep +# on every sideband pack transfer), so with the default layout the buffer +# silently overwrites static data and clones/fetches die with wild wasm traps +# once packs carry payloads bigger than a few KiB. Give git an 8 MiB stack +# (glibc-default sized) and --stack-first so any future overflow traps at the +# faulting frame instead of corrupting the data segment. +GIT_WASM_STACK_FLAGS="-Wl,-z,stack-size=8388608 -Wl,--stack-first" make -j"${MAKE_JOBS:-2}" \ uname_S=WASI \ CC="$CC_CMD" \ @@ -128,7 +137,7 @@ make -j"${MAKE_JOBS:-2}" \ AR="$AR_CMD" \ RANLIB="$RANLIB_CMD" \ CFLAGS="--target=wasm32-wasip1 --sysroot=$SYSROOT -I$ZLIB_DIR -O2 -D_WASI_EMULATED_PROCESS_CLOCKS -D_WASI_EMULATED_MMAN" \ - LDFLAGS="--target=wasm32-wasip1 --sysroot=$SYSROOT -L$ZLIB_BUILD_DIR -lwasi-emulated-process-clocks -lwasi-emulated-mman" \ + LDFLAGS="--target=wasm32-wasip1 --sysroot=$SYSROOT -L$ZLIB_BUILD_DIR -lwasi-emulated-process-clocks -lwasi-emulated-mman $GIT_WASM_STACK_FLAGS" \ CURL_CFLAGS="$CURL_CFLAGS" \ CURL_LDFLAGS="$CURL_LDFLAGS" \ CSPRNG_METHOD=getentropy \ diff --git a/toolchain/c/sysroot/lib/wasm32-wasi/libc.a b/toolchain/c/sysroot/lib/wasm32-wasi/libc.a index aa37ca6c17..f253c97663 100644 Binary files a/toolchain/c/sysroot/lib/wasm32-wasi/libc.a and b/toolchain/c/sysroot/lib/wasm32-wasi/libc.a differ