Skip to content

filesystem 1.0.0: write_file/edit_file refuse every overwrite on Windows ("was replaced while it was being opened") because lstat reports dev 0n while fstat reports the volume serial #5154

Open

Description

@mv-jumbo

Which server?

filesystem (@modelcontextprotocol/server-filesystem)

Server version

1.0.0 (npm)

How did you run the server?

npx (npm package)

Transport

stdio

Protocol era

Modern (2026-07-28)

MCP client and version

Claude Desktop 2.37337.1

Operating system and runtime

Windows 11 Home, version 10.0.26200 (build 26200), x64. Node [v23.6.1]. Allowed root D:\Repositories\vault on a local [NTFS] volume.

Server configuration

"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:\Repositories\vault"
]
}

Unpinned, so npx resolved to 1.0.0. Pinning @modelcontextprotocol/server-filesystem@2026.8.31 with the same arguments makes overwrites work again.

Steps to reproduce

  1. Start the server with an allowed directory on the affected volume (D:\Repositories\vault).
  2. Call write_file with {"path": "D:\Repositories\vault\61 - Cooking\_mcp-write-test.md", "content": "test v1\n"}. This succeeds because the file is new.
  3. Immediately call write_file on the same path with "content": "test v2\n". This fails.
  4. Call edit_file on the same path with oldText: "test v1", newText: "test v2". This fails with the same error.

Expected behavior

The existing file is overwritten in place, as described for write_file and edit_file in the 1.0.0 changelog (#5022).

Actual behavior

Both tools refuse with Refusing to write: was replaced while it was being opened. The file was not replaced:

get_file_info right after the failure shows created and modified both at 10:28:10. Nothing touched the file between its creation and the failed overwrite.
The file was created by the server itself 9 seconds before the overwrite attempt. No editor, sync client or indexer had it open.

The error is a false positive from the identity check in overwriteInPlace (src/filesystem/lib.ts, added in 7dc802f / #5022). That check compares dev and ino from fs.lstat(path) with dev and ino from handle.stat().

Confirmed cause: running both stats on the same file, with no write involved, gives the same ino but a different dev:

lstat: { dev: 0n, ino: 162974011515481835n }
fstat: { dev: 3156072087n, ino: 162974011515481835n }

The path-based stat reports dev as 0. The handle-based stat reports the real volume serial. In libuv’s Windows code these come from different sources: fs__stat_path, the GetFileInformationByName fast path, takes VolumeSerialNumber.LowPart from FILE_STAT_BASIC_INFORMATION, while fs__stat_handle takes it from FileFsVolumeInformation. On this system the fast path yields 0, so opened.dev !== expected.dev is always true and no existing file can be overwritten. Whether this depends on the filesystem, Windows build or volume type is not established. Windows CI passing suggests it doesn’t happen on every setup.

Side note: the ino here (about 1.6 × 10¹⁷) is above Number.MAX_SAFE_INTEGER, so the non-bigint stat the server uses rounds it. The comparison still matches here because both sides round the same way, but { bigint: true } would make it exact.

Suggested direction, for the maintainers to judge: avoid mixing path-based and handle-based dev values. For example, take both values from handles, or treat dev of 0 from lstat on Windows as unknown and rely on ino plus isFile(). The symlink-swap protection should stay.

Logs or error output

Diagnostic (node -e, lstat vs fstat with { bigint: true } on the same file, read-only):
  lstat: { dev: 0n,          ino: 162974011515481835n }
  fstat: { dev: 3156072087n, ino: 162974011515481835n }

Already prototyped a fix?

pinning @modelcontextprotocol/server-filesystem@2026.8.31, which still uses the temp-file + rename overwrite, avoids the problem

Before you submit

  • I searched existing issues and this is not a duplicate.
  • This is not a security vulnerability report (those go through the private advisory form).

Activity

  1. charan-rathore commented on Oct 15, 2026

    @charan-rathore

    Confirmed the root cause on current main (bbb701e). overwriteInPlace lstats the path, opens it, then requires the opened handle's device to equal the lstat device. On Windows the path-based lstat can report dev 0 (its fast path omits the volume ID) while the same file seen through the opened handle reports the real device, so every overwrite of an existing file is refused as "was replaced while it was being opened" even though the inode matches and nothing was swapped.

    Fix direction that keeps the TOCTOU protection intact: treat the device as matching when opened.dev equals expected.dev, or when the platform is win32 and the lstat-reported device is 0. The isFile and inode checks stay unconditional, and any nonzero device mismatch still refuses, so symlink swaps and real cross-device replacements behave exactly as today on every platform.

    I verified the behavior matrix with mocked lstat and handle stats: the win32 dev-0 same-inode case is wrongly rejected today and passes with that one relaxation, while non-Windows dev-0, win32 inode-mismatch, and win32 nonzero-device-mismatch cases all still refuse. The full filesystem suite (345 tests), tsc, and prettier stay green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions