-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
crash appending list and namedtuple #53093
Comments
c:\>python
Python 3.1.2 (r312:79149, Mar 21 2010, 00:41:52) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> foo = namedtuple('foo', '')
>>> [1] + foo() At this point the interpreter crashes. Also happens when foo has named arguments, and in batch scripts. foo() + [1] throws a TypeError as expected. [] + foo() returns (). The immediate cause of the crash is the CALL instruction at 1E031D5A in python31.dll jumping into uninitialized memory. |
I can't reproduce this on either 3.1.2 or py3k trunk. |
Running the exact same binary on winxp with an amd athlon processor, Even though foo()+[1] correctly raises a TypeError, the reverse [1] + foo() very bizarrely produces a length 1 tuple whose bizarre member is supposedly an instance of a empty list [] with length 1 from collections import namedtuple
foo = namedtuple('foo', '')
a = [1] + foo()
b=a[0]
print (type(a), len(a), type(b), len(type(b)), type(type(b))) # <class 'tuple'> 1 [] 1 <class 'list'> ([2]+foo() produces same output) Other than the above, any attempt to do anything with b or type(b) that I tried crashes. I presume that this is due to attribute lookups on the invalid type(b) (or something like that), which type(b) must bypass. In particular, the OP's crash is due to trying to print the tuple which tries to print its member which looks for type(b).__str__ which crashes. To anyone: investigating crashers like this is a lot easier with IDLE and an edit window than with the interactive command window. |
More experiments from collections import namedtuple
foo = namedtuple('foo', '')
a = [] + foo()
print (a, type(a), len(a))
# () <class 'tuple'> 0 ie, a standard empty tuple, whereas There are also some funny interactions. Adding after the 'foo = ' line in my original 5 line example causes the final print to crash, whereas adding the same 4 lines to the 4 line example at the beginning of this message does not obviously change anything. David, since you omitted all details, I wonder if you tested in batch mode, as I did, and hence never tried to print the malformed object, or used different OS or hardware. |
I can't reproduce on 3k trunk with Ubuntu 10.04, gcc 4.4.3 namedtuples are just a subclass of tuple with only two dunder methods defined (a plain __new__ with empty __slots__). Can you provoke the same behavior with plain tuples, or a subclass of tuple that looks like one of these? class Crasher(tuple): pass
class Crasher(tuple):
__slots__ = ()
class Crasher(tuple):
def __new__(cls,): return tuple.__new__(cls,)
__slots__ = () |
Substituting class Crasher(tuple): pass
foo = Crasher()
a = [1] + foo
b=a[0]
print (type(a), len(a), type(b), len(type(b)), type(type(b))) <class 'tuple'> 1 [] 1 <class 'list'> as before, so namedtuple is not, in particular, the culprit. |
Two more probes:
|
"can't reproduce" does not inform as to what *did* happen with which code. More experiments: class s(str): pass
foo = s()
TypeError: Can't convert 'list' object to str implicitly Why is it trying to do that? Of course, the interpreter can (implicitly) convert list to tuple, which must be what happens in OP example. The subclasses of tuple and str do not gain an __radd__ method. If we add one class s(str):
def __radd__(s,o): print('in radd, type(o)')
foo = s()
a = [1] + foo
# prints "in radd <class 'list'>" no implicit conversion is done. Reversing tuple and list class Crasher(list): pass
a = () + Crasher() # or Crasher([1])
print(a, type(a), len(a))
#[] <class 'list'> 0 # or [1] <class 'list'> 1 whereas |
|
if the id() of the left operand is identical to the id() of the first element in the result it would strongly support compiler skulldugerry. class Crasher(tuple): pass
foo = Crasher()
x = [1]
a = x + foo
b=a[0]
if id(b) == id(x):
raise Exception("It's the C compiler what did it!") The only way I can think of this coming about is the right_op getting new'd and then .extend'ing(left_op). That extend() must be going batsh*t and inserting the left_op instead of it's contained items. The C-code for extend is more fiddly than the code for concatenation so there is more room for the compiler to generate bad code. |
Good try, but for one run, the ids of foo, x, a, and b are
>>>
15719440 15717880 15273104 12266976 |
The binaries get compiled with the PGInstrument/PGUpdate configurations. |
I've just found that:
crashes, but:
gives: Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
[1].__add__(foo())
TypeError: can only concatenate list (not "foo") to list (IDLE on Windows XP) |
The bug is still present in 3.2. |
For some reasons I was able to reproduce under 64-bit Windows with the 3.3b1 official build, but neither with my own VS9.0-compiled build, nor with the 3.2 official build. To reproduce: >>> class T(tuple): pass
...
>>> t = T((1,2))
>>> [] + t
(1, 2)
>>> [3,] + t
# crash I tried to use the debugging symbols but it doesn't help a lot. The primary issue seems to be that the concatenation doesn't raise TypeError, and instead constructs an invalid object which then makes PyObject_Repr() crash. Also, it is not the Python compiler, the same thing happens with constructor calls: >>> list() + T([1,2])
(1, 2)
>>> list((3,)) + T([1,2])
# crash And no it doesn't happen with list subclasses: >>> class L(list): pass
...
>>> class T(tuple): pass
...
>>> L([]) + T([1,2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "T") to list
>>> [] + T([1,2])
(1, 2) Also, directly calling the __add__ method doesn't trigger the issue, but operator.add does: >>> l + T([1,2])
(1, 2)
>>> operator.add(list(), T([1,2]))
(1, 2)
>>> list().__add__(T([1,2]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "T") to list |
The exact same issue happens when concatenating a list subclass to a tuple: >>> () + L([1,2])
[1, 2]
>>> (3,) + L([1,2])
# crash Also, note that in this case a list is returned, not a tuple despite the first operand being a tuple. Conversely: >>> [] + T((1,2))
(1, 2) A tuple is returned, not a list. Which is exactly what happens when calling T.__add__: >>> T.__add__((), T((1,2)))
(1, 2) My intuition is that the issue is somewhere in binary_op1() (called by PyNumber_Add) in abstract.c. I can't go much further since my own builds don't exhibit the issue. (in the end, chances are it's a MSVC compiler bug) |
Raising priority. This should be investigated properly before 3.3 final. |
I can reproduce this exclusively with the pgupdate build: msbuild PCbuild\pcbuild.sln /p:Configuration=PGInstrument /p:Platform=win32 PCbuild\Win32-pgo\python.exe
Python 3.3.0b1 (default, Jul 30 2012, 23:45:42) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> foo = namedtuple('foo', '')
>>> [1] + foo()
Interpreter exits silently here. So it could be either an optimizer |
When Python is compiled by Visual Studio 10 in PGUpdate mode, duplicated functions are merged to become the same unique function. The C code of wrap_binaryfunc() and wrap_binaryfunc_l() functions is the same and so both functions get the same address. For "class List(list): pass", type_new() fills type->tp_as_number->nb_add to list_concat() because "d->d_base->wrapper == p->wrapper" is True whereas it should be False (wrap_binaryfunc vs wrap_binaryfunc_l). A workaround is to use a different code for wrap_binaryfunc() and wrap_binaryfunc_l(). A real fix is to use something else than the address of the wrapper to check the type of the operator (left, right, or the 3rd type). |
Nice detective work, Victor. Can we turn that particular optimisation off? We use function addresses for identification purposes in many more places than just this one. Having the compiler merge different pointers just because the functions happen to have the same implementation is simply *not cool* from the point of view of the CPython code base. |
Nice work Victor.
/OPT:NOICF is probably what we are looking for [1]: """ Now it makes sense that this only crops up with the PGO builds -- those are the only ones where we link with /OPT:ICF. Can someone try out this option? I would, but I don't have a Windows box handy. |
I believe the compiler is completely entitled to do so according to the C language definition. There is no guarantee that two different functions have two different addresses as long as calling the function pointer does the same thing according to the as-if rule. So we really need to fix Python, not work-around in the compiler. There may be many more compilers which use the same optimisation. Python relying on undefined behavior is simply *not cool*. |
OTOH, 6.5.9p6 says "Two pointers compare equal if and only if both are null pointers, both This is probably meant to imply that pointers to different functions must not compare equal. So if this is determined to be a compiler bug, the most natural conclusion is to stop using PGI/PGO entirely. |
I think so. Also, in our case the functions have different names, therefore
I think it is non-conformant behavior. Microsoft warns about it in their Also, this isn't really a problem with PGO. AFAICT, it is the COMDAT folding Do we even use PGO to the fullest extent? Does someone actually build an Also, PGO is only available on the Premium and Ultimate versions of VC++ [1]. |
Even if PGO is not available, wrap_binaryfunc() and wrap_binaryfunc_l() functions get the same address when Python is compiled in "PGUpdate" mode. (I tried Visual C++ 2010 Express). The issue was seen at least with the following versions: Python 3.1.2 (r312:79149, Mar 21 2010, 00:41:52) [MSC v.1500 32 bit (Intel)] on win32 So the issue was also reproduced with old Python versions compiled with Visual C++ 2008, and I'm not sure that the "ICF" optimization is only enabled in "Profile-Guided" (PG*) modes. If we choose to change Visual Studio options instead of changing the mode, we may also try /Gy- option: |
Yes, I do, on every release of Python. The test set includes at the minimum This issue wouldn't have been reported in the first place if this I don't mind just not doing it anymore; it speeds up the release process.
It was originally added because people reported measurable speedups when
I do, of course, have at least a professional edition of Visual Studio to make |
Martin v. L??wis <report@bugs.python.org> wrote:
For libmpdec/64-bit I've measured huge speedups in the order of 50% (with |
Here's a patch based on the analysis. All test cases given here |
I presume the previously crashing test cases should be added to the test suite, to detect reversion. Is there a method (faulthandler?) to keep tests going, or stop gracefully, when adding a once-crasher that could revert? |
I think we want to add those tests to the test suite as well. |
Antoine Pitrou <report@bugs.python.org> wrote:
What's a good place? Shall we just add one of the tests to test_tuple? Also, the only person to run the tests with the PGO build will probably |
Sounds good. And another of them to test_list perhaps as well :)
True, but other people may still run them *after* the release. |
New patches with tests for 3.2 and 3.3. For 3.2 I determined empirically If anyone can confirm that this is the case or has a pointer to |
We could set up a buildbot slave which does PGO builds, provided somebody |
While the actual values for the XML schema aren't documented, I expect that To confirm, just look up the setting in the UI. |
New changeset 5a8c5631463f by Martin v. Löwis in branch '2.7': |
New changeset 2638ce032151 by Martin v. Löwis in branch '3.2': New changeset 029cde4e58c5 by Martin v. Löwis in branch 'default': New changeset d3afe5d8a4da by Martin v. Löwis in branch 'default': |
Thanks for the research and the fix! |
You didn't add any test for non regression?? |
Please rephrase your question: what tests did I not add? |
Ah yes, you added new tests to Python 3.2 and 3.3, but no to Python |
The tests don't crash Python 2.7. So they are not useful as a test whether |
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: