Title: fs: rmSync with recursive returns without throwing while the directory still exists · Issue #67010 · nodejs/node · GitHub
Open Graph Title: fs: rmSync with recursive returns without throwing while the directory still exists · Issue #67010 · nodejs/node
X Title: fs: rmSync with recursive returns without throwing while the directory still exists · Issue #67010 · nodejs/node
Description: Version v24.22.1 and v26.12.1 (also v23.11.1 and v24.0.0). Not v22.23.4. Platform Linux 6.18.33.2-microsoft-standard-WSL2 x86_64 Subsystem fs What steps will reproduce the bug? Call fs.rmSync(dir, { recursive: true, force: true }) while ...
Open Graph Description: Version v24.22.1 and v26.12.1 (also v23.11.1 and v24.0.0). Not v22.23.4. Platform Linux 6.18.33.2-microsoft-standard-WSL2 x86_64 Subsystem fs What steps will reproduce the bug? Call fs.rmSync(dir, ...
X Description: Version v24.22.1 and v26.12.1 (also v23.11.1 and v24.0.0). Not v22.23.4. Platform Linux 6.18.33.2-microsoft-standard-WSL2 x86_64 Subsystem fs What steps will reproduce the bug? Call fs.rmSync(dir, ...
Opengraph URL: https://github.com/nodejs/node/issues/67010
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"fs: rmSync with recursive returns without throwing while the directory still exists","articleBody":"### Version\n\nv24.22.1 and v26.12.1 (also v23.11.1 and v24.0.0). Not v22.23.4.\n\n### Platform\n\n```text\nLinux 6.18.33.2-microsoft-standard-WSL2 x86_64\n```\n\n### Subsystem\n\nfs\n\n### What steps will reproduce the bug?\n\nCall `fs.rmSync(dir, { recursive: true, force: true })` while another thread keeps creating and removing one file inside `dir`.\n\nThe script below does that. A worker churns a file under `dir/busy/`, the main thread calls `rmSync` on `dir`, and then checks whether `dir` still exists once the call has returned.\n\n```sh\nnode repro.mjs 300\n```\n\n\u003cdetails>\n\u003csummary>repro.mjs\u003c/summary>\n\n```js\n// fs.rmSync(dir, { recursive: true, force: true }) returns without throwing\n// while `dir` still exists, when another thread removes an entry inside `dir`\n// during the walk.\nimport { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Worker, isMainThread, workerData } from \"node:worker_threads\";\n\nif (!isMainThread) {\n // Churn: keep creating and removing one file inside the tree being removed.\n const { lock, stop } = workerData;\n const flag = new Int32Array(stop);\n while (Atomics.load(flag, 0) === 0) {\n try {\n writeFileSync(lock, \"\");\n unlinkSync(lock);\n } catch {}\n }\n process.exit(0);\n}\n\nconst TRIALS = Number(process.argv[2] ?? 200);\nconst counts = { removed: 0, threw: 0, survivedSilently: 0 };\nlet example;\n\nfor (let i = 0; i \u003c TRIALS; i++) {\n const dir = mkdtempSync(join(tmpdir(), \"rmsync-\"));\n const busy = join(dir, \"busy\");\n mkdirSync(busy);\n for (let d = 0; d \u003c 20; d++) {\n mkdirSync(join(dir, `d${d}`));\n for (let f = 0; f \u003c 20; f++) writeFileSync(join(dir, `d${d}`, `f${f}`), \"x\");\n }\n const stop = new SharedArrayBuffer(4);\n const worker = new Worker(new URL(import.meta.url), { workerData: { lock: join(busy, \"lock\"), stop } });\n await new Promise((resolve) => worker.once(\"online\", resolve));\n\n try {\n rmSync(dir, { recursive: true, force: true });\n if (existsSync(dir)) {\n counts.survivedSilently++;\n example ??= readdirSync(dir, { recursive: true });\n } else {\n counts.removed++;\n }\n } catch (error) {\n counts.threw++;\n }\n\n Atomics.store(new Int32Array(stop), 0, 1);\n await new Promise((resolve) => worker.once(\"exit\", resolve));\n rmSync(dir, { recursive: true, force: true, maxRetries: 5 });\n}\n\nconsole.log(`node ${process.version}`, JSON.stringify(counts));\nif (example) console.log(\"left behind after rmSync returned:\", JSON.stringify(example));\n```\n\n\u003c/details>\n\n### How often does it reproduce? Is there a required condition?\n\nRoughly one call in five on this machine. Out of 300 calls, 65 on v24.22.1 and 61 on v26.12.1 returned with the directory still there. On v22.23.4 it was 0.\n\nIt needs another process or thread removing an entry inside the tree while `rmSync` walks it. Any program that removes a directory while something still writes into it can hit this. I found it through `git commit`. Since git 2.47 it leaves a detached `git maintenance` process touching `.git/objects` after the command returns, and a test that removed its fixture repo right after committing sometimes kept the repo.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nEither `dir` is gone when `rmSync` returns, or `rmSync` throws.\n\nThe docs for `force` say a missing path is ignored, and `maxRetries` lists the errors that are retried. Nothing allows a successful return with the path still there. v22 removed the rest of the tree in this situation. Its JS implementation skipped an entry that had vanished and carried on.\n\n### What do you see instead?\n\n`rmSync` returns normally and `dir` is still on disk, partly removed:\n\n```text\nnode v24.22.1 {\"removed\":196,\"threw\":39,\"survivedSilently\":65}\nleft behind after rmSync returned: [\"busy\",\"d1\",\"d10\",...,\"busy/lock\",\"d1/f0\",...]\n```\n\n### Additional information\n\n`RmSync` in `src/node_file.cc` calls `std::filesystem::remove_all` and treats `no_such_file_or_directory` as success:\n\n```cpp\nif (!error || error == std::errc::no_such_file_or_directory) {\n return;\n}\n```\n\n`remove_all` reports that error for an entry that disappeared during the walk as well as for a missing target, and it stops at that entry. Only the missing target means the path is gone. This came in with the C++ implementation in #53617, so v23 onward.\n\n[Proposed fix here\n](https://github.com/nodejs/node/pull/67011)","author":{"url":"https://github.com/arieller","@type":"Person","name":"arieller"},"datePublished":"2026-10-26T05:31:26Z","url":"https://github.com/nodejs/node/issues/67010"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:c7a41558-a6c6-90f2-d872-f33cd62ae5d2 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | B7EC:3BA6B9:C02C58:1004763:6AED3C48 |
| html-safe-nonce | 0e7335a5a412e2e521d075ab203718048cd62bf5f857ebebfe9bfa7c65454b34 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCN0VDOjNCQTZCOTpDMDJDNTg6MTAwNDc2Mzo2QUVEM0M0OCIsInZpc2l0b3JfaWQiOiI5Njg3MzkyNzI3OTUxNzYwMDgiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 384dd4a4dcc5863fa2dfbd58e6808f6343469fe878421be1421501e7c2b84cca |
| hovercard-subject-tag | issue:6033952263 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/67010/issue_layout |
| twitter:image | https://opengraph.githubassets.com/82fe86441e5e5685794f341672380c49a6f634f948111be9144284b10cbf48b6/nodejs/node/issues/67010 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/82fe86441e5e5685794f341672380c49a6f634f948111be9144284b10cbf48b6/nodejs/node/issues/67010 |
| og:image:alt | Version v24.22.1 and v26.12.1 (also v23.11.1 and v24.0.0). Not v22.23.4. Platform Linux 6.18.33.2-microsoft-standard-WSL2 x86_64 Subsystem fs What steps will reproduce the bug? Call fs.rmSync(dir, ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | arieller |
| hostname | github.com |
| expected-hostname | github.com |
| None | 26c92a9388ddfad3fa88d132b5d5f0b4747030233dcc4e5efb05f1cbae152184 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | c629ad99c254d306375fc8354d04321c2c362236 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width