Skip to content

Commit 84f74ce

Browse files
fs: do not swallow exceptions thrown by completion callbacks
Since 93644d5 the TryCatch guarding StringBytes::Encode in the completion callbacks of mkdtemp, realpath.native, readlink, recursive mkdir, recursive readdir and dir.read is still active when the JS callback runs. Exceptions thrown by the callback, or by the nextTick queue drained after it, are caught by it and never reported. Leave the TryCatch before calling into JS. Fixes: #65667 Refs: #57706 Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
1 parent 66f26d3 commit 84f74ce

4 files changed

Lines changed: 107 additions & 57 deletions

File tree

‎src/node_dir.cc‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,14 @@ static void AfterDirRead(uv_fs_t* req) {
248248

249249
uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr);
250250

251-
TryCatch try_catch(isolate);
252-
Local<Array> js_array;
253-
if (!DirentListToArray(env,
254-
dir->dirents,
255-
static_cast<int>(req->result),
256-
req_wrap->encoding())
257-
.ToLocal(&js_array)) {
251+
ResolveOrReject(req_wrap.get(), [&]() {
252+
MaybeLocal<Array> js_array = DirentListToArray(
253+
env, dir->dirents, static_cast<int>(req->result), req_wrap->encoding());
258254
// Clear libuv resources *before* delivering results to JS land because
259-
// that can schedule another operation on the same uv_dir_t. Ditto below.
255+
// that can schedule another operation on the same uv_dir_t.
260256
after.Clear();
261-
CHECK(try_catch.CanContinue());
262-
return req_wrap->Reject(try_catch.Exception());
263-
}
264-
265-
after.Clear();
266-
req_wrap->Resolve(js_array);
257+
return js_array;
258+
});
267259
}
268260

269261

‎src/node_file-inl.h‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,29 @@ int SyncCallAndThrowOnError(Environment* env,
413413
return SyncCallAndThrowIf(is_uv_error, env, req_wrap, fn, args...);
414414
}
415415

416+
// Delivers the value produced by `produce`, or the exception it threw, to
417+
// `req_wrap`. The TryCatch is left before calling into JS: exceptions thrown
418+
// by the callback, or by the tick queue drained afterwards, must not be caught
419+
// by it.
420+
template <typename Fn>
421+
void ResolveOrReject(FSReqBase* req_wrap, Fn&& produce) {
422+
v8::Isolate* isolate = req_wrap->env()->isolate();
423+
v8::Local<v8::Value> value;
424+
v8::Local<v8::Value> error;
425+
{
426+
v8::TryCatch try_catch(isolate);
427+
if (!produce().ToLocal(&value)) {
428+
CHECK(try_catch.CanContinue());
429+
error = try_catch.Exception();
430+
}
431+
}
432+
if (error.IsEmpty()) {
433+
req_wrap->Resolve(value);
434+
} else {
435+
req_wrap->Reject(error);
436+
}
437+
}
438+
416439
} // namespace fs
417440
} // namespace node
418441

‎src/node_file.cc‎

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -900,16 +900,10 @@ void AfterMkdirp(uv_fs_t* req) {
900900
std::string first_path(req_wrap->continuation_data()->first_path());
901901
if (first_path.empty())
902902
return req_wrap->Resolve(Undefined(req_wrap->env()->isolate()));
903-
Local<Value> path;
904-
TryCatch try_catch(req_wrap->env()->isolate());
905-
if (!StringBytes::Encode(req_wrap->env()->isolate(),
906-
first_path.c_str(),
907-
req_wrap->encoding())
908-
.ToLocal(&path)) {
909-
CHECK(try_catch.CanContinue());
910-
return req_wrap->Reject(try_catch.Exception());
911-
}
912-
return req_wrap->Resolve(path);
903+
ResolveOrReject(req_wrap, [&]() {
904+
return StringBytes::Encode(
905+
req_wrap->env()->isolate(), first_path.c_str(), req_wrap->encoding());
906+
});
913907
}
914908
}
915909

@@ -918,19 +912,11 @@ void AfterStringPath(uv_fs_t* req) {
918912
FSReqAfterScope after(req_wrap, req);
919913
FS_ASYNC_TRACE_END1(
920914
req->fs_type, req_wrap, "result", static_cast<int>(req->result))
921-
MaybeLocal<Value> link;
922-
923915
if (after.Proceed()) {
924-
TryCatch try_catch(req_wrap->env()->isolate());
925-
link = StringBytes::Encode(
926-
req_wrap->env()->isolate(), req->path, req_wrap->encoding());
927-
if (link.IsEmpty()) {
928-
CHECK(try_catch.CanContinue());
929-
req_wrap->Reject(try_catch.Exception());
930-
} else {
931-
Local<Value> val;
932-
if (link.ToLocal(&val)) req_wrap->Resolve(val);
933-
}
916+
ResolveOrReject(req_wrap, [&]() {
917+
return StringBytes::Encode(
918+
req_wrap->env()->isolate(), req->path, req_wrap->encoding());
919+
});
934920
}
935921
}
936922

