Skip to content

segfault when "viewing" closed mmap object #9537

Closed

Description

@ahaldane

This crashes in python2 and python3 with a segfault:

import mmap

# create test file
a = arange(10., dtype='f4')
a.tofile('test')

# opens it as a memmap
with open('test', 'r+b') as f:
      mm = mmap.mmap(f.fileno(),0)

#view the mmap, then close it
x = np.frombuffer(mm, dtype='f4')
mm.close()
print(x)  # segfault

In frombuffer mumpy uses the PEP3118 interface to the mmap to get a pointer to the data, so it seems we are using the interface incorrectly somewhere.

It seems to me that the "correct" behavior may be for it to be impossible to close the memmap while pointers to it exist; this is the behavior for memoryviews of mmaps. That is, the line mm.close() shoud raise an error BufferError: cannot close exported pointers, as in currently happens in python3:

>>> import mmap
>>> with open('test', 'r+b') as f:
>>>     mm = mmap.mmap(f.fileno(),0)
>>> mv = memoryview(mm)
>>> mm.close()
BufferError: cannot close exported pointers exist

however note also that in python2 we cannot create the memoryview (mv) because of an error TypeError: cannot make memory view because object does not have the buffer interface.

Note how mmap_buffer_getbuf (link) in the mmap module takes care to keep track of any buffer access to the mmap (the exports counter). Evidently we are somehow bypassing that in numpy.

