Skip to content

Commit c41a976

Browse files
fix panic with Python 3.13 shim frames (#787)
File/function names in some Py3.13 frames are empty strings. It appears that the caller (get_stack_trace) handles this condition and expects the callee (copy_string) to return an error. The callee however panics by copying zero bytes into a vector and then calling as_ptr on it, which returns a wild pointer (0x01), which is then passed to std::slice::from_raw_parts. This is not OK even if the length passed to from_raw_parts is 0.
1 parent b650375 commit c41a976

4 files changed

Lines changed: 14 additions & 3 deletions

File tree

src/python_data_access.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub fn copy_string<T: StringObject, P: ProcessMemory>(
1515
process: &P,
1616
) -> Result<String, Error> {
1717
let obj = process.copy_pointer(ptr)?;
18+
if obj.size() == 0 {
19+
return Ok(String::new());
20+
}
1821
if obj.size() >= 4096 {
1922
return Err(format_err!(
2023
"Refusing to copy {} chars of a string",

src/stack_trace.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ where
155155
// would also have to figure out what the address of PyCode_Type is (which will be
156156
// easier if something like https://github.com/python/cpython/issues/100987#issuecomment-1487227139
157157
// is merged )
158-
// Unset file/function name in py3.13 means this is a shim.
159158
if filename.is_err() || name.is_err() {
160159
frame_ptr = frame.back();
161160
set_last_frame_as_shim_entry(&mut frames);
@@ -165,7 +164,8 @@ where
165164
let name = name?;
166165

167166
// skip <shim> entries in python 3.12+
168-
if filename == "<shim>" {
167+
// Unset file/function name in py3.13 means this is a shim.
168+
if filename.is_empty() || filename == "<shim>" {
169169
frame_ptr = frame.back();
170170
set_last_frame_as_shim_entry(&mut frames);
171171
continue;

tests/integration_test.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ fn test_local_vars() {
280280
let frame = &trace.frames[0];
281281
let locals = frame.locals.as_ref().unwrap();
282282

283-
assert_eq!(locals.len(), 28);
283+
assert_eq!(locals.len(), 29);
284284

285285
let arg1 = &locals[0];
286286
assert_eq!(arg1.name, "arg1");
@@ -422,6 +422,11 @@ fn test_local_vars() {
422422
let end = unicode_val.char_indices().map(|(i, _)| i).nth(4).unwrap();
423423
assert_eq!(unicode_val[0..end], *"\"测试1");
424424

425+
// Empty string
426+
let local26 = &locals[28];
427+
assert_eq!(local26.name, "local26");
428+
assert_eq!(local26.repr, Some("\"\"".to_string()));
429+
425430
// we only support dictionary lookup on python 3.6+ right now
426431
if runner.spy.version.major == 3 && runner.spy.version.minor >= 6 {
427432
assert_eq!(

tests/scripts/local_vars.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def local_variable_lookup(arg1="foo", arg2=None, arg3=True):
4242
# https://github.com/benfred/py-spy/issues/766
4343
local25 = "测试1" * 500
4444

45+
# Empty strings should not be ignored
46+
local26 = ""
47+
4548
time.sleep(100000)
4649

4750

0 commit comments

Comments
 (0)