10000 [Bug]: double free when FT2Font constructor is interrupted by KeyboardInterrupt · Issue #21364 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

[Bug]: double free when FT2Font constructor is interrupted by KeyboardInterrupt #21364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
anntzer opened this issue Oct 15, 2021 · 8 comments · Fixed by #21407
Closed

[Bug]: double free when FT2Font constructor is interrupted by KeyboardInterrupt #21364

anntzer opened this issue Oct 15, 2021 · 8 comments · Fixed by #21407

Comments

@anntzer
Copy link
Contributor
anntzer commented Oct 15, 2021

Bug summary

Ctrl-C'ing during construction of the font cache can occasionally result in a double-free error.

Code for reproduction

rm ~/.cache/matplotlib/fontlist*.json; \python -c 'import matplotlib.pyplot; print("done")' & sleep 2 && pkill -INT python
You may have to run this a couple of times and/or adjust the sleep time to see the problem.

Actual outcome

Exception ignored in: <matplotlib.ft2font.FT2Font object at 0x7fb0e1a643a0>                                                                                                                         
Traceback (most recent call last):
  File "/home/antony/src/extern/matplotlib/lib/matplotlib/font_manager.py", line 1092, in addfont
    font = ft2font.FT2Font(path)
KeyboardInterrupt: 
free(): double free detected in tcache 2
Fatal Python error: Aborted

Thread 0x00007fb0e1a2b640 (most recent call first):
  File "/usr/lib/python3.9/threading.py", line 316 in wait
  File "/usr/lib/python3.9/threading.py", line 574 in wait
  File "/usr/lib/python3.9/threading.py", line 1284 in run
  File "/usr/lib/python3.9/threading.py", line 973 in _bootstrap_inner
  File "/usr/lib/python3.9/threading.py", line 930 in _bootstrap

Current thread 0x00007fb104faa740 (most recent call first):
  File ".../matplotlib/font_manager.py", line 1092 in addfont
  <elided>

Expected outcome

Just the KeyboardInterrupt.

Operating system

arch linux and fedora (edited: confirmed on fedora as well)

Matplotlib Version

3.5.0.dev2291+gc8a902d3f2

Matplotlib Backend

mplcairo

Python version

3.9

Jupyter version

no

Other libraries

No response

Installation

No response

Conda channel

No response

@QuLogic
Copy link
Member
QuLogic commented Oct 15, 2021

I tried adding a print just before FT2Font is called, ran in valgrind, and hit Ctrl-C whenever I saw something printed out. Since valgrind is pretty slow, it's easy to do this several times, but all I got was Exception ignored in: <matplotlib.ft2font.FT2Font object at 0x...> a bunch of times. That was with Python 3.7, maybe there's some difference with 3.9.

@anntzer
Copy link
Contributor Author
anntzer commented Oct 16, 2021

I now have another repro, which fails everytime:

python -c 'from matplotlib.ft2font import FT2Font; from matplotlib.cbook import _get_data_path; [FT2Font(str(_get_data_path("fonts/ttf/DejaVuSans.ttf"))) for _ in range(10000)]'

which gives

Exception ignored in: <matplotlib.ft2font.FT2Font object at 0x7f3cb3f549d0>
Traceback (most recent call last):
  File "<string>", line 1, in <listcomp>
OSError: [Errno 24] Too many open files: '.../matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf'
free(): double free detected in tcache 2
Fatal Python error: Aborted

Current thread 0x00007f3cd765f740 (most recent call first):
  File "<string>", line 1 in <listcomp>
  File "<string>", line 1 in <module>

(The OSError is expected, it's just a simple way to trigger an exception in the FT2Font init.)

I haven't fully figured out what was happening, but my guess is that when an exception occurs in the body of PyFT2Font_init, it exits with a not-fully-inited FT2Font, whose deallocation then tries to clear some fields not yet completely setup.

@QuLogic
Copy link
Member
QuLogic commented Oct 18, 2021

For me, this nearly immediately does:

Exception ignored in: <matplotlib.ft2font.FT2Font object at 0x7f498ccdbb30>
Traceback (most recent call last):
  File "<string>", line 1, in <listcomp>
OSError: [Errno 24] Too many open files: '/home/elliott/code/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
SystemError: PyEval_EvalFrameEx returned NULL without setting an error

so I raised the file limit, hitting Ctrl-C does:

^CException ignored in: <matplotlib.ft2font.FT2Font object at 0x7f6d87118f50>
Traceback (most recent call last):
  File "<string>", line 1, in <listcomp>
KeyboardInterrupt
Traceback (most recent call last):
  File "<string>", line 1, in <module>
SystemError: PyEval_EvalFrameEx returned NULL without setting an error

@WeatherGod
Copy link
Member
WeatherGod commented Oct 18, 2021 via email

@QuLogic
Copy link
Member
QuLogic commented Oct 18, 2021

The crash occurs in 3.8+, but not 3.7.

@QuLogic
Copy link
Member
QuLogic commented Oct 18, 2021

This can be reproduced in valgrind:

Invalid read of size 8
   at 0x22B3C980: UnknownInlinedFun (stl_vector.h:336)
   by 0x22B3C980: UnknownInlinedFun (stl_vector.h:683)
   by 0x22B3C980: FT2Font::~FT2Font() (ft2font.cpp:367)
   by 0x22B3CA25: FT2Font::~FT2Font() (ft2font.cpp:367)
   by 0x22B3C1A3: PyFT2Font_dealloc(PyFT2Font*) [clone .lto_priv.0] (ft2font_wrapper.cpp:420)
   by 0x49A0AF3: _PyEval_EvalFrameDefault (ceval.c:3790)
   by 0x49A89D6: UnknownInlinedFun (ceval.c:741)
   by 0x49A89D6: function_code_fastcall (call.c:284)
   by 0x499A84B: UnknownInlinedFun (abstract.h:127)
   by 0x499A84B: UnknownInlinedFun (ceval.c:4963)
   by 0x499A84B: _PyEval_EvalFrameDefault (ceval.c:3500)
   by 0x4999583: UnknownInlinedFun (ceval.c:741)
   by 0x4999583: _PyEval_EvalCodeWithName (ceval.c:4298)
   by 0x4A147AC: PyEval_EvalCodeEx (ceval.c:4327)
   by 0x4A1475E: PyEval_EvalCode (ceval.c:718)
   by 0x4A35A4B: run_eval_code_obj (pythonrun.c:1166)
   by 0x4A34CD2: run_mod (pythonrun.c:1188)
   by 0x4A328D0: PyRun_StringFlags (pythonrun.c:1061)
 Address 0x12d43be8 is 72 bytes inside a block of size 152 free'd
   at 0x48438DD: operator delete(void*, unsigned long) (vg_replace_malloc.c:814)
   by 0x22B3CA3B: FT2Font::~FT2Font() (ft2font.cpp:367)
   by 0x22B3C1A3: PyFT2Font_dealloc(PyFT2Font*) [clone .lto_priv.0] (ft2font_wrapper.cpp:420)
   by 0x49BDEED: UnknownInlinedFun (object.h:478)
   by 0x49BDEED: UnknownInlinedFun (object.h:541)
   by 0x49BDEED: structseq_dealloc (structseq.c:91)
   by 0x494A268: UnknownInlinedFun (object.h:478)
   by 0x494A268: _PyErr_WriteUnraisableMsg.cold (errors.c:1399)
   by 0x22B3D631: close_file_callback(FT_StreamRec_*) (ft2font_wrapper.cpp:309)
   by 0x22B4C950: FT_Stream_Close (ftstream.c:52)
   by 0x22B45873: FT_Stream_Free (ftobjs.c:228)
   by 0x22B46058: destroy_face.lto_priv.0 (ftobjs.c:949)
   by 0x22BC0AD3: FT_Done_Face.isra.0 (ftobjs.c:2443)
   by 0x22B3C976: FT2Font::~FT2Font() (ft2font.cpp:365)
   by 0x22B3CA25: FT2Font::~FT2Font() (ft2font.cpp:367)
 Block was alloc'd at
   at 0x4840FF5: operator new(unsigned long) (vg_replace_malloc.c:417)
   by 0x22B3E1A8: PyFT2Font_init(PyFT2Font*, _object*, _object*) [clone .lto_priv.0] (ft2font_wrapper.cpp:405)
   by 0x49A35AA: type_call (typeobject.c:994)
   by 0x49A3328: _PyObject_MakeTpCall (call.c:159)
   by 0x499F970: UnknownInlinedFun (abstract.h:125)
   by 0x499F970: UnknownInlinedFun (abstract.h:115)
   by 0x499F970: UnknownInlinedFun (ceval.c:4963)
   by 0x499F970: _PyEval_EvalFrameDefault (ceval.c:3500)
   by 0x49A89D6: UnknownInlinedFun (ceval.c:741)
   by 0x49A89D6: function_code_fastcall (call.c:284)
   by 0x499A84B: UnknownInlinedFun (abstract.h:127)
   by 0x499A84B: UnknownInlinedFun (ceval.c:4963)
   by 0x499A84B: _PyEval_EvalFrameDefault (ceval.c:3500)
   by 0x4999583: UnknownInlinedFun (ceval.c:741)
   by 0x4999583: _PyEval_EvalCodeWithName (ceval.c:4298)
   by 0x4A147AC: PyEval_EvalCodeEx (ceval.c:4327)
   by 0x4A1475E: PyEval_EvalCode (ceval.c:718)
   by 0x4A35A4B: run_eval_code_obj (pythonrun.c:1166)
   by 0x4A34CD2: run_mod (pythonrun.c:1188)

The important part is the initial free in the second backtrace. We're in our wrapper deallocation function PyFT2Font_dealloc, which destroys the C++ FT2Font object. This deletes the glyphs and faces that it has from FreeType with FT_Done_Glyph and FT_Done_Face. FreeType then sees that the file is unused, it seems, and calls the close_file_callback. This sees that there is an error (technically correct, but not actually caused by the close call) and calls PyErr_WriteUnraisable, which in 3.8 apparently sees that the font is unused again, and calls PyFT2Font_dealloc again.

I'm not sure if that recursion is a bug in Python or not, but it's easy enough to work around on our side by disabling the callback on shutdown.

@anntzer
Copy link
Contributor Author
anntzer commented Oct 18, 2021

Thanks for the investigation :-)

Actually, I guess the fix may be rather to only catch exceptions occuring in close()? i.e.

diff --git i/src/ft2font_wrapper.cpp w/src/ft2font_wrapper.cpp
index c0e2a20a1a..300b37ce64 100644
--- i/src/ft2font_wrapper.cpp
+++ w/src/ft2font_wrapper.cpp
@@ -297,6 +297,8 @@ exit:
 
 static void close_file_callback(FT_Stream stream)
 {
+    PyObject *type, *value, *traceback;
+    PyErr_Fetch(&type, &value, &traceback);
     PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer;
     PyObject *close_result = NULL;
     if (!(close_result = PyObject_CallMethod(self->py_file, "c
A914
lose", ""))) {
@@ -308,6 +310,7 @@ exit:
     if (PyErr_Occurred()) {
         PyErr_WriteUnraisable((PyObject*)self);
     }
+    PyErr_Restore(type, value, traceback);
 }
 
 static PyTypeObject PyFT2FontType;

At least this fixes the issue for me.

I guess this could still be a problem if close() throws an exception, but I'm not sure if that can happen at all (note that this would be the close() method of the file object returned by open(filename_passed_to_FT2Font), so it's a "standard" file-like, not a custom object (if we directly pass a file-like object to FT2Font, it doesn't get closed on destruction of the FT2Font as it is expected that the caller handles the file-like's lifetime).

@QuLogic
Copy link
Member
QuLogic commented Oct 19, 2021

Well, close does call flush, which could raise something, but I guess that's a no-op for a read-only file anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants
0