Re: [PATCH 3/3] snapshot: fix the race condition. Protect the whole replaying function.

Uli Schlachter <[email protected]>
Newsgroups gmane.comp.lib.cairo
Message-ID <[email protected]>
Hi,

On 25.05.2012 11:13, Zhigang Gong wrote:
> Simply protect the whole replaying stage here, we may fine
> tune the protect region in the future. This commit also break
> the "self-copy" bug. Here self-copy means, replay a recording
> surface to a surface which is some of the snapshot's target
> surface. Then it may cause a "race" condition even with single
> thread.
> 
> As now the accessing from the snapshot side must hold a rdlock,
> then the surface's handling side is easier than before. We do
> not need the snapshot side to touch the surface's reference counter
> now, the wrlock at the COW function is enough to avoid race.
> 
> Slightly change the surface's pointer stealing. We set the _finishing
> before call the cairo_surface_flush, then latter the first snapshot
> will steal its pointer and then clear the bit, and the other snapshots
> will have to create new buffer.
> 
> Signed-off-by: Zhigang Gong <[email protected]>
> ---
>  src/cairo-image-source.c            |   33 ++---------
>  src/cairo-image-surface.c           |    1 +
>  src/cairo-pdf-surface.c             |   13 +---
>  src/cairo-ps-surface.c              |   16 +----
>  src/cairo-recording-surface.c       |  112 +++++++++++++++++++++++++++++++++++
>  src/cairo-script-surface.c          |    5 +-
>  src/cairo-surface-private.h         |    1 -
>  src/cairo-surface-snapshot-inline.h |    5 +-
>  src/cairo-surface-snapshot.c        |   39 ++----------
>  src/cairo-surface.c                 |    7 +--
>  10 files changed, 135 insertions(+), 97 deletions(-)
> 
[...]
> diff --git a/src/cairo-image-surface.c b/src/cairo-image-surface.c
> index 98f70c5..4322f0a 100644
> --- a/src/cairo-image-surface.c
> +++ b/src/cairo-image-surface.c
> @@ -768,6 +768,7 @@ _cairo_image_surface_snapshot (void *abstract_surface)
>  	clone->color = image->color;
>  
>  	clone->owns_data = FALSE;
> +	image->base._finishing = FALSE;
>  	return &clone->base;
>      }

Whoa? Why? This smells quite fishy to me. Just changing a boolean won't stop
cairo_surface_finish() from doing its job, right?

