Skip to content

Commit

Permalink
fs: clean up Dir.read() uv_fs_t data before calling into JS
Browse files Browse the repository at this point in the history
A call into JS can schedule another operation on the same `uv_dir_t`.
In particular, when the handle is closed from the callback for a
directory read operation, there previously was a race condition window:

1. A `dir.read()` operation is submitted to libuv
2. The read operation is finished by libuv, calling `AfterDirRead()`
3. We call into JS
4. JS calls dir.close()
5. libuv posts the close request to a thread in the pool
6. The close request runs, destroying the directory handle
7. `AfterDirRead()` is being exited.

Exiting the `FSReqAfterScope` in step 7 attempts to destroy the original
uv_fs_t` from step 1, which now points to an `uv_dir_t` that has
already been destroyed in step 5.

By forcing the `FSReqAfterScope` to clean up before we call into JS,
we can be sure that no other operations on the same `uv_dir_t` are
submitted concurrently.

This addresses issues observed when running with ASAN/valgrind.

PR-URL: #33274
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
  • Loading branch information
addaleax authored and codebytere committed Jun 9, 2020
1 parent d2897a2 commit aa4611c
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 11 deletions.
10 changes: 7 additions & 3 deletions src/node_dir.cc
Expand Up @@ -197,8 +197,8 @@ static MaybeLocal<Array> DirentListToArray(
}

static void AfterDirRead(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);
BaseObjectPtr<FSReqBase> req_wrap { FSReqBase::from_req(req) };
FSReqAfterScope after(req_wrap.get(), req);

if (!after.Proceed()) {
return;
Expand All @@ -210,12 +210,12 @@ static void AfterDirRead(uv_fs_t* req) {
if (req->result == 0) {
// Done
Local<Value> done = Null(isolate);
after.Clear();
req_wrap->Resolve(done);
return;
}

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

Local<Value> error;
Local<Array> js_array;
Expand All @@ -224,9 +224,13 @@ static void AfterDirRead(uv_fs_t* req) {
req->result,
req_wrap->encoding(),
&error).ToLocal(&js_array)) {
// Clear libuv resources *before* delivering results to JS land because
// that can schedule another operation on the same uv_dir_t. Ditto below.
after.Clear();
return req_wrap->Reject(error);
}

after.Clear();
req_wrap->Resolve(js_array);
}

Expand Down
25 changes: 18 additions & 7 deletions src/node_file.cc
Expand Up @@ -540,8 +540,15 @@ FSReqAfterScope::FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req)
}

FSReqAfterScope::~FSReqAfterScope() {
Clear();
}

void FSReqAfterScope::Clear() {
if (!wrap_) return;

uv_fs_req_cleanup(wrap_->req());
delete wrap_;
wrap_->Detach();
wrap_.reset();
}

// TODO(joyeecheung): create a normal context object, and
Expand All @@ -554,12 +561,16 @@ FSReqAfterScope::~FSReqAfterScope() {
// which is also why the errors should have been constructed
// in JS for more flexibility.
void FSReqAfterScope::Reject(uv_fs_t* req) {
wrap_->Reject(UVException(wrap_->env()->isolate(),
req->result,
wrap_->syscall(),
nullptr,
req->path,
wrap_->data()));
BaseObjectPtr<FSReqBase> wrap { wrap_ };
Local<Value> exception =
UVException(wrap_->env()->isolate(),
req->result,
wrap_->syscall(),
nullptr,
req->path,
wrap_->data());
Clear();
wrap->Reject(exception);
}

bool FSReqAfterScope::Proceed() {
Expand Down
3 changes: 2 additions & 1 deletion src/node_file.h
Expand Up @@ -157,6 +157,7 @@ class FSReqAfterScope final {
public:
FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req);
~FSReqAfterScope();
void Clear();

bool Proceed();

Expand All @@ -168,7 +169,7 @@ class FSReqAfterScope final {
FSReqAfterScope& operator=(const FSReqAfterScope&&) = delete;

private:
FSReqBase* wrap_ = nullptr;
BaseObjectPtr<FSReqBase> wrap_;
uv_fs_t* req_ = nullptr;
v8::HandleScope handle_scope_;
v8::Context::Scope context_scope_;
Expand Down

0 comments on commit aa4611c

Please sign in to comment.