Fix 3.15 script validation and exception cleanup contracts

Recognize common None loads when checking script returns. Keep the public cache tag version-derived while retaining the private DA16 format boundary. Release the thread-slot borrow before exception decrefs can finalize an interpreter.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
dot committed 2026-10-12 21:13:56 +09:00
1 parent 487ae47f3c
commit 2191a9d0c8
4 files changed
+31 -49

No files matched your search

+3 -2
View File
@@ -69,9 +69,10 @@ pub const RUSTPYTHON_VERSION: &str = const {
}; };
// Private, non-distributable reconstruction ABI. This is not CPython's magic. // Private, non-distributable reconstruction ABI. This is not CPython's magic.
// Change token and tag together for every incompatible development format. // Change the token for every incompatible development format. The public
// cache tag identifies the implementation/version; magic rejects stale ABIs.
pub const PYC_MAGIC_NUMBER: u16 = 0xDA16; pub const PYC_MAGIC_NUMBER: u16 = 0xDA16;
pub const PYC_CACHE_TAG: &str = "rustpython-315-devabi2"; pub const PYC_CACHE_TAG: &str = concat!("rustpython-", env!("MAJOR_CPY"), env!("MINOR_CPY"));
// CPython format: magic_number | ('\r' << 16) | ('\n' << 24) // CPython format: magic_number | ('\r' << 16) | ('\n' << 24)
// This protects against text-mode file reads // This protects against text-mode file reads
+18 -40
View File
@@ -15,7 +15,7 @@ use crate::{
}, },
bytecode::{ bytecode::{
BorrowedConstant, CodeFlags, Constant, Instruction, BorrowedConstant, CodeFlags, Constant, Instruction,
oparg::{OpArg, OpArgState}, oparg::{CommonConstant, OpArg, OpArgState},
}, },
protocol::PyBuffer, protocol::PyBuffer,
}; };
@@ -828,47 +828,25 @@ pub(crate) fn code_returns_only_none(code: &Py<PyCode>) -> bool {
if !is_pure_function(code) { if !is_pure_function(code) {
return false; return false;
} }
let units = &code.instructions; // Final 3.15 bytecode loads None through LOAD_COMMON_CONSTANT, so it
let Some(last) = units.len().checked_sub(1) else { // need not appear in co_consts. Walk decoded operands rather than looking
return true; // one raw code unit back, which can lose EXTENDED_ARG bits.
}; let mut previous_loads_none = false;
// The last instruction either returns or raises. We can take advantage for (_, op, arg) in walk_instructions(code) {
// of that for a quick exit. if matches!(op, Instruction::ExtendedArg) {
let final_op = units.read_op(last).deoptimize(); continue;
}
// Look up None in co_consts. if matches!(op, Instruction::ReturnValue) && !previous_loads_none {
let Some(none_index) = code
.constants
.iter()
.position(|c| matches!(c.borrow_constant(), BorrowedConstant::None))
else {
// None wasn't there, which means there was no implicit return,
// "return", or "return None".
// That means there must be an explicit return (non-None), or it only
// raises.
if matches!(final_op, Instruction::ReturnValue) {
// It was an explicit return (non-None).
return false; return false;
} }
// It must end with a raise then. We still have to walk the bytecode previous_loads_none = match op {
// to see if there's any explicit return (non-None). Instruction::LoadCommonConstant { idx } => idx.get(arg) == CommonConstant::None,
return !walk_instructions(code).any(|(_, op, _)| matches!(op, Instruction::ReturnValue)); Instruction::LoadConst { consti } => code
}; .constants
// Walk the bytecode, looking for RETURN_VALUE. .get(consti.get(arg) as usize)
for (at, op, _) in walk_instructions(code) { .is_some_and(|value| matches!(value.borrow_constant(), BorrowedConstant::None)),
if !matches!(op, Instruction::ReturnValue) { _ => false,
continue; };
}
// Ignore it if it returns None.
if let Some(prev) = at.checked_sub(1)
&& matches!(units.read_op(prev).deoptimize(), Instruction::LoadConst { .. })
// We don't worry about EXTENDED_ARG for now.
&& usize::from(u8::from(units.read_arg(prev))) == none_index
{
continue;
}
return false;
} }
true true
} }
+7 -6
View File
@@ -1094,13 +1094,14 @@ pub fn get_current_frame() -> *const InterpreterFrame {
/// Called from push_exception/pop_exception/set_exception. /// Called from push_exception/pop_exception/set_exception.
#[cfg(feature = "threading")] #[cfg(feature = "threading")]
pub fn update_thread_exception(exc: Option<PyBaseExceptionRef>) { pub fn update_thread_exception(exc: Option<PyBaseExceptionRef>) {
CURRENT_THREAD_SLOT.with(|slot| { // Release the TLS borrow before dropping either exception: traceback
if let Some(s) = slot.borrow().as_ref() { // locals can finalize another interpreter and re-enter this thread slot.
// SAFETY: Called only from the owning thread. The old ref is dropped let slot = current_thread_slot();
// here on the owning thread, which is safe. let old = slot.and_then(|s| {
let _old = unsafe { s.exception.swap(exc) }; // SAFETY: Called only from the owning thread.
} unsafe { s.exception.swap(exc) }
}); });
drop(old);
} }
/// Collect all threads' current exceptions for sys._current_exceptions(). /// Collect all threads' current exceptions for sys._current_exceptions().
@@ -11,7 +11,9 @@ from pathlib import Path
if sys.implementation.name == "rustpython": if sys.implementation.name == "rustpython":
assert external.MAGIC_NUMBER == bytes((0x16, 0xDA, 13, 10)) assert external.MAGIC_NUMBER == bytes((0x16, 0xDA, 13, 10))
assert sys.implementation.cache_tag == "rustpython-315-devabi2" assert sys.implementation.cache_tag == (
f"{sys.implementation.name}-{sys.version_info.major}{sys.version_info.minor}"
)
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)