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
30 changes: 30 additions & 0 deletions Lib/test/_test_monitoring_shutdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3

# gh-115832: An object destructor running during the final GC of interpreter
# shutdown triggered an infinite loop in the instrumentation code.

import sys

class CallableCycle:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep executable fixtures out of restricted test changes

Adding this class creates new executable test logic in Lib/test, but the repository's root instructions permit test-file changes only to add or remove @unittest.expectedFailure markers and their associated TODO comments. Restore the shutdown test's marker rather than adding this helper so the change stays within the allowed test-update policy.

AGENTS.md reference: AGENTS.md:L274-L282

Useful? React with 👍 / 👎.

def __init__(self):
self._cycle = self

def __del__(self):
pass

def __call__(self, code, instruction_offset):
pass

def tracefunc(frame, event, arg):
pass

def main():
tool_id = sys.monitoring.PROFILER_ID
event_id = sys.monitoring.events.PY_START

sys.monitoring.use_tool_id(tool_id, "test profiler")
sys.monitoring.set_events(tool_id, event_id)
sys.monitoring.register_callback(tool_id, event_id, CallableCycle())

if __name__ == "__main__":
sys.exit(main())
1 change: 0 additions & 1 deletion Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2576,7 +2576,6 @@ def test_func(recorder):

class TestMonitoringAtShutdown(unittest.TestCase):

@unittest.expectedFailure # TODO: RUSTPYTHON; - requires subprocess support
def test_monitoring_live_at_shutdown(self):
# gh-115832: An object destructor running during the final GC of
# interpreter shutdown triggered an infinite loop in the
Expand Down
7 changes: 6 additions & 1 deletion crates/compiler-core/src/marshal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ fn read_const_value<R: Read, Bag: ConstantBag>(
/// Object-valued code fields as read from the stream (`r_object` TYPE_CODE).
pub struct CodeFields<V> {
pub consts: V,
pub qualname: V,
pub names: V,
pub localsplusnames: V,
pub localspluskinds: V,
Expand Down Expand Up @@ -977,7 +978,10 @@ fn deserialize_code_value_inner<R: Read, Bag: MarshalBag>(
};
let source_path_raw = read_string(rdr, refs)?;
let obj_name_raw = read_string(rdr, refs)?;
let qualname_raw = read_string(rdr, refs)?;
let qualname_value = deserialize_value_depth(rdr, bag, child_depth, refs)?;
let qualname_raw = bag
.str_from_value(&qualname_value)
.ok_or(MarshalError::BadType)?;

let first_line_raw = rdr.read_u32()? as i32;
let first_line_number = if first_line_raw > 0 {
Expand Down Expand Up @@ -1049,6 +1053,7 @@ fn deserialize_code_value_inner<R: Read, Bag: MarshalBag>(
code,
CodeFields {
consts: consts_value,
qualname: qualname_value,
names: names_value,
localsplusnames: localsplusnames_value,
localspluskinds: kinds_value,
Expand Down
115 changes: 107 additions & 8 deletions crates/vm/src/builtins/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,9 @@ pub trait IntoCodeObject {
where
Self: Sized,
{
PyCode::new(self.into_code_object(vm), &vm.ctx)
let mut code = PyCode::new(self.into_code_object(vm), &vm.ctx);
code.apply_debug_ranges(vm);
code
}
}

Expand Down Expand Up @@ -444,14 +446,15 @@ pub struct PyCode {
#[pymember(name = "co_posonlyargcount", path = "posonlyarg_count")]
#[pymember(name = "co_kwonlyargcount", path = "kwonlyarg_count")]
#[pymember(name = "co_stacksize", path = "max_stackdepth")]
#[pymember(name = "co_qualname", path = "qualname")]
#[pymember(name = "co_flags", path = "flags")]
pub code: CodeObject,
/// Slot-indexed names, equivalent to CPython's `co_localsplusnames`.
/// Derived once so frame-local proxy operations do not repeatedly scan
/// merged cell variables.
localsplus_names: Box<[&'static PyStrInterned]>,
pub(crate) co_name: PyStrRef,
#[pymember(name = "co_qualname")]
qualified_name: PyStrRef,
pub(crate) co_names: PyTupleRef,
pub(crate) co_localsplusnames: PyTupleRef,
pub(crate) co_localspluskinds: PyBytesRef,
Expand Down Expand Up @@ -625,6 +628,7 @@ impl PyCode {
Self {
first_traceable,
co_name: code.obj_name.to_owned(),
qualified_name: code.qualname.to_owned(),
co_names: names_tuple(&code.names),
co_localsplusnames: names_tuple(&localsplus_names),
co_localspluskinds: ctx.new_bytes(code.localspluskinds.to_vec()),
Expand Down Expand Up @@ -654,6 +658,10 @@ impl PyCode {
obj.downcast().map_err(|_| MarshalError::BadType)
}
code.constants = bytecode::Constants::new(LiteralTuple(downcast(fields.consts)?));
// The compiler's string conversion is lossy for surrogates; retain the
// decoded value and apply code construction's exact-string interning.
let qualname = downcast::<super::PyStr>(fields.qualname)?;
code.qualname = ctx.intern_str(qualname.as_wtf8());
let mut code = Self::new(code, ctx);
code.set_names(downcast(fields.names)?);
code.set_localsplusnames(downcast(fields.localsplusnames)?);
Expand All @@ -678,6 +686,19 @@ impl PyCode {
}
}

pub(crate) fn apply_debug_ranges(&mut self, vm: &VirtualMachine) {
if !vm.state.config.settings.code_debug_ranges
&& let Some(linetable) = remove_column_info(&self.code.linetable)
{
self.code.linetable = linetable;
self.co_linetable = vm.ctx.new_bytes(self.code.linetable.to_vec());
}
}

pub(crate) fn co_qualname(&self) -> PyStrRef {
self.qualified_name.clone()
}

#[inline(always)]
pub(crate) fn localsplus_name(&self, index: usize) -> &'static PyStrInterned {
// Bytecode operands and frame-proxy indices are validated against
Expand Down Expand Up @@ -734,11 +755,9 @@ impl PyCode {
}

pub fn new_ref_with_bag(vm: &VirtualMachine, code: CodeObject) -> PyRef<Self> {
PyRef::new_ref(
Self::new(code, &vm.ctx),
vm.ctx.types.code_type.to_owned(),
None,
)
let mut code = Self::new(code, &vm.ctx);
code.apply_debug_ranges(vm);
PyRef::new_ref(code, vm.ctx.types.code_type.to_owned(), None)
}

pub fn new_ref_from_bytecode(vm: &VirtualMachine, code: bytecode::CodeObject) -> PyRef<Self> {
Expand Down Expand Up @@ -1170,9 +1189,13 @@ impl Constructor for PyCode {

let mut code = Self::new(code, &vm.ctx);
code.co_name = args.name;
if !args.qualname.class().is(vm.ctx.types.str_type) {
code.qualified_name = args.qualname;
}
code.set_names(args.names);
code.co_linetable = args.linetable;
code.co_exceptiontable = args.exceptiontable;
code.apply_debug_ranges(vm);
Ok(code)
}
}
Expand Down Expand Up @@ -1633,7 +1656,7 @@ impl Py<PyCode> {

let qualname = match co_qualname {
OptionalArg::Present(qualname) => qualname,
OptionalArg::Missing => self.code.qualname.to_owned(),
OptionalArg::Missing => self.qualified_name.clone(),
};

// Room for one value is always reserved, even where nothing is pushed.
Expand Down Expand Up @@ -1741,9 +1764,13 @@ impl Py<PyCode> {

let mut code = PyCode::new(new_code, &vm.ctx);
code.co_name = obj_name;
if !qualname.class().is(vm.ctx.types.str_type) {
code.qualified_name = qualname;
}
code.set_names(names);
code.co_linetable = linetable;
code.co_exceptiontable = exceptiontable;
code.apply_debug_ranges(vm);
Ok(code)
}

Expand Down Expand Up @@ -1800,6 +1827,78 @@ impl ToPyObject for bytecode::CodeObject {
}
}

// Preserve entry lengths and line deltas while dropping columns, as code
// construction does under -X no_debug_ranges in CPython. Decode and re-encode
// signed deltas to normalize redundant encodings, including negative zero.
// Leave unconvertible tables unchanged rather than inventing line deltas or
// introducing new constructor errors. This is not a general table validator.
fn remove_column_info(linetable: &[u8]) -> Option<Box<[u8]>> {
let mut result = Vec::with_capacity(linetable.len());
let mut reader = LineTableReader::new(linetable);
while let Some(header) = reader.read_byte() {
if header & 0x80 == 0 {
return None;
}
let kind = (header >> 3) & 15;
if kind == PyCodeLocationInfoKind::None as u8 {
result.push(header);
} else {
result.push(0x80 | ((PyCodeLocationInfoKind::NoColumns as u8) << 3) | (header & 7));
let delta = match PyCodeLocationInfoKind::from_code(kind) {
Some(PyCodeLocationInfoKind::Long | PyCodeLocationInfoKind::NoColumns) => {
read_location_delta(&mut reader)?
}
Some(PyCodeLocationInfoKind::OneLine1) => 1,
Some(PyCodeLocationInfoKind::OneLine2) => 2,
_ => 0,
};
let mut encoded = (delta.unsigned_abs() << 1) | u32::from(delta < 0);
while encoded >= 64 {
result.push(0x40 | (encoded & 0x3f) as u8);
encoded >>= 6;
}
result.push(encoded as u8);
}
while reader.peek_byte().is_some_and(|byte| byte & 0x80 == 0) {
reader.read_byte();
}
}
Some(result.into_boxed_slice())
}

// Location deltas use an unsigned 32-bit sign/magnitude representation. Allow
// redundant zero groups, but reject overflow and truncation without unchecked
// shifts or consuming the next header. This does not validate absolute lines.
fn read_location_delta(reader: &mut LineTableReader<'_>) -> Option<i32> {
let mut value = 0u32;
let mut shift = 0;
loop {
let byte = reader.peek_byte()?;
if byte & 0x80 != 0 {
return None;
}
reader.read_byte();
let digit = u32::from(byte & 0x3f);
if shift < u32::BITS {
if digit > (u32::MAX >> shift) {
return None;
}
value |= digit << shift;
} else if digit != 0 {
return None;
}
if byte & 0x40 == 0 {
let magnitude = (value >> 1) as i32;
return Some(if value & 1 != 0 {
-magnitude
} else {
magnitude
});
}
shift = (shift + 6).min(u32::BITS);
}
}

// Helper struct for reading linetable
struct LineTableReader<'a> {
data: &'a [u8],
Expand Down
1 change: 1 addition & 0 deletions crates/vm/src/builtins/code/const_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl CompileBag<'_> {
/// merges them, in the same order.
fn new_code(&self, code: CodeObject) -> PyCode {
let mut code = PyCode::new(code, &self.vm.ctx);
code.apply_debug_ranges(self.vm);
code.co_exceptiontable = self.merge_one(code.co_exceptiontable);
code.co_linetable = self.merge_one(code.co_linetable);
code.co_names = self.merge_one(code.co_names);
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ impl PyFunction {
vm.ctx.none()
};

let qualname = vm.ctx.new_str(code.qualname.as_str());
let qualname = code.co_qualname();
let func = Self {
code: PyAtomicRef::from(code),
globals,
Expand Down
Loading
Loading