From 432857531a6969e9ae7d397c2c59406bfb21214d Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Mon, 13 Jul 2026 13:54:28 -0400 Subject: [PATCH] host: preserve output buffers on zero-byte syscalls --- host/src/kernel-worker.ts | 9 +- host/test/kernel-worker-copyback.test.ts | 164 +++++++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 host/test/kernel-worker-copyback.test.ts diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index 31709f3363..4492213997 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -4532,8 +4532,15 @@ export class CentralizedKernelWorker { if (!(desc.direction === "out" && retVal < 0)) { let copySize = size; if (desc.direction === "out" && desc.size.type === "arg") { + // For read/recv/getdents-like syscalls, retVal is the number of + // bytes produced. Successful EOF must not copy the zero-filled + // scratch buffer over bytes the caller already owns. Some + // descriptors prepend fixed metadata that is still produced when + // the variable-length result is empty. const copyRetvalAdd = desc.copyRetvalAdd ?? 0; - if (retVal > 0 && retVal + copyRetvalAdd < size) { + if (retVal === 0) { + copySize = Math.min(copyRetvalAdd, size); + } else if (retVal + copyRetvalAdd < size) { copySize = retVal + copyRetvalAdd; } } diff --git a/host/test/kernel-worker-copyback.test.ts b/host/test/kernel-worker-copyback.test.ts new file mode 100644 index 0000000000..c8eb166852 --- /dev/null +++ b/host/test/kernel-worker-copyback.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; +import { CentralizedKernelWorker } from "../src/kernel-worker"; +import { + ABI_SYSCALLS, + CHANNEL_STATUS_COMPLETE, + CH_DATA, + CH_ERRNO, + CH_RETURN, + CH_STATUS, + type SyscallArgDesc, + SYSCALL_ARGS, +} from "../src/generated/abi"; + +interface TestChannel { + pid: number; + memory: WebAssembly.Memory; + channelOffset: number; + i32View: Int32Array; + consecutiveSyscalls: number; + handling: boolean; +} + +interface CopybackHarnessWorker { + completeChannel( + channel: TestChannel, + syscallNr: number, + origArgs: number[], + argDescs: SyscallArgDesc[] | undefined, + retVal: number, + errVal: number, + ): void; +} + +function makeCopybackHarness() { + const pid = 1; + const kernelMemory = new WebAssembly.Memory({ initial: 2 }); + const processMemory = new WebAssembly.Memory({ + initial: 2, + maximum: 2, + shared: true, + }); + const channel: TestChannel = { + pid, + memory: processMemory, + channelOffset: 0, + i32View: new Int32Array(processMemory.buffer), + consecutiveSyscalls: 0, + handling: true, + }; + const worker = Object.assign( + Object.create(CentralizedKernelWorker.prototype), + { + kernelMemory, + scratchOffset: 0, + cachedKernelMem: null, + cachedKernelBuffer: null, + processes: new Map([ + [ + pid, + { + pid, + memory: processMemory, + channels: [channel], + ptrWidth: 4, + explicitMaxAddr: false, + }, + ], + ]), + clearSocketTimeout: () => {}, + clearReadinessWait: () => {}, + drainAllPtyOutputs: () => {}, + flushTcpSendPipes: () => {}, + drainAndProcessWakeupEvents: () => {}, + synchronizeSharedMemoryForBoundary: () => {}, + relistenChannel: () => {}, + }, + ) as CopybackHarnessWorker; + + return { + worker, + channel, + kernelMem: new Uint8Array(kernelMemory.buffer), + processMem: new Uint8Array(processMemory.buffer), + }; +} + +describe("CentralizedKernelWorker syscall copy-back", () => { + it("leaves the destination unchanged when read reports EOF", () => { + const { worker, channel, kernelMem, processMem } = makeCopybackHarness(); + const dest = 1024; + const original = Uint8Array.from({ length: 16 }, (_, i) => 0xa0 + i); + + processMem.set(original, dest); + kernelMem.fill(0, CH_DATA, CH_DATA + original.length); + + worker.completeChannel( + channel, + ABI_SYSCALLS.Read, + [0, dest, original.length], + SYSCALL_ARGS[ABI_SYSCALLS.Read], + 0, + 0, + ); + + expect(processMem.slice(dest, dest + original.length)).toEqual(original); + const channelView = new DataView(processMem.buffer); + expect(channelView.getBigInt64(CH_RETURN, true)).toBe(0n); + expect(channelView.getUint32(CH_ERRNO, true)).toBe(0); + expect(Atomics.load(channel.i32View, CH_STATUS / 4)).toBe( + CHANNEL_STATUS_COMPLETE, + ); + }); + + it("copies only the byte count reported by read", () => { + const { worker, channel, kernelMem, processMem } = makeCopybackHarness(); + const dest = 2048; + const original = Uint8Array.from({ length: 8 }, (_, i) => 0xc0 + i); + + processMem.set(original, dest); + kernelMem.set([1, 2, 3, 0, 0, 0, 0, 0], CH_DATA); + + worker.completeChannel( + channel, + ABI_SYSCALLS.Read, + [0, dest, original.length], + SYSCALL_ARGS[ABI_SYSCALLS.Read], + 3, + 0, + ); + + expect(Array.from(processMem.slice(dest, dest + original.length))).toEqual([ + 1, + 2, + 3, + ...original.slice(3), + ]); + }); + + it("copies fixed prefix metadata when a zero-length msgrcv succeeds", () => { + const { worker, channel, kernelMem, processMem } = makeCopybackHarness(); + const dest = 3072; + const original = Uint8Array.from({ length: 12 }, (_, i) => 0xd0 + i); + + processMem.set(original, dest); + kernelMem.set([0x11, 0x22, 0x33, 0x44, 0, 0, 0, 0], CH_DATA); + + worker.completeChannel( + channel, + ABI_SYSCALLS.Msgrcv, + [0, dest, 8], + SYSCALL_ARGS[ABI_SYSCALLS.Msgrcv], + 0, + 0, + ); + + expect(Array.from(processMem.slice(dest, dest + original.length))).toEqual([ + 0x11, + 0x22, + 0x33, + 0x44, + ...original.slice(4), + ]); + }); +});