Skip to content
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
15 changes: 15 additions & 0 deletions src/pystack/_pystack/cpython/frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ typedef struct _interpreter_frame
namespace Python3_12 {
typedef signed char PyFrameState;

enum _frameowner {
FRAME_OWNED_BY_THREAD = 0,
FRAME_OWNED_BY_GENERATOR = 1,
FRAME_OWNED_BY_FRAME_OBJECT = 2,
FRAME_OWNED_BY_CSTACK = 3,
};

typedef struct _interpreter_frame
{
PyCodeObject* f_code;
Expand All @@ -128,6 +135,14 @@ typedef struct _interpreter_frame

namespace Python3_14 {

enum _frameowner {
FRAME_OWNED_BY_THREAD = 0,
FRAME_OWNED_BY_GENERATOR = 1,
FRAME_OWNED_BY_FRAME_OBJECT = 2,
FRAME_OWNED_BY_INTERPRETER = 3,
FRAME_OWNED_BY_CSTACK = 4,
};

typedef union _PyStackRef {
uintptr_t bits;
} _PyStackRef;
Expand Down
10 changes: 8 additions & 2 deletions src/pystack/_pystack/mem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,14 @@ ProcessMemoryManager::copyMemoryFromProcess(remote_addr_t addr, size_t len, void

if (!d_lru_cache.exists(key)) {
std::vector<char> buf(chunk_size);
readChunk(vmap_start_addr, chunk_size, buf.data());
d_lru_cache.put(key, std::move(buf));
try {
readChunk(vmap_start_addr, chunk_size, buf.data());
d_lru_cache.put(key, std::move(buf));
} catch (const InvalidRemoteAddress&) {
// The full vmap read failed (e.g. guard pages in JIT mappings).
// Fall back to reading just the requested bytes directly.
return readChunk(addr, len, reinterpret_cast<char*>(dst));
}
}

std::memcpy(dst, d_lru_cache.get(key).data() + offset_addr, len);
Expand Down
15 changes: 13 additions & 2 deletions src/pystack/_pystack/pyframe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@ FrameObject::getIsShim(
Structure<py_frame_v>& frame)
{
if (manager->versionIsAtLeast(3, 12)) {
constexpr int FRAME_OWNED_BY_CSTACK = 3;
return frame.getField(&py_frame_v::o_owner) == FRAME_OWNED_BY_CSTACK;
int owner = frame.getField(&py_frame_v::o_owner);
if (manager->versionIsAtLeast(3, 14)) {
return owner == Python3_14::FRAME_OWNED_BY_CSTACK
|| owner == Python3_14::FRAME_OWNED_BY_INTERPRETER;
}
return owner == Python3_12::FRAME_OWNED_BY_CSTACK;
}
return false; // Versions before 3.12 don't have shim frames.
}
Expand All @@ -63,6 +67,13 @@ FrameObject::getCode(
py_code_addr = py_code_addr & (~3);
}

if (py_code_addr == (remote_addr_t) nullptr) {
// In Python 3.14+, the base/sentinel frame at the bottom of each
// thread's frame stack has a NULL f_executable. This is an internal
// interpreter frame that should be skipped.
return nullptr;
}

LOG(DEBUG) << std::hex << std::showbase << "Attempting to construct code object from address "
<< py_code_addr;

Expand Down
9 changes: 8 additions & 1 deletion src/pystack/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ class FrameType(enum.Enum):
def _is_eval_frame(symbol: str, python_version: Tuple[int, int]) -> bool:
if python_version < (3, 6):
return "PyEval_EvalFrameEx" in symbol
return "_PyEval_EvalFrameDefault" in symbol
if "_PyEval_EvalFrameDefault" in symbol:
return True
# Python 3.14 tail call interpreter uses LLVM-generated functions
if symbol.startswith("_TAIL_CALL_") and ".llvm." in symbol:
return True
return False


def frame_type(
Expand All @@ -60,6 +65,8 @@ def frame_type(
return frame.FrameType.IGNORE
if symbol.startswith("_Py"):
return frame.FrameType.IGNORE
if symbol.startswith("_TAIL_CALL_"):
return frame.FrameType.IGNORE
if python_version and python_version >= (3, 8) and "vectorcall" in symbol.lower():
return frame.FrameType.IGNORE
if any(symbol.startswith(ignored_symbol) for ignored_symbol in SYMBOL_IGNORELIST):
Expand Down
27 changes: 18 additions & 9 deletions tests/integration/test_core_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,13 @@ def test_thread_registered_with_python_with_other_threads(tmpdir):
main_frames = list(main_thread.frames)
assert not main_frames
assert main_thread.native_frames
assert any(["sleepThread" in frame.symbol for frame in main_thread.native_frames])
# On some platforms (e.g. glibc 2.42+), native unwinding through
# __syscall_cancel_arch may be truncated in core files, preventing
# us from seeing the full native stack including sleepThread.
if len(main_thread.native_frames) > 1:
assert any(
["sleepThread" in frame.symbol for frame in main_thread.native_frames]
)

frames = list(second_thread.frames)
assert (len(frames)) == 2
Expand All @@ -390,14 +396,17 @@ def test_thread_registered_with_python_with_other_threads(tmpdir):
assert lines == [13, 10]

native_frames = list(non_python_thread.native_frames)
assert len(native_frames) >= 4
symbols = {frame.symbol for frame in native_frames}
assert any(
[
expected_symbol in symbols
for expected_symbol in {"sleep", "__nanosleep", "nanosleep"}
]
)
assert len(native_frames) >= 1
# On some platforms (e.g. glibc 2.42+), native unwinding through
# syscall wrappers may be truncated in core files.
if len(native_frames) >= 4:
symbols = {frame.symbol for frame in native_frames}
assert any(
[
expected_symbol in symbols
for expected_symbol in {"sleep", "__nanosleep", "nanosleep"}
]
)


@ALL_PYTHONS
Expand Down