diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 421438b94..ae53f5d4e 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -186,7 +186,10 @@ static bool scr_fss_try_inline(ScrFssOp *op) { if (errno == EOPNOTSUPP || errno == EINVAL || errno == ENOSYS) f->inline_ok = false; return false; } - if (n == 0 && op->kind == SCR_FSS_WRITE) return false; + /* A zero is not taken as EOF here: some kernels (Linux 5.9 and 5.10) + * answer 0 instead of EAGAIN for an uncached RWF_NOWAIT read, so the + * pool's read decides, as it does for a zero-byte write. */ + if (n == 0) return false; if (op->kind == SCR_FSS_READ && n > 0 && (size_t)n < op->len) { op->got = (size_t)n; return false; diff --git a/tests/fixtures/fs-stream-nowait-read.ts b/tests/fixtures/fs-stream-nowait-read.ts new file mode 100644 index 000000000..5e9fd3739 --- /dev/null +++ b/tests/fixtures/fs-stream-nowait-read.ts @@ -0,0 +1,23 @@ +// Reads a file it writes first through a ReadStream and prints the byte +// count and a checksum (fs-stream-nowait.test.ts runs it with a preadv2 +// shim). +import { createReadStream, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const dir = mkdtempSync(join(tmpdir(), "scr-nowait-")); +const file = join(dir, "data.bin"); +const data = Buffer.alloc(300000); +for (let i = 0; i < data.length; i++) data[i] = (i * 13) & 0xff; +writeFileSync(file, data); +let bytes = 0; +let sum = 0; +createReadStream(file, { highWaterMark: 16384 }) + .on("data", (c: Buffer) => { + bytes += c.length; + for (const b of c) sum = (sum + b) % 65521; + }) + .on("end", () => { + console.log(`bytes=${bytes} sum=${sum}`); + rmSync(dir, { recursive: true }); + }); diff --git a/tests/fixtures/fs-stream-nowait-zero.c b/tests/fixtures/fs-stream-nowait-zero.c new file mode 100644 index 000000000..69626997f --- /dev/null +++ b/tests/fixtures/fs-stream-nowait-zero.c @@ -0,0 +1,13 @@ +/* LD_PRELOAD shim for fs-stream-nowait.test.ts: preadv2 with RWF_NOWAIT + * answers 0, as Linux 5.9 and 5.10 do for an uncached RWF_NOWAIT read; + * other calls go to the real preadv2. */ +#define _GNU_SOURCE +#include +#include + +ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags) { + if (flags & RWF_NOWAIT) return 0; + ssize_t (*real)(int, const struct iovec *, int, off_t, int) = + (ssize_t (*)(int, const struct iovec *, int, off_t, int))dlsym(RTLD_NEXT, "preadv2"); + return real(fd, iov, iovcnt, offset, flags); +} diff --git a/tests/harness/fs-stream-nowait.test.ts b/tests/harness/fs-stream-nowait.test.ts new file mode 100644 index 000000000..a6ba9e074 --- /dev/null +++ b/tests/harness/fs-stream-nowait.test.ts @@ -0,0 +1,44 @@ +/* fs streams read a regular file with RWF_NOWAIT on the loop thread first + * (scr_fs_stream.c). Linux 5.9 and 5.10 answer 0 instead of EAGAIN for + * an uncached RWF_NOWAIT read; a preloaded preadv2 shim reproduces that + * answer, and the stream must still read the whole file, as Node does. */ +import { execFileSync, spawnSync } from "node:child_process"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +describe.skipIf(process.platform !== "linux" || sanitize)("fs stream RWF_NOWAIT reads", () => { + test("a zero from an RWF_NOWAIT read is not taken as EOF", async () => { + const outDir = join(cacheDir, "fs-stream-nowait"); + const result = await compile(join(repoRoot, "tests/fixtures/fs-stream-nowait-read.ts"), { + outPath: join(outDir, "read"), + outDir, + backend: "llvm", + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const shim = join(outDir, "nowait-zero.so"); + execFileSync("clang", [ + "-shared", + "-fPIC", + join(repoRoot, "tests/fixtures/fs-stream-nowait-zero.c"), + "-o", + shim, + "-ldl", + ]); + const expected = "bytes=300000 sum=50681\n"; + const plain = spawnSync(result.binaryPath, [], { encoding: "utf8" }); + expect(plain.stdout).toBe(expected); + const shimmed = spawnSync(result.binaryPath, [], { + encoding: "utf8", + env: { ...process.env, LD_PRELOAD: shim }, + }); + expect(shimmed.status).toBe(0); + expect(shimmed.stdout).toBe(expected); + }); +});