Skip to content

Commit 07524ca

Browse files
committed
Harden --locals against trashed memory
When attempting to produce a repr for local variables, attempt to cope better with trashed memory: - Use "<invalid object at 0x1234>" as the repr of a `PyObject*` pointing directly at invalid memory, or one whose `ob_type` points at invalid memory. - Use "<list object at 0x1234>" as, for instance, the repr of a list containing an invalid `ob_items` pointer. More generally, use this for any object where we can determine the type, but generating the repr according to that type's rules fails due to accessing unmapped memory. Test this with some ctypes abuse. Signed-off-by: Matt Wozniski <[email protected]>
1 parent c0b8b49 commit 07524ca

File tree

3 files changed

+119
-21
lines changed

3 files changed

+119
-21
lines changed

news/194.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Heap corruption could cause PyStack to fail to generate a stack when ``--locals`` mode was used. This has been fixed by falling back to a reasonable default when attempting to format the repr of a local variable causes a dereference of an invalid pointer.

src/pystack/_pystack/pytypes.cpp

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ std::string
503503
GenericObject::toString(ssize_t max_size) const
504504
{
505505
std::stringstream os;
506-
os << "<" << std::hex << std::showbase << d_classname << " at " << d_addr << ">";
506+
os << "<" << std::hex << d_classname << " at 0x" << d_addr << ">";
507507
std::string object_str = os.str();
508508
return limitOutput(object_str, max_size);
509509
}
@@ -521,19 +521,39 @@ NoneObject::toString([[maybe_unused]] ssize_t max_size) const
521521

522522
Object::Object(const std::shared_ptr<const AbstractProcessManager>& manager, remote_addr_t addr)
523523
: d_addr(addr)
524+
, d_type_addr(0)
525+
, d_classname()
526+
, d_flags()
524527
, d_manager(manager)
525528
{
526529
LOG(DEBUG) << std::hex << std::showbase << "Copying PyObject data from address " << addr;
527530

528531
PyObject obj;
529-
manager->copyObjectFromProcess(d_addr, &obj);
532+
try {
533+
manager->copyObjectFromProcess(d_addr, &obj);
534+
} catch (RemoteMemCopyError& ex) {
535+
LOG(WARNING) << std::hex << std::showbase << "Failed to read PyObject data from address "
536+
<< d_addr;
537+
d_classname = "invalid object";
538+
return;
539+
}
530540

531541
PyTypeObject cls;
532542
LOG(DEBUG) << std::hex << std::showbase << "Copying typeobject from address " << obj.ob_type;
533543
d_type_addr = reinterpret_cast<remote_addr_t>(obj.ob_type);
534-
manager->copyMemoryFromProcess((remote_addr_t)obj.ob_type, manager->offsets().py_type.size, &cls);
544+
try {
545+
manager->copyMemoryFromProcess(
546+
(remote_addr_t)obj.ob_type,
547+
manager->offsets().py_type.size,
548+
&cls);
535549

536-
d_flags = manager->getField(cls, &py_type_v::o_tp_flags);
550+
d_flags = manager->getField(cls, &py_type_v::o_tp_flags);
551+
} catch (RemoteMemCopyError& ex) {
552+
LOG(WARNING) << std::hex << std::showbase << "Failed to read typeobject from address "
553+
<< obj.ob_type;
554+
d_classname = "invalid object";
555+
return;
556+
}
537557

538558
remote_addr_t name_addr = manager->getField(cls, &py_type_v::o_tp_name);
539559
try {
@@ -577,24 +597,33 @@ Object::toString(ssize_t max_size) const
577597
if (max_size <= 5) {
578598
return ELLIPSIS;
579599
}
580-
std::stringstream os;
581-
std::visit(
582-
overloaded{
583-
[&](const auto& arg) { os << arg.toString(max_size); },
584-
[&](const bool arg) { os << arg; },
585-
[&](const long arg) { os << arg; },
586-
[&](const double arg) { os << arg; },
587-
[&](const std::string& arg) {
588-
std::string truncated = arg;
589-
if (static_cast<size_t>(max_size) < arg.size()) {
590-
truncated = arg.substr(0, max_size - 3) + "...";
591-
}
592-
os << truncated;
593-
},
594-
},
595-
toConcreteObject());
600+
try {
601+
std::stringstream os;
602+
std::visit(
603+
overloaded{
604+
[&](const auto& arg) { os << arg.toString(max_size); },
605+
[&](const bool arg) { os << arg; },
606+
[&](const long arg) { os << arg; },
607+
[&](const double arg) { os << arg; },
608+
[&](const std::string& arg) {
609+
std::string truncated = arg;
610+
if (static_cast<size_t>(max_size) < arg.size()) {
611+
truncated = arg.substr(0, max_size - 3) + "...";
612+
}
613+
os << truncated;
614+
},
615+
},
616+
toConcreteObject());
617+
618+
return os.str();
619+
} catch (RemoteMemCopyError& ex) {
620+
LOG(WARNING) << std::hex << std::showbase << "Failed to create a repr for object of type "
621+
<< d_classname << " at address " << d_addr;
596622

597-
return os.str();
623+
std::stringstream os;
624+
os << std::hex << std::showbase << "<" << d_classname << " object at " << d_addr << ">";
625+
return os.str();
626+
}
598627
}
599628

600629
long

tests/integration/test_local_variables.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,3 +558,71 @@ def second_func(the_argument):
558558

559559
second_func_frame = find_frame(frames, "second_func")
560560
assert "the_argument" not in second_func_frame.arguments
561+
562+
563+
@ALL_PYTHONS
564+
@ALL_SOURCES
565+
def test_trashed_locals(generate_threads, python, tmpdir):
566+
# GIVEN
567+
_, python_executable = python
568+
569+
program_template = """
570+
import ctypes
571+
import sys
572+
import time
573+
574+
class ListObject(ctypes.Structure):
575+
_fields_ = [
576+
("ob_refcnt", ctypes.c_ssize_t),
577+
("ob_type", ctypes.c_void_p),
578+
("ob_size", ctypes.c_ssize_t),
579+
("ob_item", ctypes.c_void_p),
580+
]
581+
582+
class TupleObject(ctypes.Structure):
583+
_fields_ = [
584+
("ob_refcnt", ctypes.c_ssize_t),
585+
("ob_type", ctypes.c_void_p),
586+
("ob_size", ctypes.c_ssize_t),
587+
("ob_item0", ctypes.c_void_p),
588+
("ob_item1", ctypes.c_void_p),
589+
]
590+
591+
def main():
592+
bad_type = (1, 2, 3)
593+
bad_elem = (4, 5, 6)
594+
nullelem = (7, 8, 9)
595+
bad_list = [0, 1, 2]
596+
597+
TupleObject.from_address(id(bad_type)).ob_type = 0xded
598+
TupleObject.from_address(id(bad_elem)).ob_item1 = 0xbad
599+
TupleObject.from_address(id(nullelem)).ob_item1 = 0x0
600+
ListObject.from_address(id(bad_list)).ob_item = 0x0
601+
602+
fifo = sys.argv[1]
603+
with open(sys.argv[1], "w") as fifo:
604+
fifo.write("ready")
605+
time.sleep(1000)
606+
607+
main()
608+
"""
609+
610+
script = tmpdir / "the_script.py"
611+
script.write_text(program_template, encoding="utf-8")
612+
613+
# WHEN
614+
threads = generate_threads(python_executable, script, tmpdir, locals=True)
615+
616+
# THEN
617+
618+
assert len(threads) == 1
619+
(thread,) = threads
620+
621+
frames = list(thread.frames)
622+
assert (len(frames)) == 2
623+
624+
main = find_frame(frames, "main")
625+
assert re.match(r"^<invalid object at 0x[0-9a-f]{4,}>$", main.locals["bad_type"])
626+
assert main.locals["bad_elem"] == "(4, <invalid object at 0xbad>, 6)"
627+
assert main.locals["nullelem"] == "(7, <invalid object at 0x0>, 9)"
628+
assert re.match(r"^<list object at 0x[0-9a-f]{4,}>$", main.locals["bad_list"])

0 commit comments

Comments
 (0)