>  
> diff --git a/src/cairo-recording-surface.c b/src/cairo-recording-surface.c
> index 02d8afd..85e65f7 100644
> --- a/src/cairo-recording-surface.c
> +++ b/src/cairo-recording-surface.c
> @@ -89,6 +89,7 @@
>  #include "cairo-recording-surface-inline.h"
>  #include "cairo-surface-wrapper-private.h"
>  #include "cairo-traps-private.h"
> +#include "cairo-surface-snapshot-inline.h"
>  
>  typedef enum {
>      CAIRO_RECORDING_REPLAY,
> @@ -1287,6 +1288,42 @@ _cairo_recording_surface_get_visible_commands (cairo_recording_surface_t *surfac
>      return num_visible;
>  }
>  
> +static cairo_surface_snapshot_t *
> +_cairo_pattern_get_snapshot (cairo_pattern_t *pattern)
> +{
> +    if (pattern->type == CAIRO_PATTERN_TYPE_SURFACE) {
> +        cairo_surface_pattern_t *surface_pattern =
> +            (cairo_surface_pattern_t *) pattern;
> +        cairo_surface_t *surface = surface_pattern->surface;
> +
> +        if (_cairo_surface_is_snapshot(surface))
> +	    return (cairo_surface_snapshot_t *) surface;
> +    }
> +    return NULL;
> +}
> +
> +/*
> + * Begin to access the snapshot to replay on the target surface. To avoid race
> + * condition we need to get the rdlock. Thus other thread should not modify the
> + * source during we are accessing the original source.
> + *
> + * We need to check the target, if the target is source target of the snapshot, we
> + * need to break the self-copy here.
> + *
> + * */

Minor nitpicking: That "*/" looks misplaced.

> +static int
> +_cairo_snapshot_begin_access_target(cairo_surface_snapshot_t *snapshot,
> +				    cairo_surface_t *target)
> +{
> +    if (snapshot->target == target) {
> +	target->snapshot_detach(&snapshot->base);
> +	return 0;
> +    }
> +    CAIRO_RWLOCK_RDLOCK(snapshot->rwlock);
> +    return 1;
> +}

This function should return cairo_bool_t, not int.

>  static cairo_status_t
>  _cairo_recording_surface_replay_internal (cairo_recording_surface_t	*surface,
>  					  const cairo_rectangle_int_t *surface_extents,
> @@ -1355,21 +1397,42 @@ _cairo_recording_surface_replay_internal (cairo_recording_surface_t	*surface,
>  
>  	switch (command->header.type) {
>  	case CAIRO_COMMAND_PAINT:
> +	    snapshot = _cairo_pattern_get_snapshot(&command->paint.source.base);
> +	    if (snapshot)
> +		locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	    status = _cairo_surface_wrapper_paint (&wrapper,
>  						   command->header.op,
>  						   &command->paint.source.base,
>  						   command->header.clip);
> +	    if (locked)
> +	        CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	    break;
>  
>  	case CAIRO_COMMAND_MASK:
> +
> +            snapshot = _cairo_pattern_get_snapshot(&command->mask.source.base);
> +	    if (snapshot)
> +		locked = _cairo_snapshot_begin_access_target(snapshot, target);
> +
> +	    mask_snapshot = _cairo_pattern_get_snapshot(&command->mask.mask.base);
> +	    if (mask_snapshot)
> +		mask_locked = _cairo_snapshot_begin_access_target(mask_snapshot, target);

Couldn't this deadlock?

Thread A uses mask 1 and source 2, thread B uses mask 2 and source 1. If both
first lock their source, they'd deadlock on the mask.

>  	    status = _cairo_surface_wrapper_mask (&wrapper,
>  						  command->header.op,
>  						  &command->mask.source.base,
>  						  &command->mask.mask.base,
>  						  command->header.clip);
> +	    if (mask_locked)
> +	        CAIRO_RWLOCK_UNLOCK(mask_snapshot->rwlock);
> +	    if (locked)
> +	        CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	    break;
>  
>  	case CAIRO_COMMAND_STROKE:
> +	    snapshot = _cairo_pattern_get_snapshot(&command->stroke.source.base);
> +	    if (snapshot)
> +		locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	    status = _cairo_surface_wrapper_stroke (&wrapper,
>  						    command->header.op,
>  						    &command->stroke.source.base,
> @@ -1380,9 +1443,14 @@ _cairo_recording_surface_replay_internal (cairo_recording_surface_t	*surface,
>  						    command->stroke.tolerance,
>  						    command->stroke.antialias,
>  						    command->header.clip);
> +	    if (locked)
> +	        CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	    break;
>  
>  	case CAIRO_COMMAND_FILL:
> +	    snapshot = _cairo_pattern_get_snapshot(&command->fill.source.base);
> +	    if (snapshot)
> +		locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	    status = CAIRO_INT_STATUS_UNSUPPORTED;
>  	    if (_cairo_surface_wrapper_has_fill_stroke (&wrapper)) {
>  		cairo_command_t *stroke_command;
> @@ -1434,9 +1502,14 @@ _cairo_recording_surface_replay_internal (cairo_recording_surface_t	*surface,
>  						      command->fill.antialias,
>  						      command->header.clip);
>  	    }
> +	    if (locked)
> +	        CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	    break;
>  
>  	case CAIRO_COMMAND_SHOW_TEXT_GLYPHS:
> +	    snapshot = _cairo_pattern_get_snapshot(&command->show_text_glyphs.source.base);
> +	    if (snapshot)
> +		locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	    status = _cairo_surface_wrapper_show_text_glyphs (&wrapper,
>  							      command->header.op,
>  							      &command->show_text_glyphs.source.base,
> @@ -1446,6 +1519,8 @@ _cairo_recording_surface_replay_internal (cairo_recording_surface_t	*surface,
>  							      command->show_text_glyphs.cluster_flags,
>  							      command->show_text_glyphs.scaled_font,
>  							      command->header.clip);
> +	    if (locked)
> +	        CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	    break;

Does anyone have a good idea how the locking could be simplified here? That
looks quite complex.

> @@ -1480,6 +1555,8 @@ _cairo_recording_surface_replay_one (cairo_recording_surface_t	*surface,
>      cairo_surface_wrapper_t wrapper;
>      cairo_command_t **elements, *command;
>      cairo_int_status_t status;
> +    cairo_surface_snapshot_t *snapshot, *mask_snapshot;
> +    int locked = 0, mask_locked = 0;

This is used as a boolean, so cairo_bool_t here, too, please.
>  
>      if (unlikely (surface->base.status))
>  	return surface->base.status;
> @@ -1501,25 +1578,48 @@ _cairo_recording_surface_replay_one (cairo_recording_surface_t	*surface,
>      if (index > surface->commands.num_elements)
>  	return _cairo_error (CAIRO_STATUS_READ_ERROR);
>  
> +    if (target->type == CAIRO_SURFACE_TYPE_RECORDING)
> +	_cairo_recording_surface_break_self_copy_loop((cairo_recording_surface_t*)target);
> +
>      elements = _cairo_array_index (&surface->commands, 0);
>      command = elements[index];
>      switch (command->header.type) {
>      case CAIRO_COMMAND_PAINT:
> +	snapshot = _cairo_pattern_get_snapshot(&command->paint.source.base);
> +	if (snapshot)
> +	    locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	status = _cairo_surface_wrapper_paint (&wrapper,
>  					       command->header.op,
>  					       &command->paint.source.base,
>  					       command->header.clip);
> +	if (locked)
> +	    CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	break;
>  
>      case CAIRO_COMMAND_MASK:
> +	snapshot = _cairo_pattern_get_snapshot(&command->mask.source.base);
> +	if (snapshot)
> +	    locked = _cairo_snapshot_begin_access_target(snapshot, target);
> +
> +	mask_snapshot = _cairo_pattern_get_snapshot(&command->mask.mask.base);
> +	if (mask_snapshot)
> +	    mask_locked = _cairo_snapshot_begin_access_target(snapshot, target);
> +
>  	status = _cairo_surface_wrapper_mask (&wrapper,
>  					      command->header.op,
>  					      &command->mask.source.base,
>  					      &command->mask.mask.base,
>  					      command->header.clip);
> +	if (mask_locked)
> +	    CAIRO_RWLOCK_UNLOCK(mask_snapshot->rwlock);
> +	if (locked)
> +	    CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	break;
>  
>      case CAIRO_COMMAND_STROKE:
> +	snapshot = _cairo_pattern_get_snapshot(&command->stroke.source.base);
> +	if (snapshot)
> +	    locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	status = _cairo_surface_wrapper_stroke (&wrapper,
>  						command->header.op,
>  						&command->stroke.source.base,
> @@ -1530,9 +1630,14 @@ _cairo_recording_surface_replay_one (cairo_recording_surface_t	*surface,
>  						command->stroke.tolerance,
>  						command->stroke.antialias,
>  						command->header.clip);
> +	if (locked)
> +	    CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	break;
>  
>      case CAIRO_COMMAND_FILL:
> +	snapshot = _cairo_pattern_get_snapshot(&command->fill.source.base);
> +	if (snapshot)
> +	    locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	status = _cairo_surface_wrapper_fill (&wrapper,
>  					      command->header.op,
>  					      &command->fill.source.base,
> @@ -1541,9 +1646,14 @@ _cairo_recording_surface_replay_one (cairo_recording_surface_t	*surface,
>  					      command->fill.tolerance,
>  					      command->fill.antialias,
>  					      command->header.clip);
> +	if (locked)
> +	    CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	break;
>  
>      case CAIRO_COMMAND_SHOW_TEXT_GLYPHS:
> +	snapshot = _cairo_pattern_get_snapshot(&command->show_text_glyphs.source.base);
> +	if (snapshot)
> +	    locked = _cairo_snapshot_begin_access_target(snapshot, target);
>  	status = _cairo_surface_wrapper_show_text_glyphs (&wrapper,
>  							  command->header.op,
>  							  &command->show_text_glyphs.source.base,
> @@ -1553,6 +1663,8 @@ _cairo_recording_surface_replay_one (cairo_recording_surface_t	*surface,
>  							  command->show_text_glyphs.cluster_flags,
>  							  command->show_text_glyphs.scaled_font,
>  							  command->header.clip);
> +	if (locked)
> +	    CAIRO_RWLOCK_UNLOCK(snapshot->rwlock);
>  	break;
>  
>      default:

I'd like to simplify this, too, but I don't have any good idea on how. :-(
(On and the MASK version has the same possible-deadlock)

[...]
> diff --git a/src/cairo-surface-private.h b/src/cairo-surface-private.h
> index d23fc05..321719d 100644
> --- a/src/cairo-surface-private.h
> +++ b/src/cairo-surface-private.h
> @@ -64,7 +64,6 @@ struct _cairo_surface {
>      unsigned int unique_id;
>      unsigned int serial;
>      cairo_damage_t *damage;
> -
>      unsigned _finishing : 1;
>      unsigned finished : 1;
>      unsigned is_clear : 1;

That looks quite random. Why is it in this patch?

> diff --git a/src/cairo-surface-snapshot-inline.h b/src/cairo-surface-snapshot-inline.h
> index 48b24a4..2fe05ac 100644
> --- a/src/cairo-surface-snapshot-inline.h
> +++ b/src/cairo-surface-snapshot-inline.h
> @@ -48,12 +48,9 @@ _cairo_surface_snapshot_is_reused (cairo_surface_t *surface)
>  static inline cairo_surface_t *
>  _cairo_surface_snapshot_get_target (cairo_surface_t *surface)
>  {
> -    cairo_surface_snapshot_t *snapshot = (cairo_surface_snapshot_t *) surface;
>      cairo_surface_t *target;
>  
> -    CAIRO_RWLOCK_WRLOCK (snapshot->rwlock);
> -    target = _cairo_surface_reference (snapshot->target);
> -    CAIRO_RWLOCK_UNLOCK (snapshot->rwlock);
> +    target = ((cairo_surface_snapshot_t *) surface)->target;
>  
>      return target;
>  }
> diff --git a/src/cairo-surface-snapshot.c b/src/cairo-surface-snapshot.c
> index 30e58fd..268e3b3 100644
> --- a/src/cairo-surface-snapshot.c
> +++ b/src/cairo-surface-snapshot.c
> @@ -67,15 +67,10 @@ static cairo_status_t
>  _cairo_surface_snapshot_flush (void *abstract_surface)
>  {
>      cairo_surface_snapshot_t *surface = abstract_surface;
> -    cairo_surface_t *target;
> -    cairo_status_t status;
>  
> -    target = _cairo_surface_snapshot_get_target (&surface->base);
> -    cairo_surface_flush (target);
> -    status = target->status;
> -    cairo_surface_destroy (target);
> +    cairo_surface_flush (surface->target);
>  
> -    return status;
> +    return surface->target->status;
>  }

Before, this code made sure that it held a reference to the snapshot while
reading its status. Why? Could that cause problems here (=use after free)?

[...]
> diff --git a/src/cairo-surface.c b/src/cairo-surface.c
> index 35ac391..a172992 100644
> --- a/src/cairo-surface.c
> +++ b/src/cairo-surface.c
> @@ -858,11 +858,6 @@ cairo_surface_destroy (cairo_surface_t *surface)
>  
>      if (! surface->finished) {
>  	_cairo_surface_finish_snapshots (surface);
> -	/* We may have been referenced by a snapshot prior to have
> -	 * detaching it with the copy-on-write.
> -	 */
> -	if (CAIRO_REFERENCE_COUNT_GET_VALUE (&surface->ref_count))
> -	    return;

How about adding an assert?

assert (! CAIRO_REFERENCE_COUNT_HAS_REFERENCE (&surface->ref_count));

Perhaps this assertion should best be done right before the free() at the end of
this function instead of here...

> @@ -909,10 +904,10 @@ cairo_surface_get_reference_count (cairo_surface_t *surface)
>  static void
>  _cairo_surface_finish_snapshots (cairo_surface_t *surface)
>  {
> +    surface->_finishing = TRUE;
>      cairo_surface_flush (surface);
>  
>      /* update the snapshots *before* we declare the surface as finished */
> -    surface->_finishing = TRUE;

I'm curious for why this is necessary. Could you tell me? If you remember,
perhaps this deserves a little comment on which weird edge case makes this
necessary.

Cheers,
Uli
-- 
"Do you know that books smell like nutmeg or some spice from a foreign land?"
                                                  -- Faber in Fahrenheit 451
--
cairo mailing list
[email protected]
http://lists.cairographics.org/mailman/listinfo/cairo
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.