Repository: matplotlib/matplotlib
Pull request: FIX: add guardrails for too big tk figures
## PR Summary

Depending on the version of xserver creating a too big tk window
either corrupts xorg (!) or causes the process to exit.  This adds a
guard rail that will convert this into a python exception instead.

closes #17460


## PR Checklist

- [ ] Has Pytest style unit tests
- [k] Code is [Flake 8](http://flake8.pycqa.org/en/latest/) compliant

Not sure if it is worth adding a test for this as it is such an out-there condition.

Problem statement:
set_size_inches with a width over 14090 crashes Xorg
<!--To help us understand and resolve your issue, please fill out the form to the best of your ability.-->
<!--You can feel free to delete the sections that do not apply.-->

### Bug report

**Bug summary**

Calling set_size_inches with w=14090.5 causes Xorg to lock up and corrupt the screen. With a width of 14090 or less it doesn't happen.

Note Xorg doesn't actually crash, just displays garbage all over the screen and stops working.

I have looked in dmesg and the Xorg logs and couldn't find anything unusual.

I should note that matplotlib is using the Tk backend.

I realise that 14090 inches is an absurd value - I encountered this issue because this value was computed incorrectly from some other code. Cue several hours of confusion as I thought my graphics hardware was broken!

Apologies if this has nothing to do with matplotlib, but I'm unsure of how to debug this further.

**Code for reproduction**

<!--A minimum code snippet required to reproduce the bug.
Please make sure to minimize the number of dependencies required, and provide
any necessary plotted data.
Avoid using threads, as Matplotlib is (explicitly) not thread-safe.-->

```python
import matplotlib.pyplot as plt
plt.plot([1, 2, 3])
fig = plt.gcf()
crash = True
fig.set_size_inches(14090.5 if crash else 14090, 10)
plt.show()
```

**Actual outcome**

<!--The output produced by the above code, which may be a screenshot, console output, etc.-->
Xorg splats garbage all over the screen and becomes unresponsive.

**Expected outcome**

A window appears with the width equal to the width of the screen. This is what happens if I set crash = False in the above code.

**Matplotlib version**
<!--Please specify your platform and versions of the relevant libraries you are using:-->
  * Operating system: Debian 9.8
  * Matplotlib version: 2.0.0
  * Matplotlib backend: TkAgg
  * Python version: 3.5.3
  * Jupyter version (if applicable):
  * Other libraries: 

<!--Please tell us how you installed matplotlib and python e.g., from source, pip, conda-->
<!--If you installed from conda, please specify which channel you used if not the default-->

Python and matplotlib installed from Debian package repos.

Hints from issue discussion:
Thanks for the report!

Can you try with a newer version of Matplotlib?  2.0 is pretty old at this point.  We have added some better checking of too-big images.  We do not want to clip to the screen size as you may need to make a figure that is bigger than your screen if you were going to print it on a poster, however we do have some relatively small sized pointers in the code that works with the raster images.  The way we put images in the GUI is we render them via Agg and then use the GUI framework to paint the RBGA array onto a GUI element on the screen.  My suspicion from your description is that when your window gets big enough something in tk / X is having a buffer overflow which corrupts something inside of X.

If I try this with the QtAgg backend I get 

```
✔  ~
jupiter@17:04  ➤  ipython
Python 3.8.2 (default, Apr  8 2020, 14:31:25) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %matplotlib                                                                                                                                                  
Using matplotlib backend: Qt5Agg

In [2]: qt5ct: using qt5ct plugin                                                                                                                                    
In [2]: import matplotlib.pyplot as plt                                                                                                                              

In [3]: fig = plt.figure()                                                                                                                                           

In [4]: fig.set_size_inches(14100, 10)                                                                                                                               

In [5]: QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
qt.qpa.xcb: QXcbConnection: XCB error: 11 (BadAlloc), sequence: 551, resource id: 491, major code: 53 (CreatePixmap), minor code: 0
QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
qt.qpa.xcb: QXcbConnection: XCB error: 4 (BadPixmap), sequence: 563, resource id: 60817425, major code: 54 (FreePixmap), minor code: 0
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_qt5.py", line 498, in _draw_idle
    self.draw()
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 388, in draw
    self.renderer = self.get_renderer(cleared=True)
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 404, in get_renderer
    self.renderer = RendererAgg(w, h, self.figure.dpi)
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 92, in __init__
    self._renderer = _RendererAgg(int(width), int(height), dpi)
ValueError: Image size of 1410000x1000 pixels is too large. It must be less than 2^16 in each direction.
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_qt5agg.py", line 57, in paintEvent
    reg = self.copy_from_bbox(bbox)
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 377, in copy_from_bbox
    renderer = self.get_renderer()
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 404, in get_renderer
    self.renderer = RendererAgg(w, h, self.figure.dpi)
  File "/usr/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py", line 92, in __init__
    self._renderer = _RendererAgg(int(width), int(height), dpi)
ValueError: Image size of 1410000x1000 pixels is too large. It must be less than 2^16 in each direction.

If you suspect this is an IPython 7.14.0 bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@python.org

You can print a more detailed traceback right now with "%tb", or use "%debug"
to interactively debug it.

Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
    %config Application.verbose_crash=True

```

This does suggest that we are the ones with the buffer overflow.

If I try this with tkagg I get

```
✔  ~
jupiter@17:06  ➤  ipython
Python 3.8.2 (default, Apr  8 2020, 14:31:25) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %matplotlib tk                                                                                                                                               

In [2]: import matplotlib.pyplot as plt                                                                                                                              

In [3]: fig = plt.figure()                                                                                                                                           

In [4]: fig.set_size_inches(14100, 10)                                                                                                                               

Exception in Tkinter callback
In [5]: X Error of failed request:  BadAlloc (insufficient resources for operation)                                                                                  
  Major opcode of failed request:  53 (X_CreatePixmap)
  Serial number of failed request:  1297
  Current serial number in output stream:  1849
✘  ~
```

Neither of which are great, but both of which are better than corrupting X!
Thanks for the quick response.

I have now tried it using 

  - The latest Anaconda distribution (python 3.7.6 & matplotlib 3.1.0)
  - The latest Anaconda distribution with matplotlib updated to 3.2.1 from PyPI

In both cases, I am able to reproduce the reported issue using the TkAgg backend.

The Qt4Agg and Qt5Agg backends do not have an issue, and do not complain with an error - they pop up a figure just fine.

Note that this is running `python crash.py` directly from the command line instead of via IPython, if that makes a difference.

I also tried switching to a different virtual terminal and running `DISPLAY=:1 anaconda3/bin/python crash.py` so I could see its output. No error is printed, matplotlib appears to think it is working as normal. If I then switch to X's vt, I see a corrupted display.
If Xorg is crashing because a large window is opened, that's a bug in Xorg. It should return an error to the application, or limit the window size. You should report this to Debian.
Note, that's assuming it's really Xorg that's corrupted, and not, e.g., Matplotlib's window over top everything.
I (luckily?) still can not reproduce this ever running from the prompt, 

```
✔  ~
jupiter@22:03  ➤  python /tmp/crash.py 
X Error of failed request:  BadAlloc (insufficient resources for operation)
  Major opcode of failed request:  53 (X_CreatePixmap)
  Serial number of failed request:  408
  Current serial number in output stream:  410
✘  ~
jupiter@22:03  ➤  
```

If I set `crash=False` I get a figure.  Apparently somehing in the show process re-sizes the figure down to one that fits on the screen (checked via the prints).

I tested on Arch and Ubuntu 20.04 both of which seem to be running `xorg-server 1.20.8`, if I am reading the debian package logs right you are using 1.19.2.

Given that I see at exactly the same threshold and you only see the corruption with tk I think this is a tk / xorg interaction.

-------

changed the example a bit to make sure I got tk and added some status prints:

```python
import matplotlib
matplotlib.use('tkagg')
import matplotlib.pyplot as plt
plt.plot([1, 2, 3])
fig = plt.gcf()
crash = True
fig.set_size_inches(14090.5 if crash else 14090, 10)
print(fig.get_size_inches())
plt.show(block=False)
plt.pause(.1)
print(fig.get_size_inches())
plt.show()

```



I also tracked the problematic size down to 1_409_023 pixels in either direction.

If you make figure that is just small enough in each dimension you can get a segfault, but given that it is memory-exhaustion I am not sure we want to try and catch that.  Further, it is coming out of Python's tk binding. 
```
jupiter@22:43  ➤  python -X faulthandler /tmp/crash.py 
Fatal Python error: Segmentation fault

Current thread 0x00007fdb853a4740 (most recent call first):
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 4067 in blank
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/_backend_tk.py", line 75 in blit
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/backend_tkagg.py", line 10 in draw
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/_backend_tk.py", line 253 in idle_draw
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 806 in callit
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 1885 in __call__
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 1313 in update_idletasks
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/_backend_tk.py", line 556 in set_cursor
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backend_bases.py", line 2869 in _wait_cursor_for_draw_cm
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/contextlib.py", line 117 in __enter__
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/backend_agg.py", line 398 in draw
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/backend_tkagg.py", line 9 in draw
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/_backend_tk.py", line 242 in resize
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 1885 in __call__
  File "/home/tcaswell/.pybuild/bleeding/lib/python3.10/tkinter/__init__.py", line 1422 in mainloop
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backends/_backend_tk.py", line 889 in mainloop
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/backend_bases.py", line 3436 in show
  File "/home/tcaswell/source/p/matplotlib/lib/matplotlib/pyplot.py", line 327 in show
  File "/tmp/crash.py", line 12 in <module>
Segmentation fault (core dumped)
```
fwiw I can't repro on fedora31/xorg-1.20.6.
`fig = figure(); fig.set_size_inches(...)` gives me
```
<elided>
  File ".../matplotlib/backends/backend_agg.py", line 96, in __init__
    self._renderer = _RendererAgg(int(width), int(height), dpi)
ValueError: Image size of 1410000x1000 pixels is too large. It must be less than 2^16 in each direction.
```
and `figure(figsize=...)` gives me
```
<elided>
  File ".../tkinter/__init__.py", line 4006, in __init__
    self.tk.call(('image', 'create', imgtype, name,) + options)
_tkinter.TclError: not enough free memory for image buffer
```
Yes, I'm fairly certain this used to happen in older Fedora, but then it was fixed, which is why I think it should be reported to Debian, as they should usually be backporting patch like this that fix DoSs.

Commit message: FIX: add guardrails for too big tk figures on linux

Depending on the version of xserver creating a too big tk window
either corrupts xorg (!) or causes the process to exit.  This adds a
guard rail that will convert this into a python exception instead.

closes #17460

Patch to review:
diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py
index 9132a859f90f..fcbae09db8dc 100644
--- a/lib/matplotlib/backends/_backend_tk.py
+++ b/lib/matplotlib/backends/_backend_tk.py
@@ -431,6 +431,17 @@ def _get_toolbar(self):
         return toolbar
 
     def resize(self, width, height):
+        max_size = 1_400_000  # the measured max on xorg 1.20.8 was 1_409_023
+
+        if (width > max_size or height > max_size) and sys.platform == 'linux':
+            raise ValueError(
+                'You have requested to resize the '
+                f'Tk window to ({width}, {height}), one of which '
+                f'is bigger than {max_size}.  At larger sizes xorg will '
+                'either exit with an error on never versions (~1.20) or '
+                'cause corruption on older version (~1.19).  We '
+                'do not expect a window over a million pixel wide or tall '
+                'to be intended behavior.')
         self.canvas._tkcanvas.configure(width=width, height=height)
 
     def show(self):


Code review comments:
diff_hunk: @@ -431,6 +431,17 @@ def _get_toolbar(self):
         return toolbar
 
     def resize(self, width, height):
+        max_size = 1_400_000  # the measured max on xorg 1.20.8 was 1_409_023
+
+        if (width > max_size or height > max_size) and sys.platform == 'linux':
+            raise ValueError(
+                'You have requested to resize the '
+                f'Tk window to ({width}, {height}), one of which '
+                f'is bigger than {max_size}.  At larger sizes xorg will '
+                'either exit with an error on never versions (~1.20) or '
path: lib/matplotlib/backends/_backend_tk.py
text: @user1:
```suggestion
                'either exit with an error on newer versions (~1.20) or '
```