From 04fe79a79160e5632db1f9e35498f866cd4e1c18 Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Thu, 27 Aug 2026 08:55:16 -0400 Subject: [PATCH] fs: validate the callback passed to fs.rm() Every other callback based fs API runs its callback through makeCallback(), so a missing or non-function callback is reported synchronously as ERR_INVALID_ARG_TYPE. fs.rm() skipped that step, so the bad callback was not noticed until rimraf tried to invoke it, by which point the removal had already happened and the resulting "callback is not a function" TypeError was thrown from an async completion handler where it could not be caught. Refs: https://github.com/nodejs/node/issues/65578 Signed-off-by: Paul Bouchon --- lib/fs.js | 1 + test/parallel/test-fs-rm.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/fs.js b/lib/fs.js index d1823ce97298..f29009031b70 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1534,6 +1534,7 @@ function rm(path, options, callback) { const h = vfsState.handlers; if (h !== null && vfsVoid(h.rm(path, options), callback)) return; + callback = makeCallback(callback); path = getValidatedPath(path); validateRmOptions(path, options, false, (err, options) => { diff --git a/test/parallel/test-fs-rm.js b/test/parallel/test-fs-rm.js index e92bf8c07971..b39ae06703c8 100644 --- a/test/parallel/test-fs-rm.js +++ b/test/parallel/test-fs-rm.js @@ -485,6 +485,29 @@ if (isGitPresent) { }); } +// A missing callback is reported up front, the same way the other callback +// based fs APIs report it, rather than throwing from the removal itself once +// the work has already happened. +// Refs: https://github.com/nodejs/node/issues/65578 +{ + const dirname = tmpdir.resolve('rm-missing-callback'); + fs.mkdirSync(dirname, { recursive: true }); + + for (const args of [ + [dirname], + [dirname, { recursive: true }], + [dirname, { recursive: true }, 'not a function'], + ]) { + assert.throws(() => fs.rm(...args), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + + assert.ok(fs.existsSync(dirname), 'rm must not remove anything without a callback'); + fs.rmSync(dirname, { recursive: true }); +} + { // IBMi has a different access permission mechanism // This test should not be run as `root`