Skip to content

Commit 8cf6499

Browse files
legendecasruyadorno
authored andcommitted
report: print javascript stack on fatal error
Try to print JavaScript stack on fatal error. OOMError needs to be distinguished from fatal error since no new handle can be created at that time. PR-URL: #44242 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
1 parent 7a56787 commit 8cf6499

8 files changed

Lines changed: 196 additions & 8 deletions

File tree

‎src/api/environment.cc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ void SetIsolateErrorHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
239239
auto* fatal_error_cb = s.fatal_error_callback ?
240240
s.fatal_error_callback : OnFatalError;
241241
isolate->SetFatalErrorHandler(fatal_error_cb);
242+
isolate->SetOOMErrorHandler(OOMErrorHandler);
242243

243244
if ((s.flags & SHOULD_NOT_SET_PREPARE_STACK_TRACE_CALLBACK) == 0) {
244245
auto* prepare_stack_trace_cb = s.prepare_stack_trace_callback ?

‎src/node_errors.cc‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,36 @@ void OnFatalError(const char* location, const char* message) {
501501
ABORT();
502502
}
503503

504+
voidOOMErrorHandler(constchar* location, bool is_heap_oom) {
505+
constchar* message =
506+
is_heap_oom ? "Allocation failed - JavaScript heap out of memory"
507+
: "Allocation failed - process out of memory";
508+
if (location) {
509+
FPrintF(stderr, "FATAL ERROR: %s %s\n", location, message);
510+
} else {
511+
FPrintF(stderr, "FATAL ERROR: %s\n", message);
512+
}
513+
514+
Isolate* isolate = Isolate::TryGetCurrent();
515+
Environment* env = nullptr;
516+
if (isolate != nullptr) {
517+
env = Environment::GetCurrent(isolate);
518+
}
519+
bool report_on_fatalerror;
520+
{
521+
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
522+
report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
523+
}
524+
525+
if (report_on_fatalerror) {
526+
report::TriggerNodeReport(
527+
isolate, env, message, "OOMError", "", Local<Object>());
528+
}
529+
530+
fflush(stderr);
531+
ABORT();
532+
}
533+
504534
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
505535
v8::Local<v8::Context> context,
506536
v8::Local<v8::Value> source,

‎src/node_errors.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ void AppendExceptionLine(Environment* env,
2121

2222
[[noreturn]]voidFatalError(constchar* location, constchar* message);
2323
voidOnFatalError(constchar* location, constchar* message);
24+
voidOOMErrorHandler(constchar* location, bool is_heap_oom);
2425

2526
// Helpers to construct errors similar to the ones provided by
2627
// lib/internal/errors.js.

‎src/node_report.cc‎

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
constexprintNODE_REPORT_VERSION = 2;
2727
constexprintNANOS_PER_SEC = 1000 * 1000 * 1000;
2828
constexprdoubleSEC_PER_MICROS = 1e-6;
29+
constexprintMAX_FRAME_COUNT = 10;
2930

3031
namespacenode {
3132
namespacereport {
@@ -43,6 +44,10 @@ using v8::Maybe;
4344
using v8::MaybeLocal;
4445
using v8::Nothing;
4546
using v8::Object;
47+
using v8::RegisterState;
48+
using v8::SampleInfo;
49+
using v8::StackFrame;
50+
using v8::StackTrace;
4651
using v8::String;
4752
using v8::TryCatch;
4853
using v8::V8;
@@ -62,6 +67,9 @@ static void PrintJavaScriptErrorStack(JSONWriter* writer,
6267
Isolate* isolate,
6368
Local<Value> error,
6469
constchar* trigger);
70+
staticvoidPrintJavaScriptStack(JSONWriter* writer,
71+
Isolate* isolate,
72+
constchar* trigger);
6573
staticvoidPrintJavaScriptErrorProperties(JSONWriter* writer,
6674
Isolate* isolate,
6775
Local<Value> error);
@@ -269,8 +277,6 @@ static void WriteNodeReport(Isolate* isolate,
269277
// Report summary JavaScript error stack backtrace
270278
PrintJavaScriptErrorStack(&writer, isolate, error, trigger);
271279

272-
// Report summary JavaScript error properties backtrace
273-
PrintJavaScriptErrorProperties(&writer, isolate, error);
274280
writer.json_objectend(); // the end of 'javascriptStack'
275281

276282
// Report V8 Heap and Garbage Collector information
@@ -317,7 +323,7 @@ static void WriteNodeReport(Isolate* isolate,
317323
env,
318324
"Worker thread subreport",
319325
trigger,
320-
Local<Object>(),
326+
Local<Value>(),
321327
os);
322328

323329
Mutex::ScopedLock lock(workers_mutex);
@@ -534,19 +540,80 @@ static Maybe<std::string> ErrorToString(Isolate* isolate,
534540
return Just<>(std::string(*sv, sv.length()));
535541
}
536542

543+
staticvoidPrintEmptyJavaScriptStack(JSONWriter* writer) {
544+
writer->json_keyvalue("message", "No stack.");
545+
writer->json_arraystart("stack");
546+
writer->json_element("Unavailable.");
547+
writer->json_arrayend();
548+
549+
writer->json_objectstart("errorProperties");
550+
writer->json_objectend();
551+
}
552+
553+
// Do our best to report the JavaScript stack without calling into JavaScript.
554+
staticvoidPrintJavaScriptStack(JSONWriter* writer,
555+
Isolate* isolate,
556+
constchar* trigger) {
557+
// Can not capture the stacktrace when the isolate is in a OOM state.
558+
if (!strcmp(trigger, "OOMError")) {
559+
PrintEmptyJavaScriptStack(writer);
560+
return;
561+
}
562+
563+
HandleScope scope(isolate);
564+
RegisterState state;
565+
state.pc = nullptr;
566+
state.fp = &state;
567+
state.sp = &state;
568+
569+
// in-out params
570+
SampleInfo info;
571+
void* samples[MAX_FRAME_COUNT];
572+
isolate->GetStackSample(state, samples, MAX_FRAME_COUNT, &info);
573+
574+
Local<StackTrace> stack = StackTrace::CurrentStackTrace(
575+
isolate, MAX_FRAME_COUNT, StackTrace::kDetailed);
576+
577+
if (stack->GetFrameCount() == 0) {
578+
PrintEmptyJavaScriptStack(writer);
579+
return;
580+
}
581+
582+
writer->json_keyvalue("message", trigger);
583+
writer->json_arraystart("stack");
584+
for (int i = 0; i < stack->GetFrameCount(); i++) {
585+
Local<StackFrame> frame = stack->GetFrame(isolate, i);
586+
587+
Utf8Value function_name(isolate, frame->GetFunctionName());
588+
Utf8Value script_name(isolate, frame->GetScriptName());
589+
constint line_number = frame->GetLineNumber();
590+
constint column = frame->GetColumn();
591+
592+
std::string stack_line = SPrintF(
593+
"at %s (%s:%d:%d)", *function_name, *script_name, line_number, column);
594+
writer->json_element(stack_line);
595+
}
596+
writer->json_arrayend();
597+
writer->json_objectstart("errorProperties");
598+
writer->json_objectend();
599+
}
600+
537601
// Report the JavaScript stack.
538602
staticvoidPrintJavaScriptErrorStack(JSONWriter* writer,
539603
Isolate* isolate,
540604
Local<Value> error,
541605
constchar* trigger) {
606+
if (error.IsEmpty()) {
607+
returnPrintJavaScriptStack(writer, isolate, trigger);
608+
}
609+
542610
TryCatch try_catch(isolate);
543611
HandleScope scope(isolate);
544612
Local<Context> context = isolate->GetCurrentContext();
545613
std::string ss = "";
546-
if ((!strcmp(trigger, "FatalError")) ||
547-
(!strcmp(trigger, "Signal")) ||
548-
(!ErrorToString(isolate, context, error).To(&ss))) {
549-
ss = "No stack.\nUnavailable.\n";
614+
if (!ErrorToString(isolate, context, error).To(&ss)) {
615+
PrintEmptyJavaScriptStack(writer);
616+
return;
550617
}
551618

552619
int line = ss.find('\n');
@@ -569,6 +636,9 @@ static void PrintJavaScriptErrorStack(JSONWriter* writer,
569636
}
570637
writer->json_arrayend();
571638
}
639+
640+
// Report summary JavaScript error properties backtrace
641+
PrintJavaScriptErrorProperties(writer, isolate, error);
572642
}
573643

574644
// Report a native stack backtrace
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#include<node.h>
2+
#include<v8.h>
3+
4+
using v8::FunctionCallbackInfo;
5+
using v8::Isolate;
6+
using v8::Local;
7+
using v8::MaybeLocal;
8+
using v8::Object;
9+
using v8::Value;
10+
11+
voidTriggerFatalError(const FunctionCallbackInfo<Value>& args) {
12+
Isolate* isolate = args.GetIsolate();
13+
14+
// Trigger a v8 ApiCheck failure.
15+
MaybeLocal<Value> value;
16+
value.ToLocalChecked();
17+
}
18+
19+
voidinit(Local<Object> exports) {
20+
NODE_SET_METHOD(exports, "triggerFatalError", TriggerFatalError);
21+
}
22+
23+
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
}
8+
]
9+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
constcommon=require('../../common');
4+
constassert=require('assert');
5+
constpath=require('path');
6+
constspawnSync=require('child_process').spawnSync;
7+
consthelper=require('../../common/report.js');
8+
consttmpdir=require('../../common/tmpdir');
9+
10+
constbinding=path.resolve(__dirname,`./build/${common.buildType}/binding`);
11+
12+
if(process.argv[2]==='child'){
13+
(functionchildMain(){
14+
constaddon=require(binding);
15+
addon.triggerFatalError();
16+
})();
17+
return;
18+
}
19+
20+
constARGS=[
21+
__filename,
22+
'child',
23+
];
24+
25+
{
26+
// Verify that --report-on-fatalerror is respected when set.
27+
tmpdir.refresh();
28+
constargs=['--report-on-fatalerror', ...ARGS];
29+
constchild=spawnSync(process.execPath,args,{cwd: tmpdir.path});
30+
assert.notStrictEqual(child.status,0,'Process exited unexpectedly');
31+
32+
constreports=helper.findReports(child.pid,tmpdir.path);
33+
assert.strictEqual(reports.length,1);
34+
35+
constreport=reports[0];
36+
helper.validate(report);
37+
38+
constcontent=require(report);
39+
assert.strictEqual(content.header.trigger,'FatalError');
40+
41+
// Check that the javascript stack is present.
42+
assert.strictEqual(content.javascriptStack.stack.findIndex((frame)=>frame.match('childMain')),0);
43+
}
44+
45+
{
46+
// Verify that --report-on-fatalerror is respected when not set.
47+
constargs=ARGS;
48+
constchild=spawnSync(process.execPath,args,{cwd: tmpdir.path});
49+
assert.notStrictEqual(child.status,0,'Process exited unexpectedly');
50+
constreports=helper.findReports(child.pid,tmpdir.path);
51+
assert.strictEqual(reports.length,0);
52+
}

test/report/test-report-fatal-error.js renamed to test/report/test-report-fatalerror-oomerror.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ const ARGS = [
4444
constreport=reports[0];
4545
helper.validate(report);
4646

47+
constcontent=require(report);
4748
// Errors occur in a context where env is not available, so thread ID is
4849
// unknown. Assert this, to verify that the underlying env-less situation is
4950
// actually reached.
50-
assert.strictEqual(require(report).header.threadId,null);
51+
assert.strictEqual(content.header.threadId,null);
52+
assert.strictEqual(content.header.trigger,'OOMError');
5153
}
5254

5355
{

0 commit comments

Comments
 (0)