Skip to content

Commit 23ae30f

Browse files
committed
gh-158539: Handle deep exception chains without changing debug offsets
1 parent 5243d8d commit 23ae30f

4 files changed

Lines changed: 42 additions & 213 deletions

File tree

‎Include/internal/pycore_debug_offsets.h‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,11 @@ typedef struct _Py_DebugOffsets {
113113
uint64_t gil_requested;
114114
uint64_t current_exception;
115115
uint64_t exc_state;
116-
uint64_t exc_info;
117116
} thread_state;
118117

119118
// Exception stack item offset
120119
struct {
121120
uint64_t exc_value;
122-
uint64_t previous_item;
123121
} err_stackitem;
124122

125123
// InterpreterFrame offset;
@@ -306,11 +304,9 @@ typedef struct _Py_DebugOffsets {
306304
.gil_requested = offsetof(PyThreadState, gil_requested), \
307305
.current_exception = offsetof(PyThreadState, current_exception), \
308306
.exc_state = offsetof(PyThreadState, exc_state), \
309-
.exc_info = offsetof(PyThreadState, exc_info), \
310307
}, \
311308
.err_stackitem = { \
312309
.exc_value = offsetof(_PyErr_StackItem, exc_value), \
313-
.previous_item = offsetof(_PyErr_StackItem, previous_item), \
314310
}, \
315311
.interpreter_frame = { \
316312
.size = sizeof(_PyInterpreterFrame), \

‎Lib/test/test_external_inspection.py‎

Lines changed: 23 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -3046,23 +3046,6 @@ class TestExceptionDetectionScenarios(RemoteInspectionTestBase):
30463046
30473047
4. finally_no_exception: Finally block with no exception raised
30483048
-> Should NOT have HAS_EXCEPTION (no exception state)
3049-
3050-
5. except_block_in_generator: Thread inside an except block running in a
3051-
generator
3052-
-> SHOULD have HAS_EXCEPTION (exc_info points at the generator's
3053-
_PyErr_StackItem, not the thread's embedded exc_state)
3054-
3055-
6. except_block_in_genexpr_callee: Except block in a function called from a
3056-
generator expression
3057-
-> SHOULD have HAS_EXCEPTION (same reason as 5)
3058-
3059-
7. except_block_in_coroutine: Thread inside an except block running in a
3060-
coroutine
3061-
-> SHOULD have HAS_EXCEPTION (same reason as 5)
3062-
3063-
8. except_block_in_callee_from_coroutine: Except block in a function called
3064-
from a coroutine
3065-
-> SHOULD have HAS_EXCEPTION (same reason as 5)
30663049
"""
30673050

30683051
def _make_single_scenario_script(self, port, scenario):
@@ -3146,105 +3129,6 @@ def target_thread():
31463129
while True:
31473130
time.sleep(0.01)
31483131
3149-
t = threading.Thread(target=target_thread)
3150-
t.start()
3151-
t.join()
3152-
""",
3153-
"except_block_in_generator": f"""\
3154-
import socket
3155-
import threading
3156-
import time
3157-
3158-
def target_thread():
3159-
'''Inside except block that runs in a generator'''
3160-
conn = socket.create_connection(("localhost", {port}))
3161-
conn.sendall(b"ready:" + str(threading.get_native_id()).encode())
3162-
3163-
def gen():
3164-
try:
3165-
raise ValueError("test")
3166-
except ValueError:
3167-
while True:
3168-
time.sleep(0.01)
3169-
yield
3170-
3171-
for _ in gen():
3172-
pass
3173-
3174-
t = threading.Thread(target=target_thread)
3175-
t.start()
3176-
t.join()
3177-
""",
3178-
"except_block_in_genexpr_callee": f"""\
3179-
import socket
3180-
import threading
3181-
import time
3182-
3183-
def target_thread():
3184-
'''Inside except block in a function called from a generator expression'''
3185-
conn = socket.create_connection(("localhost", {port}))
3186-
conn.sendall(b"ready:" + str(threading.get_native_id()).encode())
3187-
3188-
def callee():
3189-
try:
3190-
raise ValueError("test")
3191-
except ValueError:
3192-
while True:
3193-
time.sleep(0.01)
3194-
3195-
list(callee() for _ in range(1))
3196-
3197-
t = threading.Thread(target=target_thread)
3198-
t.start()
3199-
t.join()
3200-
""",
3201-
"except_block_in_coroutine": f"""\
3202-
import asyncio
3203-
import socket
3204-
import threading
3205-
import time
3206-
3207-
def target_thread():
3208-
'''Inside except block that runs in a coroutine'''
3209-
conn = socket.create_connection(("localhost", {port}))
3210-
conn.sendall(b"ready:" + str(threading.get_native_id()).encode())
3211-
3212-
async def coro():
3213-
try:
3214-
raise ValueError("test")
3215-
except ValueError:
3216-
while True:
3217-
time.sleep(0.01)
3218-
3219-
asyncio.run(coro())
3220-
3221-
t = threading.Thread(target=target_thread)
3222-
t.start()
3223-
t.join()
3224-
""",
3225-
"except_block_in_callee_from_coroutine": f"""\
3226-
import asyncio
3227-
import socket
3228-
import threading
3229-
import time
3230-
3231-
def target_thread():
3232-
'''Inside except block in a function called from a coroutine'''
3233-
conn = socket.create_connection(("localhost", {port}))
3234-
conn.sendall(b"ready:" + str(threading.get_native_id()).encode())
3235-
3236-
def callee():
3237-
try:
3238-
raise ValueError("test")
3239-
except ValueError:
3240-
while True:
3241-
time.sleep(0.01)
3242-
3243-
async def coro():
3244-
callee()
3245-
3246-
asyncio.run(coro())
3247-
32483132
t = threading.Thread(target=target_thread)
32493133
t.start()
32503134
t.join()
@@ -3408,72 +3292,6 @@ def test_finally_no_exception_no_flag(self):
34083292
self.assertIsNotNone(thread_tid, "Thread ID not received")
34093293
self._check_exception_status(p, thread_tid, expect_exception=False)
34103294

3411-
@unittest.skipIf(
3412-
sys.platform not in ("linux", "darwin", "win32"),
3413-
"Test only runs on supported platforms (Linux, macOS, or Windows)",
3414-
)
3415-
@unittest.skipIf(
3416-
sys.platform == "android", "Android raises Linux-specific exception"
3417-
)
3418-
def test_except_block_in_generator_has_exception(self):
3419-
"""gh-158539: a handler running in a generator has HAS_EXCEPTION.
3420-
3421-
Generators repoint ``tstate->exc_info`` at their own
3422-
``_PyErr_StackItem``, so the embedded ``exc_state`` stays empty and the
3423-
profiler must follow ``exc_info`` to see the handled exception.
3424-
"""
3425-
with self._run_scenario_process("except_block_in_generator") as (p, thread_tid):
3426-
self.assertIsNotNone(thread_tid, "Thread ID not received")
3427-
self._check_exception_status(p, thread_tid, expect_exception=True)
3428-
3429-
@unittest.skipIf(
3430-
sys.platform not in ("linux", "darwin", "win32"),
3431-
"Test only runs on supported platforms (Linux, macOS, or Windows)",
3432-
)
3433-
@unittest.skipIf(
3434-
sys.platform == "android", "Android raises Linux-specific exception"
3435-
)
3436-
def test_except_block_in_genexpr_callee_has_exception(self):
3437-
"""gh-158539: a handler in a function called from a generator expression.
3438-
3439-
The handler itself lives in an ordinary function, but the generator
3440-
expression on the stack means ``exc_info`` does not point at the
3441-
thread's embedded ``exc_state``.
3442-
"""
3443-
with self._run_scenario_process(
3444-
"except_block_in_genexpr_callee"
3445-
) as (p, thread_tid):
3446-
self.assertIsNotNone(thread_tid, "Thread ID not received")
3447-
self._check_exception_status(p, thread_tid, expect_exception=True)
3448-
3449-
@unittest.skipIf(
3450-
sys.platform not in ("linux", "darwin", "win32"),
3451-
"Test only runs on supported platforms (Linux, macOS, or Windows)",
3452-
)
3453-
@unittest.skipIf(
3454-
sys.platform == "android", "Android raises Linux-specific exception"
3455-
)
3456-
def test_except_block_in_coroutine_has_exception(self):
3457-
"""gh-158539: a handler running in a coroutine has HAS_EXCEPTION."""
3458-
with self._run_scenario_process("except_block_in_coroutine") as (p, thread_tid):
3459-
self.assertIsNotNone(thread_tid, "Thread ID not received")
3460-
self._check_exception_status(p, thread_tid, expect_exception=True)
3461-
3462-
@unittest.skipIf(
3463-
sys.platform not in ("linux", "darwin", "win32"),
3464-
"Test only runs on supported platforms (Linux, macOS, or Windows)",
3465-
)
3466-
@unittest.skipIf(
3467-
sys.platform == "android", "Android raises Linux-specific exception"
3468-
)
3469-
def test_except_block_in_callee_from_coroutine_has_exception(self):
3470-
"""gh-158539: a handler in a function called from a coroutine."""
3471-
with self._run_scenario_process(
3472-
"except_block_in_callee_from_coroutine"
3473-
) as (p, thread_tid):
3474-
self.assertIsNotNone(thread_tid, "Thread ID not received")
3475-
self._check_exception_status(p, thread_tid, expect_exception=True)
3476-
34773295

34783296
class TestExceptionDetectionInProcess(RemoteInspectionTestBase):
34793297
"""gh-158539: HAS_EXCEPTION for handlers running in generators/coroutines.
@@ -3488,7 +3306,7 @@ class TestExceptionDetectionInProcess(RemoteInspectionTestBase):
34883306
def setUpClass(cls):
34893307
try:
34903308
RemoteUnwinder(os.getpid(), all_threads=True).get_stack_trace()
3491-
except Exception as exc:
3309+
except PermissionError as exc:
34923310
raise unittest.SkipTest(f"self-inspection is unavailable: {exc}")
34933311

34943312
def _check_running_handler(
@@ -3646,6 +3464,28 @@ def gen():
36463464

36473465
self._check_running_handler(target, expect_exception=False)
36483466

3467+
def test_outer_handler_while_nested_generators_run(self):
3468+
def target(ready, stop):
3469+
def gen(depth):
3470+
if depth:
3471+
yield from gen(depth - 1)
3472+
else:
3473+
self._busy_until_stopped(ready, stop)
3474+
yield
3475+
3476+
try:
3477+
raise ValueError("outer")
3478+
except ValueError:
3479+
for _ in gen(32):
3480+
pass
3481+
3482+
self._check_running_handler(
3483+
target,
3484+
expect_exception=True,
3485+
mode=PROFILING_MODE_EXCEPTION,
3486+
skip_non_matching_threads=True,
3487+
)
3488+
36493489
def test_generator_finally_after_except(self):
36503490
"""The handled exception is cleared before the generator's finally."""
36513491
def target(ready, stop):

‎Modules/_remote_debugging/debug_offsets_validation.h‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
#define FIELD_SIZE(type, member) sizeof(((type *)0)->member)
3232

3333
enum {
34-
PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 904,
34+
PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 888,
3535
PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 104,
3636
};
3737

@@ -249,15 +249,15 @@ validate_fixed_field(
249249
#define PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS(APPLY, buffer_size) \
250250
APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size)
251251

252+
/* current_exception also covers the adjacent exc_info pointer. */
252253
#define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \
253254
APPLY(thread_state, native_thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \
254255
APPLY(thread_state, interp, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
255256
APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
256257
APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), _Alignof(unsigned int), buffer_size); \
257258
APPLY(thread_state, holds_gil, sizeof(int), _Alignof(int), buffer_size); \
258259
APPLY(thread_state, gil_requested, sizeof(int), _Alignof(int), buffer_size); \
259-
APPLY(thread_state, current_exception, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
260-
APPLY(thread_state, exc_info, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
260+
APPLY(thread_state, current_exception, 2 * sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
261261
APPLY(thread_state, thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \
262262
APPLY(thread_state, next, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
263263
APPLY(thread_state, current_frame, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
@@ -352,16 +352,11 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets
352352
PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(
353353
PY_REMOTE_DEBUG_VALIDATE_FIELD,
354354
SIZEOF_THREAD_STATE);
355+
/* exc_value also covers the adjacent previous_item pointer. */
355356
PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD(
356357
err_stackitem,
357358
exc_value,
358-
sizeof(uintptr_t),
359-
_Alignof(uintptr_t),
360-
sizeof(_PyErr_StackItem));
361-
PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD(
362-
err_stackitem,
363-
previous_item,
364-
sizeof(uintptr_t),
359+
2 * sizeof(uintptr_t),
365360
_Alignof(uintptr_t),
366361
sizeof(_PyErr_StackItem));
367362
PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD(

‎Modules/_remote_debugging/threads.c‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717
#include <sys/wait.h>
1818
#endif
1919

20-
/* Upper bound on how far the handled-exception chain (exc_info->previous_item)
21-
* is followed in remote memory. The chain is normally at most a couple of
22-
* entries deep; the bound only guards against corrupted memory. */
23-
#define MAX_EXCEPTION_CHAIN_DEPTH 16
20+
/* Bound traversal of corrupted remote exception chains. */
21+
#define MAX_EXCEPTION_CHAIN_DEPTH (2 << 15)
22+
23+
/* Derive unexported offsets from adjacent fields to keep the debug-offset
24+
* table compatible across patch releases. */
25+
static_assert(offsetof(PyThreadState, exc_info) ==
26+
offsetof(PyThreadState, current_exception) + sizeof(uintptr_t));
27+
static_assert(offsetof(_PyErr_StackItem, previous_item) ==
28+
offsetof(_PyErr_StackItem, exc_value) + sizeof(uintptr_t));
2429

2530
/* ============================================================================
2631
* THREAD ITERATION FUNCTIONS
@@ -441,25 +446,18 @@ unwind_stack_for_thread(
441446
has_exception = 1;
442447
}
443448

444-
// Check the exception currently being handled by an except block.
445-
//
446-
// The active _PyErr_StackItem is normally exc_state, embedded in the
447-
// thread state, but generators, coroutines and async generators repoint
448-
// tstate->exc_info at their own _PyErr_StackItem while they run. Reading
449-
// only the embedded exc_state therefore misses every handler that runs in
450-
// a generator or coroutine, or in a function one of them calls. Follow
451-
// exc_info and walk previous_item like _PyErr_GetTopmostException() so
452-
// that an outer handler is still found while a generator without a handler
453-
// of its own is running.
449+
// Generators and coroutines use their own exception stack items.
450+
// Follow exc_info to find the innermost handler, as sys.exception() does.
454451
if (!has_exception) {
455452
uintptr_t exc_info = GET_MEMBER(uintptr_t, ts,
456-
unwinder->debug_offsets.thread_state.exc_info);
453+
unwinder->debug_offsets.thread_state.current_exception +
454+
sizeof(uintptr_t));
457455
uintptr_t exc_state_addr =
458456
*current_tstate + unwinder->debug_offsets.thread_state.exc_state;
459457
uintptr_t exc_value_offset =
460458
unwinder->debug_offsets.err_stackitem.exc_value;
461459
uintptr_t previous_item_offset =
462-
unwinder->debug_offsets.err_stackitem.previous_item;
460+
exc_value_offset + sizeof(uintptr_t);
463461

464462
for (int depth = 0; exc_info != 0 && depth < MAX_EXCEPTION_CHAIN_DEPTH;
465463
depth++)

0 commit comments

Comments
 (0)