Various problems when copying a recording surface with tags to a PDF surface
See https://lists.cairographics.org/archives/cairo/2020-December/029151.html
I'll just copy the text here and apply some light markdown:
Hi! The cairo library is fantastic and I enjoy working with it. However, I've encountered an invalid memory access bug if I perform some output enclosed in a tag to a recording surface that has a defined extent, and then copy the recording surface to a PDF surface.
I've found this problem using cairo packaged for Debian, Debian's version 1.16.0-4. I haven't tested cairo from upstream, but I have appended to this email a simple test program (greatly simplified from my real program) that reproduces the problem. It does the following:
* Creates a recording surface and draws a rectangle in it
surrounded by a CAIRO_TAG_DEST destination tag.
* Creates a PDF surface.
* Copies the recording surface to the PDF surface.
* Call cairo_surface_show_page() on the PDF surface.
* Destroys both surfaces.
To try it, save it as cairo-test.c and compile it with: gcc -Wall -Wextra -g cairo-test.c -o cairo-test -lcairo
If you run it, it produces output.pdf. This isn't interesting, it's just a 100x100 point PDF with a black rectangle drawn in the middle.
Now turn on glibc malloc perturbation and malloc checking, like this, and run it again:
export MALLOC_PERTURB_=165
export MALLOC_CHECK_=2
./cairo-test
On my system, it yields a segmentation fault with this backtrace:
0x00007ffff7eee801 in _cairo_surface_detach_snapshot (
snapshot=0xfffffffffffffee8) at ../../../../src/cairo-surface.c:343
343 ../../../../src/cairo-surface.c: No such file or directory.
(gdb) bt
#0 0x00007ffff7eee801 in _cairo_surface_detach_snapshot (
snapshot=0xfffffffffffffee8) at ../../../../src/cairo-surface.c:343
#1 0x00007ffff7eee5bc in _cairo_surface_detach_snapshots (
surface=0x7ffff7f5ede0 <_cairo_surface_nil>)
at ../../../../src/cairo-surface.c:334
#2 _cairo_surface_flush (surface=0x7ffff7f5ede0 <_cairo_surface_nil>,
flags=0) at ../../../../src/cairo-surface.c:1626
#3 0x00007ffff7eeaa03 in _cairo_surface_snapshot_flush (
abstract_surface=0x555555560160, flags=0)
at ../../../../src/cairo-surface-snapshot.c:74
#4 0x00007ffff7eee735 in _cairo_surface_finish_snapshots (
surface=0x555555560160) at ../../../../src/cairo-surface.c:1019
#5 INT_cairo_surface_destroy (surface=0x555555560160)
at ../../../../src/cairo-surface.c:963
#6 0x00007ffff7eee5bc in _cairo_surface_detach_snapshots (
surface=0x55555555c0d0) at ../../../../src/cairo-surface.c:334
#7 _cairo_surface_flush (surface=0x55555555c0d0, flags=0)
at ../../../../src/cairo-surface.c:1626
#8 0x00007ffff7eee735 in _cairo_surface_finish_snapshots (
surface=0x55555555c0d0) at ../../../../src/cairo-surface.c:1019
#9 INT_cairo_surface_destroy (surface=0x55555555c0d0)
at ../../../../src/cairo-surface.c:963
#10 0x00005555555554e9 in main (argc=1, argv=0x7fffffffe038)
at cairo-test.c:65
"valgrind ./cairo-test" reports something similar:
Invalid read of size 8
at 0x48EF801: _cairo_surface_detach_snapshot (cairo-surface.c:343)
by 0x48EF5BB: _cairo_surface_detach_snapshots (cairo-surface.c:334)
by 0x48EF5BB: _cairo_surface_flush (cairo-surface.c:1626)
by 0x48EBA02: _cairo_surface_snapshot_flush (cairo-surface-snapshot.c:74)
by 0x48EF734: _cairo_surface_finish_snapshots (cairo-surface.c:1019)
by 0x48EF734: cairo_surface_destroy (cairo-surface.c:963)
by 0x48EF5BB: _cairo_surface_detach_snapshots (cairo-surface.c:334)
by 0x48EF5BB: _cairo_surface_flush (cairo-surface.c:1626)
by 0x48EF734: _cairo_surface_finish_snapshots (cairo-surface.c:1019)
by 0x48EF734: cairo_surface_destroy (cairo-surface.c:963)
by 0x1094E8: main (cairo-test.c:65)
Address 0xffffffffffffffe0 is not stack'd, malloc'd or (recently)
Tags
The problem is related to the tag. If you run it with --no-tag to avoid creating the tag, there is no problem, with or without valgrind.
Extents on recording surface
The problem is related to specifying extents on the recording surface. If you run it with --no-extents to avoid specifying the extents, there is no problem.
However, "valgrind --leak-check=full ./cairo-test --no-extents" does report a leak:
384 bytes in 1 blocks are definitely lost in loss record 4 of 11
at 0x483877F: malloc (vg_replace_malloc.c:307)
by 0x48EBAF9: _cairo_surface_snapshot (cairo-surface-snapshot.c:264)
by 0x48C986B: _cairo_pattern_init_snapshot (cairo-pattern.c:422)
by 0x48DA700: _cairo_recording_surface_paint (cairo-recording-surface.c:743)
by 0x48F0277: _cairo_surface_paint (cairo-surface.c:2198)
by 0x48F0277: _cairo_surface_paint (cairo-surface.c:2171)
by 0x48F0277: _cairo_surface_paint (cairo-surface.c:2198)
by 0x48F0277: _cairo_surface_paint (cairo-surface.c:2171)
by 0x48A76D5: _cairo_gstate_paint (cairo-gstate.c:1061)
by 0x48FD044: cairo_paint (cairo.c:2220)
by 0x10930C: copy_surface (cairo-test.c:25)
by 0x1094CA: main (cairo-test.c:57)
cairo_surface_show_page()
The problem manifests a little differently if one deletes the call to cairo_surface_show_page(). Run it with --no-show-page to do this. This crashes with or without valgrind. valgrind reports the error location as:
Invalid read of size 4
at 0x48B2953: _cairo_image_analyze_transparency (cairo-image-surface.c:1214)
by 0x48D959C: _cairo_recording_surface_merge_source_attributes.isra.9 (cairo-recording-surface.c:1779)
by 0x48D9CDF: _cairo_recording_surface_replay_internal (cairo-recording-surface.c:2007)
by 0x48DB04A: _cairo_recording_surface_replay_and_create_regions (cairo-recording-surface.c:2197)
by 0x48BBC2B: _paint_page (cairo-paginated-surface.c:417)
by 0x48BC232: _cairo_paginated_surface_show_page (cairo-paginated-surface.c:583)
by 0x48BC31F: _cairo_paginated_surface_finish (cairo-paginated-surface.c:205)
by 0x48EE2B1: _cairo_surface_finish (cairo-surface.c:1030)
by 0x48EF757: cairo_surface_destroy (cairo-surface.c:970)
by 0x1094F4: main (cairo-test.c:66)
Address 0x0 is not stack'd, malloc'd or (recently) free'd
I'd very much appreciate your guidance!
Thanks,
Ben.
#include <cairo/cairo-pdf.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void
draw_rectangle (cairo_surface_t *surface, bool add_tag)
{
cairo_t *cr = cairo_create (surface);
if (add_tag)
cairo_tag_begin (cr, CAIRO_TAG_DEST, "name='mydest'");
cairo_rectangle (cr, 25, 25, 50, 50);
cairo_stroke (cr);
if (add_tag)
cairo_tag_end (cr, CAIRO_TAG_DEST);
cairo_destroy (cr);
}
static void
copy_surface (cairo_surface_t *dst, cairo_surface_t *src)
{
cairo_t *cr = cairo_create (dst);
cairo_set_source_surface (cr, src, 0, 0);
cairo_paint (cr);
cairo_destroy (cr);
}
int
main (int argc, char *argv[])
{
bool add_tag = true;
bool use_extents = true;
bool show_page = true;
for (int i = 1; i < argc; i++)
if (!strcmp (argv[i], "--no-tag"))
add_tag = false;
else if (!strcmp (argv[i], "--no-extents"))
use_extents = false;
else if (!strcmp (argv[i], "--no-show-page"))
show_page = false;
else
{
fprintf (stderr, "%s: not one of the known options "
"(--no-tag, --no-extents, --no-show-page)\n", argv[i]);
exit (1);
}
/* Create recording surface and draw a rectangle on it. */
cairo_rectangle_t extents = { .width = 100, .height = 100 };
cairo_surface_t *recording = cairo_recording_surface_create (
CAIRO_CONTENT_COLOR_ALPHA, use_extents ? &extents : NULL);
draw_rectangle (recording, add_tag);
/* Create PDF surface and copy the recording surface to it. */
cairo_surface_t *pdf = cairo_pdf_surface_create ("output.pdf", 100.0, 100.0);
copy_surface (pdf, recording);
if (show_page)
{
/* Bang! */
cairo_surface_show_page (pdf);
}
cairo_surface_destroy (recording); /* Alternate bang! */
cairo_surface_destroy (pdf);
return 0;
}
/*
* Local variables:
* compile-command: "gcc -Wall -Wextra -g cairo-test.c -o cairo-test -lcairo"
* End:
*/