Skip to content

fs.openSync with UV_FS_O_FILEMAP flag returns EINVAL on Windows #27974

@Hona

Description

@Hona

What version of Bun is running?

1.3.10+30e609e08

What platform is your computer?

Microsoft Windows NT 10.0.22631.0 x64

What steps can reproduce the bug?

fs.openSync rejects the UV_FS_O_FILEMAP flag on Windows with EINVAL. Node.js supports this flag (it's a libuv constant exposed via fs.constants.UV_FS_O_FILEMAP). Bun exposes the constant but doesn't handle it.

Minimal repro — save as repro_minimal.cjs:

const fs = require("fs");
const path = require("path");
const os = require("os");

const { O_CREAT, O_TRUNC, O_WRONLY, UV_FS_O_FILEMAP } = fs.constants;
const flag = UV_FS_O_FILEMAP | O_TRUNC | O_CREAT | O_WRONLY;

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repro-"));
const file = path.join(tmp, "test.txt");

try {
  const fd = fs.openSync(file, flag, 0o666);
  fs.writeSync(fd, "hello world");
  fs.closeSync(fd);
  console.log("PASS:", fs.readFileSync(file, "utf8"));
} catch (err) {
  console.log("FAIL:", err.code, err.message);
}

fs.rmSync(tmp, { recursive: true, force: true });
$ node repro_minimal.cjs
PASS: hello world

$ bun repro_minimal.cjs
FAIL: EINVAL EINVAL: invalid argument, open 'C:\Users\...\test.txt'

Real-world impact — save as repro_tar.cjs (requires npm install tar@7):

const tar = require("tar");
const fs = require("fs");
const path = require("path");
const os = require("os");

async function main() {
  console.log("runtime:", typeof Bun !== "undefined" ? "bun" : "node");

  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repro-tar-"));
  const tgzPath = path.join(tmp, "cowsay.tgz");
  const outDir = path.join(tmp, "out");
  fs.mkdirSync(outDir);

  const resp = await fetch("https://registry.npmjs.org/cowsay/-/cowsay-1.6.0.tgz");
  fs.writeFileSync(tgzPath, Buffer.from(await resp.arrayBuffer()));

  const warnings = [];
  await tar.x({
    file: tgzPath,
    cwd: outDir,
    strip: 1,
    onwarn: (code, message) => warnings.push({ code, message: String(message).slice(0, 100) }),
  });

  const entries = fs.readdirSync(outDir);
  console.log("warnings:", warnings.length);
  if (warnings.length > 0) console.log("first:", warnings[0].code, warnings[0].message);
  console.log("extracted:", entries);
  console.log("has package.json:", entries.includes("package.json"));

  fs.rmSync(tmp, { recursive: true, force: true });
}

main().catch(console.error);
$ node repro_tar.cjs
runtime: node
warnings: 0
extracted: [ 'build', 'cli.js', 'cows', 'index.d.ts', 'index.js', 'lib', 'LICENSE', 'package.json', 'README.md' ]
has package.json: true

$ bun repro_tar.cjs
runtime: bun
warnings: 202
first: TAR_ENTRY_ERROR EINVAL: invalid argument, open '.../out/LICENSE'
extracted: [ "build", "cows", "lib" ]
has package.json: false

What is the expected behavior?

fs.openSync(path, UV_FS_O_FILEMAP | O_TRUNC | O_CREAT | O_WRONLY, 0o666) should succeed on Windows, consistent with Node.js. Bun already exposes fs.constants.UV_FS_O_FILEMAP (value 536870912 / 0x20000000), so it should be handled by the underlying open syscall.

What do you see instead?

EINVAL: invalid argument, open '...' — the flag is rejected.

Additional information

This silently breaks @npmcli/arborist (used by npm install) and any other package that uses tar@7 on Windows under Bun. tar uses UV_FS_O_FILEMAP for all files < 512KB on Windows as a performance optimization (see tar/src/get-write-flag.ts). Since tar's error handler emits a warning rather than throwing, every file write silently fails — only directories are created. The result is that node_modules appears populated but contains no actual files.

Workaround: set process.env.__FAKE_PLATFORM__ = "linux" before tar is imported, which makes it use the plain 'w' flag instead.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions