Segfault / threading inconsistencies around PangoFcFontMap
At some point after Pango 1.48.3, librsvg's test suite started crashing sometimes, not all the time (seldom on my machine; annoyingly often on CI pipelines).
I haven't been able to get a good stack trace - this bug doesn't seem to happen while under gdb. A core file doesn't indicate obvious places for a crash in any threads; they are all doing something different.
Sometimes the tests print this, and sometimes they crash without printing anything:
(process:26951): Pango-CRITICAL **: 14:53:50.576: pango_fc_patterns_unref: assertion 'pats->ref_count > 0' failed
What librsvg does for the text-related tests:
pub unsafe fn load_test_fonts() {
let font_paths = [
"tests/resources/Roboto-Regular.ttf",
"tests/resources/Roboto-Italic.ttf",
"tests/resources/Roboto-Bold.ttf",
"tests/resources/Roboto-BoldItalic.ttf",
];
let config = fontconfig::FcConfigCreate();
for path in &font_paths {
let path_cstring = CString::new(*path).unwrap();
if fontconfig::FcConfigAppFontAddFile(config, path_cstring.as_ptr() as *const _) == 0 {
panic!("Could not load font file {} for tests; aborting", path,);
}
}
let font_map = pangocairo::FontMap::for_font_type(cairo::FontType::FontTypeFt).unwrap();
let raw_font_map: *mut pango::ffi::PangoFontMap = font_map.to_glib_none().0;
pango_fc_font_map_set_config(raw_font_map as *mut _, config);
fontconfig::FcConfigDestroy(config);
pangocairo::FontMap::set_default(Some(font_map.downcast::<pangocairo::FontMap>().unwrap()));
}
Basically:
- create a
FcConfig
populated with a few font files fontmap = pango_cairo_font_map_new_for_font_type(CAIRO_FONT_TYPE_FT)
pango_fc_font_map_set_config(fontmap, my_fc_config)
pango_cairo_font_map_set_default(fontmap)
Later, when librsvg's API is actually called, it will call Pango and use pango_cairo_font_map_get_default()
- thus getting what the test suite put with set_default
.
This is not an initialization step run once for the whole test suite; every test runs it and tests get run on multiple threads.
From the docs, pango_cairo_font_map_set_default()
is per-thread; it uses GPrivate
to hold a PangoFontMap
.
However, pangofc-fontmap.c does this:
-
pango_fc_font_map_init
initializes the instance and doesstart_init_in_thread
. From what I can tell,priv->config == NULL
means that the user hasn't set a particularFcConfig
and calls likeFcFoo(NULL)
will fall back to the global configuration that was set up byFcInit
... - ... but shortly after, librsvg's test suite calls
pango_fc_font_map_set_config
which fills inpriv->config
, and setsfc_initialized = 2
without first locking the mutex for that shared data. - ... but that does not mean that the default configuration is actually ready for the threads that are waiting for it.
If I remove fc_initialized = 2
from pango_fc_font_map_set_config
, the crash doesn't happen anymore. Semantically I understand this to mean "I don't know if the global config was initialized, so I won't set a flag for it - I just have a custom config from the user".
(Because I'm sure I'll forget this, pango_cairo_font_map_new_for_font_type
creates a PangoCairoFcFontMap
, which inherits from PangoFcFontMap
, which is where pango_fc_font_map_init
happens.)