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
Raise an error if a dict is modified during a lookup #58413
Comments
Lib/test/crashers/nasty_eq_vs_dict.py does crash Python because of an I proposed to make the lookup fail with a RuntimeError if the |
Another possibility is to avoid the modification of a dictionary |
I much prefer dict_lookup.patch to nomodify.patch. There is a very subtle semantic change, but I think it is a positive one. |
nomodify.patch is the correct fix, but I agree that dict_lookup.patch is better (and sufficient) in practive.
Yes, that's how I chose the exception. >>> d={k: str(k) for k in range(10)}
>>> for k in d:
... del d[k]
...
RuntimeError: dictionary changed size during iteration
>>> d={k for k in range(10)}
>>> for k in d:
... d.remove(k)
...
RuntimeError: Set changed size during iteration
>>> d=list(range(10))
>>> def keyfunc(x):
... d.append(1)
... return x
...
>>> d.sort(key=keyfunc)
ValueError: list modified during sort |
Yeah, dict_lookup.patch seems fine. |
New changeset 934aaf2191d0 by Victor Stinner in branch 'default': |
Can't this be triggered by non-malicious code that just happened to have a python comparison and get hit with a thread switch? I'm not sure how often it happens, but today it would not be visible to the user; after the patch, users will see a sporadic failure that they can't easily defend against. Would it be worth adding a counter to lookdict, so that one modification is OK, but 5 aren't? |
The issue was triggered without threads.If the __eq__ method of the
If you implement a special type modifying the dict that contains the Honestly, if you get RuntimeError, you should change your program, not |
Jim Jewett wrote:
So, they are writing to a dict in one thread while reading from the same
I suspect, they are already seeing sporadic failures. We should document the new behaviour, as it is a change in semantics. |
Oh, I mean: trigger the issue with threads. I hope that your objects |
On Tue, Mar 6, 2012 at 8:56 AM, Mark Shannon <report@bugs.python.org> wrote:
+1 on everything you said. |
On Tue, Mar 6, 2012 at 11:56 AM, Mark Shannon wrote:
Correct. For example, it could be a configuration manager, or a
How? The chain terminates as soon as the dict doesn't resize; without |
On Tue, Mar 6, 2012 at 9:43 AM, Jim Jewett <report@bugs.python.org> wrote:
>
> Jim Jewett <jimjjewett@gmail.com> added the comment:
>
> On Tue, Mar 6, 2012 at 11:56 AM, Mark Shannon wrote:
>
>> Jim Jewett:
>>> Can't this be triggered by non-malicious code that just happened
>>> to have a python comparison and get hit with a thread switch?
>
>> So, they are writing to a dict in one thread while reading from the
>> same dict in another thread, without any external locks and with
>> keys written in Python.
>
> Correct. For example, it could be a configuration manager, or a
> cache, or even a worklist. If they're just adding new keys, or even
> deleting other (==> NOT the one being looked up) keys, why should that
> keep them from finding the existing, unchanged keys? Use a lock or a built-in key class. I realize that neither is ideal,
Now I'm torn. If we'd have this RuntimeError from the start, would we Note that Victor's alternate fix (nomodify.diff) has the same problem |
On Tue, Mar 6, 2012 at 1:05 PM, Guido van Rossum Jim Jewett:
I would personally have considered it a flaw. Wrapping all dict
In that case, you've explicitly asked for the whole set (or at least a But in this case, you're asking for a specific key, and the key is The RuntimeError exposes a race condition to user code -- and it may
Right; I'm saying that raising an error is the wrong thing to do. But there is an analogy to the hash-code change vs collision counting. (It really was a question initially, but I've now convinced at least myself.) I would rather see something like replacing 349 else { with 349 else { where lookdict_paranoid is a near-copy of lookdict that adds a else {
/* The compare did major nasty stuff *again*. */
if (paranoia < PARANOIA_LEVEL) {
return lookdict_paranoid(mp, key, hash, paranoia+1);
}
/* Something is wrong; raise a RuntimeError. */
} |
New changeset 0255bafbccf2 by Victor Stinner in branch 'default': |
Guido: So, what do you think? Do you like the new behaviour, or would you prefer a softer solution like retrying N times? Python < 3.3 retries the lookup an unlimited number of times which lead to a crash, that's the issue fixed by my change. |
I'm still torn. Can you implement the counter without adding an extra field to the dict object? I worry that we'll get the same objection we had when MAL proposed collision counting as a solution to the hash DoS attack. The numbers 0, 1 and infinity are special; all others are to be treated with skepticism. I'm prepared to make the current semantic change a 3.3 feature, but I'm not sure it's safe to backport. |
Add a counter requires to change the prototype of the C lookup function:
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, Py_hash_t hash); I don't know if it is a problem for ABI compatibility or extension
Is it really useful to only retry once? Retrying once means that it Or do I misunderstand the problem? |
On Thu, Mar 8, 2012 at 4:56 PM, STINNER Victor <report@bugs.python.org> wrote:
Agreed.
Sorry, I was joking; anyway, 1 in this context would mean do the Given all this I think we should keep it as you have committed and add |
On Thu, Mar 8, 2012 at 7:43 PM, STINNER Victor wrote:
How does it lead to a crash? I think it just leads to an infinite Guido van Rossume:
Yes. The logic does add complexity, but there is no per-dict charge,
That should still be the signature for the primary lookup; it can do
Yes; it isn't clear whether to cut off after 1 retry or 3 or 10...
Without threads, there shouldn't be any problems for innocent code. -jJ |
Unmodified CPython (without the patch) already passes the new test in the patch. The unmodified code already raises a Runtime error; a recursion limit exceeded error! |
You should try Lib/test/crashers/nasty_eq_vs_dict.py, not my test. |
New changeset a428f85de29c by Victor Stinner in branch 'default': |
Guido> Given all this I think we should keep it as you have committed I updated What's New in Python 3.3 document. I also wrote an email to python-dev to notify all developers of this change. Let's close the issue. |
STINNER Victor wrote:
The tests still need to fixed test_mutating_lookuptest doesn't fail on the unmodified version of cpython. There should be at least one failing test for this issue. |
New changeset 70fbb02d588c by Victor Stinner in branch 'default': |
Was the crasher ever converted into a unittest? I think that should be done regardless of the outcome of the ongoing discussion about this issue. |
New changeset 93748e2d64e3 by Antoine Pitrou in branch 'default': |
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: