Version
v26.10.0 (also present in v26.8.0 through v26.10.0 and v24.21.0; not present in v26.7.0 or v22.23.3)
Platform
Linux archpc 7.2.4-arch1-2 #1 SMP PREEMPT_DYNAMIC Tue, 08 Sep 2026 10:22:31 +0000 x86_64 GNU/Linux
Arch Linux, nodejs 26.10.0-1, glibc 2.44
Subsystem
fs
What steps will reproduce the bug?
#!/usr/bin/env bash
# Producer: 24 short writes (~96 KiB, each read back as a short read), then one large write.
node -e '
const chunk = "x".repeat(4000);
let i = 0;
const t = setInterval(() => {
process.stdout.write(chunk);
if (++i === 24) { clearInterval(t); process.stdout.write("y".repeat(400000)); }
}, 5);
' | node -e 'console.log(require("fs").readFileSync(0, "utf8").length)'
echo "exit=${PIPESTATUS[1]}"
How often does it reproduce? Is there a required condition?
10 of 10 runs with the script above. The input must be a pipe (or FIFO, or /dev/stdin backed by one) that delivers more than 64 KiB in reads shorter than 8 KiB before the first full 8 KiB read. Regular files are not affected. The encoding does not matter: the input above is pure ASCII.
Real-world trigger: some-test-runner | node -e 'require("fs").readFileSync(0, "utf8")...', where the producer writes in small chunks. It crashed at 7 of 60 runs with a ~420 KB input written in 4093-byte chunks.
What is the expected behavior? Why is that the expected behavior?
Prints 496000 and exits 0, as v26.7.0 does and as readFileSync(0).toString("utf8") does on v26.10.0.
What do you see instead?
malloc(): invalid size (unsorted)
exit=134
SIGABRT from glibc. Symbolized stack (from a real crash, via debuginfod.archlinux.org):
#6 malloc_printerr (str="malloc(): invalid size (unsorted)") at malloc.c:5093
#7 _int_malloc (av=<main_arena>, bytes=427486) at malloc.c:3683
#10 __GI___libc_realloc (oldmem=0x0, bytes=427486) at malloc.c:3198
#11 node::UncheckedRealloc<char16_t> (pointer=0x0, n=<optimized out>) at ../../src/util-inl.h:267
#13 node::MaybeStackBuffer<unsigned short, 256ul>::AllocateSufficientStorage (storage=213743) at ../../src/util-inl.h:544
#14 node::StringBytes::Encode (...) at ../../src/string_bytes.cc:659
#15 node::fs::ReadFileUtf8 (args=...) at ../../src/node_file.cc:3559
The abort happens later than the corruption. The corruption itself is a heap buffer overflow in ReadFileUtf8.
Additional information
Cause, in ReadFileUtf8 (src/node_file.cc#L3511-L3523 at v26.10.0):
- While
big == nullptr, every short read (r < 8192) is appended to result and the loop continues. On a pipe, result can grow past 64 KiB this way.
- On the first full 8 KiB read, the code allocates
big_cap = kMinChunk (64 KiB) and runs memcpy(big, result.data(), result.size()) with no check that result.size() <= big_cap. That write overflows the heap buffer.
- After that,
big_len > big_cap, so big_len == big_cap never matches, and big_cap - big_len in the next uv_buf_init underflows.
A possible fix: size the first allocation from what is already buffered, for example big_cap = std::max(kMinChunk, result.size() + sizeof(buffer)), and make the growth check big_len >= big_cap.
Workaround: fs.readFileSync(0).toString("utf8") avoids the fast path.
Version
v26.10.0 (also present in v26.8.0 through v26.10.0 and v24.21.0; not present in v26.7.0 or v22.23.3)
Platform
Subsystem
fs
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
10 of 10 runs with the script above. The input must be a pipe (or FIFO, or
/dev/stdinbacked by one) that delivers more than 64 KiB in reads shorter than 8 KiB before the first full 8 KiB read. Regular files are not affected. The encoding does not matter: the input above is pure ASCII.Real-world trigger:
some-test-runner | node -e 'require("fs").readFileSync(0, "utf8")...', where the producer writes in small chunks. It crashed at 7 of 60 runs with a ~420 KB input written in 4093-byte chunks.What is the expected behavior? Why is that the expected behavior?
Prints
496000and exits 0, as v26.7.0 does and asreadFileSync(0).toString("utf8")does on v26.10.0.What do you see instead?
SIGABRT from glibc. Symbolized stack (from a real crash, via debuginfod.archlinux.org):
The abort happens later than the corruption. The corruption itself is a heap buffer overflow in
ReadFileUtf8.Additional information
Cause, in
ReadFileUtf8(src/node_file.cc#L3511-L3523 at v26.10.0):big == nullptr, every short read (r < 8192) is appended toresultand the loopcontinues. On a pipe,resultcan grow past 64 KiB this way.big_cap = kMinChunk(64 KiB) and runsmemcpy(big, result.data(), result.size())with no check thatresult.size() <= big_cap. That write overflows the heap buffer.big_len > big_cap, sobig_len == big_capnever matches, andbig_cap - big_lenin the nextuv_buf_initunderflows.A possible fix: size the first allocation from what is already buffered, for example
big_cap = std::max(kMinChunk, result.size() + sizeof(buffer)), and make the growth checkbig_len >= big_cap.Workaround:
fs.readFileSync(0).toString("utf8")avoids the fast path.