Skip to content
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

bpo-35615: Fix crashes when copying a Weak{Key,Value}Dictionary. #11384

Merged
merged 6 commits into from Feb 7, 2019

Conversation

ltfish
Copy link
Contributor

@ltfish ltfish commented Dec 31, 2018

Protect dict iterations by wrapping them with _IterationGuard in the
following methods:

- WeakValueDictionary.copy()
- WeakValueDictionary.__deepcopy__()
- WeakKeyDictionary.copy()
- WeakKeyDictionary.__deepcopy__()

https://bugs.python.org/issue35615

Protect dict iterations by wrapping them with _IterationGuard in the
following methods:

- WeakValueDictionary.copy()
- WeakValueDictionary.__deepcopy__()
- WeakKeyDictionary.copy()
- WeakKeyDictionary.__deepcopy__()
@the-knights-who-say-ni
Copy link

Hello, and thanks for your contribution!

I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA).

Unfortunately we couldn't find an account corresponding to your GitHub username on bugs.python.org (b.p.o) to verify you have signed the CLA (this might be simply due to a missing "GitHub Name" entry in your b.p.o account settings). This is necessary for legal reasons before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue.

You can check yourself to see if the CLA has been received.

Thanks again for your contribution, we look forward to reviewing it!

@serhiy-storchaka serhiy-storchaka added type-bug An unexpected behavior, bug, or error needs backport to 3.7 labels Dec 31, 2018
Copy link
Member

@pitrou pitrou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. We might want to add a test (see check_weak_destroy_and_mutate_while_iterating in test_weakref.py).

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to write a test reproducing the bug? (to then make sure that the fix works as expected)

@ltfish
Copy link
Contributor Author

ltfish commented Jan 4, 2019

Is it possible to write a test reproducing the bug? (to then make sure that the fix works as expected)

As much as I want, I couldn't write a reliable test case in a standalone Python file to trigger this bug. I could, however, reliably trigger it when debugging my multithreaded PySide2 application. We also happened to trigger it once on our CI after hundreds of runs. I'm open to ideas of how to write a reliable test case to trigger this bug!

@pitrou
Copy link
Member

pitrou commented Jan 4, 2019

I explained above where to look for and how to add a test. Please take a look.

Edit: ok, the challenge here is to execute arbitrary code during the copy method since the iterator doesn't survive the method call. Other methods using IterationGuard are easier to test since they are generators, so you can easily execute code in-between loop iterations.

@ltfish
Copy link
Contributor Author

ltfish commented Jan 4, 2019

@pitrou I took a look at check_weak_destroy_and_mutate_while_iterating(). It seems to me that this test case is ensuring keys/values of weakref dictionary can be destroyed during iterations. However, without monkeypatch, I cannot inject code to delete an object during the execution copy() or __deepcopy__() -- unless I'm missing something. Any suggestions?

@pitrou
Copy link
Member

pitrou commented Jan 4, 2019

@ltfish You're right, I was too optimistic. I think we're left to using two threads: one that heavily copies a very large weakdict, another that loses references to the weakdict keys' or values', one at a time, in more or less random order.

@ltfish
Copy link
Contributor Author

ltfish commented Jan 4, 2019

@pitrou Thanks. Lemme give it a try!

@ltfish ltfish force-pushed the fix/weakref_dicts_copy_crash branch from a8af3a7 to bfaeb71 Compare January 5, 2019 00:35
@ltfish ltfish force-pushed the fix/weakref_dicts_copy_crash branch from bfaeb71 to e2b9d24 Compare January 5, 2019 00:36
@ltfish
Copy link
Contributor Author

ltfish commented Jan 5, 2019

On master branch:

$ ../python -m unittest -v test.test_weakref.MappingTestCase
test_make_weak_keyed_dict_from_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_keyed_dict_from_weak_keyed_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_keyed_dict_repr (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_from_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_from_weak_valued_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_misc (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_repr (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_key_dict_copy (test.test_weakref.MappingTestCase) ... FAIL
test_threaded_weak_key_dict_deepcopy (test.test_weakref.MappingTestCase) ... FAIL
test_threaded_weak_value_dict_copy (test.test_weakref.MappingTestCase) ... FAIL
test_threaded_weak_value_dict_deepcopy (test.test_weakref.MappingTestCase) ... FAIL
test_threaded_weak_valued_consistency (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_valued_pop (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_valued_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_bad_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_cascading_deletes (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_popitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_update (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_iters (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_len_cycles (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_len_race (test.test_weakref.MappingTestCase) ... ok
test_weak_keys (test.test_weakref.MappingTestCase) ... ok
test_weak_keys_destroy_while_iterating (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_popitem (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_update (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_iters (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_len_cycles (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_len_race (test.test_weakref.MappingTestCase) ... ok
test_weak_values (test.test_weakref.MappingTestCase) ... ok
test_weak_values_destroy_while_iterating (test.test_weakref.MappingTestCase) ... ok

======================================================================
FAIL: test_threaded_weak_key_dict_copy (test.test_weakref.MappingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1760, in test_threaded_weak_key_dict_copy
    self.check_threaded_weak_dict_copy(weakref.WeakKeyDictionary, False)
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1753, in check_threaded_weak_dict_copy
    self.fail("Caught a RuntimeError: \"%s\". WeakRef dictionary copying failed." % exc[0])
AssertionError: Caught a RuntimeError: "dictionary changed size during iteration". WeakRef dictionary copying failed.

======================================================================
FAIL: test_threaded_weak_key_dict_deepcopy (test.test_weakref.MappingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1765, in test_threaded_weak_key_dict_deepcopy
    self.check_threaded_weak_dict_copy(weakref.WeakKeyDictionary, True)
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1753, in check_threaded_weak_dict_copy
    self.fail("Caught a RuntimeError: \"%s\". WeakRef dictionary copying failed." % exc[0])
AssertionError: Caught a RuntimeError: "dictionary changed size during iteration". WeakRef dictionary copying failed.

======================================================================
FAIL: test_threaded_weak_value_dict_copy (test.test_weakref.MappingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1770, in test_threaded_weak_value_dict_copy
    self.check_threaded_weak_dict_copy(weakref.WeakValueDictionary, False)
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1753, in check_threaded_weak_dict_copy
    self.fail("Caught a RuntimeError: \"%s\". WeakRef dictionary copying failed." % exc[0])
AssertionError: Caught a RuntimeError: "dictionary changed size during iteration". WeakRef dictionary copying failed.

======================================================================
FAIL: test_threaded_weak_value_dict_deepcopy (test.test_weakref.MappingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1775, in test_threaded_weak_value_dict_deepcopy
    self.check_threaded_weak_dict_copy(weakref.WeakValueDictionary, True)
  File "/home/fish/projects/cpython/Lib/test/test_weakref.py", line 1753, in check_threaded_weak_dict_copy
    self.fail("Caught a RuntimeError: \"%s\". WeakRef dictionary copying failed." % exc[0])
AssertionError: Caught a RuntimeError: "dictionary changed size during iteration". WeakRef dictionary copying failed.

----------------------------------------------------------------------
Ran 34 tests in 4.820s

FAILED (failures=4)

With this PR:

$ ../python -m unittest -v test.test_weakref.MappingTestCase
test_make_weak_keyed_dict_from_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_keyed_dict_from_weak_keyed_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_keyed_dict_repr (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_from_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_from_weak_valued_dict (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_misc (test.test_weakref.MappingTestCase) ... ok
test_make_weak_valued_dict_repr (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_key_dict_copy (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_key_dict_deepcopy (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_value_dict_copy (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_value_dict_deepcopy (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_valued_consistency (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_valued_pop (test.test_weakref.MappingTestCase) ... ok
test_threaded_weak_valued_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_bad_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_cascading_deletes (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_popitem (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_dict_update (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_iters (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_len_cycles (test.test_weakref.MappingTestCase) ... ok
test_weak_keyed_len_race (test.test_weakref.MappingTestCase) ... ok
test_weak_keys (test.test_weakref.MappingTestCase) ... ok
test_weak_keys_destroy_while_iterating (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_delitem (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_popitem (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_setdefault (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_dict_update (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_iters (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_len_cycles (test.test_weakref.MappingTestCase) ... ok
test_weak_valued_len_race (test.test_weakref.MappingTestCase) ... ok
test_weak_values (test.test_weakref.MappingTestCase) ... ok
test_weak_values_destroy_while_iterating (test.test_weakref.MappingTestCase) ... ok

----------------------------------------------------------------------
Ran 34 tests in 6.228s

OK

@ltfish
Copy link
Contributor Author

ltfish commented Jan 24, 2019

Hi there, is there any update on this PR?

@pitrou
Copy link
Member

pitrou commented Jan 24, 2019

Sorry @ltfish. I'm gonna review soon.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I would prefer to have a second core dev like @pitrou to review this change.

As soon as the change has a test, it looks good to me :-)

Does the tests fail without the fix?

@ltfish
Copy link
Contributor Author

ltfish commented Jan 25, 2019

Does the tests fail without the fix?

Please see my previous reply. Without the fix, my newly added test case fails reliably on my machine. I can also open a PR without my fix if needed (to make sure the test case works as expected on the CI).

rhelmot added a commit to angr/angr that referenced this pull request Feb 5, 2019
rhelmot added a commit to angr/angr that referenced this pull request Feb 5, 2019
@pitrou pitrou merged commit 96d37db into python:master Feb 7, 2019
@miss-islington
Copy link
Contributor

Thanks @ltfish for the PR, and @pitrou for merging it 🌮🎉.. I'm working now to backport this PR to: 3.7.
🐍🍒⛏🤖

@pitrou
Copy link
Member

pitrou commented Feb 7, 2019

Thanks @ltfish !

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Feb 7, 2019
…honGH-11384)

Protect dict iterations by wrapping them with _IterationGuard in the
following methods:

- WeakValueDictionary.copy()
- WeakValueDictionary.__deepcopy__()
- WeakKeyDictionary.copy()
- WeakKeyDictionary.__deepcopy__()
(cherry picked from commit 96d37db)

Co-authored-by: Fish <[email protected]>
@bedevere-bot
Copy link

GH-11785 is a backport of this pull request to the 3.7 branch.

pitrou pushed a commit that referenced this pull request Feb 7, 2019
…11384) (GH-11785)

Protect dict iterations by wrapping them with _IterationGuard in the
following methods:

- WeakValueDictionary.copy()
- WeakValueDictionary.__deepcopy__()
- WeakKeyDictionary.copy()
- WeakKeyDictionary.__deepcopy__()
(cherry picked from commit 96d37db)

Co-authored-by: Fish <[email protected]>
@ltfish ltfish deleted the fix/weakref_dicts_copy_crash branch February 7, 2019 20:48
@ltfish
Copy link
Contributor Author

ltfish commented Feb 7, 2019

@pitrou @vstinner Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants