-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
python RLock implementation unsafe with signals #57906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This affects the python implementation of RLock only. If a signal occurs during RLock.acquire() or release() and then operates on the same lock to acquire() or release() it, process hangs or assertions can be triggered. The attached test script demonstrates the issue on Python 2.6 and 3.2, and code inspection suggests this is still valid for 2.7 and 3.4. To use it, run the script and then 'kill -SIGUSR2' the process id it prints out. Keep sending SIGUSR2 until you get bored or:
We found this debugging application server hangs during log rotation (https://bugs.launchpad.net/launchpad/+bug/861742) where if the thread that the signal is received in was about to log a message the deadlock case would crop up from time to time. Of course, this wasn't ever safe code, and we're changing it (to have the signal handler merely set a integer flag that the logging handler can consult without locking). However, I wanted to raise this upstream, and I note per issue bpo-3618 that the 3.x IO module apparently includes a minimal version of the Python RLock implementation (or just uses RLock itself). Either way this bug probably exists for applications simply doing IO, either when there is no C RLock implementation available, or all the time (depending on exactly what the IO module is doing nowadays). |
Adding myself to the nosy list because I am interested in the issue, but I can't take care of this myself for a few weeks, at least. Robert, would you consider to try to write a patch? |
I'm not sure it is sensibly implementable in pure python: the semantics of signal handling (AIUI) are that the vm is interrupted, sets a flag to say 'when the GIL is released or the next bytecode interpretation happens, please process signal X' [the former for C extensions, the latter for regular code], and then the OS level signal handler returns. The current C or bytecode execution completes and then we dispatch to the python signal handler. Now, what we would want here is that attempts to acquire/release the RLock in a signal handler would behave as though the RLock.acquire/release methods were atomic. The general way to enforce such atomicity is via a lock or mutex. Now, because the call stack looks like this: The other acquire, if it already holds this supposed mutex, would block the python signal handler acquire call from obtaining it; The inner acquire would have to error *whether or not a timeout was passed*, because the non-signal-code won't progress and complete until the python signal handler code returns. One could, in principle, use stack inspection to determine how far through a nested acquire/release the outer code is, but that seems, uhm, pathological to me :). The crux of the problem is detecting whether the non-reentrant thread.lock is held because (a) another thread holds it or (b) this thread holds it. If we can inspect its internals, we could determine that this thread holds it - and thus we must be reentered into the acquire/release methods. With that tricky thing established, we could look at the remaining logic to make sure that each line is either atomic or conditional on reentrancy. Simpler I think to require CRLocks always, and provide a portable C implementation which will always complete before returning to execution in the calling thread, avoiding this issue entirely. |
Yes, using synchronization primitives or doing I/O in Python signal handlers isn't a good idea. Perhaps the signal docs should be clearer about that.
Indeed, setting a variable (or using set_wakeup_fd()) is the right approach. |
The core of the problem is that we don't just want those methods to be atomic or thread-safe, but reentrant (or rather async-safe). Note that the RLock implementation already checks whether the lock is already acquire by the current thread, but there's a "race": If the signal handler is called after _block has been acquired, but before self._owner is set, the next call to acquire from the signal handler won't "realize" that the block has already been acquired by the current thread, and will deadlock. Now, the fun part: this affects not only RLock, but every Python code performing "atomic" actions: condition variables, barriers, etc. There are some constraints on what can be done from a signal handler, and it should probably be documented. Note that another solution would be to use a dedicated thread for signal management (like Java does), but that's another story. Also, this shouldn't be a problem for the buffered I/O code, since the code already accounts for this possibility (if the lock is already acquired by the current thread, an exception is raised). Now, there's something I don't understand: I've just had a quick look, but AFAICT, there's no reason why the C version of RLock could not be available: am I missing something? Why do we even have a Python implementation? |
That sounds like a good solution in the middle-term. Are there any
Yes, but raising an exception is less helpful than doing what the user
The C version is quite recent, and there's a school of thought that we |
Just to be clear: the approach I was suggesting is to have a resident However I see two drawbacks:
Hmmm, I'm not convinced by this argument in this specific case: since |
In the issue bpo-13550, it was discussed to remove completly the logging machinery from the threading module. If we remove it, we don't need the Python implementation of RLock. We already removed the Python implementation of subprocess.Popen._execute_child() in Python 3.3 because of issues with fork, signals and threads. |
Normally I advocate very strongly for Python implementation of C accelerated modules, but when the two implementations are not equivalent, having a simpler Python one around does not help anyone (not users, other language implementors etc). True reentrancy is possible but quite hard to achieve. So, FWIW, +1 on just having a C implementation. The dedicated signal thread sounds useful too - it would ease the issues for folk using other parts of the stdlib, and would in principle permit a pure-python RLock to hang around. |
Hmm, but that would break single-threaded programs which expect their
I don't know if that's still useful to build Python without threads. I |
Yes, that's why I said "that"s another story" ;-)
There are another problems, for example it's very well known that The first and most simple thing we could do would be to nuke the |
To me, yes, but I think it's better to ask on python-dev as for nuking |
I just ran into this issue in the logging module using 2.7. Here's the TB in Traceback (most recent call last):
File "./crawler.py", line 531, in <module>
main(argv[1:]1:)
File "./crawler.py", line 522, in main
MCP(config).run()
File "./crawler.py", line 332, in run
self.reaper()
File "./crawler.py", line 359, in reaper
logging.debug('MCP process alive: %s', state)
File "/usr/local/lib/python2.7/logging/__init__.py", line 1600, in debug
root.debug(msg, *args, **kwargs)
File "/usr/local/lib/python2.7/logging/__init__.py", line 1120, in debug
self._log(DEBUG, msg, args, **kwargs)
File "/usr/local/lib/python2.7/logging/__init__.py", line 1250, in _log
self.handle(record)
File "/usr/local/lib/python2.7/logging/__init__.py", line 1260, in handle
self.callHandlers(record)
File "/usr/local/lib/python2.7/logging/__init__.py", line 1300, in callHandlers
hdlr.handle(record)
File "/usr/local/lib/python2.7/logging/__init__.py", line 746, in handle
self.release()
File "/usr/local/lib/python2.7/logging/__init__.py", line 700, in release
self.lock.release()
File "/usr/local/lib/python2.7/threading.py", line 147, in release
self.__block.release()
thread.error: release unlocked lock Since I'm not using threads, getting thread errors was a little bit of |
I am using Python 2.6.5 (we will be upgrading to Python 2.7.9 soon) and I recently ran into this bug. If I do any locking in a signal handler with RLocks, the entire system can deadlock. I'm using this to serialize my IO so we don't have mismatched lines in our logs. It looks like RLock was implemented in C in 3.2. While I don't care about the performance benefits of that rewrite, having a non-deadlocking RLock implementation would be nice. Not sure if this issue can be fixed in 2.7, but it would be nice. C RLock implementation here: |
FYI I proposed a fix for eventlet to fix eventlet with Python 3 when monkey-patching is used: The change forces the Python implementation of RLock, which is compatible with eventlet monkey-patching. The Python implementation of RLock gets the thread identifier from the monkey-patched threading module, whereas the C implementation calls directly a C function 8000 to get the identifier which is incompatible with eventlet. If the Python implementation is simply dropped, eventlet may uses its own implementation (copy/paste code from older Python version). |
I disagree that Python 3.4 is affected: RLock has been reimplemented in C in Python 3.2. Only the Python implementation of RLock has the bug, but it's not used by fault, you have to explicitly use a private attribute of the threading module to get it. Python 2.6 only accept security fixes, so in fact only Python 2.7 may get a fix. The question is if it worth to backport 300 lines of C code from Python 3 to Python 2 in the _thread module. @benjamin, release manager of Pythn 2.7: What do you think? I may help to backport the C implementation of RLock. |
This bug has existed for ages. People who want sane behaviour should just switch to Python 3. Closing. |
hmm issue still exist in master branch. |
For the third time, only the Python implementation has the bug, and it's not used by default. So the bug was fixed in Python 3 since 3.2. It's time to upgrade guys ;-) |
STINNER Victor wrote:
|
Please make some effort to try to understand the issue. I attach hang2.py which doesn't force the Python implementation of RLock. You might try hang2.py on Python 2 and Python 3 if you are not convinced yet that Python 3 has not the bug... |
STINNER Victor wrote:
|
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: