Skip to content
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

Fix crash setting attributed text on multiple threads #1141

Merged
merged 1 commit into from
Oct 9, 2018

Conversation

maicki
Copy link
Contributor

@maicki maicki commented Sep 25, 2018

Currently some race condition can happen within -[ASTextNode setAttributedText:]:

  • Thread A calls setAttributedText:
  • We change the text with the lock held and we grab the length of the _attributedString
  • After getting the length a thread switch happens
  • A thread B calls setAttributedText:. The attributed text instance var get's updated and it finishes the method
  • Thread switch happens back to A and it's going again. It goes into the if block and try to access the last character based on the old length that is not up to date anymore and it crashes

You can repro it if you simulate a thread switch and the crash on the descender line via a semaphore like:

- (void)setAttributedText:(NSAttributedString *)attributedText semaphore:(dispatch_semaphore_t)semaphore
{
  if (attributedText == nil) {
    attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil];
  }
  
  // Don't hold textLock for too long.
  {
    ASLockScopeSelf();
    if (ASObjectIsEqual(attributedText, _attributedText)) {
      return;
    }

    _attributedText = ASCleanseAttributedStringOfCoreTextAttributes(attributedText);
#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS
	  [ASTextNode _registerAttributedText:_attributedText];
#endif
  }
    
  // Since truncation text matches style of attributedText, invalidate it now.
  [self _invalidateTruncationText];
  
  NSUInteger length = _attributedText.length;
  if (length > 0) {
    if (semaphore) {
      // Wait until the sempahore is released
      dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
    
    self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText];
    self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:length - 1 effectiveRange:NULL] descender];
  }

  // Tell the display node superclasses that the cached layout is incorrect now
  [self setNeedsLayout];

  // Force display to create renderer with new size and redisplay with new string
  [self setNeedsDisplay];
  
  
  // Accessiblity
  self.accessibilityLabel = _attributedText.string;
  self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string.
}

And in the frontend with something like:

_textNode = [[ASTextNode alloc] init];

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Set some text
        [self.textNode setAttributedText:[[NSAttributedString alloc] initWithString:@"Short" attributes:@{}] semaphore:nil];
    dispatch_semaphore_signal(sem);
});

// Set some textand let it wait on the semaphore
[_textNode setAttributedText:[[NSAttributedString alloc] initWithString:@"Longer longer text" attributes:@{}] semaphore:sem];

@maicki
Copy link
Contributor Author

maicki commented Sep 25, 2018

@Adlai-Holler Any reason we are not using ASCleanseAttributedStringOfCoreTextAttributes in ASTextNode2?

#endif
NSAttributedString *cleanedAttributedString = ASCleanseAttributedStringOfCoreTextAttributes(attributedText);

[self _locked_invalidateTruncationText];
Copy link
Member

@nguyenhuy nguyenhuy Sep 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so invalidating the truncation text must be done while we still hold the lock. Because after we release it, another thread may set a new truncation text that will then be cleared by this thread, or may draw this soon-to-be-invalidated text.

Let's leave a comment/explaination here.

if (length > 0) {
ASLayoutElementStyle *style = [self _locked_style];
style.ascender = [[self class] ascenderWithAttributedString:cleanedAttributedString];
style.descender = [[attributedText attribute:NSFontAttributeName atIndex:cleanedAttributedString.length - 1 effectiveRange:NULL] descender];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal with ascender and descender here. We basically use the lock as a transactional one.

// Accessiblity
self.accessibilityLabel = _attributedText.string;
self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string.
self.isAccessibilityElement = (_attributedText.length != 0); // We're an accessibility element by default if there is a string.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_attributedText is accessed without the lock here. Can we do these operations while having the locks as well?

At least, we need to access the text via its setter (self.attributedText) so that if it's changed by another thread right before this, we'll end up grabbing the updated text (and its length) here. Also, it's no longer safe to use the attributedText param of this method.

@@ -1233,12 +1234,17 @@ - (void)_invalidateTruncationText
{
{
ASLockScopeSelf();
_composedTruncationText = nil;
[self _locked_invalidateTruncationText];
}

[self setNeedsDisplay];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like now is the time to let callers of this (unlocked) method to call -setNeedsDisplay themselves. It's a bit confusing to only automatically do it with the unlock version.

@@ -1061,10 +1062,15 @@ - (NSUInteger)lineCount
- (void)_invalidateTruncationText
{
ASLockScopeSelf();
_textContainer.truncationToken = nil;
[self _locked_invalidateTruncationText];
[self setNeedsDisplay];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually ok to call -setNeedsDisplay here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked that myself too, but I didn't want to change it within this diff. In theory the code in ASTextNode2 should be adjusted to the same as in ASTextNode after this lands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@maicki maicki force-pushed the MSFixTextNodeMultithreadingIssue branch from b394f45 to 75d5d0e Compare October 4, 2018 14:32
@maicki maicki force-pushed the MSFixTextNodeMultithreadingIssue branch from d950b99 to 515e8b9 Compare October 4, 2018 15:43
@maicki
Copy link
Contributor Author

maicki commented Oct 8, 2018

@nguyenhuy Would appreciate if you could give that another look today. Thanks!

@@ -1061,10 +1062,15 @@ - (NSUInteger)lineCount
- (void)_invalidateTruncationText
{
ASLockScopeSelf();
_textContainer.truncationToken = nil;
[self _locked_invalidateTruncationText];
[self setNeedsDisplay];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@maicki maicki merged commit a3763c9 into master Oct 9, 2018
@maicki maicki deleted the MSFixTextNodeMultithreadingIssue branch October 17, 2018 16:45
mikezucc pushed a commit to mikezucc/Texture that referenced this pull request Nov 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants