Repository: python-pillow/Pillow

Issue:
SIGBUS when overwriting the file you opened
### What did you do?

I have a program that takes the palette of one (indexed, 8-bit) bitmap file, applies it to another bitmap file, then attempts to replace the second file in-place with the updated version. Doing so consistently results in a SIGBUS. Apparently there is a minimum file size where this happens; I've seen the test program pass for very small bitmaps. I'm attaching two files of the same size I originally used (but not the same content), if I can get it work.

### What did you expect to happen?

No SIGBUS. The use of mmap should be an implementation detail, but its use "leaks out" in this case, and as described, cannot be turned off except by hacking the Pillow installation.

### What actually happened?

SIGBUS.
I have tracked the issue down to the mmap functionality in Pillow, which cannot be disabled from the caller. I can think of three workarounds, two of which I've verified make the test program work.

1. Use os.unlink to make sure that the replacement file gets a new inode (TESTED)
2. Force use_mmap to False within ImageFile.py (TESTED)
3. Use the O_TMPFILE trick mentioned in the open(2) manpage (UNTESTED)

Of course, it would be even better if this was handled properly within Pillow.


### What are your OS, Python and Pillow versions?

* OS: Linux (three versions tested, 3.2.x, 4.17.8 and 4.18.10)
* Python: Version 2.7.3 and 3.7.2rc1
* Pillow: PIL version 1.1.7 and Pillow version 5.3.0 and 5.4.0

The test program below also provides test modes that disable the getpalette/putpalette calls at the heart of the palette replacement process. These calls turned out to be a red herring, but are included here for completeness.

```python
import os
from PIL import Image
import sys

TestModes = {0: 'No failure: getpalette/putpalette AND unlink',
             1: 'No failure: No getpalette/putpalette BUT unlink',
             2: 'Failure: No unlink BUT getpalette/putpalette',
             3: 'Failure: No getpalette/putpalette AND no unlink'}

if len(sys.argv) < 3:
    print("Syntax: python replanepal.py <image file> <palette file> [<test mode>]")
    sys.exit(0)

FAIL = int(sys.argv[3]) if len(sys.argv) > 3 else 0

print("Running test with test mode: %s" % TestModes[FAIL])

img = Image.open(sys.argv[1])
mainpal = Image.open(sys.argv[2])

if FAIL in [0, 2]:
    palval = mainpal.getpalette()
    img.putpalette(palval)

if FAIL in [0, 1]:
    os.unlink(sys.argv[1])

img.save(sys.argv[1])
print("Apparently, all went well!")
```



Discussion:
Test files here

[Test_Images.zip](https://github.com/python-pillow/Pillow/files/2682726/Test_Images.zip)


Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1958,7 +1958,7 @@ def save(self, fp, format=None, **params):
             filename = fp.name
 
         # may mutate self!
-        self.load()
+        self._ensure_mutable()
 
         save_all = params.pop('save_all', False)
         self.encoderinfo = params


Test patch:
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -3,6 +3,8 @@
 from PIL import Image
 from PIL._util import py3
 import os
+import sys
+import shutil
 
 
 class TestImage(PillowTestCase):
@@ -121,6 +123,16 @@ def test_internals(self):
         im.paste(0, (0, 0, 100, 100))
         self.assertFalse(im.readonly)
 
+    @unittest.skipIf(sys.platform.startswith('win32'),
+                     "Test requires opening tempfile twice")
+    def test_readonly_save(self):
+        temp_file = self.tempfile("temp.bmp")
+        shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
+
+        im = Image.open(temp_file)
+        self.assertTrue(im.readonly)
+        im.save(temp_file)
+
     def test_dump(self):
         im = Image.new("L", (10, 10))
         im._dump(self.tempfile("temp_L.ppm"))