@@ -939,20 +925,12 @@ void AfterStringPtr(uv_fs_t* req) {
939925
FSReqAfterScope after(req_wrap, req);
940926
FS_ASYNC_TRACE_END1(
941927
req->fs_type, req_wrap, "result", static_cast<int>(req->result))
942-
MaybeLocal<Value> link;
943-
944928
if (after.Proceed()) {
945-
TryCatch try_catch(req_wrap->env()->isolate());
946-
link = StringBytes::Encode(req_wrap->env()->isolate(),
947-
static_cast<const char*>(req->ptr),
948-
req_wrap->encoding());
949-
if (link.IsEmpty()) {
950-
CHECK(try_catch.CanContinue());
951-
req_wrap->Reject(try_catch.Exception());
952-
} else {
953-
Local<Value> val;
954-
if (link.ToLocal(&val)) req_wrap->Resolve(val);
955-
}
929+
ResolveOrReject(req_wrap, [&]() {
930+
return StringBytes::Encode(req_wrap->env()->isolate(),
931+
static_cast<const char*>(req->ptr),
932+
req_wrap->encoding());
933+
});
956934
}
957935
}
958936

@@ -2700,14 +2678,9 @@ class ReadDirRecursiveRequest {
27002678
walk_.error_path().c_str()));
27012679
}
27022680

2703-
Local<Value> value;
2704-
TryCatch try_catch(isolate);
2705-
if (!MarshalRecursiveReadDir(isolate, walk_, encoding_, with_types_)
2706-
.ToLocal(&value)) {
2707-
CHECK(try_catch.CanContinue());
2708-
return req_wrap->Reject(try_catch.Exception());
2709-
}
2710-
req_wrap->Resolve(value);
2681+
ResolveOrReject(req_wrap.get(), [&]() {
2682+
return MarshalRecursiveReadDir(isolate, walk_, encoding_, with_types_);
2683+
});
27112684
}
27122685

27132686
private:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
// Refs: https://github.com/nodejs/node/issues/65667
3+
// Exceptions thrown after an fs operation completes must reach
4+
// 'uncaughtException' instead of being swallowed.
5+
const common = require('../common');
6+
const tmpdir = require('../common/tmpdir');
7+
const assert = require('assert');
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
tmpdir.refresh();
12+
const dir = tmpdir.path;
13+
const file = path.join(dir, 'file');
14+
const link = path.join(dir, 'link');
15+
fs.writeFileSync(file, '');
16+
17+
// The callback throws.
18+
const callbackCases = {
19+
'mkdtemp': (cb) => fs.mkdtemp(path.join(dir, 'x-'), cb),
20+
'realpath.native': (cb) => fs.realpath.native(dir, cb),
21+
'mkdir recursive': (cb) => fs.mkdir(path.join(dir, 'a', 'b'), { recursive: true }, cb),
22+
'readdir recursive': (cb) => fs.readdir(dir, { recursive: true }, cb),
23+
};
24+
if (common.canCreateSymLink()) {
25+
fs.symlinkSync(file, link);
26+
callbackCases.readlink = (cb) => fs.readlink(link, cb);
27+
}
28+
29+
// A nextTick callback scheduled after the promise settles throws.
30+
const promiseCases = {
31+
'promises.mkdtemp': () => fs.promises.mkdtemp(path.join(dir, 'p-')),
32+
'dir.read': async () => {
33+
const d = await fs.promises.opendir(dir);
34+
await d.read();
35+
process.nextTick(() => d.closeSync());
36+
},
37+
};
38+
39+
const cases = [
40+
...Object.entries(callbackCases).map(([name, run]) => [name, () => {
41+
run(common.mustSucceed(() => { throw new Error(name); }));
42+
}]),
43+
...Object.entries(promiseCases).map(([name, run]) => [name, async () => {
44+
await run();
45+
process.nextTick(() => { throw new Error(name); });
46+
}]),
47+
];
48+
49+
let current;
50+
process.on('uncaughtException', common.mustCall((err) => {
51+
assert.strictEqual(err.message, current);
52+
next();
53+
}, cases.length));
54+
55+
function next() {
56+
const entry = cases.shift();
57+
if (entry === undefined) return;
58+
current = entry[0];
59+
entry[1]();
60+
}
61+
62+
next();

0 commit comments

Comments
 (0)