Skip to content

Commit 85a6c5a

Browse files
committed
Add tests for the ghost unwinding
1 parent ea497f3 commit 85a6c5a

File tree

8 files changed

+615
-7
lines changed

8 files changed

+615
-7
lines changed

setup.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,25 @@ def build_js_files(self):
337337
name="memray._test_utils",
338338
sources=[
339339
"src/memray/_memray_test_utils.pyx",
340+
"src/memray/_memray/ghost_stack_test_utils.cpp",
341+
*GHOST_STACK_SOURCES,
340342
],
341343
language="c++",
342344
extra_compile_args=["-std=c++17", "-Wall", *EXTRA_COMPILE_ARGS],
343345
extra_link_args=["-std=c++17", *EXTRA_LINK_ARGS],
346+
extra_objects=[*GHOST_STACK_OBJECTS],
344347
define_macros=DEFINE_MACROS,
345348
undef_macros=UNDEF_MACROS,
346349
)
347350

351+
MEMRAY_TEST_EXTENSION.include_dirs = [
352+
"src",
353+
str(GHOST_STACK_LOCATION / "include"),
354+
]
355+
356+
if IS_LINUX:
357+
MEMRAY_TEST_EXTENSION.libraries = ["unwind"]
358+
348359
MEMRAY_INJECT_EXTENSION = Extension(
349360
name="memray._inject",
350361
sources=[

src/memray/_memray/ghost_stack/src/ghost_stack.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,9 @@ class GhostStackImpl {
198198
trampolines_installed_ = false;
199199

200200
// Increment epoch to signal state change
201-
uint64_t new_epoch = epoch_.fetch_add(1, std::memory_order_release) + 1;
202-
LOG_DEBUG(" New epoch=%lu (entries preserved for stale trampolines)\n", (unsigned long)new_epoch);
201+
epoch_.fetch_add(1, std::memory_order_release);
202+
LOG_DEBUG(" New epoch=%lu (entries preserved for stale trampolines)\n",
203+
(unsigned long)epoch_.load(std::memory_order_acquire));
203204
}
204205
LOG_DEBUG("=== reset EXIT ===\n");
205206
}
@@ -240,8 +241,8 @@ class GhostStackImpl {
240241
(unsigned long)epoch_.load(std::memory_order_acquire));
241242

242243
// Increment epoch FIRST to signal any in-flight operations
243-
uint64_t new_epoch = epoch_.fetch_add(1, std::memory_order_release) + 1;
244-
LOG_DEBUG(" New epoch=%lu\n", (unsigned long)new_epoch);
244+
epoch_.fetch_add(1, std::memory_order_release);
245+
LOG_DEBUG(" New epoch=%lu\n", (unsigned long)epoch_.load(std::memory_order_acquire));
245246

246247
entries_.clear();
247248
tail_.store(0, std::memory_order_release);
@@ -347,9 +348,8 @@ class GhostStackImpl {
347348
// Only update tail_ if we find a match - don't corrupt it during search
348349
for (size_t i = tail; i > 0; --i) {
349350
if (entries_[i - 1].stack_pointer == sp) {
350-
size_t skipped = tail - (i - 1);
351351
LOG_DEBUG("longjmp detected: found matching SP at index %zu (skipped %zu frames)\n",
352-
i - 1, skipped);
352+
i - 1, tail - (i - 1));
353353

354354
// Update tail_ to skip all the frames that were bypassed by longjmp
355355
tail_.store(i - 1, std::memory_order_release);
@@ -723,4 +723,4 @@ uintptr_t ghost_exception_handler(void* exception) {
723723
return ret;
724724
}
725725

726-
} // extern "C"
726+
} // extern
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include "ghost_stack_test_utils.h"
2+
3+
#include <Python.h>
4+
#include <cstdint>
5+
6+
#ifdef MEMRAY_HAS_GHOST_STACK
7+
# include "ghost_stack.h"
8+
# define UNW_LOCAL_ONLY
9+
# include <libunwind.h>
10+
#endif
11+
12+
extern "C" {
13+
14+
PyObject*
15+
ghost_stack_test_backtrace(void)
16+
{
17+
#ifdef MEMRAY_HAS_GHOST_STACK
18+
void* frames[256];
19+
size_t n = ghost_stack_backtrace(frames, 256);
20+
PyObject* result = PyList_New(static_cast<Py_ssize_t>(n));
21+
if (!result) return nullptr;
22+
for (size_t i = 0; i < n; i++) {
23+
PyObject* addr = PyLong_FromUnsignedLongLong(reinterpret_cast<uintptr_t>(frames[i]));
24+
if (!addr) {
25+
Py_DECREF(result);
26+
return nullptr;
27+
}
28+
PyList_SET_ITEM(result, static_cast<Py_ssize_t>(i), addr);
29+
}
30+
return result;
31+
#else
32+
Py_RETURN_NONE;
33+
#endif
34+
}
35+
36+
PyObject*
37+
libunwind_test_backtrace(void)
38+
{
39+
#ifdef MEMRAY_HAS_GHOST_STACK
40+
void* frames[256];
41+
int n = unw_backtrace(frames, 256);
42+
if (n < 0) n = 0;
43+
PyObject* result = PyList_New(static_cast<Py_ssize_t>(n));
44+
if (!result) return nullptr;
45+
for (int i = 0; i < n; i++) {
46+
PyObject* addr = PyLong_FromUnsignedLongLong(reinterpret_cast<uintptr_t>(frames[i]));
47+
if (!addr) {
48+
Py_DECREF(result);
49+
return nullptr;
50+
}
51+
PyList_SET_ITEM(result, static_cast<Py_ssize_t>(i), addr);
52+
}
53+
return result;
54+
#else
55+
Py_RETURN_NONE;
56+
#endif
57+
}
58+
59+
void
60+
ghost_stack_test_reset(void)
61+
{
62+
#ifdef MEMRAY_HAS_GHOST_STACK
63+
ghost_stack_reset();
64+
#endif
65+
}
66+
67+
void
68+
ghost_stack_test_init(void)
69+
{
70+
#ifdef MEMRAY_HAS_GHOST_STACK
71+
ghost_stack_init(nullptr);
72+
#endif
73+
}
74+
75+
int
76+
ghost_stack_test_has_support(void)
77+
{
78+
#ifdef MEMRAY_HAS_GHOST_STACK
79+
return 1;
80+
#else
81+
return 0;
82+
#endif
83+
}
84+
85+
} // extern "C"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <Python.h>
4+
#include <stddef.h>
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
// Returns a Python list of frame addresses from ghost_stack_backtrace
11+
// Returns Py_None if MEMRAY_HAS_GHOST_STACK is not defined
12+
PyObject* ghost_stack_test_backtrace(void);
13+
14+
// Returns a Python list of frame addresses from unw_backtrace (libunwind)
15+
// Returns Py_None if MEMRAY_HAS_GHOST_STACK is not defined
16+
PyObject* libunwind_test_backtrace(void);
17+
18+
// Reset ghost_stack shadow stack
19+
void ghost_stack_test_reset(void);
20+
21+
// Initialize ghost_stack
22+
void ghost_stack_test_init(void);
23+
24+
// Check if ghost_stack support is available
25+
int ghost_stack_test_has_support(void);
26+
27+
#ifdef __cplusplus
28+
}
29+
#endif

src/memray/_memray_test_utils.pyx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,50 @@ cdef class PrimeCaches:
285285
return self
286286
def __exit__(self, *args):
287287
sys.setprofile(self.old_profile)
288+
289+
290+
# Ghost stack test utilities
291+
cdef extern from "_memray/ghost_stack_test_utils.h":
292+
object ghost_stack_test_backtrace()
293+
object libunwind_test_backtrace()
294+
void ghost_stack_test_reset()
295+
void ghost_stack_test_init()
296+
int ghost_stack_test_has_support()
297+
298+
299+
def has_ghost_stack_support():
300+
"""Check if ghost_stack support is available."""
301+
return ghost_stack_test_has_support() != 0
302+
303+
304+
cdef class GhostStackTestContext:
305+
"""Context manager for ghost_stack testing.
306+
307+
Usage:
308+
with GhostStackTestContext() as ctx:
309+
frames = ctx.backtrace()
310+
libunwind_frames = ctx.libunwind_backtrace()
311+
"""
312+
313+
def __enter__(self):
314+
# init is defensive in case ghost_stack wasn't initialized globally;
315+
# reset clears any stale shadow stack state from previous operations
316+
ghost_stack_test_init()
317+
ghost_stack_test_reset()
318+
return self
319+
320+
def __exit__(self, exc_type, exc_val, exc_tb):
321+
ghost_stack_test_reset()
322+
return False
323+
324+
def backtrace(self):
325+
"""Capture ghost_stack frames and return as list of addresses."""
326+
return ghost_stack_test_backtrace()
327+
328+
def libunwind_backtrace(self):
329+
"""Capture libunwind frames for comparison."""
330+
return libunwind_test_backtrace()
331+
332+
def reset(self):
333+
"""Reset ghost_stack shadow stack."""
334+
ghost_stack_test_reset()

0 commit comments

Comments
 (0)