Race condition between cairo_scaled_font_create and cairo_scaled_font_destroy due to scaled fonts with zero reference count in font_map->hash_table
This simple test case eventually crashes with cairo-scaled-font.c:1326: cairo_scaled_font_destroy: Assertion '! scaled_font->cache_frozen' failed. Aborted (core dumped)
even that it is using separate objects in each thread.
Here is what could be happening step by step from view of both threads after they got into while(true)
loop:
-
thread 0: executes
cairo_scaled_font_create
, effects are that newscaled_font
is created usingfont_face->backend->scaled_font_create
, it is put into hash tablefont_map->hash_table
and intofont_map->mru_scaled_font
so reference count after returning fromcairo_scaled_font_create
is 2 -
thread 0: executes
cairo_scaled_font_text_extents
andcairo_scaled_font_destroy
and lets say is interrupted right aftercairo_scaled_font_destroy
by system, reference count is decreased by 1 to 1 -
thread 1: executing
cairo_scaled_font_create
, this will also create newscaled_font
and place it intofont_map->hash_table
and also intofont_map->mru_scaled_font
so oldmru_scaled_font
which was used by thread 0 is going to be destroyed on line 1193 bycairo_scaled_font_destroy (old);
(in real world it will be more probably line 1118 which happens if first cycle was already executed but logic here is similar), insidecairo_scaled_font_destroy
is called_cairo_reference_count_dec_and_test (&scaled_font->ref_count)
and reference count drops from 1 to 0 which means it is going to be also "freed", but lets say now right beforeassert (! scaled_font->cache_frozen)
is thread 1 interrupted by system -
thread 0: enters second cycle in loop, executes
cairo_scaled_font_create
, font inmru_scaled_font
does not match so it is going to try hash table lookup_cairo_hash_table_lookup
and gets samescaled_font
as before which has already reference count 0, (it is still not marked as "holdover" and is not infont_map->holdovers
array but that is irrelevant for now and not the problem), so thisscaled_font
is placed into MRU again and returned into caller with reference count 2, here this is really problem because thread 1 is going to "definitely destroy" it because it already reached reference count 0 before but thread 0 is going to use it -
thread 0: executes
cairo_scaled_font_text_extents
which will setscaled_font->cache_frozen
to TRUE somewhere inside and lets say thread 0 is interrupted while it is still set to TRUE -
thread 1:
assert (! scaled_font->cache_frozen)
causes above mentioned crash due to failed assertion
#include <cairo.h>
void infinite_loop_called_by_multiple_threads(cairo_font_face_t *font_face, cairo_matrix_t *m, cairo_font_options_t *options)
{
while(true)
{
cairo_scaled_font_t *scaled_font = cairo_scaled_font_create(font_face, m, m, options);
cairo_text_extents_t extents;
cairo_scaled_font_text_extents(scaled_font, "m", &extents);
cairo_scaled_font_destroy(scaled_font);
}
}
#include <vector>
#include <thread>
#include <cassert>
#include <cairo-ft.h>
int main()
{
std::vector<std::thread> threads;
for(int t = 0; t < 2; t++)
threads.emplace_back([t]()
{
FT_Library lib;
FT_Init_FreeType(&lib);
FT_Face face;
assert(!FT_New_Face(lib, "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 0, &face));
cairo_font_face_t *font_face = cairo_ft_font_face_create_for_ft_face(face, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP);
cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, 100, 100);
cairo_t *ctx = cairo_create(surface);
cairo_matrix_t m;
cairo_matrix_init_identity(&m);
cairo_font_options_t *options = cairo_font_options_create();
infinite_loop_called_by_multiple_threads(font_face, &m, options);
});
for(auto &th : threads)th.join();
return 0;
}
And here is output from gdb where can be seen that ref_count
is 2 even that in cairo_scaled_font_destroy
it was already decremented and passed test that it decremented from 1 to 0:
a.out: ../../../../src/cairo-scaled-font.c:1326: cairo_scaled_font_destroy: Assertion `! scaled_font->cache_frozen' failed.
Thread 2 "a.out" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff7318700 (LWP 171302)]
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007ffff79da859 in __GI_abort () at abort.c:79
#2 0x00007ffff79da729 in __assert_fail_base (fmt=0x7ffff7b70588 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=0x7ffff7f6c2c6 "! scaled_font->cache_frozen", file=0x7ffff7f6bff0 "../../../../src/cairo-scaled-font.c", line=1326, function=<optimized out>) at assert.c:92
#3 0x00007ffff79ebf36 in __GI___assert_fail (assertion=assertion@entry=0x7ffff7f6c2c6 "! scaled_font->cache_frozen", file=file@entry=0x7ffff7f6bff0 "../../../../src/cairo-scaled-font.c", line=line@entry=1326, function=function@entry=0x7ffff7f6c410 <__PRETTY_FUNCTION__.13812> "cairo_scaled_font_destroy") at assert.c:101
#4 0x00007ffff7eed013 in INT_cairo_scaled_font_destroy (scaled_font=scaled_font@entry=0x7fffe800d4a0) at ../../../../src/cairo-scaled-font.c:1326
#5 0x00007ffff7eed6ed in INT_cairo_scaled_font_create (font_face=0x7ffff0003c40, font_matrix=<optimized out>, ctm=0x7ffff7317c10, options=0x7ffff000e170) at ../../../../src/cairo-scaled-font.c:1118
#6 0x0000555555556489 in infinite_loop_called_by_multiple_threads (font_face=0x7ffff0003c40, m=0x7ffff7317c10, options=0x7ffff000e170) at main.cpp:7
#4 0x00007ffff7eed013 in INT_cairo_scaled_font_destroy (scaled_font=scaled_font@entry=0x7fffe800d4a0) at ../../../../src/cairo-scaled-font.c:1326
1326 ../../../../src/cairo-scaled-font.c: No such file or directory.
(gdb) p *scaled_font
$1 = {..., ref_count = {ref_count = 2}, ..., cache_frozen = 1, ...}