Skip to content
This repository was archived by the owner on Dec 5, 2019. It is now read-only.

Commit 2c24259

Browse files
committed
Merge pull request ReactiveCocoa#1456 from talko/serial
Fix lack of thread safety in -[RACSerialDisposable disposable]
2 parents 7d161bb + 399ed83 commit 2c24259

File tree

2 files changed

+64
-68
lines changed

2 files changed

+64
-68
lines changed

ReactiveCocoaFramework/ReactiveCocoa/RACSerialDisposable.m

Lines changed: 42 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@
1010
#import <libkern/OSAtomic.h>
1111

1212
@interface RACSerialDisposable () {
13-
// A reference to the receiver's `disposable`. This variable must only be
14-
// modified atomically.
15-
//
16-
// If this is `self`, no `disposable` has been set, but the receiver has not
17-
// been disposed of yet. `self` is never stored retained.
18-
//
19-
// If this is `nil`, the receiver has been disposed.
13+
// The receiver's `disposable`. This variable must only be referenced while
14+
// _spinLock is held.
15+
RACDisposable * _disposable;
16+
17+
// YES if the receiver has been disposed. This variable must only be modified
18+
// while _spinLock is held.
19+
BOOL _disposed;
20+
21+
// A spinlock to protect access to _disposable and _disposed.
2022
//
21-
// Otherwise, this is a retained reference to the inner disposable and the
22-
// receiver has not been disposed of yet.
23-
void * volatile _disposablePtr;
23+
// It must be used when _disposable is mutated or retained and when _disposed
24+
// is mutated.
25+
OSSpinLock _spinLock;
2426
}
2527

2628
@end
@@ -30,12 +32,17 @@ @implementation RACSerialDisposable
3032
#pragma mark Properties
3133

3234
- (BOOL)isDisposed {
33-
return _disposablePtr == nil;
35+
return _disposed;
3436
}
3537

3638
- (RACDisposable *)disposable {
37-
RACDisposable *disposable = (__bridge id)_disposablePtr;
38-
return (disposable == self ? nil : disposable);
39+
RACDisposable *result;
40+
41+
OSSpinLockLock(&_spinLock);
42+
result = _disposable;
43+
OSSpinLockUnlock(&_spinLock);
44+
45+
return result;
3946
}
4047

4148
- (void)setDisposable:(RACDisposable *)disposable {
@@ -50,16 +57,6 @@ + (instancetype)serialDisposableWithDisposable:(RACDisposable *)disposable {
5057
return serialDisposable;
5158
}
5259

53-
- (id)init {
54-
self = [super init];
55-
if (self == nil) return nil;
56-
57-
_disposablePtr = (__bridge void *)self;
58-
OSMemoryBarrier();
59-
60-
return self;
61-
}
62-
6360
- (id)initWithBlock:(void (^)(void))block {
6461
self = [self init];
6562
if (self == nil) return nil;
@@ -69,65 +66,42 @@ - (id)initWithBlock:(void (^)(void))block {
6966
return self;
7067
}
7168

72-
- (void)dealloc {
73-
self.disposable = nil;
74-
}
75-
7669
#pragma mark Inner Disposable
7770

7871
- (RACDisposable *)swapInDisposable:(RACDisposable *)newDisposable {
79-
void * const selfPtr = (__bridge void *)self;
80-
81-
// Only retain the new disposable if it's not `self`.
82-
// Take ownership before attempting the swap so that a subsequent swap
83-
// receives an owned reference.
84-
void *newDisposablePtr = selfPtr;
85-
if (newDisposable != nil) {
86-
newDisposablePtr = (void *)CFBridgingRetain(newDisposable);
72+
RACDisposable *existingDisposable;
73+
BOOL alreadyDisposed;
74+
75+
OSSpinLockLock(&_spinLock);
76+
alreadyDisposed = _disposed;
77+
if (!alreadyDisposed) {
78+
existingDisposable = _disposable;
79+
_disposable = newDisposable;
8780
}
81+
OSSpinLockUnlock(&_spinLock);
8882

89-
void *existingDisposablePtr;
90-
// Keep trying while we're not disposed.
91-
while ((existingDisposablePtr = _disposablePtr) != NULL) {
92-
if (!OSAtomicCompareAndSwapPtrBarrier(existingDisposablePtr, newDisposablePtr, &_disposablePtr)) {
93-
continue;
94-
}
95-
96-
// Return nil if _disposablePtr was set to self. Otherwise, release
97-
// the old value and return it as an object.
98-
if (existingDisposablePtr == selfPtr) {
99-
return nil;
100-
} else {
101-
return CFBridgingRelease(existingDisposablePtr);
102-
}
83+
if (alreadyDisposed) {
84+
[newDisposable dispose];
85+
return nil;
10386
}
10487

105-
// At this point, we've found out that we were already disposed.
106-
[newDisposable dispose];
107-
108-
// Failed to swap, clean up the ownership we took prior to the swap.
109-
if (newDisposable != nil) {
110-
CFRelease(newDisposablePtr);
111-
}
112-
113-
return nil;
88+
return existingDisposable;
11489
}
11590

11691
#pragma mark Disposal
11792

11893
- (void)dispose {
119-
void *existingDisposablePtr;
120-
121-
while ((existingDisposablePtr = _disposablePtr) != NULL) {
122-
if (OSAtomicCompareAndSwapPtrBarrier(existingDisposablePtr, NULL, &_disposablePtr)) {
123-
if (existingDisposablePtr != (__bridge void *)self) {
124-
RACDisposable *existingDisposable = CFBridgingRelease(existingDisposablePtr);
125-
[existingDisposable dispose];
126-
}
94+
RACDisposable *existingDisposable;
12795

128-
break;
129-
}
96+
OSSpinLockLock(&_spinLock);
97+
if (!_disposed) {
98+
existingDisposable = _disposable;
99+
_disposed = YES;
100+
_disposable = nil;
130101
}
102+
OSSpinLockUnlock(&_spinLock);
103+
104+
[existingDisposable dispose];
131105
}
132106

133107
@end

ReactiveCocoaFramework/ReactiveCocoaTests/RACSerialDisposableSpec.m

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,26 @@
134134
expect(weakInnerDisposable).to.beNil();
135135
});
136136

137+
it(@"should not crash when racing between swapInDisposable and disposable", ^{
138+
__block BOOL stop = NO;
139+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long long)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
140+
stop = YES;
141+
});
142+
143+
RACSerialDisposable *serialDisposable = [[RACSerialDisposable alloc] init];
144+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
145+
while (!stop) {
146+
[serialDisposable swapInDisposable:[RACDisposable disposableWithBlock:^{}]];
147+
}
148+
});
149+
150+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
151+
while (!stop) {
152+
[serialDisposable disposable];
153+
}
154+
});
155+
156+
expect(stop).will.beTruthy();
157+
});
158+
137159
SpecEnd

0 commit comments

Comments
 (0)