Skip to content

Commit 83914a9

Browse files
committed
Fixes infinite recursion on raise e from e, refs RustPython#2779
1 parent 81200b1 commit 83914a9

File tree

2 files changed

+201
-17
lines changed

2 files changed

+201
-17
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import platform
2+
import sys
3+
4+
5+
# Regression to:
6+
# https://github.com/RustPython/RustPython/issues/2779
7+
8+
class MyError(Exception):
9+
pass
10+
11+
12+
e = MyError('message')
13+
14+
try:
15+
raise e from e
16+
except MyError as exc:
17+
# It was a segmentation fault before, will print info to stdout:
18+
sys.excepthook(type(exc), exc, exc.__traceback__)
19+
assert isinstance(exc, MyError)
20+
assert exc.__cause__ is e
21+
assert exc.__context__ is None
22+
else:
23+
assert False, 'exception not raised'
24+
25+
try:
26+
raise ValueError('test') from e
27+
except ValueError as exc:
28+
sys.excepthook(type(exc), exc, exc.__traceback__) # ok, will print two excs
29+
assert isinstance(exc, ValueError)
30+
assert exc.__cause__ is e
31+
assert exc.__context__ is None
32+
else:
33+
assert False, 'exception not raised'
34+
35+
36+
# New case:
37+
# potential recursion on `__context__` field
38+
39+
e = MyError('message')
40+
41+
try:
42+
try:
43+
raise e
44+
except MyError as exc:
45+
raise e
46+
else:
47+
assert False, 'exception not raised'
48+
except MyError as exc:
49+
sys.excepthook(type(exc), exc, exc.__traceback__)
50+
assert exc.__cause__ is None
51+
assert exc.__context__ is None
52+
else:
53+
assert False, 'exception not raised'
54+
55+
e = MyError('message')
56+
57+
try:
58+
try:
59+
raise e
60+
except MyError as exc:
61+
raise exc
62+
else:
63+
assert False, 'exception not raised'
64+
except MyError as exc:
65+
sys.excepthook(type(exc), exc, exc.__traceback__)
66+
assert exc.__cause__ is None
67+
assert exc.__context__ is None
68+
else:
69+
assert False, 'exception not raised'
70+
71+
e = MyError('message')
72+
73+
try:
74+
try:
75+
raise e
76+
except MyError as exc:
77+
raise e from e
78+
else:
79+
assert False, 'exception not raised'
80+
except MyError as exc:
81+
sys.excepthook(type(exc), exc, exc.__traceback__)
82+
assert exc.__cause__ is e
83+
assert exc.__context__ is None
84+
else:
85+
assert False, 'exception not raised'
86+
87+
e = MyError('message')
88+
89+
try:
90+
try:
91+
raise e
92+
except MyError as exc:
93+
raise exc from e
94+
else:
95+
assert False, 'exception not raised'
96+
except MyError as exc:
97+
sys.excepthook(type(exc), exc, exc.__traceback__)
98+
assert exc.__cause__ is e
99+
assert exc.__context__ is None
100+
else:
101+
assert False, 'exception not raised'
102+
103+
104+
# New case:
105+
# two exception in a recursion loop
106+
107+
class SubError(MyError):
108+
pass
109+
110+
e = MyError('message')
111+
d = SubError('sub')
112+
113+
114+
try:
115+
raise e from d
116+
except MyError as exc:
117+
# It was a segmentation fault before, will print info to stdout:
118+
sys.excepthook(type(exc), exc, exc.__traceback__)
119+
assert isinstance(exc, MyError)
120+
assert exc.__cause__ is d
121+
assert exc.__context__ is None
122+
else:
123+
assert False, 'exception not raised'
124+
125+
e = MyError('message')
126+
127+
try:
128+
raise d from e
129+
except SubError as exc:
130+
# It was a segmentation fault before, will print info to stdout:
131+
sys.excepthook(type(exc), exc, exc.__traceback__)
132+
assert isinstance(exc, SubError)
133+
assert exc.__cause__ is e
134+
assert exc.__context__ is None
135+
else:
136+
assert False, 'exception not raised'
137+
138+
139+
# New case:
140+
# explicit `__context__` manipulation.
141+
142+
e = MyError('message')
143+
e.__context__ = e
144+
145+
try:
146+
raise e
147+
except MyError as exc:
148+
# It was a segmentation fault before, will print info to stdout:
149+
if platform.python_implementation() == 'RustPython':
150+
# For some reason `CPython` hangs on this code:
151+
sys.excepthook(type(exc), exc, exc.__traceback__)
152+
assert isinstance(exc, MyError)
153+
assert exc.__cause__ is None
154+
assert exc.__context__ is e

