-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
tls: use after free in tls_wrap #18860
Conversation
Hi, thanks for catching this! This would work, but the original approach there was kind of hacky anyway, and if it doesn’t work I’d probably not try to accomodate it by changing the API so that it does… One alternative would be to not re-use the Diff in the folddiff --git a/src/tls_wrap.cc b/src/tls_wrap.cc
index f2a84b83f32d..8bd15fca01f4 100644
--- a/src/tls_wrap.cc
+++ b/src/tls_wrap.cc
@@ -303,10 +303,12 @@ void TLSWrap::EncOut() {
void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
- // Report back to the previous listener as well. This is only needed for the
- // "empty" writes that are passed through directly to the underlying stream.
- if (req_wrap != nullptr)
- previous_listener_->OnStreamAfterWrite(req_wrap, status);
+ if (current_empty_write_ != nullptr) {
+ WriteWrap* finishing = current_empty_write_;
+ current_empty_write_ = nullptr;
+ finishing->Done(status);
+ return;
+ }
if (ssl_ == nullptr)
status = UV_ECANCELED;
@@ -572,18 +574,17 @@ int TLSWrap::DoWrite(WriteWrap* w,
// However, if there is any data that should be written to the socket,
// the callback should not be invoked immediately
if (BIO_pending(enc_out_) == 0) {
- // We destroy the current WriteWrap* object and create a new one that
- // matches the underlying stream, rather than the TLSWrap itself.
-
- // Note: We cannot simply use w->object() because of the "optimized"
- // way in which we read persistent handles; the JS object itself might be
- // destroyed by w->Dispose(), and the Local<Object> we have is not a
- // "real" handle in the sense the V8 is aware of its existence.
- Local<Object> req_wrap_obj =
- w->GetAsyncWrap()->persistent().Get(env()->isolate());
- w->Dispose();
- w = underlying_stream()->CreateWriteWrap(req_wrap_obj);
- return stream_->DoWrite(w, bufs, count, send_handle);
+ CHECK_EQ(current_empty_write_, nullptr);
+ current_empty_write_ = w;
+ StreamWriteResult res =
+ underlying_stream()->Write(bufs, count, send_handle);
+ if (!res.async) {
+ env()->SetImmediate([](Environment* env, void* data) {
+ TLSWrap* self = static_cast<TLSWrap*>(data);
+ self->OnStreamAfterWrite(self->current_empty_write_, 0);
+ }, this, object());
+ }
+ return 0;
}
}
diff --git a/src/tls_wrap.h b/src/tls_wrap.h
index afd19c027e70..245a6d518ac5 100644
--- a/src/tls_wrap.h
+++ b/src/tls_wrap.h
@@ -152,6 +152,7 @@ class TLSWrap : public AsyncWrap,
std::vector<uv_buf_t> pending_cleartext_input_;
size_t write_size_;
WriteWrap* current_write_ = nullptr;
+ WriteWrap* current_empty_write_ = nullptr;
bool write_callback_scheduled_ = false;
bool started_;
bool established_; What do you think? (Edit: The nicenesses of this approach are that |
Thanks Anna, that fix looks a lot cleaner. I'm still reviewing the content, but in the meantime I've applied it to node-chakracore and kicked off a CI run to verify (https://ci.nodejs.org/job/chakracore-test/247/). |
0e7c314
to
75ea02d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems a little pointless to say it, but this LGTM 😉
Applied changes to this PR and started a CI: https://ci.nodejs.org/job/node-test-pull-request/13281/ The original changes are still preserved in a commit that can be squashed out. |
Looks like we have clean CIs. Thanks for the suggestion @addaleax, I wasn't feeling great about my hack, but I figured it was a good starting point. This looks a lot cleaner. |
3bef7f8
to
bf569ad
Compare
/cc @apapirovski @bnoordhuis @jasnell Could I get a couple more eyes on this change? I'd like to land it in the next day or so. |
bf569ad
to
0fb138f
Compare
0fb138f
to
b40293b
Compare
i just came across this crash today (on mac) on 9.5.0 release (and master i built this morning). is it possibly related/fixed by this?
|
@devsnek Maybe, but not necessarily. Do you have a reproduction, core dump or something of that sort? |
@addaleax it happened while i was working on code for node-fetch, and i'm not familiar enough with the source to really know whats up with it. if it helps i added a wrap with tls.connect which should be automatically choosing between http/1.1 and h2 and i haven't updated the rest of the code base for handling an http2 response stream yet so it would be treating it as regular stream and something might be being called with the wrong arity or types (thats my guess) |
I might be wrong but it doesn’t look that way to me. The stack trace suggests that HTTP/2 tries to schedule a write on a TLS socket while another write is currently under way, which shouldn’t be happening, and I haven’t seen that pop up anywhere. It’s most likely a bug that’s caused by me, so I’d be more than interested to figure out how that is happening. |
@devsnek Also, do you know whether this just started happening with 9.5.0? Or is it older than that? |
@addaleax i can go back a few versions |
9.4.0/9.5.0/9.6.0/master -> SIGABRT 9.3.0 ->
request to httpbin.org would also mean its using https.request not http2.connect.request |
To give an update here, we pretty much figured out what’s going on in IRC; it’s not related to this PR. I’d land this PR by tomorrow evening if nobody objects to that, it fixes a real bug and it would be nice to have that in |
Thanks @addaleax! I'll plan to land this tomorrow afternoon unless there are any objections. |
20f91e3
to
438dae3
Compare
One last CI before landing: https://ci.nodejs.org/job/node-test-pull-request/13353/ |
The root cause is that `req_wrap` is created in `StreamBase::Write` and passed to `TLSWrap::DoWrite`. In the TLS case the object gets disposed and replaced with a new instance, but the caller's pointer is never updated. When the `StreamBase::Write` method returns, it returns a pointer to the freed object to the caller. In some cases when the object memory has already been reused an assert is hit in `WriteWrap::SetAllocatedStorage` because the pointer is non-null. PR-URL: nodejs#18860 Refs: nodejs#18676 Reviewed-By: Anna Henningsen <anna@addaleax.net>
438dae3
to
743f890
Compare
Landed in 743f890 |
The root cause is that `req_wrap` is created in `StreamBase::Write` and passed to `TLSWrap::DoWrite`. In the TLS case the object gets disposed and replaced with a new instance, but the caller's pointer is never updated. When the `StreamBase::Write` method returns, it returns a pointer to the freed object to the caller. In some cases when the object memory has already been reused an assert is hit in `WriteWrap::SetAllocatedStorage` because the pointer is non-null. PR-URL: nodejs#18860 Refs: nodejs#18676 Reviewed-By: Anna Henningsen <anna@addaleax.net>
The root cause is that `req_wrap` is created in `StreamBase::Write` and passed to `TLSWrap::DoWrite`. In the TLS case the object gets disposed and replaced with a new instance, but the caller's pointer is never updated. When the `StreamBase::Write` method returns, it returns a pointer to the freed object to the caller. In some cases when the object memory has already been reused an assert is hit in `WriteWrap::SetAllocatedStorage` because the pointer is non-null. PR-URL: nodejs#18860 Refs: nodejs#18676 Reviewed-By: Anna Henningsen <anna@addaleax.net>
The root cause is that
req_wrap
is created inStreamBase::Write
and passed to
TLSWrap::DoWrite
. In the TLS case the object getsdisposed and replaced with a new instance, but the caller's pointer is
never updated. When the
StreamBase::Write
method returns, it returnsa pointer to the freed object to the caller. In some cases when the
object memory has already been reused an assert is hit in
WriteWrap::SetAllocatedStorage
because the pointer is non-null.Refs: #18676
This was introduced in @addaleax recent PR (#18676) and hasn't propagated to any release branches yet, so I'm going ahead and opening a PR here.
In node-chakracore this was causing a pretty consistent crash only on macOS.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
tls