Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,15 @@ 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. A
// message describing the reason for the abort may or may not be passed as a
// parameter to the handler. The handler should not return, but node will ensure
// that the process exits after the handler is called regardless of whether or
// not it returns. Passing nullptr restores the default handler. This is
// process-global and may be invoked before any Isolate or Environment exists.
using AbortHandler = void (*)(const char* message);
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
30 changes: 27 additions & 3 deletions src/node_errors.cc
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,30 @@ void AppendExceptionLine(Environment* env,
.FromMaybe(false));
}

namespace {
// Default handler: Dumps native + JS backtraces to stderr and exits. This
// indirectly calls backtrace so it can not be marked as [[noreturn]] (see the
// comment on node::Assert() below). `message` is ignored because the
// assertion/fatal-error message, if any, is already printed to stderr by the
// caller (Assert()/OnFatalError()) before this handler runs.
void DefaultAbortHandler(const char* /*message*/) {
DumpNativeBacktrace(stderr);
DumpJavaScriptBacktrace(stderr);
fflush(stderr);
ABORT_NO_BACKTRACE();
}
// 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 All @@ -406,7 +430,7 @@ void Assert(const AssertionInfo& info) {
info.message);

fflush(stderr);
ABORT();
ABORT_WITH_MESSAGE(info.message);
}

enum class EnhanceFatalException { kEnhance, kDontEnhance };
Expand Down Expand Up @@ -584,7 +608,7 @@ static void ReportFatalException(Environment* env,
}

fflush(stderr);
ABORT();
ABORT_WITH_MESSAGE(message);
}

void OOMErrorHandler(const char* location, const v8::OOMDetails& details) {
Expand Down Expand Up @@ -620,7 +644,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details) {
}

fflush(stderr);
ABORT();
ABORT_WITH_MESSAGE(message);
}

v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
Expand Down
10 changes: 6 additions & 4 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 @@ -140,11 +143,10 @@ void DumpJavaScriptBacktrace(FILE* fp);
// correct backtracing.
// `ABORT` must be a macro and not a [[noreturn]] function to make sure the
// backtrace is correct.
#define ABORT() \
#define ABORT() ABORT_WITH_MESSAGE(nullptr)
Comment thread
maxhfisher marked this conversation as resolved.
Outdated
#define ABORT_WITH_MESSAGE(message) \
do { \
node::DumpNativeBacktrace(stderr); \
node::DumpJavaScriptBacktrace(stderr); \
fflush(stderr); \
node::GetAbortHandler()(message); \
ABORT_NO_BACKTRACE(); \
} while (0)

Expand Down
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(const char* /*message*/) {
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'],
}
]
}
45 changes: 45 additions & 0 deletions test/addons/abort-handler/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { exec } = 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;
}

const escapedArgs =
common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`;
if (!common.isWindows) {
// Do not create core files, as it can take a lot of disk space on
// continuous testing and developers' machines.
escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0];
}

exec(...escapedArgs, common.mustCall((err, stdout, stderr) => {
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}`);

// The child aborts. Whether that surfaces as the SIGABRT signal or as exit
// code 134 depends on shell wrapping: the `ulimit -c 0 && ...` prefix makes
// /bin/sh wait on (rather than exec-replace itself with) the node grandchild,
// so sh reports the aborted grandchild as a normal exit with code 134.
// common.nodeProcessAborted() accepts both forms.
assert.ok(
err && common.nodeProcessAborted(err.code, err.signal),
`Expected the child to abort, got code=${err?.code} signal=${err?.signal}`);
}));
48 changes: 48 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,51 @@ TEST_F(EnvironmentTest, LoadEnvironmentWithCallbackWithESModule) {
printf("Frame: %s\n", *frame_str);
EXPECT_EQ(frame_str.ToString(), " at embedded:esm.mjs:3:15");
}

namespace {
void CustomAbortHandlerForContractTest(const char* message) {}

bool abort_handler_dispatch_flag = false;
const char* abort_handler_received_message = nullptr;
void AbortHandlerThatSetsDispatchFlag(const char* message) {
abort_handler_dispatch_flag = true;
abort_handler_received_message = message;
}
} // 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;
abort_handler_received_message = nullptr;

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);

// Dispatch through the public GetAbortHandler() accessor directly (not via
// the ABORT() macro, so nothing terminates), and verify the message is
// passed through unchanged.
node::GetAbortHandler()("some-test-message");
EXPECT_TRUE(abort_handler_dispatch_flag);
EXPECT_STREQ(abort_handler_received_message, "some-test-message");

node::SetAbortHandler(old);
}
Loading