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

3.1 cannot unpickle 2.7-created pickle #49430

Closed
pitrou opened this issue Feb 7, 2009 · 25 comments
Closed

3.1 cannot unpickle 2.7-created pickle #49430

pitrou opened this issue Feb 7, 2009 · 25 comments
Assignees
Labels
extension-modules C modules in the Modules dir type-bug An unexpected behavior, bug, or error

Comments

@pitrou
Copy link
Member

pitrou commented Feb 7, 2009

BPO 5180
Nosy @malemburg, @mdickinson, @abalkin, @pitrou, @avassalotti
Superseder
  • bpo-9267: Update pickle opcode documentation in pickletools for 3.x
  • Files
  • 27.bench
  • pickle-bug.py: run with python2
  • unpickle-bug.py: run with python3
  • issue5180-pickle.diff
  • unpickle-bug-2.py
  • issue5180.diff: _pickle.c fixes with tests
  • issue5180a.diff: Patch for py3k branch, revision 82911.
  • issue5180b.diff
  • 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:

    assignee = 'https://github.com/abalkin'
    closed_at = <Date 2010-07-17.23:02:54.180>
    created_at = <Date 2009-02-07.22:28:53.400>
    labels = ['extension-modules', 'type-bug']
    title = '3.1 cannot unpickle 2.7-created pickle'
    updated_at = <Date 2010-07-17.23:02:54.179>
    user = 'https://github.com/pitrou'

    bugs.python.org fields:

    activity = <Date 2010-07-17.23:02:54.179>
    actor = 'belopolsky'
    assignee = 'belopolsky'
    closed = True
    closed_date = <Date 2010-07-17.23:02:54.180>
    closer = 'belopolsky'
    components = ['Extension Modules']
    creation = <Date 2009-02-07.22:28:53.400>
    creator = 'pitrou'
    dependencies = []
    files = ['12973', '17784', '17785', '17786', '17787', '17792', '18017', '18037']
    hgrepos = []
    issue_num = 5180
    keywords = ['patch']
    message_count = 25.0
    messages = ['81351', '81352', '108768', '108769', '108771', '108796', '108799', '108812', '108814', '108815', '108817', '108819', '108821', '108846', '108866', '108879', '108880', '108885', '108897', '110383', '110536', '110541', '110559', '110592', '110613']
    nosy_count = 5.0
    nosy_names = ['lemburg', 'mark.dickinson', 'belopolsky', 'pitrou', 'alexandre.vassalotti']
    pr_nums = []
    priority = 'high'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = '9267'
    type = 'behavior'
    url = 'https://bugs.python.org/issue5180'
    versions = ['Python 3.1', 'Python 3.2']

    @pitrou
    Copy link
    Member Author

    pitrou commented Feb 7, 2009

    The pickle was generated by pybench. You can try to display it by running:

    ./python Tools/pybench/pybench.py --debug -s 27.bench

    It fails with the following traceback:

    Traceback (most recent call last):
      File "Tools/pybench/pybench.py", line 954, in <module>
        PyBenchCmdline()
      File "/home/antoine/py3k/__svn__/Tools/pybench/CommandLine.py", line
    349, in __init__
        rc = self.main()
      File "Tools/pybench/pybench.py", line 888, in main
        bench = pickle.load(f)
      File "/home/antoine/py3k/__svn__/Lib/pickle.py", line 1335, in load
        return Unpickler(file, encoding=encoding, errors=errors).load()
    _pickle.UnpicklingError: bad pickle data

    @pitrou pitrou added extension-modules C modules in the Modules dir type-bug An unexpected behavior, bug, or error labels Feb 7, 2009
    @pitrou
    Copy link
    Member Author

    pitrou commented Feb 7, 2009

    Here is the pickle file.

    @abalkin
    Copy link
    Member

    abalkin commented Jun 27, 2010

    If I disable _pickle, I get a more meaningful trace:

    File "Tools/pybench/pybench.py", line 954, in <module>
    PyBenchCmdline()
    File "/Users/sasha/Work/python-svn/py3k-commit/Tools/pybench/CommandLine.py", line 349, in __init__
    rc = self.main()
    File "Tools/pybench/pybench.py", line 888, in main
    bench = pickle.load(f)
    File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1321, in load
    encoding=encoding, errors=errors).load()
    File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 830, in load
    dispatchkey[0]
    File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1055, in load_inst
    klass = self.find_class(module, name)
    File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1115, in find_class
    __import__(module, level=0)
    File "/Users/sasha/Work/python-svn/py3k-commit/Tools/pybench/Unicode.py", line 17
    s = unicode(u''.join(map(str,range(100))))
    ^
    SyntaxError: invalid syntax

    Apparently, pybench/Unicode.py has never been converted to py3k. This is likely not the only problem:

    >>> from pybench import *
    >>> import pickle
    >>> pickle.load(open('27.bench'))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1321, in load
        encoding=encoding, errors=errors).load()
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 829, in load
        assert isinstance(key, bytes_types)
    AssertionError

    This looks like the infamous bytes vs string problem ...

    @abalkin
    Copy link
    Member

    abalkin commented Jun 27, 2010

    The bytes/string issu was a red herring: with pickle.load(open('27.bench', 'b')), I get the same stack trace as from command line pybench invocation.

    >>> pickle.load(open('27.bench', 'rb'))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1321, in load
        encoding=encoding, errors=errors).load()
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 830, in load
        dispatch[key[0]](self)
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1055, in load_inst
        klass = self.find_class(module, name)
      File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1115, in find_class
        __import__(module, level=0)
      File "/Users/sasha/Work/python-svn/py3k-commit/Tools/pybench/Unicode.py", line 17
        s = unicode(u''.join(map(str,range(100))))
                      ^
    SyntaxError: invalid syntax

    @abalkin
    Copy link
    Member

    abalkin commented Jun 27, 2010

    Hmm. It looks like another pickle vs. _pickle issue. Attached patch is a result of 2to3 applied to Tools/pybench (and a minor manual fix for pickle import) with _pickle import disabled.

    With the patch applied,

    $ ./python Tools/pybench/pybench.py --debug -s 27.bench

    runs fine and produces results identical to those for a similar run from the trunk.

    Looks like it is time to fire up gdb.

    @malemburg
    Copy link
    Member

    Please keep the Python3 version of pybench compatible to Python 2.6 and 2.7.

    @abalkin
    Copy link
    Member

    abalkin commented Jun 27, 2010

    I have found bpo-1634499 which deals with running pybench. I suggest that we reopen that issue and move discussion of bpo-5180.diff there.

    I really like the idea to keep single source for 2.x and 3.x pybench, but we need to add some machinery to run 2.x pybench under python3.x transparently.

    Marc-Andre,

    are you taking over the pybench or the _pickle issue?

    @abalkin
    Copy link
    Member

    abalkin commented Jun 28, 2010

    It looks like I was able to get to the root of the problem. I am attaching two files that demonstrate the issue:

    ==> pickle-bug.py <==
    import pickle
    import sys
    class Bug:
    pass
    bug = Bug()
    f = open(sys.argv[1], 'w')
    pickle.Pickler(f, protocol=0).dump(bug)
    f.close()

    ==> unpickle-bug.py <==
    import pickle
    import sys
    class Bug:
    pass
    bug = pickle._Unpickler(open(sys.argv[1], 'rb')).load() # works
    print(bug)
    bug = pickle.Unpickler(open(sys.argv[1], 'rb')).load() # doesn't
    print(bug)

    $ python2 pickle-bug.py /tmp/bug.pkl
    $ python3 unpickle-bug.py /tmp/bug.pkl
    <__main__.Bug object at 0x1006b6f40>
    Traceback (most recent call last):
      File "unpickle-bug.py", line 7, in <module>
        bug = pickle.Unpickler(open(sys.argv[1], 'rb')).load() # doesn't
    _pickle.UnpicklingError: bad pickle data

    The problematic pickle is really small, so I hope I'll report that I have a fix soon.

    0: (    MARK
    1: i        INST       '\_\_main__ Bug' (MARK at 0)
    

    15: p PUT 0
    18: ( MARK
    19: d DICT (MARK at 18)
    20: p PUT 1
    23: b BUILD
    24: . STOP
    highest protocol among opcodes = 0

    @abalkin
    Copy link
    Member

    abalkin commented Jun 28, 2010

    As promised, here is the fix (bpo-5180-pickle.diff) for the "unpickle-bug.py" issue. Unfortunately, it looks like more bugs remain. 27.bench is still not loadable.

    @abalkin
    Copy link
    Member

    abalkin commented Jun 28, 2010

    The remaining bug is a bit harder. If you try to run unpickle-bug-2.py on the same pickle, you get

    $ python3 unpickle-bug-2.py /tmp/bug.pkl 
    <__main__.Bug object at 0x1006a8f40>
    Traceback (most recent call last):
      File "unpickle-bug-2.py", line 9, in <module>
        bug = pickle.Unpickler(open(sys.argv[1], 'rb')).load() # doesn't
    TypeError: ('__init__() takes exactly 2 arguments (1 given)', <class '__main__.Bug'>, ())

    The problem is acknowledged in the source code of instantiate function in _pickle.c:

    /* XXX: The pickle.py module does not create instances this way when the                                             
       args tuple is empty. See Unpickler.\_instantiate(). \*/
    

    Clearly, this is wrong because pickle.py way succeeds where _pickle does not.

    PS: I did not mean to unassign MAL, but I think this is more of alexandre.vassalotti's issue.

    @abalkin
    Copy link
    Member

    abalkin commented Jun 28, 2010

    I am attaching a patch which makes python3 read 27.bench without errors. I think this should be applied while a complete solution for unpickling old style class instances from text mode (protocol = 0) pickles is found.

    @malemburg
    Copy link
    Member

    Alexander Belopolsky wrote:

    Alexander Belopolsky <belopolsky@users.sourceforge.net> added the comment:

    I am attaching a patch which makes python3 read 27.bench without errors. I think this should be applied while a complete solution for unpickling old style class instances from text mode (protocol = 0) pickles is found.

    Please use a different strategy for "porting" the Unicode tests to
    Python3:

    Create a new test modules Bytes (which reuses the Strings
    tests) and a new test module Strings3 which reuses the Unicode
    tests. The Strings3 module should then use the patches you
    applied to the Unicode module.

    Finally, let the two test module Strings and Unicode fail with
    an ImportError in Python3.

    @malemburg
    Copy link
    Member

    Alexander Belopolsky wrote:
    > 
    > Alexander Belopolsky <belopolsky@users.sourceforge.net> added the comment:
    > 
    > The bytes/string issu was a red herring: with pickle.load(open('27.bench', 'b')), I get the same stack trace as from command line pybench invocation.
    > 
    > 
    >>>> pickle.load(open('27.bench', 'rb'))
    > Traceback (most recent call last):
    >   File "<stdin>", line 1, in <module>
    >   File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1321, in load
    >     encoding=encoding, errors=errors).load()
    >   File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 830, in load
    >     dispatch[key[0]](self)
    >   File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1055, in load_inst
    >     klass = self.find_class(module, name)
    >   File "/Users/sasha/Work/python-svn/py3k-commit/Lib/pickle.py", line 1115, in find_class
    >     __import__(module, level=0)
    >   File "/Users/sasha/Work/python-svn/py3k-commit/Tools/pybench/Unicode.py", line 17
    >     s = unicode(u''.join(map(str,range(100))))
    >                   ^
    > SyntaxError: invalid syntax

    Note that pybench is written in a way that it can handle ImportErrors
    and SyntaxErrors gracefully. This is per design, since test modules can
    well be written for more recent Python versions that support a certain
    features or syntax not present in earlier versions.

    In the above case, it may be a good idea to redirect pickle to
    the new Strings3 module (see my other message). Likewise, the
    Strings test module should redirect to the new Bytes test module
    in Python3.

    The Unicode and Strings modules should not be loaded in Python3.
    Instead, the Bytes and Strings3 modules should be used. This can
    be changed in Setup.py of pybench.

    @abalkin
    Copy link
    Member

    abalkin commented Jun 28, 2010

    I am attaching a patch which focuses on fixing _pickle behavior. I opened a separate bpo-9102 to deal with pybench specific problems.

    Marc-Andre,

    I am reassigning this issue to myself and assigning bpo-9102 to you. I hope you don't mind.

    My patch attempts to emulate 2.x PyInstance_NewRaw with a call to tp_alloc. I considered using tp_new, but although __new__ is not special for classic classes, there is nothing to prevent users to define __new__ in their classic classes or add __new__ when they port these classes to 3.x. (BTW, I wonder what 2to3 does if classic class has __new__ defined.) This is slightly unorthodox use of tp_alloc, so I will follow up with a question on python-dev.

    @abalkin abalkin assigned abalkin and unassigned malemburg Jun 28, 2010
    @avassalotti
    Copy link
    Member

    Thank you for the nice investigative work!

    I will try my best to review this patch by next week.

    @pitrou
    Copy link
    Member Author

    pitrou commented Jun 28, 2010

    My patch attempts to emulate 2.x PyInstance_NewRaw with a call to
    tp_alloc.

    This is certainly the wrong thing to do. You could at least try PyBaseObject_Type.tp_new (see object_new() in typeobject.c), but even this is not necessarily right (for example if the class is derived from an extension type defining its own tp_new).

    So, IMO, the right thing to do would be to choose the first base type that isn't a Python-defined class and use its tp_new:

        staticbase = subtype;
        while (staticbase && (staticbase->tp_flags & Py_TPFLAGS_HEAPTYPE))
            staticbase = staticbase->tp_base;

    (these two lines are from tp_new_wrapper() in typeobject.c)

    That way you choose the right constructor function, yet don't call the Python-defined __new__ function. That's the only reasonable way of doing it I can imagine. It also follows the following behaviour:

    >>> class C(int):
    ...   def __new__(cls, *a):
    ...     print "__new__", a
    ...     return int.__new__(cls, *a)
    ... 
    >>> C(5)
    __new__ (5,)
    5
    >>> s = pickle.dumps(C(5))
    __new__ (5,)
    >>> x = pickle.loads(s)
    >>> x
    5

    As you can see, int.__new__ has been called on unpickling but not C.__new__ (no print).

    @pitrou
    Copy link
    Member Author

    pitrou commented Jun 28, 2010

    (and to follow on my example, I'd point that the unpickled instance is still an instance of the right class:

    >>> type(x)
    <class '__main__.C'>

    )

    @abalkin
    Copy link
    Member

    abalkin commented Jun 29, 2010

    On Mon, Jun 28, 2010 at 7:32 PM, Antoine Pitrou <report@bugs.python.org> wrote:

    > My patch attempts to emulate 2.x PyInstance_NewRaw with a call to
    > tp_alloc.

    This is certainly the wrong thing to do. You could at least try
    PyBaseObject_Type.tp_new (see object_new() in typeobject.c), but
    even this is not necessarily right (for example if the
    class is derived from an extension type defining its own tp_new).

    Well, this is not so obviously wrong as you make it sound. If you look closer at object_new(), you will see that in sane situations it reduces to type->tp_alloc(type, 0) call. Remember, this is called in very special circumstances: when unpickling old style class instances from a 2.x generated pickle. No arguments are available for tp_new call, so excess_args check does not apply. The other check is for an ABC, but the situation where an old-style class becomes an ABC in 2.x to 3.x transition is hardly sane. If such sanity check is necessary, I would rather check the flag explicitly in _pickle.c rather than having to generate dummy args and kwds to satisfy tp_new signature.

    So, IMO, the right thing to do would be to choose the first base
    type that isn't a Python-defined class and use its tp_new:

       staticbase = subtype;
       while (staticbase && (staticbase->tp_flags & Py_TPFLAGS_HEAPTYPE))
           staticbase = staticbase->tp_base;

    (these two lines are from tp_new_wrapper() in typeobject.c)

    This looks like overengineering to me. I think 3.x should simply refuse to unpickle old style class instances into anything that is not subclassed in python directly from object. It would be helpful at this point if you could provide a test case where tp_alloc logic does not work. Regardless of what algorithm we ultimately choose, I would like to include such test in regrtest.

    PS: Note that in my patch I've eliminated one control path that was calling instantiate(..) erroneously, but I have not reviewed the code carefully enough to eliminate the possibility that instantiate(..) would be called to instantiate new-style class instances.

    PPS: What is you opinion on the pickle.py trick of monkey patching __class__ on an instance of an empty class? Do you think this can be fooled?

    @pitrou
    Copy link
    Member Author

    pitrou commented Jun 29, 2010

    Well, this is not so obviously wrong as you make it sound. If you
    look closer at object_new(), you will see that in sane situations it
    reduces to type->tp_alloc(type, 0) call.

    Today, yes. But tomorrow it may entail additional operations.

    If such sanity check is necessary, I would rather check the flag
    explicitly in _pickle.c rather than having to generate dummy args and
    kwds to satisfy tp_new signature.

    I find it better to "generate dummy args and kwds to satisfy tp_new
    signature". I see no point in a partial inlining of an internal function
    when we could call it instead.

    > So, IMO, the right thing to do would be to choose the first base
    > type that isn't a Python-defined class and use its tp_new:
    >
    > staticbase = subtype;
    > while (staticbase && (staticbase->tp_flags & Py_TPFLAGS_HEAPTYPE))
    > staticbase = staticbase->tp_base;
    >
    > (these two lines are from tp_new_wrapper() in typeobject.c)
    >

    This looks like overengineering to me. I think 3.x should simply
    refuse to unpickle old style class instances into anything that is not
    subclassed in python directly from object.

    Right, but it would lead to almost the same code... Since you have to
    find the first non-Python base type and check that it is "object".

    It would be helpful at this point if you could provide a test case
    where tp_alloc logic does not work.

    I'll try to find one.

    PPS: What is you opinion on the pickle.py trick of monkey patching
    __class__ on an instance of an empty class? Do you think this can be
    fooled?

    I don't think so.

    @abalkin
    Copy link
    Member

    abalkin commented Jul 15, 2010

    Antoine,

    I think I have found a better solution. Since we are dealing with classic classes, they should not define __new__. If in porting to 3.x, users introduce __new__ that requires arguments, they will not be able to unpickle 2.x pickles no matter what we do.

    Therefore, I propose to replace _EmptyClass trick in pickle.py with a call to klass.__new__:

    •        value = \_EmptyClass()
      
    •        value.\_\_class__ = klass
      

    + value = klass.__new__(klass)

    and do the same in _pickle.c's instantiate:

    r = PyObject_CallMethod(cls, "__new__", "O", cls);

    Note that I am not even calling tp_new here directly, so all the hoops in tp_new_wrapper get jumped through.

    The advantage of this scheme is that python and C code will work exactly the same and users that have corner cases for which the scheme does not work will be able to figure out what is going on by looking at the python code.

    Attaching issue5180a.diff

    @abalkin
    Copy link
    Member

    abalkin commented Jul 17, 2010

    Antoine said on IRC that he is ok with the latest approach. Does anyone want to review the patch before it goes in?

    @avassalotti
    Copy link
    Member

    I have fixed some style nits in your patch. It would be nice to have tests for the different control paths in instantiate(). But, I realize that would be a bit annoying.

    Apart from that, the patch looks good.

    @abalkin
    Copy link
    Member

    abalkin commented Jul 17, 2010

    Alexandre,

    I am not sure your change form PyObject_Size(args) to Py_SIZE(args) is correct. As far as I can tell, args come from pickle machine stack without any type checks. The equivalent code in 2.x cPickle uses PyObject_Size and checks for errors. I'll try to come up with a test case for the error situation, but it is a bit tricky because pickle should be produced in 2.x in order for this function to be used.

    @avassalotti
    Copy link
    Member

    The args argument is always a tuple created with Pdata_poptuple(). You can add an explicit type check. If this check fails a RuntimeError should be raised, because this would indicate a programming error in pickle.

    @abalkin
    Copy link
    Member

    abalkin commented Jul 17, 2010

    Committed issue5180b.diff with minor additions in r82937 (r82938 for 3.1):

    1. Added an assert that args is a tuple in instantiate() with an explanation why it is true.

    2. Added a test for the other code branch instantiate. (Not all conditions are tested, but I think this is now good enough.)

    The explanation of INST and OBJ opcodes in pickletools is now out of date, so I am adding bpo-9267 as a superseder.

    @abalkin abalkin closed this as completed Jul 17, 2010
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    extension-modules C modules in the Modules dir type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    4 participants