vm/src/exceptions.rs

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ use crate::types::create_type_with_slots;
1111
use crate::StaticType;
1212
use crate::VirtualMachine;
1313
use crate::{
14-
IntoPyObject, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult, PyValue,
15-
TryFromObject, TypeProtocol,
14+
IdProtocol, IntoPyObject, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult,
15+
PyValue, TryFromObject, TypeProtocol,
1616
};
1717

1818
use crossbeam_utils::atomic::AtomicCell;
1919
use itertools::Itertools;
20+
use std::collections::HashSet;
2021
use std::fmt;
2122
use std::fs::File;
2223
use std::io::{self, BufRead, BufReader};
@@ -194,21 +195,8 @@ pub fn write_exception<W: Write>(
194195
vm: &VirtualMachine,
195196
exc: &PyBaseExceptionRef,
196197
) -> Result<(), W::Error> {
197-
if let Some(cause) = exc.cause() {
198-
write_exception(output, vm, &cause)?;
199-
writeln!(
200-
output,
201-
"\nThe above exception was the direct cause of the following exception:\n"
202-
)?;
203-
} else if let Some(context) = exc.context() {
204-
write_exception(output, vm, &context)?;
205-
writeln!(
206-
output,
207-
"\nDuring handling of the above exception, another exception occurred:\n"
208-
)?;
209-
}
210-
211-
write_exception_inner(output, vm, exc)
198+
let seen = &mut HashSet::<usize>::new();
199+
write_exception_recursive(output, vm, exc, seen)
212200
}
213201

214202
fn print_source_line<W: Write>(
@@ -253,6 +241,48 @@ fn write_traceback_entry<W: Write>(
253241
Ok(())
254242
}
255243

244+
fn write_exception_recursive<W: Write>(
245+
output: &mut W,
246+
vm: &VirtualMachine,
247+
exc: &PyBaseExceptionRef,
248+
seen: &mut HashSet<usize>,
249+
) -> Result<(), W::Error> {
250+
// This function should not be called directly,
251+
// use `wite_exception` as a public interface.
252+
// It is similar to `print_exception_recursive` from `CPython`.
253+
seen.insert(exc.as_object().get_id());
254+
if let Some(cause) = exc.cause() {
255+
// This can be a special case: `raise e from e`,
256+
// we just ignore it and treat like `raise e` without any extra steps.
257+
if !seen.contains(&cause.as_object().get_id()) {
258+
write_exception_recursive(output, vm, &cause, seen)?;
259+
writeln!(
260+
output,
261+
"\nThe above exception was the direct cause of the following exception:\n"
262+
)?;
263+
} else {
264+
seen.insert(cause.as_object().get_id());
265+
}
266+
} else if let Some(context) = exc.context() {
267+
// This can be a special case:
268+
// e = ValueError('e')
269+
// e.__context__ = e
270+
// In this case, we just ignore
271+
// `__context__` part from going into recursion.
272+
if !seen.contains(&context.as_object().get_id()) {
273+
write_exception_recursive(output, vm, &context, seen)?;
274+
writeln!(
275+
output,
276+
"\nDuring handling of the above exception, another exception occurred:\n"
277+
)?;
278+
} else {
279+
seen.insert(context.as_object().get_id());
280+
}
281+
}
282+
283+
write_exception_inner(output, vm, exc)
284+
}
285+
256286
/// Print exception with traceback
257287
pub fn write_exception_inner<W: Write>(
258288
output: &mut W,

0 commit comments

Comments
 (0)