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

fix: use Node.js isolate setup logic in bindings #24579

Merged
merged 4 commits into from Jul 20, 2020

Conversation

codebytere
Copy link
Member

@codebytere codebytere commented Jul 16, 2020

Description of Change

Closes #24577.

This fixes an issue whereby any code in Node.js that invokes various assert functionality in the browser or renderer processes will throw with TypeError: call.getFileName is not a function. The reason this happens is that Node.js overrides the Promise rejection callback used by the Isolate, so the WeakMap used in their assert logic is never populated.

To remedy this, the best long-term solution, I feel, is to better align our isolate setup functionality with that available to us via the embedder-focused SetIsolateUpForNode. We already approximated a lot of this functionality, so this allows us to remove that. It does, however create a small new issue wherein we do not want to use the promise rejection callback that Node.js uses in the renderer process, because it does not send PromiseRejectionEvents to the global script context. This causes code like:

window.addEventListener('unhandledrejection', function (event) {
  // ...your code here to handle the unhandled rejection...

  // Prevent the default handling (such as outputting the
  // error to the console)

  event.preventDefault();
});

to fail since the 'unhandledrejection' rejection event would never be fired. We need thus to use the one Blink already provides, and to augment IsolateSettings accordingly.

I've upstreamed this at nodejs/node#34387.

cc @nornagon @zcbenz @MarshallOfSound

Checklist

Release Notes

Notes: Fixed an issue where many uses of the Node.js assert module would throw in both the browser and renderer processes.

@codebytere codebytere requested a review from a team as a code owner July 16, 2020 02:00
@electron-cation electron-cation bot added the new-pr 🌱 PR opened in the last 24 hours label Jul 16, 2020
@codebytere codebytere changed the title fix: use Node.js isolate setup logic in bindings [wip] fix: use Node.js isolate setup logic in bindings Jul 16, 2020
@codebytere codebytere force-pushed the use-node-setup-logic-in-bindings branch from c5518bc to cebd4b6 Compare July 16, 2020 02:56
@codebytere codebytere changed the title [wip] fix: use Node.js isolate setup logic in bindings fix: use Node.js isolate setup logic in bindings Jul 16, 2020
@deepak1556
Copy link
Member

Couple of question,

  • The reason this happens is that Node.js overrides the Promise rejection callback used by the Isolate,

AFAIK in the browser process we setup the callbacks in https://github.com/electron/electron/blob/master/patches/node/feat_add_flags_for_low-level_hooks_and_exceptions.patch as g_standalone_mode and in the renderer blink does it. Where does node override here ? Maybe the problem/solution statement is a bit confusing, can you please elaborate whats happening ?

@codebytere
Copy link
Member Author

codebytere commented Jul 16, 2020

@deepak1556 you're totally right - we can remove a lot of that low-level flag logic now, as it's just being redundantly set/called twice 😅

I've also moved the flags into process-specific locations. With regard to:

AFAIK in the browser process we setup the callbacks in https://github.com/electron/electron/blob/master/patches/node/feat_add_flags_for_low-level_hooks_and_exceptions.patch as g_standalone_mode and in the renderer blink does it

I removed the invocation in the patch, so it's now just disabled for the renderer process where Blink does it.

We setup our own fatal error handlers for all the process in https://github.com/electron/electron/blob/master/shell/common/api/electron_bindings.cc#L89 , now will that be overriden by the handler from SetIsolateUpForNode ?

Yes - we can remove this now as the embedder interface handles it.

@MarshallOfSound
Copy link
Member

I think we should avoid letting node control blinks isolate, can't we fix the assert module to work without nodes hooks? Isolate wide modifications are bad in the renderer as not every context in the given isolate are actually node contexts. So this can have unexpected effects / context leakage in the renderer process.

@codebytere
Copy link
Member Author

codebytere commented Jul 16, 2020

tl;dr the comparable surface area of difference is small enough, and the net benefit great enough of using embedding functionality provided to us, that I think choosing to leverage this functionality is a better choice than to call the vast majority of it all ourselves in the exact same way Node.js would :)

A more in-depth breakdown:

The embedder interface this change leverages, SetIsolateUpForNode, calls 8 discrete pieces of isolate functionality:

isolate->AddMessageListenerWithErrorLevel( errors::PerIsolateMessageListenerg);
isolate->SetAbortOnUncaughtExceptionCallback(abort_callback);
isolate->SetFatalErrorHandler(fatal_error_cb);
isolate->SetPrepareStackTraceCallback(prepare_stack_trace_cb);
isolate->SetMicrotasksPolicy(s.policy);
isolate->SetAllowWasmCodeGenerationCallback(allow_wasm_codegen_cb);
v8::CpuProfiler::UseDetailedSourcePositionsForProfiling(isolate);

