Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stream_base,tls_wrap: notify on destruct #11947

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,12 +399,6 @@ TLSSocket.prototype._wrapHandle = function(wrap) {
res = null;
});

if (wrap) {
wrap.on('close', function() {
res.onStreamClose();
});
}

return res;
};

Expand Down
9 changes: 8 additions & 1 deletion src/stream_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,14 @@ class StreamResource {
const uv_buf_t* buf,
uv_handle_type pending,
void* ctx);
typedef void (*DestructCb)(void* ctx);

StreamResource() : bytes_read_(0) {
}
virtual ~StreamResource() = default;
virtual ~StreamResource() {
if (!destruct_cb_.is_empty())
destruct_cb_.fn(destruct_cb_.ctx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If TLSWrap is the only user of this, wouldn't it make more sense to set stream_ = nullptr in ~TLSWrap()?

(Also, that inheritance chain... TLSWrap -> StreamWrap -> StreamBase -> StreamResource, with multiple inheritance, CRTP and ad hoc function pointers thrown in for good measure. Barf!)

Copy link
Contributor Author

@trevnorris trevnorris Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis setting stream_ = nullptr in ~TLSWrap() actually isn't enough. And actually, just managed to create a new test that still segfaults. So I'm revisiting the patch.

EDIT: The failure I mentioned just happened to occur under similar circumstances, but unrelated to this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis okay. so this took me far longer than I'd have liked but I now remember why I made the change this way to begin with.

TLSWrap::stream_ points to the StreamBase* instance that's passed to TLSWrap::TLSWrap(). Each of these pointers are assigned to their own FunctionTemplate instance as an internal aligned pointer, and the lifetime of each is independent from the other. Meaning stream_ may be deleted even though the TLSWrap instance remains alive. In JS they're just attached as properties to each other.

So if the StreamBase instance is closed and released then stream_ will point to invalid memory, even though the TLSWrap instance is still alive. Which means the necessary step is to zero out the stream_ field in the TLSWrap instance, and also add stream != nullptr checks to the native methods like TLSWrap::IsAlive(), so it is no longer pointing to an invalid memory address.

To clarify (if that's possible) TLSWrap inherits from StreamBase and it holds a pointer to another StreamBase instance.

I'll update the commit message with all this information.

FYI all that class template multiple inheritance stuff going on is what lead me to this solution in c0e6c66:

#define ASSIGN_OR_RETURN_UNWRAP(ptr, obj, ...)                                \
  do {                                                                        \
    *ptr =                                                                    \
        Unwrap<typename node::remove_reference<decltype(**ptr)>::type>(obj);  \
    if (*ptr == nullptr)                                                      \
      return __VA_ARGS__;                                                     \
  } while (0)

In order to handle unwrapping in methods like template<class Base, ...> StreamBase::JSMethod() that need to convert a void* of a class instance in a template method to its parent class, that also has multiple inheritance.

}

virtual int DoShutdown(ShutdownWrap* req_wrap) = 0;
virtual int DoTryWrite(uv_buf_t** bufs, size_t* count);
Expand Down Expand Up @@ -186,15 +190,18 @@ class StreamResource {

inline void set_alloc_cb(Callback<AllocCb> c) { alloc_cb_ = c; }
inline void set_read_cb(Callback<ReadCb> c) { read_cb_ = c; }
inline void set_destruct_cb(Callback<DestructCb> c) { destruct_cb_ = c; }

inline Callback<AfterWriteCb> after_write_cb() { return after_write_cb_; }
inline Callback<AllocCb> alloc_cb() { return alloc_cb_; }
inline Callback<ReadCb> read_cb() { return read_cb_; }
inline Callback<DestructCb> destruct_cb() { return destruct_cb_; }

private:
Callback<AfterWriteCb> after_write_cb_;
Callback<AllocCb> alloc_cb_;
Callback<ReadCb> read_cb_;
Callback<DestructCb> destruct_cb_;
uint64_t bytes_read_;

friend class StreamBase;
Expand Down
16 changes: 7 additions & 9 deletions src/tls_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ TLSWrap::TLSWrap(Environment* env,
stream_->set_after_write_cb({ OnAfterWriteImpl, this });
stream_->set_alloc_cb({ OnAllocImpl, this });
stream_->set_read_cb({ OnReadImpl, this });
stream_->set_destruct_cb({ OnDestructImpl, this });

set_alloc_cb({ OnAllocSelf, this });
set_read_cb({ OnReadSelf, this });
Expand Down Expand Up @@ -687,6 +688,12 @@ void TLSWrap::OnReadImpl(ssize_t nread,
}


void TLSWrap::OnDestructImpl(void* ctx) {
TLSWrap* wrap = static_cast<TLSWrap*>(ctx);
wrap->clear_stream();
}


void TLSWrap::OnAllocSelf(size_t suggested_size, uv_buf_t* buf, void* ctx) {
buf->base = node::Malloc(suggested_size);
buf->len = suggested_size;
Expand Down Expand Up @@ -808,14 +815,6 @@ void TLSWrap::EnableSessionCallbacks(
}


void TLSWrap::OnStreamClose(const FunctionCallbackInfo<Value>& args) {
TLSWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());

wrap->stream_ = nullptr;
}


void TLSWrap::DestroySSL(const FunctionCallbackInfo<Value>& args) {
TLSWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
Expand Down Expand Up @@ -946,7 +945,6 @@ void TLSWrap::Initialize(Local<Object> target,
env->SetProtoMethod(t, "enableSessionCallbacks", EnableSessionCallbacks);
env->SetProtoMethod(t, "destroySSL", DestroySSL);
env->SetProtoMethod(t, "enableCertCb", EnableCertCb);
env->SetProtoMethod(t, "onStreamClose", OnStreamClose);

StreamBase::AddMethods<TLSWrap>(env, t, StreamBase::kFlagHasWritev);
SSLWrap<TLSWrap>::AddMethods(env, t);
Expand Down
4 changes: 3 additions & 1 deletion src/tls_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class TLSWrap : public AsyncWrap,

size_t self_size() const override { return sizeof(*this); }

void clear_stream() { stream_ = nullptr; }

protected:
static const int kClearOutChunkSize = 16384;

Expand Down Expand Up @@ -142,6 +144,7 @@ class TLSWrap : public AsyncWrap,
const uv_buf_t* buf,
uv_handle_type pending,
void* ctx);
static void OnDestructImpl(void* ctx);

void DoRead(ssize_t nread, const uv_buf_t* buf, uv_handle_type pending);

Expand All @@ -158,7 +161,6 @@ class TLSWrap : public AsyncWrap,
static void EnableCertCb(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
static void OnStreamClose(const v8::FunctionCallbackInfo<v8::Value>& args);

#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
static void GetServername(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down
42 changes: 42 additions & 0 deletions test/parallel/test-tls-retain-handle-no-abort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const common = require('../common');
const assert = require('assert');

if (!common.hasCrypto) {
common.skip('missing crypto');
return;
}
const tls = require('tls');
const fs = require('fs');
const util = require('util');

const sent = 'hello world';
const serverOptions = {
isServer: true,
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem')
};

let ssl = null;

process.on('exit', function() {
assert.ok(ssl !== null);
// If the internal pointer to stream_ isn't cleared properly then this
// will abort.
util.inspect(ssl);
});

const server = tls.createServer(serverOptions, function(s) {
s.on('data', function() { });
s.on('end', function() {
server.close();
s.destroy();
});
}).listen(0, function() {
const c = new tls.TLSSocket();
ssl = c.ssl;
c.connect(this.address().port, function() {
c.end(sent);
});
});