Skip to content

fix: clamp negative truncate length to zero (uninitialized-memory leak)#1261

Merged
streamich merged 1 commit into
streamich:masterfrom
chatman-media:fix/truncate-negative-length-memory-leak
Jun 20, 2026
Merged

fix: clamp negative truncate length to zero (uninitialized-memory leak)#1261
streamich merged 1 commit into
streamich:masterfrom
chatman-media:fix/truncate-negative-length-memory-leak

Conversation

@chatman-media

Copy link
Copy Markdown
Contributor

Problem

Node.truncate(len) (in fs-core) only special-cases len === 0. A negative length falls through to the if (len <= this.size) branch and sets this.size to the negative value:

truncate(len: number = 0) {
  if (!len) { /* empty file */ return; }   // only len === 0
  if (len <= this.size) this.size = len;    // -1 <= 5  →  size = -1
  ...
}

getBuffer() then does this.buf.subarray(0, this.size). With this.size === -1, subarray treats the end index relative to the buffer's length and returns bytes past the used region — i.e. uninitialized memory from the bufferAllocUnsafe backing buffer.

Reproduction

import { Volume } from 'memfs';

const vol = new Volume();
vol.writeFileSync('/a', 'hello');
vol.truncateSync('/a', -1);

vol.readFileSync('/a').length;            // 63  (!!)  — expected 0
vol.readFileSync('/a').toString('hex');   // 68656c6c6f<uninitialized heap bytes…>

Node.js for comparison:

fs.truncateSync(file, -1);   // file is now 0 bytes
fs.ftruncateSync(fd, -100);  // 0 bytes

Node's fs.truncate / fs.ftruncate clamp a negative length to an empty file (the C++ layer floors negative offsets at 0). memfs instead produced a larger file leaking adjacent heap contents.

Fix

Treat a negative len the same as 0 in Node.truncate — change the len === 0 guard to len <= 0. One-line behavioral change; len === 0 is unaffected.

Tests

Regression tests added at two levels, both verified to fail before the fix and pass after:

  • fs-core unit: Node#truncate(-1) → size 0, empty buffer.
  • fs-node public API: vol.truncateSync('/neg', -1)readFileSync(...).length === 0 (guards against the leak).

Full fs-core + fs-node suites pass (817 tests), typecheck and prettier:check clean.

Context

While investigating #942 (close-after-unlink — already resolved by the Superblock refactor), differential testing against Node's real fs surfaced this separate negative-length truncate parity gap, which has the added concern of exposing uninitialized memory.

`Node.truncate(len)` only special-cased `len === 0`; a negative length
fell through to `len <= this.size` and set `this.size` to the negative
value. `getBuffer()` then calls `buf.subarray(0, this.size)`, which
interprets the negative end relative to the buffer length and returns
bytes past the used region — leaking uninitialized memory from the
`bufferAllocUnsafe` backing buffer.

For example, after `writeFileSync('/a', 'hello')`, calling
`truncateSync('/a', -1)` produced a 63-byte file containing "hello"
followed by uninitialized heap bytes. Node.js `fs.truncate` /
`fs.ftruncate` clamp negative lengths to an empty file, so do the same.

Adds regression tests at the `Node` and `Volume` (public `truncateSync`)
levels.

@streamich streamich left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@streamich streamich merged commit b5c6c62 into streamich:master Jun 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants