Skip to content

Commit 53364fc

Browse files
legendecasjuanarbol
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 f3fd4e1 commit 53364fc

File tree

8 files changed

+196
-8
lines changed

8 files changed

+196
-8
lines changed

src/api/environment.cc

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ void SetIsolateErrorHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
237237
auto* fatal_error_cb = s.fatal_error_callback ?
238238
s.fatal_error_callback : OnFatalError;
239239
isolate->SetFatalErrorHandler(fatal_error_cb);
240+
isolate->SetOOMErrorHandler(OOMErrorHandler);
240241

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

src/node_errors.cc

+30
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,36 @@ void OnFatalError(const char* location, const char* message) {
459459
ABORT();
460460
}
461461

462+
void OOMErrorHandler(const char* location, bool is_heap_oom) {
463+
const char* message =
464+
is_heap_oom ? "Allocation failed - JavaScript heap out of memory"
465+
: "Allocation failed - process out of memory";
466+
if (location) {
467+
FPrintF(stderr, "FATAL ERROR: %s %s\n", location, message);
468+
} else {
469+
FPrintF(stderr, "FATAL ERROR: %s\n", message);
470+
}
471+
472+
Isolate* isolate = Isolate::TryGetCurrent();
473+
Environment* env = nullptr;
474+
if (isolate != nullptr) {
475+
env = Environment::GetCurrent(isolate);
476+
}
477+
bool report_on_fatalerror;
478+
{
479+
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
480+
report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
481+
}
482+
483+
if (report_on_fatalerror) {
484+
report::TriggerNodeReport(
485+
isolate, env, message, "OOMError", "", Local<Object>());
486+
}
487+
488+
fflush(stderr);
489+
ABORT();
490+
}
491+
462492
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
463493
v8::Local<v8::Context> context,
464494
v8::Local<v8::Value> source,

src/node_errors.h

+1
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

+77-7
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;
@@ -62,6 +67,9 @@ static void PrintJavaScriptErrorStack(JSONWriter* writer,
6267
Isolate* isolate,
6368
Local<Value> 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);
@@ -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+
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,
540604
Local<Value> error,
541605
const char* trigger) {
606+
if (error.IsEmpty()) {
607+
return PrintJavaScriptStack(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
+23
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)
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+
}

test/addons/report-fatalerror/test.js

+52
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 test/report/test-report-fatalerror-oomerror.js

+3-1
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)