Renderer Isolate Setup Prior to Change

 isolate->SetMicrotasksPolicy(s.policy);
 isolate->SetAllowWasmCodeGenerationCallback(allow_wasm_codegen_cb);
 v8::CpuProfiler::UseDetailedSourcePositionsForProfiling(isolate);
 isolate->AddMessageListenerWithErrorLevel(errors::PerIsolateMessageListeners);

Renderer Isolate Setup After Change

isolate->SetMicrotasksPolicy(s.policy);
isolate->SetAllowWasmCodeGenerationCallback(allow_wasm_codegen_cb);
v8::CpuProfiler::UseDetailedSourcePositionsForProfiling(isolate);
isolate->AddMessageListenerWithErrorLevel( errors::PerIsolateMessageListeners);
isolate->SetAbortOnUncaughtExceptionCallback(abort_callback);
isolate->SetFatalErrorHandler(fatal_error_cb);

That leaves the only difference consequent to this PR in the Node.js-Blink isolate relationship as:


isolate->SetAbortOnUncaughtExceptionCallback(abort_callback);
isolate->SetFatalErrorHandler(fatal_error_cb);



We choose to call isolate->SetFatalErrorHandler(fatal_error_cb) ourselves in Node bindings and bind it to the following function:

void FatalErrorCallback(const char* location, const char* message) {
  LOG(ERROR) << "Fatal error in V8: " << location << " " << message;
  ElectronBindings::Crash();
}




Which compares with the one we’d now be invoking with Node.js:

void OnFatalError(const char* location, const char* message) {
  if (location) {
    FPrintF(stderr, "FATAL ERROR: %s %s\n", location, message);
  } else {
    FPrintF(stderr, "FATAL ERROR: %s\n", message);
  }

  Isolate* isolate = Isolate::GetCurrent();
  Environment* env = Environment::GetCurrent(isolate);
  bool report_on_fatalerror;
  {
    Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
    report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
  }

  if (report_on_fatalerror) {
    report::TriggerNodeReport(
        isolate, env, message, "FatalError", "", Local<Object>());
  }

  fflush(stderr);
  ABORT();
}



Stripping out cli functionality we already prevent in the renderer process:


void OnFatalError(const char* location, const char* message) {
  if (location) {
    FPrintF(stderr, "FATAL ERROR: %s %s\n", location, message);
  } else {
    FPrintF(stderr, "FATAL ERROR: %s\n", message);
  }

  fflush(stderr);
  ABORT();
}



This introduces no side effects and is a comparable output, I would say.

Lastly, we’re left with: isolate->SetAbortOnUncaughtExceptionCallback(abort_callback);


Blink currently never calls this. This callback is used explicitly by embedders to help V8 determine if it should abort when it throws and no internal handler is predicted to catch the exception.

Node.js currently chooses to do this if:

1) The user has passed the —abort-on-uncaught-exception flag, which is irrelevant to is in the renderer as we disallow it

2) The user is in a scope where uncaught exceptions should not result in an abort; this only applies in the context of the Node.js ESM loader which is also already irrelevant in a renderer context as we disallow it in the renderer process as of #24301.


The only other case resulting in an abort in Node.js’ callback is when the Environment itself is stopping. As such, I personally feel comfortable allowing this setup functionality to run, but I would definitely be open to perhaps upstreaming a toggle to prevent calling SetAbortOnUncaughtExceptionCallback to Node.js.



@electron-cation electron-cation bot removed the new-pr 🌱 PR opened in the last 24 hours label Jul 17, 2020
deepak1556
deepak1556 previously approved these changes Jul 20, 2020
@deepak1556 deepak1556 dismissed their stale review July 20, 2020 05:16

oops accidental approval, the fatal error handler set in bindings has not been removed.

@codebytere codebytere force-pushed the use-node-setup-logic-in-bindings branch from 09182a1 to d2bf92b Compare July 20, 2020 05:34
Copy link
Member

@MarshallOfSound MarshallOfSound left a comment

Choose a reason for hiding this comment

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

Approving given the wonderful explanation, but I would like to err on the side of not backporting this as I'm still quite fearful 🤔

@codebytere codebytere merged commit bcba4ba into master Jul 20, 2020
@release-clerk
Copy link

release-clerk bot commented Jul 20, 2020

Release Notes Persisted

Fixed an issue where many uses of the Node.js assert module would throw in both the browser and renderer processes.

@codebytere codebytere deleted the use-node-setup-logic-in-bindings branch July 20, 2020 19:41
georgexu99 pushed a commit to georgexu99/electron that referenced this pull request Jul 28, 2020
* fix: use Node.js isolate setup logic in bindings

* Flags should be more process-specific

* Remove redundant isolate function setting

* Remove old SetFatalErrorHandler call
georgexu99 pushed a commit to georgexu99/electron that referenced this pull request Jul 28, 2020
* fix: use Node.js isolate setup logic in bindings

* Flags should be more process-specific

* Remove redundant isolate function setting

* Remove old SetFatalErrorHandler call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

assert.ok(false) throws TypeError instead of AssertionError
3 participants