Repository: rasterio/rasterio

Issue:
Segfault when closing WarpedVRT
## Expected behavior and actual behavior.

I use contextily which itself call rasterio. Everything works fine with version 1.1.5, but I have a segfault with version 1.1.6 : 
```
Fatal Python error: Segmentation fault
Current thread 0x00007fe33542d740 (most recent call first):
  File "/home/user/.pyenv/versions/project/lib/python3.8/site-packages/rasterio/vrt.py", line 126 in close
  File "/home/user/.pyenv/versions/project/lib/python3.8/site-packages/rasterio/vrt.py", line 123 in __del__
  File "/home/user/.pyenv/versions/project/lib/python3.8/site-packages/contextily/plotting.py", line 148 in add_basemap
```

## Steps to reproduce the problem.

Run the following program : 
```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import contextily as ctx
import matplotlib.pyplot as plt

if __name__ == '__main__':
    fig, ax = plt.subplots()
    ctx.add_basemap(ax=ax, crs="EPSG:3857")
```

## Operating system

Debian 10

## Rasterio version and provenance
1.1.6 installed from PyPI using poetry.



Discussion:
Confirmed. From gdb:

```
Starting program: /home/sean/.virtualenvs/ctx/bin/python /tmp/issue2000.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff0f46700 (LWP 24957)]
[New Thread 0x7ffff0745700 (LWP 24958)]
[New Thread 0x7fffebf44700 (LWP 24959)]
/home/sean/.virtualenvs/ctx/lib/python3.7/site-packages/contextily/tile.py:629: UserWarning: The inferred zoom level of 27 is not valid for the current tile provider (valid zooms: 0 - 18).
  warnings.warn(msg)

Thread 1 "python" received signal SIGSEGV, Segmentation fault.
0x00007fffe01f834c in GDALDataset::ReleaseRef() () from /home/sean/.virtualenvs/ctx/lib/python3.7/site-packages/rasterio/../rasterio.libs/libgdal-rasterio-bf79a549.so.20.5.4
(gdb) up
#1  0x00007fffe0187689 in VRTWarpedDataset::CloseDependentDatasets() () from /home/sean/.virtualenvs/ctx/lib/python3.7/site-packages/rasterio/../rasterio.libs/libgdal-rasterio-bf79a549.so.20.5.4
(gdb) up
#2  0x00007fffe018775a in VRTWarpedDataset::~VRTWarpedDataset() () from /home/sean/.virtualenvs/ctx/lib/python3.7/site-packages/rasterio/../rasterio.libs/libgdal-rasterio-bf79a549.so.20.5.4
```
GDAL's `VRTWarpedDataset` destructor closes the GDAL dataset that we have passed to rasterio's `WarpedVRT` constructor. If that dataset was in a `MemoryFile` and that `MemoryFile` was closed first, `VRTWarpedDataset::CloseDependentDatasets()` will crash.

Resolving patch:
diff --git a/rasterio/errors.py b/rasterio/errors.py
--- a/rasterio/errors.py
+++ b/rasterio/errors.py
@@ -126,3 +126,7 @@ class ResamplingAlgorithmError(RasterioError):
 
 class TransformError(RasterioError):
     """Raised when transform arguments are invalid"""
+
+
+class WarpedVRTError(RasterioError):
+    """Raised when WarpedVRT can't be initialized"""
diff --git a/rasterio/io.py b/rasterio/io.py
--- a/rasterio/io.py
+++ b/rasterio/io.py
@@ -85,7 +85,7 @@ class MemoryFile(MemoryFileBase):
      'width': 791}
 
     """
-    def __init__(self, file_or_bytes=None, dirname=None, filename=None, ext=''):
+    def __init__(self, file_or_bytes=None, dirname=None, filename=None, ext='.tif'):
         """Create a new file in memory
 
         Parameters
@@ -141,8 +141,8 @@ def __enter__(self):
         return self
 
     def __exit__(self, *args, **kwargs):
-        self._env.__exit__()
         self.close()
+        self._env.__exit__()
 
 
 class ZipMemoryFile(MemoryFile):


Test patch:
diff --git a/tests/test_warpedvrt.py b/tests/test_warpedvrt.py
--- a/tests/test_warpedvrt.py
+++ b/tests/test_warpedvrt.py
@@ -9,16 +9,19 @@
 import numpy
 import pytest
 
-from .conftest import requires_gdal21, requires_gdal2
 import rasterio
 from rasterio.crs import CRS
-from rasterio.enums import Resampling, MaskFlags, ColorInterp
-from rasterio.errors import RasterioDeprecationWarning, WarpOptionsError
+from rasterio.enums import Resampling, MaskFlags
+from rasterio.errors import RasterioDeprecationWarning, WarpOptionsError, WarpedVRTError
+from rasterio.io import MemoryFile
 from rasterio import shutil as rio_shutil
 from rasterio.vrt import WarpedVRT
 from rasterio.warp import transform_bounds
 from rasterio.windows import Window
 
+from .conftest import requires_gdal21, requires_gdal2
+
+log = logging.getLogger(__name__)
 
 # Custom markers.
 credentials = pytest.mark.skipif(
@@ -102,6 +105,7 @@ def test_warped_vrt_add_alpha(dsrec, path_rgb_byte_tif):
         assert len(records) == 1
         assert "1 N GTiff" in records[0]
 
+
 @requires_gdal21(reason="Nodata deletion requires GDAL 2.1+")
 def test_warped_vrt_msk_add_alpha(path_rgb_msk_byte_tif, caplog):
     """Add an alpha band to the VRT to access per-dataset mask of a source"""
@@ -565,3 +569,42 @@ def test_warped_vrt_resizing_repro():
         with WarpedVRT(rgb, crs="EPSG:3857", height=10, width=10) as vrt:
             assert vrt.height == 10
             assert vrt.width == 10
+
+
+def test_vrt_src_mode(path_rgb_byte_tif):
+    """VRT source dataset must be opened in read mode"""
+
+    with rasterio.open(path_rgb_byte_tif) as src:
+        profile = src.profile
+        bands = src.read()
+
+    with MemoryFile() as memfile:
+
+        with memfile.open(**profile) as dst:
+            dst.write(bands)
+
+            with pytest.raises(WarpedVRTError):
+                vrt = WarpedVRT(dst, crs="EPSG:3857")
+
+
+def test_vrt_src_kept_alive(path_rgb_byte_tif):
+    """VRT source dataset is kept alive, preventing crashes"""
+
+    with rasterio.open(path_rgb_byte_tif) as dst:
+        vrt = WarpedVRT(dst, crs="EPSG:3857")
+
+    assert (vrt.read() != 0).any()
+    vrt.close()
+
+
+def test_vrt_mem_src_kept_alive(path_rgb_byte_tif):
+    """VRT in-memory source dataset is kept alive, preventing crashes"""
+
+    with open(path_rgb_byte_tif, "rb") as fp:
+        bands = fp.read()
+
+    with MemoryFile(bands) as memfile, memfile.open() as dst:
+        vrt = WarpedVRT(dst, crs="EPSG:3857")
+
+    assert (vrt.read() != 0).any()
+    vrt.close()