Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correctly support stack overflows across platforms #982

Merged
merged 13 commits into from
May 22, 2024
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
**Fixes**:

- Allow `crashpad` to run under [Epic's Anti-Cheat Client](https://dev.epicgames.com/docs/game-services/anti-cheat/using-anti-cheat#external-crash-dumpers) by deferring the full `crashpad_handler` access rights to the client application until a crash occurred. ([#980](https://github.com/getsentry/sentry-native/pull/980), [crashpad#99](https://github.com/getsentry/crashpad/pull/99))
- Reserve enough stack space on Windows for our handler to run when the stack is exhausted from stack-overflow. ([#982](https://github.com/getsentry/sentry-native/pull/982))
- Only configure a `sigaltstack` in `inproc` if no previous configuration exists on Linux and Android. ([#982](https://github.com/getsentry/sentry-native/pull/982))
- Store transaction `data` in the event property `extra` since the `data` property is discarded by `relay`. ([#986](https://github.com/getsentry/sentry-native/issues/986))

**Docs**:
Expand Down
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,14 @@ if(SENTRY_BUILD_EXAMPLES)
target_link_libraries(sentry_example PRIVATE sentry)

if(MSVC)
target_compile_options(sentry_example PRIVATE $<BUILD_INTERFACE:/wd5105>)
target_compile_options(sentry_example PRIVATE $<BUILD_INTERFACE:/wd5105 /wd4717>)

# to test handling SEH by-passing exceptions we need to enable the control flow guard
target_compile_options(sentry_example PRIVATE $<BUILD_INTERFACE:/guard:cf>)
else()
# Disable all optimizations for the `sentry_example` in gcc/clang. This allows us to keep crash triggers simple.
# The effects besides reproducible code-gen across compiler versions, will be negligible for build- and runtime.
target_compile_options(sentry_example PRIVATE $<BUILD_INTERFACE:-O0>)
endif()

# set static runtime if enabled
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ The example currently supports the following commands:
- `on-crash`: Installs an `on_crash()` callback that retains the crash event.
- `discarding-on-crash`: Installs an `on_crash()` callback that discards the crash event.
- `override-sdk-name`: Changes the SDK name via the options at runtime.
- `stack-overflow`: Provokes a stack-overflow.

Only on Windows using crashpad with its WER handler module:

Expand Down
11 changes: 11 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <assert.h>

#ifdef SENTRY_PLATFORM_WINDOWS
# include <malloc.h>
# include <synchapi.h>
# define sleep_s(SECONDS) Sleep((SECONDS)*1000)
#else
Expand Down Expand Up @@ -161,6 +162,13 @@ trigger_crash()
memset((char *)invalid_mem, 1, 100);
}

static void
trigger_stack_overflow()
{
alloca(1024);
trigger_stack_overflow();
}

int
main(int argc, char **argv)
{
Expand Down Expand Up @@ -322,6 +330,9 @@ main(int argc, char **argv)
if (has_arg(argc, argv, "crash")) {
trigger_crash();
}
if (has_arg(argc, argv, "stack-overflow")) {
trigger_stack_overflow();
}
#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \
&& !defined(__MINGW64__)
if (has_arg(argc, argv, "fastfail")) {
Expand Down
4 changes: 4 additions & 0 deletions src/backends/sentry_backend_breakpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ extern "C" {
#include "sentry_database.h"
#include "sentry_envelope.h"
#include "sentry_options.h"
#ifdef SENTRY_PLATFORM_WINDOWS
# include "sentry_os.h"
#endif
#include "sentry_path.h"
#include "sentry_string.h"
#include "sentry_sync.h"
Expand Down Expand Up @@ -209,6 +212,7 @@ sentry__breakpad_backend_startup(
sentry_path_t *current_run_folder = options->run->run_path;

#ifdef SENTRY_PLATFORM_WINDOWS
sentry__reserve_thread_stack();
backend->data = new google_breakpad::ExceptionHandler(
current_run_folder->path, NULL, sentry__breakpad_backend_callback, NULL,
google_breakpad::ExceptionHandler::HANDLER_EXCEPTION);
Expand Down
4 changes: 4 additions & 0 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ crashpad_backend_startup(
}
}

#ifdef SENTRY_PLATFORM_WINDOWS
sentry__reserve_thread_stack();
#endif

// The crashpad client uses shell lookup rules (absolute path, relative
// path, or bare executable name that is looked up in $PATH).
// However, it crashes hard when it cant resolve the handler, so we make
Expand Down
50 changes: 28 additions & 22 deletions src/backends/sentry_backend_inproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include "sentry_database.h"
#include "sentry_envelope.h"
#include "sentry_options.h"
#if defined(SENTRY_PLATFORM_WINDOWS)
# include "sentry_os.h"
#endif
#include "sentry_scope.h"
#include "sentry_sync.h"
#include "sentry_transport.h"
Expand Down Expand Up @@ -34,10 +37,6 @@
* Both breakpad and crashpad are way more defensive in the setup of their
* signal stacks and take existing stacks into account (or reuse them).
*/
#ifndef SENTRY_PLATFORM_ANDROID
# define SETUP_SIGALTSTACK
#endif

#define SIGNAL_DEF(Sig, Desc) \
{ \
Sig, #Sig, Desc \
Expand All @@ -57,9 +56,7 @@ struct signal_slot {
# define SIGNAL_STACK_SIZE 65536
static struct sigaction g_sigaction;
static struct sigaction g_previous_handlers[SIGNAL_COUNT];
# ifdef SETUP_SIGALTSTACK
static stack_t g_signal_stack;
# endif
static stack_t g_signal_stack = { 0 };
static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = {
SIGNAL_DEF(SIGILL, "IllegalInstruction"),
SIGNAL_DEF(SIGTRAP, "Trap"),
Expand Down Expand Up @@ -112,16 +109,24 @@ startup_inproc_backend(
}
}

// install our own signal handler
# ifdef SETUP_SIGALTSTACK
g_signal_stack.ss_sp = sentry_malloc(SIGNAL_STACK_SIZE);
if (!g_signal_stack.ss_sp) {
return 1;
// set up an alternate signal stack if noone defined one before
stack_t old_sig_stack;
if (sigaltstack(NULL, &old_sig_stack) == -1 || old_sig_stack.ss_sp == NULL
|| old_sig_stack.ss_size == 0) {
SENTRY_TRACEF("installing signal stack (size: %d)", SIGNAL_STACK_SIZE);
g_signal_stack.ss_sp = sentry_malloc(SIGNAL_STACK_SIZE);
if (!g_signal_stack.ss_sp) {
return 1;
}
g_signal_stack.ss_size = SIGNAL_STACK_SIZE;
g_signal_stack.ss_flags = 0;
sigaltstack(&g_signal_stack, 0);
} else {
SENTRY_TRACEF(
"using existing signal stack (size: %d)", old_sig_stack.ss_size);
}
g_signal_stack.ss_size = SIGNAL_STACK_SIZE;
g_signal_stack.ss_flags = 0;
sigaltstack(&g_signal_stack, 0);
# endif

// install our own signal handler
sigemptyset(&g_sigaction.sa_mask);
g_sigaction.sa_sigaction = handle_signal;
g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK;
Expand All @@ -134,12 +139,12 @@ startup_inproc_backend(
static void
shutdown_inproc_backend(sentry_backend_t *UNUSED(backend))
{
# ifdef SETUP_SIGALTSTACK
g_signal_stack.ss_flags = SS_DISABLE;
sigaltstack(&g_signal_stack, 0);
sentry_free(g_signal_stack.ss_sp);
g_signal_stack.ss_sp = NULL;
# endif
if (g_signal_stack.ss_sp) {
g_signal_stack.ss_flags = SS_DISABLE;
sigaltstack(&g_signal_stack, 0);
sentry_free(g_signal_stack.ss_sp);
g_signal_stack.ss_sp = NULL;
}
reset_signal_handlers();
}

Expand Down Expand Up @@ -184,6 +189,7 @@ static int
startup_inproc_backend(
sentry_backend_t *UNUSED(backend), const sentry_options_t *UNUSED(options))
{
sentry__reserve_thread_stack();
g_previous_handler = SetUnhandledExceptionFilter(&handle_exception);
SetErrorMode(SEM_FAILCRITICALERRORS);
return 0;
Expand Down
35 changes: 34 additions & 1 deletion src/sentry_os.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#ifdef SENTRY_PLATFORM_WINDOWS

# include <winver.h>
# include <windows.h>
# define CURRENT_VERSION "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"

void *
Expand Down Expand Up @@ -137,6 +137,39 @@ sentry__get_os_context(void)
return sentry_value_new_null();
}

void
sentry__reserve_thread_stack(void)
{
const unsigned long expected_stack_size = 64 * 1024;
unsigned long stack_size = 0;
SetThreadStackGuarantee(&stack_size);
if (stack_size < expected_stack_size) {
stack_size = expected_stack_size;
SetThreadStackGuarantee(&stack_size);
}
}

# if defined(SENTRY_BUILD_SHARED)

BOOL APIENTRY
DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
(void)hModule;
(void)lpReserved;

switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
sentry__reserve_thread_stack();
break;
default:
return TRUE;
}
return TRUE;
}

# endif // defined(SENTRY_BUILD_SHARED)

#elif defined(SENTRY_PLATFORM_MACOS)

# include <sys/sysctl.h>
Expand Down
3 changes: 3 additions & 0 deletions src/sentry_os.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "sentry_boot.h"

#ifdef SENTRY_PLATFORM_WINDOWS

typedef struct {
uint32_t major;
uint32_t minor;
Expand All @@ -13,6 +14,8 @@ typedef struct {

int sentry__get_kernel_version(windows_version_t *win_ver);
int sentry__get_windows_version(windows_version_t *win_ver);
void sentry__reserve_thread_stack(void);

#endif

sentry_value_t sentry__get_os_context(void);
Expand Down
26 changes: 17 additions & 9 deletions tests/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,7 @@ def assert_stacktrace(envelope, inside_exception=False, check_size=True):
)


def assert_breadcrumb(envelope):
event = envelope.get_event()

def assert_breadcrumb_inner(breadcrumbs):
expected = {
"type": "http",
"message": "debug crumb",
Expand All @@ -169,7 +167,12 @@ def assert_breadcrumb(envelope):
"reason": "OK",
},
}
assert any(matches(b, expected) for b in event["breadcrumbs"])
assert any(matches(b, expected) for b in breadcrumbs)


def assert_breadcrumb(envelope):
event = envelope.get_event()
assert_breadcrumb_inner(event["breadcrumbs"])


def assert_attachment(envelope):
Expand Down Expand Up @@ -297,17 +300,22 @@ def _validate_breadcrumb_seq(seq, breadcrumb_func):
assert is_valid_timestamp(breadcrumb["timestamp"])


def assert_crashpad_upload(req):
multipart = gzip.decompress(req.get_data())
msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart)
attachments = _load_crashpad_attachments(msg)

def assert_overflowing_breadcrumb(attachments):
if len(attachments.breadcrumb1) > 3:
_validate_breadcrumb_seq(range(97), lambda i: attachments.breadcrumb1[3 + i])
_validate_breadcrumb_seq(
range(97, 101), lambda i: attachments.breadcrumb2[i - 97]
)
else:
assert_breadcrumb_inner(attachments.breadcrumb1)


def assert_crashpad_upload(req):
multipart = gzip.decompress(req.get_data())
msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart)
attachments = _load_crashpad_attachments(msg)

assert_overflowing_breadcrumb(attachments)
assert attachments.event["level"] == "fatal"

assert any(
Expand Down
43 changes: 39 additions & 4 deletions tests/test_integration_crashpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ def test_crashpad_reinstall(cmake, httpserver):
def test_crashpad_wer_crash(cmake, httpserver, run_args):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"})

# If we are building on a Windows without WER enabled this test doesn't make sense
if not os.path.exists(tmp_path / "crashpad_wer.dll"):
return

# make sure we are isolated from previous runs
shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True)

Expand Down Expand Up @@ -193,6 +189,45 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args):
assert_crashpad_upload(multipart)


def test_crashpad_dumping_stack_overflow(cmake, httpserver):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"})

# make sure we are isolated from previous runs
shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True)

env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK")
httpserver.expect_request("/api/123456/envelope/").respond_with_data("OK")

with httpserver.wait(timeout=10) as waiting:
child = run(
tmp_path,
"sentry_example",
["log", "start-session", "attachment", "stack-overflow"],
env=env,
)
assert child.returncode # well, it's a crash after all

assert waiting.result

# the session crash heuristic on Mac uses timestamps, so make sure we have
# a small delay here
time.sleep(1)

run(tmp_path, "sentry_example", ["log", "no-setup"], check=True, env=env)

assert len(httpserver.log) == 2
session, multipart = (
(httpserver.log[0][0], httpserver.log[1][0])
if is_session_envelope(httpserver.log[0][0].get_data())
else (httpserver.log[1][0], httpserver.log[0][0])
)

envelope = Envelope.deserialize(session.get_data())
assert_session(envelope, {"status": "crashed", "errors": 1})
assert_crashpad_upload(multipart)


def is_session_envelope(data):
return b'"type":"session"' in data

Expand Down
Loading
Loading