Skip to content
Open
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
68 changes: 68 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,74 @@ def test_call_invalid(self):
with self.assertRaises(interpreters.NotShareableError):
interp.call(func, op, 'eggs!')

def test_call_result_unpickling_raises(self):
# gh-151892: a caller-side result-unpickle failure left the exception
# set in _PyXI_Exit(), tripping an assertion instead of propagating.
source = dedent("""
from concurrent import interpreters

def _raise_on_unpickle():
raise ValueError('unpickling failed')

def _raise_notshareable_on_unpickle():
raise interpreters.NotShareableError('nope')

class BadResult:
def __reduce__(self):
return (_raise_on_unpickle, ())

class NotShareableResult:
def __reduce__(self):
return (_raise_notshareable_on_unpickle, ())

class ReduceRaises:
def __reduce__(self):
raise RuntimeError('reduce failed')

def make_bad_result():
return BadResult()

def make_notshareable_result():
return NotShareableResult()

def make_reduce_raises():
return ReduceRaises()
""")
with import_helper.ready_to_import('unpicklable_result', source) as (
name, path):
interp = interpreters.create()
interp.prepare_main(_moddir=os.path.dirname(path))
interp.exec(dedent(f"""
import sys
sys.path.insert(0, _moddir)
import {name} as mod
"""))

# Guards this fix: caller-side unpickle raises (ValueError).
with self.subTest('unpickle raises'):
with self.assertRaises(
interpreters.NotShareableError) as cm:
interp.call(eval, 'mod.make_bad_result()')
# __cause__ is a generic Exception (the type cannot cross
# interpreters); don't tighten this to assertIsInstance.
self.assertIn('ValueError', str(cm.exception.__cause__))
self.assertIn('unpickling failed',
str(cm.exception.__cause__))

# Same branch as above, but the unpickle error is itself a
# NotShareableError; verify it is preserved as the __cause__.
with self.subTest('unpickle raises NotShareableError'):
with self.assertRaises(interpreters.NotShareableError) as cm:
interp.call(eval, 'mod.make_notshareable_result()')
self.assertIn('NotShareableError',
str(cm.exception.__cause__))

# Adjacent coverage: callee-side reduce raises (the pre-existing
# capture_session_error path); does not exercise this fix.
with self.subTest('reduce raises'):
with self.assertRaises(interpreters.NotShareableError):
interp.call(eval, 'mod.make_reduce_raises()')

def test_callable_requires_frame(self):
# There are various functions that require a current frame.
interp = interpreters.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed a crash in :meth:`concurrent.interpreters.Interpreter.call` when
reconstructing the result in the calling interpreter raised an exception
(for example when unpickling the returned object failed). The failure is
now reported as :exc:`concurrent.interpreters.NotShareableError` instead of
aborting the interpreter.
15 changes: 14 additions & 1 deletion Python/crossinterp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2841,10 +2841,23 @@ _PyXI_Exit(_PyXI_session *session, _PyXI_failure *override,
PyErr_FormatUnraisable(
"Exception ignored while capturing preserved objects");
}
else if (_PyErr_Occurred(tstate)) {
// Restoring the preserved namespace (e.g. unpickling a result)
// raised an exception, which we propagate as the failure.
_PyXI_failure _override = XI_FAILURE_INIT;
_override.code = _PyXI_ERR_PRESERVE_FAILURE;
_propagate_not_shareable_error(tstate, &_override);
PyObject *exc = xi_error_resolve_current_exc(tstate, &_override);
assert(exc != NULL);
failure = xi_error_set_exc(tstate, &err, exc);
Py_DECREF(exc);
if (failure == NULL && _override.code == _PyXI_ERR_NOT_SHAREABLE) {
xi_error_set_override(tstate, &err, &_override);
}
}
else {
xi_error_set_override_code(
tstate, &err, _PyXI_ERR_PRESERVE_FAILURE);
_propagate_not_shareable_error(tstate, err.override);
}
}
if (result != NULL) {
Expand Down
Loading