Activity

  1. ahaldane commented on Aug 10, 2017

    @ahaldane
    MemberAuthor

    This was reported on the mailing list in this message

  2. orkolorko commented on Aug 10, 2017

    @orkolorko

    The bug was not found by me, but by Maurizio Monge maurizio.monge at im.ufrj.br; I just used it to check the behaviour of numpy in a specific case.

  3. pv commented on Aug 10, 2017

    @pv
    Member
  4. jakirkham commented on Aug 3, 2018

    @jakirkham
    Contributor

    There were some fixes for buffers recently in PR ( #10740 ) included in NumPy 1.15.0. Does this issue persist with those changes?

  5. eric-wieser commented on Apr 20, 2019

    @eric-wieser
    Member

    No errors on python 3 for me in 1.17.x

  6. seberg commented on Apr 20, 2019

    @seberg
    Member

    Does segfault for me, maybe windows is different? I somewhat thought I saw a warning/error here on closing the memmap even though it still has references.

  7. eric-wieser commented on Apr 20, 2019

    @eric-wieser
    Member

    My mistake, failed to copy the print line when I tested this. Fails on windows for me.

  8. removed
    57 - Close?Issues which may be closable unless discussion continued
    on Apr 20, 2019
  9. devinrsmith commented on Apr 21, 2020

    @devinrsmith

    I've run into similar issues when trying to use np.frombuffer against our own objects that implement the buffer protocol. I think the issue is that numpy mis-uses the buffer api - it gets and immediately releases the buffer instead of releasing the buffer during the the returned arrays destructor.

    https://github.com/numpy/numpy/blob/v1.18.3/numpy/core/src/multiarray/ctors.c#L3817-L3823

  10. seberg commented on Apr 21, 2020

    @seberg
    Member

    To be clear, this comes from a time when the code would have been perfectly correct, because view->obj was always defined as the exporting object, and PyBuffer_Release was not able to do anything more than decreasing the refcount of obj.

    NumPy itself doesn't even use the "free" function right now, because it leads to annoying side effects with the PyArg_Parse* functions.

    So, as long as the data pointed to by your object remains available for the lifetime of that object, you are actually fine. Otherwise, it seems we would have to do the internal reference counting dance that memoryview has to do. I do not know how tricky that is, but I am not sure I am willing to do that. So the only thing I can think of is: Wrap all "buffers" directly in memoryviews. I wonder if that would be better to begin with... if holding on to the buffer is important for the memory to remain valid, why not promote it to a python object, they already do memory management?

    But of course, in NumPy we thus use sys.refcount in one or two places, to warn users that they are doing invalid things. With mmap+memoryview, you actually get a real warning also on PyPy, since it does its own reference counting...

    Long story short, I did not realize that memoryviews had this ingrained so much, and mmap actually uses it semi-prominently when I opened https://bugs.python.org/issue39471 to at least clarify things. I am not sure we should even consider doing that internal refcounting dance... But luckily, we now have only python 3, so we could just wrap everything in a memoryview and use that. Of course after that we may have to brace for impact of users expecting arr.base not to be a memoryview suddenly?

  11. devinrsmith commented on Apr 21, 2020

    @devinrsmith

    Thanks for chiming in - I certainly don't know the intricate implementation details of numpy, but I think the assumption that "the object remains available for the lifetime of that object" might be a bad assumption. At the very least, it needs to be warned and documented.

    For example, this seems like it should be valid:

    def to_numpy(x: SomeCustomInt32BufferProtocol):
      return np.frombuffer(x, dtype=np.int32)
    
    y = to_numpy(SomeCustomInt32BufferProtocol(...))

    Due to the current implementation of np.frombuffer though, there is no way for the custom impl to know that anything is still referencing its underlying buffer.

    I think due to the inherent nature of the buffer protocol, libraries/consumers that call PyObject_GetBuffer are expected to manage their calls back into PyBuffer_Release with care.

  12. eric-wieser commented on Apr 21, 2020

    @eric-wieser
    Member

    @seberg, I'm afraid I never quite followed the conclusion of your one-sided discussion on that issue. Was it ever clear what the intended semantics were?

  13. devinrsmith commented on Apr 21, 2020

    @devinrsmith

    I might be mis-interpreting your issue https://bugs.python.org/issue39471, but it seems like the testbuffer impl doesn't allocate during the getbufferproc stage:

    https://github.com/python/cpython/blob/v3.8.2/Modules/_testbuffer.c#L1527-L1529

    That said, I think it's completely legitimate for getbufferproc to allocate instead of returning "views" - it should be an implementation detail that shouldn't matter to a generic consumer (the generic consumer in this case being np.frombuffer). Of course, as a specific consumer, using a specific buffer protocol library, I should be aware about the expectations of the implementation. IE, mmap and np.array implement the buffer protocol with efficient "view" semantics.

    As a hypothetical - I could implement a getbufferproc that returns new random bytes for each call.

  14. 6 remaining items

  15. seberg commented on Apr 22, 2020

    @seberg
    Member

    The only assumption that it has, is that buffer->obj is going to be the exporting object and not some new object created during the export. I think that is fine, if you want to be pedantic, I guess we could check that.
    Of course the only reason to do that is to safe a few cpu cycles creating a memorymap and retain the arr.base attribute. So it may not be worth the trouble.
    CPython currently uses (has to use) that assumption also during argument parsing to retain backward compatibility (the reason why NumPy does not use releasebuffer, which is seriously annoying).

  16. eric-wieser commented on Apr 22, 2020

    @eric-wieser
    Member

    CPython currently uses (has to use) that assumption also during argument parsing to retain backward compatibility

    In what way is that a compatibility choice? Presumably the issue is that ParseTuple... and friends do not have a code that means "get me a pointer and and object to manage its lifetime with".

  17. eric-wieser commented on Apr 22, 2020

    @eric-wieser
    Member

    Either way - it's probably good to get some input from a cpython buffer protocol maintainer.

    @skrah and @abalkin might be your best bet there, at least based on the reviewers assigned to python/cpython#5576 and python/cpython#5561

  18. seberg commented on Apr 22, 2020

    @seberg
    Member

    Because PyArg_ParseTuple("%z") (or whatever), can keep working with NumPy arrays because NumPy arrays do not implement releasebuffer and thus it can assume that the pointer is valid for the whole lifetime of the exporting object. So I think it is "compatibility" with the old buffer-protocol where the lifetime assumption was correct. And the reason why NumPy is stuck with it currently.

  19. eric-wieser commented on Apr 22, 2020

    @eric-wieser
    Member

    The compatibility choice is ours though right, not cpython's? Cpython's behavior is required to be correct.

  20. seberg commented on Apr 22, 2020

    @seberg
    Member

    @eric-wieser I guess I have a different angle. Implicitly CPython choose to say that the old-buffer protocol definition (lifetime is bound to exporting object) is valid. Otherwise they would have to deprecate and remove that special case path, leaving NumPy no choice (or no gain in sticking with the old implementation).
    Of course, in all likelyhood nobody ever seriously tried to think about deprecating it in CPython, and simply put in the backward compatibility skim to close an issue or two.

  21. seberg commented on Apr 22, 2020

    @seberg
    Member

    So maybe its different. The choice is ours, but CPython gives us less choice to move to the new buffer-protocol definition.
    And maybe the solution is actually to just screw it, and release NumPy with a flag NPY_USE_RELEASE_BUFFER=0. Keep at at 0 for a while in release versions, put it to 1 in pre-releases and wait out until the shouting dies down...

  22. eric-wieser commented on Apr 22, 2020

    @eric-wieser
    Member

    . Implicitly CPython choose to say that the old-buffer protocol definition (lifetime is bound to exporting object) is valid

    My reading of this is different. CPython has concluded that if your buffer has no release process, then one of the following is true:

    • The buffer doesn't need releasing, as its lifetime is bound to the object
    • The buffer doesn't need releasing, as its lifetime is global

    In essence, the reason goes "I cannot release this buffer, therefore no one can, and I can guarantee it will stick around as long as the pointer I return".

  23. seberg commented on Apr 22, 2020

    @seberg
    Member

    Yes, that is correct of course. But it doesn't help with the fact that it puts NumPy in a position where transitioning to the new meaning is tricky. Even though it would help us a lot (if we assume realloc's do not happen at least – if we do not assume that, it would still help us a lot, but we would have to do more shenanigans, much less than now though).

    It seems also a bit annoying that the ownership tracking has to go through memoryview objects in this way. I admit, its probably just that something about that setup rubs me the wrong way.

    But yes, you are right. There is nothing wrong as such, its just all not ideal and potentially Python could help us, since the argument-parsing is the main/only reason I have ever seen for NumPy to remain stuck with the old protocol/interpretation.

    EDIT: But yes, from a Python perspective/protocol perspective, it is fully correct. Although it feels a bit weird if we may have good reason to check for bf_releasebuffer.

  24. pv commented on Jun 22, 2020

    @pv
    Member

    FWIW, Numpy is already using memoryviews for the life cycle management in other places:

    PyObject *memoryview = PyMemoryView_FromObject(op);
    if (memoryview == NULL) {
    PyErr_Clear();
    }
    else {
    tmp = _array_from_buffer_3118(memoryview);

    This was done, as the Py_buffer has to be stored somewhere (and the struct cannot be copied) and it was simplest to let a separate base object manage it. Frombuffer was not touched at the time, as it was using the old unsafe PyObject_AsReadBuffer/AsWriteBuffer API that still worked. But maybe it should have been changed back then.

  25. added 2 commits that reference this issue on Apr 11, 2022
    9789345
    622ca48
  26. added a commit that references this issue on May 5, 2022
    1c51494
  27. added a commit that references this issue on Jun 10, 2022
    949ee07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions