Skip to content

3.1 cannot unpickle 2.7-created pickle #49430

Closed

Description

@pitrou
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']

    Activity

    1. pitrou commented on Feb 7, 2009

      @pitrou
      MemberAuthor

      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
    2. pitrou commented on Feb 7, 2009

      @pitrou
      MemberAuthor

      Here is the pickle file.

    3. abalkin commented on Jun 27, 2010

      @abalkin
      Member

      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 ...

    4. abalkin commented on Jun 27, 2010

      @abalkin
      Member

      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
    5. abalkin commented on Jun 27, 2010

      @abalkin
      Member

      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.

    6. malemburg commented on Jun 27, 2010

      @malemburg
      Member

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

    7. abalkin commented on Jun 27, 2010

      @abalkin
      Member

      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?

    8. abalkin commented on Jun 28, 2010

      @abalkin
      Member

      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

    9. abalkin commented on Jun 28, 2010

      @abalkin
      Member

      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.

    10. abalkin commented on Jun 28, 2010

      @abalkin
      Member

      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.

    11. 4 remaining items

    12. avassalotti commented on Jun 28, 2010

      @avassalotti
      Member

      Thank you for the nice investigative work!

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

    13. pitrou commented on Jun 28, 2010

      @pitrou
      MemberAuthor

      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).

    14. pitrou commented on Jun 28, 2010

      @pitrou
      MemberAuthor

      (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'>

      )

    15. abalkin commented on Jun 29, 2010

      @abalkin
      Member

      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?

    16. pitrou commented on Jun 29, 2010

      @pitrou
      MemberAuthor

      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.

    17. abalkin commented on Jul 15, 2010

      @abalkin
      Member

      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

    18. abalkin commented on Jul 17, 2010

      @abalkin
      Member

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

    19. avassalotti commented on Jul 17, 2010

      @avassalotti
      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.

    20. abalkin commented on Jul 17, 2010

      @abalkin
      Member

      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.

    21. avassalotti commented on Jul 17, 2010

      @avassalotti
      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.

    22. abalkin commented on Jul 17, 2010

      @abalkin
      Member

      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.

    23. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    extension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions