Skip to content

Commit e7121be

Browse files
marcopiracciniaduh95
authored andcommitted
fs: stop recursive mkdir from retrying ENOENT forever
When mkdir() fails with ENOENT, the recursive algorithm assumes the parent is missing, creates it and retries. On procfs mkdir() keeps returning ENOENT although the parent exists, and under a concurrent rmdir() the parent can disappear again, so the retry loop never terminates: `fs.mkdirSync('/proc/x', { recursive: true })` spins at 100% CPU. Retry a path that failed with ENOENT only once and report ENOENT the second time, like the non-recursive call does. Fixes: #66268 Signed-off-by: marcopiraccini <marco.piraccini@gmail.com> PR-URL: #66340 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent 510dfd8 commit e7121be

5 files changed

Lines changed: 68 additions & 0 deletions

File tree

‎src/node_file-inl.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "node_file.h"
77
#include "req_wrap-inl.h"
88

9+
#include <algorithm>
10+
911
namespace node {
1012
namespace fs {
1113

@@ -27,6 +29,15 @@ void FSContinuationData::MaybeSetFirstPath(const std::string& path) {
2729
}
2830
}
2931

32+
bool FSContinuationData::ShouldRetryENOENT(const std::string& path) {
33+
if (std::find(enoent_paths_.begin(), enoent_paths_.end(), path) !=
34+
enoent_paths_.end()) {
35+
return false;
36+
}
37+
enoent_paths_.push_back(path);
38+
return true;
39+
}
40+
3041
std::string FSContinuationData::PopPath() {
3142
CHECK(!paths_.empty());
3243
std::string path = std::move(paths_.back());

‎src/node_file.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ typedef void (*uv_fs_callback_t)(uv_fs_t*);
219219

220220
void FSContinuationData::MemoryInfo(MemoryTracker* tracker) const {
221221
tracker->TrackField("paths", paths_);
222+
tracker->TrackField("enoent_paths", enoent_paths_);
222223
}
223224

224225
FileHandleReadWrap::~FileHandleReadWrap() = default;
@@ -1966,6 +1967,9 @@ int MKDirpSync(uv_loop_t* loop,
19661967
std::string dirname =
19671968
next_path.substr(0, next_path.find_last_of(kPathSeparator));
19681969
if (dirname != next_path) {
1970+
if (!req_wrap->continuation_data()->ShouldRetryENOENT(next_path)) {
1971+
return err;
1972+
}
19691973
req_wrap->continuation_data()->PushPath(std::move(next_path));
19701974
req_wrap->continuation_data()->PushPath(std::move(dirname));
19711975
} else if (req_wrap->continuation_data()->paths().empty()) {
@@ -2047,6 +2051,10 @@ int MKDirpAsync(
20472051
std::string dirname =
20482052
path.substr(0, path.find_last_of(kPathSeparator));
20492053
if (dirname != path) {
2054+
if (!req_wrap->continuation_data()->ShouldRetryENOENT(path)) {
2055+
req_wrap->continuation_data()->Done(err);
2056+
break;
2057+
}
20502058
req_wrap->continuation_data()->PushPath(path);
20512059
req_wrap->continuation_data()->PushPath(std::move(dirname));
20522060
} else if (req_wrap->continuation_data()->paths().empty()) {

‎src/node_file.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ class FSContinuationData : public MemoryRetainer {
114114
inline std::string PopPath();
115115
// Used by mkdirp to track the first path created:
116116
inline void MaybeSetFirstPath(const std::string& path);
117+
// Used by mkdirp to retry a path that failed with ENOENT only once: its
118+
// parent has been created or checked by then, so retrying again cannot help.
119+
inline bool ShouldRetryENOENT(const std::string& path);
117120
inline void Done(int result);
118121

119122
int mode() const { return mode_; }
@@ -129,6 +132,7 @@ class FSContinuationData : public MemoryRetainer {
129132
uv_fs_t* req_;
130133
int mode_;
131134
std::vector<std::string> paths_;
135+
std::vector<std::string> enoent_paths_;
132136
std::string first_path_;
133137
};
134138

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
// Refs: https://github.com/nodejs/node/issues/66268
3+
// On procfs, mkdir fails with ENOENT although the parent exists. A recursive
4+
// mkdir must report the error instead of retrying forever.
5+
const common = require('../common');
6+
const fs = require('fs');
7+
8+
if (!common.isLinux) common.skip('procfs is Linux only');
9+
10+
const assert = require('assert');
11+
const dir = `/proc/node-test-${process.pid}`;
12+
const expected = { code: 'ENOENT', syscall: 'mkdir' };
13+
14+
assert.throws(() => fs.mkdirSync(dir, { recursive: true }), expected);
15+
16+
fs.mkdir(dir, { recursive: true }, common.mustCall((err) => {
17+
assert.strictEqual(err.code, expected.code);
18+
assert.strictEqual(err.syscall, expected.syscall);
19+
}));
20+
21+
assert.rejects(fs.promises.mkdir(dir, { recursive: true }), expected)
22+
.then(common.mustCall());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
// This tests heap snapshot integration of a pending recursive mkdir.
4+
5+
const common = require('../common');
6+
const tmpdir = require('../common/tmpdir');
7+
const { validateByRetainingPath } = require('../common/heap');
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
11+
tmpdir.refresh();
12+
13+
{
14+
const nodes = validateByRetainingPath('Node / FSContinuationData', []);
15+
assert.strictEqual(nodes.length, 0);
16+
}
17+
18+
fs.mkdir(tmpdir.resolve('a', 'b'), { recursive: true }, common.mustSucceed());
19+
20+
{
21+
const nodes = validateByRetainingPath('Node / FSContinuationData', []);
22+
assert.strictEqual(nodes.length, 1);
23+
}

0 commit comments

Comments
 (0)