Skip to content

Commit c0694d9

Browse files
authored
Reject stale GenericAlias parameters on Python 3.15 (#9069)
* Reject GenericAlias parameters added after caching Validate stale cached parameters and retain the Python regression without obsolete reference prerequisites. Assisted-by: OpenAI Codex:model identifier unavailable * Compute GenericAlias parameters lazily for Python 3.15 Populate the parameter cache on first lookup or subscription. Preserve cached identity and outer lookup replacement, propagate descriptor failures, and release displaced parameters outside the cache lock. Add Python regressions for timing, retries, reentrancy, recursion and concurrent callbacks. Assisted-by: OpenAI Codex:model identifier unavailable * Check GenericAlias recursion at lazy parameter materialization Align the existing cyclic and deeply nested fixtures with certified CPython 3.15. Keep RecursionError assertions, cover both parameter lookup and subscription on both interpreters, and retain the separate deep-hash regression. Assisted-by: OpenAI Codex:model identifier unavailable * Keep finite GenericAlias stack stress native-specific Preserve shared cyclic recursion checks and all RustPython stress assertions. Finite acyclic depth is not a portable CPython C-stack limit across optimized builds. Assisted-by: OpenAI Codex:model identifier unavailable * Materialize union parameters lazily like GenericAlias Share the reentrant-safe parameter cache between aliases and unions. Preserve parameter identity without applying union argument equality or flattening. Cover nested alias timing, descriptor retry, reentrant replacement, empty caches and concurrent callbacks in Python regressions checked against final CPython 3.15. Assisted-by: OpenAI Codex:model identifier unavailable * Consolidate union regressions in the typing snippet Preserve all existing regression bodies and coverage while following the requested snippet organization. Assisted-by: OpenAI Codex:model identifier unavailable
1 parent e8cb0c7 commit c0694d9

5 files changed

Lines changed: 521 additions & 51 deletions

File tree

‎crates/vm/src/builtins/genericalias.rs‎

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// spell-checker:ignore iparam gaiterobject
2-
use crate::common::lock::LazyLock;
2+
use crate::common::lock::{LazyLock, PyMutex};
33

44
use super::type_;
55
use crate::{
@@ -40,7 +40,7 @@ pub struct PyGenericAlias {
4040
origin: PyObjectRef,
4141
#[pymember(name = "__args__")]
4242
args: PyTupleRef,
43-
parameters: PyTupleRef,
43+
parameters: PyMutex<Option<PyTupleRef>>,
4444
#[pymember(name = "__unpacked__")]
4545
starred: bool, // for __unpacked__ attribute
4646
}
@@ -105,13 +105,12 @@ impl PyGenericAlias {
105105
origin: impl Into<PyObjectRef>,
106106
args: PyTupleRef,
107107
starred: bool,
108-
vm: &VirtualMachine,
108+
_vm: &VirtualMachine,
109109
) -> PyResult<Self> {
110-
let parameters = make_parameters(&args, vm)?;
111110
Ok(Self {
112111
origin: origin.into(),
113112
args,
114-
parameters,
113+
parameters: PyMutex::new(None),
115114
starred,
116115
})
117116
}
@@ -130,6 +129,10 @@ impl PyGenericAlias {
130129
Self::new(origin, args, false, vm)
131130
}
132131

132+
fn parameters(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
133+
cached_parameters(&self.parameters, &self.args, vm)
134+
}
135+
133136
fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
134137
fn repr_item(obj: &PyObject, vm: &VirtualMachine) -> PyResult<String> {
135138
if obj.is(&vm.ctx.ellipsis) {
@@ -206,7 +209,8 @@ impl PyGenericAlias {
206209
}
207210

208211
fn __getitem__(zelf: &Py<Self>, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult {
209-
let new_args = subs_parameters(zelf.as_object(), &zelf.args, &zelf.parameters, needle, vm)?;
212+
let parameters = zelf.parameters(vm)?;
213+
let new_args = subs_parameters(zelf.as_object(), &zelf.args, &parameters, needle, vm)?;
210214

211215
Ok(Self::new(zelf.origin.clone(), new_args, false, vm)?.into_pyobject(vm))
212216
}
@@ -236,8 +240,8 @@ impl PyGenericAlias {
236240
)]
237241
impl Py<PyGenericAlias> {
238242
#[pygetset]
239-
fn __parameters__(&self) -> PyObjectRef {
240-
self.parameters.clone().into()
243+
fn __parameters__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
244+
self.parameters(vm)
241245
}
242246

243247
#[pygetset]
@@ -312,11 +316,36 @@ impl Py<PyGenericAlias> {
312316
}
313317
}
314318

319+
pub(crate) fn cached_parameters(
320+
cache: &PyMutex<Option<PyTupleRef>>,
321+
args: &Py<PyTuple>,
322+
vm: &VirtualMachine,
323+
) -> PyResult<PyTupleRef> {
324+
let cached = cache.lock().clone();
325+
if let Some(parameters) = cached {
326+
return Ok(parameters);
327+
}
328+
// Attribute lookup can re-enter this alias or union. Compute without the
329+
// lock, then replace even a cache populated by the re-entrant lookup.
330+
let parameters = make_parameters(args, vm);
331+
let previous = core::mem::replace(&mut *cache.lock(), parameters.as_ref().ok().cloned());
332+
// A failed outer lookup clears the cache populated by a nested lookup too.
333+
// Cached parameters can own objects with Python finalizers.
334+
drop(previous);
335+
parameters
336+
}
337+
315338
pub(crate) fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
316339
make_parameters_from_slice(args.as_slice(), vm)
317340
}
318341

319342
fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyTupleRef> {
343+
vm.with_recursion(" in __parameter__ calculation", || {
344+
make_parameters_inner(args, vm)
345+
})
346+
}
347+
348+
fn make_parameters_inner(args: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyTupleRef> {
320349
let mut parameters: Vec<PyObjectRef> = Vec::with_capacity(args.len());
321350

322351
for arg in args {
@@ -326,16 +355,21 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyRe
326355
}
327356

328357
// Check for __typing_subst__ attribute
329-
if arg.get_attr(identifier!(vm, __typing_subst__), vm).is_ok() {
358+
if vm
359+
.get_attribute_opt(arg, identifier!(vm, __typing_subst__))?
360+
.is_some()
361+
{
330362
if tuple_index(&parameters, arg).is_none() {
331363
parameters.push(arg.clone());
332364
}
333-
} else if let Ok(subparams) = arg.get_attr(identifier!(vm, __parameters__), vm)
334-
&& let Ok(sub_params) = subparams.try_to_ref::<PyTuple>(vm)
365+
} else if let Some(subparams) =
366+
vm.get_attribute_opt(arg, identifier!(vm, __parameters__))?
335367
{
336-
for sub_param in sub_params {
337-
if tuple_index(&parameters, sub_param).is_none() {
338-
parameters.push(sub_param.clone());
368+
if let Some(sub_params) = subparams.downcast_ref::<PyTuple>() {
369+
for sub_param in sub_params {
370+
if tuple_index(&parameters, sub_param).is_none() {
371+
parameters.push(sub_param.clone());
372+
}
339373
}
340374
}
341375
} else if arg.try_to_ref::<PyTuple>(vm).is_ok() || arg.try_to_ref::<PyList>(vm).is_ok() {
@@ -346,9 +380,7 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyRe
346380
let list = arg.downcast_ref::<PyList>().unwrap();
347381
list.borrow_vec().to_vec()
348382
};
349-
let sub = vm.with_recursion(" while computing __parameters__", || {
350-
make_parameters_from_slice(&items, vm)
351-
})?;
383+
let sub = make_parameters_from_slice(&items, vm)?;
352384
for sub_param in sub.as_slice() {
353385
if tuple_index(&parameters, sub_param).is_none() {
354386
parameters.push(sub_param.clone());
@@ -558,7 +590,10 @@ pub(crate) fn subs_parameters(
558590
if let Some(iparam) = tuple_index(parameters.as_slice(), arg) {
559591
subst.call((arg_items[iparam].clone(),), vm)?
560592
} else {
561-
subs_tvars(arg.clone(), parameters, &arg_items, vm)?
593+
return Err(vm.new_type_error(format!(
594+
"argument {} with __typing_subst__ was not found in __parameters__",
595+
arg.repr(vm)?
596+
)));
562597
}
563598
} else {
564599
subs_tvars(arg.clone(), parameters, &arg_items, vm)?

‎crates/vm/src/builtins/union.rs‎

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{genericalias, type_};
2-
use crate::common::lock::LazyLock;
2+
use crate::common::lock::{LazyLock, PyMutex};
33
use crate::{
44
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
55
atomic_func,
@@ -24,7 +24,7 @@ pub struct PyUnion {
2424
hashable_args: Option<PyRef<PyFrozenSet>>,
2525
/// Tuple of initially unhashable args, or None if all args were hashable
2626
unhashable_args: Option<PyTupleRef>,
27-
parameters: PyTupleRef,
27+
parameters: PyMutex<Option<PyTupleRef>>,
2828
}
2929

3030
impl fmt::Debug for PyUnion {
@@ -42,14 +42,13 @@ impl PyPayload for PyUnion {
4242

4343
impl PyUnion {
4444
/// Create a new union from dedup result (internal use)
45-
fn from_components(result: UnionComponents, vm: &VirtualMachine) -> PyResult<Self> {
46-
let parameters = make_parameters(&result.args, vm)?;
47-
Ok(Self {
45+
fn from_components(result: UnionComponents) -> Self {
46+
Self {
4847
args: result.args,
4948
hashable_args: result.hashable_args,
5049
unhashable_args: result.unhashable_args,
51-
parameters,
52-
})
50+
parameters: PyMutex::new(None),
51+
}
5352
}
5453

5554
/// Direct access to args field (_Py_union_args)
@@ -127,8 +126,8 @@ impl Py<PyUnion> {
127126
}
128127

129128
#[pygetset]
130-
fn __parameters__(&self) -> PyObjectRef {
131-
self.parameters.clone().into()
129+
fn __parameters__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
130+
genericalias::cached_parameters(&self.parameters, &self.args, vm)
132131
}
133132

134133
#[pymethod]
@@ -239,12 +238,6 @@ pub(crate) fn or_op(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine)
239238
make_union(&tuple, vm)
240239
}
241240

242-
fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
243-
let parameters = genericalias::make_parameters(args, vm)?;
244-
let result = dedup_and_flatten_args(&parameters, vm)?;
245-
Ok(result.args)
246-
}
247-
248241
fn flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
249242
let mut total_args = 0;
250243
for arg in args {
@@ -372,19 +365,15 @@ pub fn make_union(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult {
372365
let result = dedup_and_flatten_args(args, vm)?;
373366
Ok(match result.args.as_slice().len() {
374367
1 => result.args.as_slice()[0].to_owned(),
375-
_ => PyUnion::from_components(result, vm)?.to_pyobject(vm),
368+
_ => PyUnion::from_components(result).to_pyobject(vm),
376369
})
377370
}
378371

379372
impl PyUnion {
380373
fn getitem(zelf: &Py<Self>, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult {
381-
let new_args = genericalias::subs_parameters(
382-
zelf.as_object(),
383-
&zelf.args,
384-
&zelf.parameters,
385-
needle,
386-
vm,
387-
)?;
374+
let parameters = genericalias::cached_parameters(&zelf.parameters, &zelf.args, vm)?;
375+
let new_args =
376+
genericalias::subs_parameters(zelf.as_object(), &zelf.args, &parameters, needle, vm)?;
388377

389378
Ok(if new_args.as_slice().is_empty() {
390379
make_union(&new_args, vm)?

0 commit comments

Comments
 (0)