-
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
src: fix async hooks crashing when there is no node context #19134
Conversation
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.
LGTM modulo style nit(s), thanks!
src/env.cc
Outdated
Environment* env = Environment::GetCurrent(promise->CreationContext()); | ||
auto context = promise->CreationContext(); | ||
// if the context is undefined (not a node context) then skip | ||
if (context->GetEmbedderData(node::Environment::kContextEmbedderDataIndex)->IsUndefined()) { |
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.
I think make lint
should give you an error, because this line is longer than 80 characters?
Also, maybe look at the C++ style guide, e.g. comments should ideally be capitalized and punctuated. :)
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.
both done :)
src/env.cc
Outdated
auto context = promise->CreationContext(); | ||
auto dataIndex = node::Environment::kContextEmbedderDataIndex; | ||
// If the context is undefined (not a node context) then skip. | ||
if (context->GetEmbedderData(dataIndex)->IsUndefined()) { |
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.
@bnoordhuis is this still undefined behavior?
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.
Hm. I’m reading up on the V8 code, and it seems like a real issue here is that this might actually perform an out-of-bounds read if the context has less than dataIndex
fields…
I assume the check is left out more or less intentionally on the V8 side, so I guess the options here are:
- Add a method to
v8::Context
that tells us how many embedder data fields there are - Let Node attach some kind of property (e.g. a private symbol) on a context-specific object, like the global object or the
ExtrasBindingObject
, then use that to tell whether an Environment has been assigned to a context or not … this might be a bit slower but wouldn’t require changes to V8
/cc @nodejs/v8
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.
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.
how about HasEmbedderData(index)
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.
@xaviergonz Do you have any thoughts on this? In the end, it’s your PR, so you obviously have some say in what happens here.
In particular, wrt the second option, that should be doable without waiting for anything else to happen, and it would be nice if you could indicate whether you’d want to try it yourself or prefer for somebody else to take a stab.
(If you do: We’d be really, really glad if we can help in any way. Don’t be shy to ask questions, here or on IRC!)
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.
I'm somewhat certain that Isolate::GetCurrentContext
should give you the same context, since we run the microtask in the context of the Promise. If Object::CreationContext
and Isolate::GetCurrentContext
returns different contexts, you got a problem anyways, since Environment::GetCurrent
uses the latter.
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.
@hashseed How do we get the isolate without Isolate::GetCurrent()
or promise->GetIsolate()
? Both seem worse than what we have now…
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.
promise->GetIsolate
get's it by masking off the object address to get to the heap page, where the isolate address is stored. That's not too bad.
Isolate::GetCurrent
reads it from TLS, which can be slow.
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.
promise->GetIsolate
get's it by masking off the object address to get to the heap page, where the isolate address is stored. That's not too bad.
V8_DEPRECATE_SOON("Keep track of isolate correctly", Isolate* GetIsolate());
😢
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.
Yeah about that...
Just updated with all the comments |
Btw, the magic number is the hex number for ascii "node" :) |
So, I discussed this with @bmeurer a bit.
|
Still, even if that api is available, wouldn't we need two embedded data indexes to make sure that it is a node context? Something like:
vs
|
Also I was wondering, (about the other approach that does not rely on v8 api changing) is it ok to set the embedder index with the magic number inside the promise template itself? that way you wouldn't need to access isolate/global.process at all I think, just: if promise->getinternalfield(32 or whatever) is the magic number, we are on a node context |
To be extra sure, with a clean solution, yes. But we know that Chromium contexts only uses two slots. And assuming this does not change dramatically, we can just check the number of slots against 33 for Node.js. |
Another thing I thought about is having a cache, a map<context address, boolean> that for each EnvPromiseHook does:
pros: only need to make the expensive operation once per context, no v8 changes needed |
Contexts live on V8's heap and can change their addresses. Also, where would the map live? As a static global value? |
Yes, a static global value, so you'd be leaking 1-4 byte per context that creates a promise. Though even when it changes the address the only thing that'd happen is that there would be a cache miss and it would need to check for the magic number again (I don't know how often a context changes its address). |
I'd just wait for the change in V8 and float it to use it in this fix. I'm actually wondering whether this should be fixed in Electron instead. That's the only case where embedder data in contexts are used in two different ways. |
Would need to be changed in chromium then, since it is basically using the chromium generated context in those cases. |
Neither Chromium nor Node.js run into this issue on their own because embedder data are not designed to be used for two different embedders in the same isolate. I'd expect Electron to float a fix because it uses Node.js and Chromium contexts side by side. |
Is it possible for electron to change the embedder data of a chromium context? |
There would be a case where it would fail though now that I think about it, if two different contexts (one node and another chromium) eventually swapped places in memory and ended up being in the same address, but I don't know if that's possible. |
Ok, better idea :D on env start:
then on env promise hook
pros: no changes to v8, fast |
Updated to pull request to the latest idea. Confirmed to be working with electron. |
@addaleax any thoughts? |
didn't work, same unit test failing, so reverting |
Ok, I found the offending test:
apparently process.domain becomes undefined, since the context is actually not a node context (due to vm.runInNewContext) now, what I don't know is if this is something correct (vm.runInNewContext creates a non node context) or it is wrong and because vm.runInNewContext should actually create an environment. @addaleax @apapirovski Any thoughts? |
Found the issue, moving the setAlignedPointerInEmbedderData to AssignToContext fixed it :) |
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.
Sorry for being so absent – thanks, this definitely still looks good!
Let’s see that we can get this landed, and thanks for your patience.
Looks like the number of commits in here is making trouble for the CI … CI without rebase: https://ci.nodejs.org/job/node-test-pull-request/15851/ |
Thanks for all the hard work! Landed in fb87d8a 🎉 |
PR-URL: #19134 Fixes: #19104 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
PR-URL: #19134 Fixes: #19104 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
just wondering, in which version of node will it land? a pull request for electron depends on this one, that's why I ask 😁 |
@xaviergonz It’s on the v10.x-staging branch, so it will most likely be in the next v10.x release (that is, most likely this or next week). Since you’re particularly interested in having this in Electron, /cc @codebytere – I think Electron might be able to just cherry-pick this commit? |
@xaviergonz yes, we should be able to cherry-pick this back into our next node upgrade |
@codebytere nice! the relevant pull request for electron is electron/electron#12109 (comment) |
This commits basically skips any promise env hooks when there's no node context currently active, which would otherwise end up in a crash.
Fixes: #19104
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
src (async_hooks)