Skip to content

Commit 8631278

Browse files
committed
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.
1 parent d6e626d commit 8631278

File tree

10 files changed

+212
-23
lines changed

10 files changed

+212
-23
lines changed

src/api/environment.cc

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

244245
if ((s.flags & SHOULD_NOT_SET_PREPARE_STACK_TRACE_CALLBACK) == 0) {
245246
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
@@ -493,6 +493,36 @@ void OnFatalError(const char* location, const char* message) {
493493
ABORT();
494494
}
495495

496+
void OOMErrorHandler(const char* location, bool is_heap_oom) {
497+
const char* message =
498+
is_heap_oom ? "Allocation failed - JavaScript heap out of memory"
499+
: "Allocation failed - process out of memory";
500+
if (location) {
501+
FPrintF(stderr, "FATAL ERROR: %s %s\n", location, message);
502+
} else {
503+
FPrintF(stderr, "FATAL ERROR: %s\n", message);
504+
}
505+
506+
Isolate* isolate = Isolate::TryGetCurrent();
507+
Environment* env = nullptr;
508+
if (isolate != nullptr) {
509+
env = Environment::GetCurrent(isolate);
510+
}
511+
bool report_on_fatalerror;
512+
{
513+
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
514+
report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
515+
}
516+
517+
if (report_on_fatalerror) {
518+
report::TriggerNodeReport(
519+
isolate, env, message, "OOMError", "", Local<Object>());
520+
}
521+
522+
fflush(stderr);
523+
ABORT();
524+
}
525+
496526
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
497527
v8::Local<v8::Context> context,
498528
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]] void FatalError(const char* location, const char* message);
2323
void OnFatalError(const char* location, const char* message);
24+
void OOMErrorHandler(const char* 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: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
constexpr int NODE_REPORT_VERSION = 2;
2727
constexpr int NANOS_PER_SEC = 1000 * 1000 * 1000;
2828
constexpr double SEC_PER_MICROS = 1e-6;
29+
constexpr int MAX_FRAME_COUNT = 10;
2930

3031
namespace node {
3132
namespace report {
@@ -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;
@@ -55,13 +60,16 @@ static void WriteNodeReport(Isolate* isolate,
5560
const char* trigger,
5661
const std::string& filename,
5762
std::ostream& out,
58-
Local<Value> error,
63+
MaybeLocal<Value> maybe_error,
5964
bool compact);
6065
static void PrintVersionInformation(JSONWriter* writer);
6166
static void PrintJavaScriptErrorStack(JSONWriter* writer,
6267
Isolate* isolate,
63-
Local<Value> error,
68+
MaybeLocal<Value> maybe_error,
6469
const char* trigger);
70+
static void PrintJavaScriptStack(JSONWriter* writer,
71+
Isolate* isolate,
72+
const char* trigger);
6573
static void PrintJavaScriptErrorProperties(JSONWriter* writer,
6674
Isolate* isolate,
6775
Local<Value> error);
@@ -81,7 +89,7 @@ std::string TriggerNodeReport(Isolate* isolate,
8189
const char* message,
8290
const char* trigger,
8391
const std::string& name,
84-
Local<Value> error) {
92+
MaybeLocal<Value> error) {
8593
std::string filename;
8694

8795
// Determine the required report filename. In order of priority:
@@ -166,9 +174,9 @@ void GetNodeReport(Isolate* isolate,
166174
Environment* env,
167175
const char* message,
168176
const char* trigger,
169-
Local<Value> error,
177+
MaybeLocal<Value> maybe_error,
170178
std::ostream& out) {
171-
WriteNodeReport(isolate, env, message, trigger, "", out, error, false);
179+
WriteNodeReport(isolate, env, message, trigger, "", out, maybe_error, false);
172180
}
173181

174182
// Internal function to coordinate and write the various
@@ -179,7 +187,7 @@ static void WriteNodeReport(Isolate* isolate,
179187
const char* trigger,
180188
const std::string& filename,
181189
std::ostream& out,
182-
Local<Value> error,
190+
MaybeLocal<Value> maybe_error,
183191
bool compact) {
184192
// Obtain the current time and the pid.
185193
TIME_TYPE tm_struct;
@@ -267,10 +275,8 @@ static void WriteNodeReport(Isolate* isolate,
267275
if (isolate != nullptr) {
268276
writer.json_objectstart("javascriptStack");
269277
// Report summary JavaScript error stack backtrace
270-
PrintJavaScriptErrorStack(&writer, isolate, error, trigger);
278+
PrintJavaScriptErrorStack(&writer, isolate, maybe_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+
MaybeLocal<Value>(),
321327
os);
322328

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

543+
static void PrintEmptyJavaScriptStack(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+
static void PrintJavaScriptStack(JSONWriter* writer,
555+
Isolate* isolate,
556+
const char* 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+
const int line_number = frame->GetLineNumber();
590+
const int 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
static void PrintJavaScriptErrorStack(JSONWriter* writer,
539603
Isolate* isolate,
540-
Local<Value> error,
604+
MaybeLocal<Value> maybe_error,
541605
const char* trigger) {
606+
Local<Value> error;
607+
if (!maybe_error.ToLocal(&error)) {
608+
return PrintJavaScriptStack(writer, isolate, trigger);
609+
}
610+
542611
TryCatch try_catch(isolate);
543612
HandleScope scope(isolate);
544613
Local<Context> context = isolate->GetCurrentContext();
545614
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";
615+
if (!ErrorToString(isolate, context, error).To(&ss)) {
616+
PrintEmptyJavaScriptStack(writer);
617+
return;
550618
}
551619

552620
int line = ss.find('\n');
@@ -569,6 +637,9 @@ static void PrintJavaScriptErrorStack(JSONWriter* writer,
569637
}
570638
writer->json_arrayend();
571639
}
640+
641+
// Report summary JavaScript error properties backtrace
642+
PrintJavaScriptErrorProperties(writer, isolate, error);
572643
}
573644

574645
// Report a native stack backtrace

src/node_report.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ std::string TriggerNodeReport(v8::Isolate* isolate,
2424
const char* message,
2525
const char* trigger,
2626
const std::string& name,
27-
v8::Local<v8::Value> error);
27+
v8::MaybeLocal<v8::Value> maybe_error);
2828
void GetNodeReport(v8::Isolate* isolate,
2929
Environment* env,
3030
const char* message,
3131
const char* trigger,
32-
v8::Local<v8::Value> error,
32+
v8::MaybeLocal<v8::Value> maybe_error,
3333
std::ostream& out);
3434

3535
// Function declarations - utility functions in src/node_report_utils.cc

src/node_report_module.cc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ using v8::FunctionCallbackInfo;
2222
using v8::HandleScope;
2323
using v8::Isolate;
2424
using v8::Local;
25+
using v8::MaybeLocal;
2526
using v8::Object;
2627
using v8::String;
2728
using v8::Value;
@@ -56,14 +57,13 @@ void GetReport(const FunctionCallbackInfo<Value>& info) {
5657
Environment* env = Environment::GetCurrent(info);
5758
Isolate* isolate = env->isolate();
5859
HandleScope scope(isolate);
59-
Local<Object> error;
60+
MaybeLocal<Value> error;
6061
std::ostringstream out;
6162

6263
CHECK_EQ(info.Length(), 1);
63-
if (!info[0].IsEmpty() && info[0]->IsObject())
64-
error = info[0].As<Object>();
65-
else
66-
error = Local<Object>();
64+
if (!info[0].IsEmpty() && info[0]->IsObject()) {
65+
error = info[0];
66+
}
6767

6868
GetNodeReport(
6969
isolate, env, "JavaScript API", __func__, error, out);
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+
void TriggerFatalError(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+
void init(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+
const common = require('../../common');
4+
const assert = require('assert');
5+
const path = require('path');
6+
const spawnSync = require('child_process').spawnSync;
7+
const helper = require('../../common/report.js');
8+
const tmpdir = require('../../common/tmpdir');
9+
10+
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
11+
12+
if (process.argv[2] === 'child') {
13+
(function childMain() {
14+
const addon = require(binding);
15+
addon.triggerFatalError();
16+
})();
17+
return;
18+
}
19+
20+
const ARGS = [
21+
__filename,
22+
'child',
23+
];
24+
25+
{
26+
// Verify that --report-on-fatalerror is respected when set.
27+
tmpdir.refresh();
28+
const args = ['--report-on-fatalerror', ...ARGS];
29+
const child = spawnSync(process.execPath, args, { cwd: tmpdir.path });
30+
assert.notStrictEqual(child.status, 0, 'Process exited unexpectedly');
31+
32+
const reports = helper.findReports(child.pid, tmpdir.path);
33+
assert.strictEqual(reports.length, 1);
34+
35+
const report = reports[0];
36+
helper.validate(report);
37+
38+
const content = 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+
const args = ARGS;
48+
const child = spawnSync(process.execPath, args, { cwd: tmpdir.path });
49+
assert.notStrictEqual(child.status, 0, 'Process exited unexpectedly');
50+
const reports = 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
const report = reports[0];
4545
helper.validate(report);
4646

47+
const content = 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)