Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,16 @@ NODE_EXTERN void SetProcessExitHandler(
std::function<void(Environment*, int)>&& handler);
NODE_EXTERN void DefaultProcessExitHandler(Environment* env, int exit_code);

// Sets a process-global handler invoked when Node.js programmatically aborts
// (the ABORT() macro: failed CHECK/DCHECK, UNREACHABLE(), node::Assert(), and
// OnFatalError()). The handler may return, in which case Node.js still
// terminates the process (via abort()/_exit), or it may terminate the
// process itself. The default handler writes the native and JavaScript
// backtraces to stderr. Passing nullptr restores the default handler. This is
// process-global and may be invoked before any Isolate or Environment exists.
using AbortHandler = void (*)();
NODE_EXTERN void SetAbortHandler(AbortHandler handler);

// This may return nullptr if context is not associated with a Node instance.
NODE_EXTERN Environment* GetCurrentEnvironment(v8::Local<v8::Context> context);
NODE_EXTERN IsolateData* GetEnvironmentIsolateData(Environment* env);
Expand Down
20 changes: 20 additions & 0 deletions src/node_errors.cc
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,26 @@ void AppendExceptionLine(Environment* env,
.FromMaybe(false));
}

namespace {
// Default handler: reproduces previous ABORT() output (native + JS backtrace to
// stderr). Termination is NOT done here. The ABORT() macro backstops it.
void DefaultAbortHandler() {
DumpNativeBacktrace(stderr);
DumpJavaScriptBacktrace(stderr);
fflush(stderr);
}
// Constant-initialized, so this is valid from load time, safe even for a
// CHECK() during early startup, before any SetAbortHandler call.
AbortHandler g_abort_handler = DefaultAbortHandler;
} // namespace

void SetAbortHandler(AbortHandler handler) {
g_abort_handler = handler ? handler : DefaultAbortHandler;
}
AbortHandler GetAbortHandler() {
return g_abort_handler;
}

void Assert(const AssertionInfo& info) {
std::string name = GetHumanReadableProcessName();

Expand Down
7 changes: 4 additions & 3 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ void NODE_EXTERN_PRIVATE Assert(const AssertionInfo& info);
void DumpNativeBacktrace(FILE* fp);
void DumpJavaScriptBacktrace(FILE* fp);

// Returns the currently installed abort handler which is never null.
AbortHandler GetAbortHandler();

// Windows 8+ does not like abort() in Release mode
#ifdef _WIN32
#define ABORT_NO_BACKTRACE() _exit(static_cast<int>(node::ExitCode::kAbort))
Expand All @@ -142,9 +145,7 @@ void DumpJavaScriptBacktrace(FILE* fp);
// backtrace is correct.
#define ABORT() \
do { \
node::DumpNativeBacktrace(stderr); \
node::DumpJavaScriptBacktrace(stderr); \
fflush(stderr); \
node::GetAbortHandler()(); \
ABORT_NO_BACKTRACE(); \
} while (0)

Expand Down
4 changes: 4 additions & 0 deletions test/abort/test-addon-abort-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';
require('../common');
process.env.ALLOW_CRASHES = true;
require('../addons/abort-handler/test');
18 changes: 18 additions & 0 deletions test/addons/abort-handler/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <node.h>
#include <v8.h>
#include <cstdio>

namespace {
void TestAbortHandler() {
fputs("CUSTOM_ABORT_HANDLER_RAN\n", stderr);
fflush(stderr);
}

void InstallAbortHandler(const v8::FunctionCallbackInfo<v8::Value>&) {
node::SetAbortHandler(TestAbortHandler);
}
} // namespace

NODE_MODULE_INIT() {
NODE_SET_METHOD(exports, "installAbortHandler", InstallAbortHandler);
}
9 changes: 9 additions & 0 deletions test/addons/abort-handler/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
43 changes: 43 additions & 0 deletions test/addons/abort-handler/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const bindingPath = path.resolve(
__dirname, 'build', common.buildType, 'binding.node');

if (!fs.existsSync(bindingPath))
common.skip('binding not built yet');

if (process.argv[2] === 'child') {
const binding = require(bindingPath);
binding.installAbortHandler();
process.abort();
return;
}

// We do not want to generate core files / actually crash the process when
// running this test as a regular addon test. It is also required as an
// abort test with ALLOW_CRASHES set (see ../../abort/test-addon-abort-handler.js).
if (!process.env.ALLOW_CRASHES)
common.skip('test needs ALLOW_CRASHES to spawn a crashing child');

const result = spawnSync(process.execPath, [__filename, 'child']);

const stderr = result.stderr.toString();
assert.ok(
stderr.includes('CUSTOM_ABORT_HANDLER_RAN'),
`Expected custom abort handler marker in stderr, got:\n${stderr}`);
assert.ok(
!stderr.includes('Native stack trace'),
`Expected the custom handler to replace the default dump, got:\n${stderr}`);

if (common.isWindows) {
assert.strictEqual(result.status, 134);
assert.strictEqual(result.signal, null);
} else {
assert.strictEqual(result.status, null);
assert.strictEqual(result.signal, 'SIGABRT');
}
40 changes: 40 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,43 @@ TEST_F(EnvironmentTest, LoadEnvironmentWithCallbackWithESModule) {
printf("Frame: %s\n", *frame_str);
EXPECT_EQ(frame_str.ToString(), " at embedded:esm.mjs:3:15");
}

namespace {
void CustomAbortHandlerForContractTest() {}

bool abort_handler_dispatch_flag = false;
void AbortHandlerThatSetsDispatchFlag() {
abort_handler_dispatch_flag = true;
}
} // namespace

TEST(AbortHandlerTest, DefaultIsNonNullAndSetAbortHandlerRoundTrips) {
node::AbortHandler old = node::GetAbortHandler();

// There should always be a non-null default handler installed.
EXPECT_NE(node::GetAbortHandler(), nullptr);

node::SetAbortHandler(CustomAbortHandlerForContractTest);
EXPECT_EQ(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

node::SetAbortHandler(nullptr);
EXPECT_NE(node::GetAbortHandler(), nullptr);
EXPECT_NE(node::GetAbortHandler(), CustomAbortHandlerForContractTest);

node::SetAbortHandler(old);
}

TEST(AbortHandlerTest, InstalledHandlerIsInvokedWhenCalled) {
node::AbortHandler old = node::GetAbortHandler();
abort_handler_dispatch_flag = false;

node::SetAbortHandler(AbortHandlerThatSetsDispatchFlag);
node::AbortHandler h = node::GetAbortHandler();
// Fail cleanly (instead of crashing on a null call) if the handler wasn't
// actually installed.
ASSERT_NE(h, nullptr);
h();
EXPECT_TRUE(abort_handler_dispatch_flag);

node::SetAbortHandler(old);
}