[issue29988] (async) with blocks and try/finally are not as KeyboardInterrupt-safe as one might like
Nathaniel Smith
report at bugs.python.org
Wed May 24 18:16:52 EDT 2017
Nathaniel Smith added the comment:
> If all you need is that with foo: pass guarantees that either both or neither of __enter__ and __exit__ are called, for C context managers, and only C context managers, then the fix is trivial.
It would be nice to have it for 'async with foo: pass' as well, which is a little less trivial because the 'await' dance is inlined into the bytecode (see the initial post in this bug), but basically yes.
> Do you have any way to reliably test for this failure mode?
Unfortunately no, I haven't implemented one. Let's see, though...
The test I wrote for issue30039 demonstrates one way to trigger a signal on a precise bytecode, by writing some code in C that calls raise(SIGNALNUMBER), and then calling it immediately before the bytecode where we want the signal to be raised (simulating the situation where the signal happens to arrive while the C function is running -- note that raise() is convenient because unlike kill() it works cross-platform, even on Windows).
This might be sufficient for testing the 'async with' version; it looks like an __await__ method or iterator implemented in C and calling raise() would deliver a signal at a point that should be protected but isn't.
The tricky case is plain 'with'. We can write something like:
with lock:
raise_signal()
and this gives bytecode like:
1 0 LOAD_NAME 0 (lock)
2 SETUP_WITH 12 (to 16)
4 POP_TOP
2 6 LOAD_NAME 1 (raise_signal)
8 CALL_FUNCTION 0
10 POP_TOP
12 POP_BLOCK
14 LOAD_CONST 0 (None)
>> 16 WITH_CLEANUP_START
So the problem is that at offset 8 is where we can run arbitrary code, but the race condition is if a signal arrives between offsets 12 and 16.
One possibility would be to set up a chain of Py_AddPendingCall handlers, something like:
int chain1(void* _) {
Py_AddPendingCall(chain2, 0);
return 0;
}
int chain2(void* _) {
Py_AddPendingCall(chain3, 0);
return 0;
}
int chain3(void* _) {
raise(SIGINT);
return 0;
}
(or to reduce brittleness, maybe use the void* to hold an int controlling the length of the chain, which would make it easy to run tests with chains of length 1, 2, 3, ...)
......except consulting ceval.c I see that currently this won't work, because it looks like if you call Py_AddPendingCall from inside a pending call callback, then Py_MakePendingCalls will execute the newly added callback immediately after the first one returns. It could be made to work by having Py_MakePendingCalls do a first pass to check the current length of the queue, and then use that as the bound on how many calls it makes.
----------
_______________________________________
Python tracker <report at bugs.python.org>
<http://bugs.python.org/issue29988>
_______________________________________
More information about the Python-bugs-list
mailing list