Repository: denoland/deno
Pull request: fix(node/fs): add missing stat path argument validation

We didn't validate the `path` argument that's passed to `fs.stat()` and `fs.statSync()` which lead to wrong errors being thrown. The `@rollup/plugin-node-resolve` code calls it with `undefined` quite a lot which lead to `nitro` and `nuxt` failing.

Fixes https://github.com/denoland/deno/issues/26700

Changed files:
filename: ext/node/polyfills/_fs/_fs_stat.ts
patch: @@ -6,6 +6,7 @@
 import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
 import { promisify } from "ext:deno_node/internal/util.mjs";
 import { primordials } from "ext:core/mod.js";
+import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs";
 
 const { ObjectCreate, ObjectAssign } = primordials;
 
@@ -379,6 +380,7 @@ export function stat(
     ? optionsOrCallback
     : { bigint: false };
 
+  path = getValidatedPath(path).toString();
   if (!callback) throw new Error("No callback function supplied");
 
   Deno.stat(path).then(
@@ -409,6 +411,8 @@ export function statSync(
   path: string | URL,
   options: statOptions = { bigint: false, throwIfNoEntry: true },
 ): Stats | BigIntStats | undefined {
+  path = getValidatedPath(path).toString();
+
   try {
     const origin = Deno.statSync(path);
     return CFISBIS(origin, options.bigint);

filename: tests/unit_node/_fs/_fs_stat_test.ts
patch: @@ -1,7 +1,7 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 import { assertCallbackErrorUncaught } from "../_test_utils.ts";
 import { BigIntStats, stat, Stats, statSync } from "node:fs";
-import { assertEquals, fail } from "@std/assert";
+import { assert, assertEquals, fail } from "@std/assert";
 
 export function assertStats(actual: Stats, expected: Deno.FileInfo) {
   assertEquals(actual.dev, expected.dev);
@@ -152,3 +152,38 @@ Deno.test({
     assertEquals(stats.isSocket(), false);
   },
 });
+
+Deno.test({
+  name: "[node/fs] stat invalid path error",
+  async fn() {
+    try {
+      await new Promise<Stats>((resolve, reject) => {
+        stat(
+          // deno-lint-ignore no-explicit-any
+          undefined as any,
+          (err, stats) => err ? reject(err) : resolve(stats),
+        );
+      });
+      fail();
+    } catch (err) {
+      assert(err instanceof TypeError);
+      // deno-lint-ignore no-explicit-any
+      assertEquals((err as any).code, "ERR_INVALID_ARG_TYPE");
+    }
+  },
+});
+
+Deno.test({
+  name: "[node/fs] statSync invalid path error",
+  fn() {
+    try {
+      // deno-lint-ignore no-explicit-any
+      statSync(undefined as any);
+      fail();
+    } catch (err) {
+      assert(err instanceof TypeError);
+      // deno-lint-ignore no-explicit-any
+      assertEquals((err as any).code, "ERR_INVALID_ARG_TYPE");
+    }
+  },
+});

filename: tests/unit_node/fs_test.ts
patch: @@ -3,6 +3,7 @@
 /// <reference lib="deno.ns" />
 import { assert, assertEquals, assertRejects, assertThrows } from "@std/assert";
 import { join } from "node:path";
+import { fileURLToPath } from "node:url";
 import { tmpdir } from "node:os";
 import {
   closeSync,
@@ -160,7 +161,9 @@ Deno.test(
     } catch (error: unknown) {
       assertEquals(
         `${error}`,
-        `Error: ENOENT: no such file or directory, stat '${fileUrl.pathname}'`,
+        `Error: ENOENT: no such file or directory, stat '${
+          fileURLToPath(fileUrl)
+        }'`,
       );
     }
   },