Skip to content

Commit 7a59909

Browse files
HendrikHuebnerHendrik Hübner
andauthored
Fix KVO bugs related to cyclical observer dependencies (#608) (#612)
* Fix KVO bugs related to cyclical observer dependencies (#608) - Replaced fragile dependent-keypath visited tracking with node keys `(object pointer, key name)` in dependency expansion. - Added explicit ancestor/on-stack tracking to distinguish true cycles from safe revisits - On revisit: - if node is on ancestor stack: treat as cycle and stop expansion - if visited but not on stack: allow expansion to continue - This prevents infinite recursive rebuilds on cyclic dependencies while preserving correct branch wiring after nil-to-non-nil rematerialization and shared-path alias cases. * Refactor and fix gcc errors * Disabled tests on non-clang platforms --------- Co-authored-by: Hendrik Hübner <hhuebner@MacBookPro.localdomain>
1 parent 793e3fa commit 7a59909

6 files changed

Lines changed: 697 additions & 22 deletions

File tree

Source/NSKVOInternal.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@
106106
NSMutableDictionary<NSString *, NSMutableArray<_NSKVOKeyObserver *> *>
107107
*_keyObserverMap;
108108
NSInteger _dependencyDepth;
109-
NSMutableSet<NSString *> *_existingDependentKeys;
109+
NSMutableSet<id> *_existingDependentKeys;
110+
NSMutableSet<id> *_dependencyAncestorKeys;
110111
gs_mutex_t _lock;
111112
}
112113
- (instancetype)init;

Source/NSKVOSupport.m

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -207,38 +207,83 @@ - (void) dealloc
207207
[super dealloc];
208208
}
209209

210-
- (void) pushDependencyStack
210+
// While exploring the observer graph, for dependencies, we use this to
211+
// detect cycles to prevent infinite recursion.
212+
- (void) beginDependencyExpansionScope
211213
{
212214
GS_MUTEX_LOCK(_lock);
213215
if (_dependencyDepth == 0)
214216
{
215217
_existingDependentKeys = [NSMutableSet new];
218+
_dependencyAncestorKeys = [NSMutableSet new];
216219
}
217220
++_dependencyDepth;
218221
GS_MUTEX_UNLOCK(_lock);
219222
}
220223

221-
- (BOOL) lockDependentKeypath: (NSString *)keypath
224+
/* Nodes (observable values) in the observer graph are uniquely identified
225+
* using a combination of the object pointer and the name of the value */
226+
- (id) dependencyKeyForObject: (id)object key: (NSString *)key
222227
{
228+
return [NSArray arrayWithObjects:
229+
(object ?: [NSNull null]),
230+
(key ?: @""),
231+
nil];
232+
}
233+
234+
- (void) pushObserverToCurrentAncestorStack: (_NSKVOKeyObserver *)keyObserver
235+
{
236+
id ancestorKey = [self dependencyKeyForObject: keyObserver.object
237+
key: keyObserver.key];
238+
GS_MUTEX_LOCK(_lock);
239+
[_dependencyAncestorKeys addObject: ancestorKey];
240+
GS_MUTEX_UNLOCK(_lock);
241+
}
242+
243+
- (void) popObserverFromCurrentAncestorStack: (_NSKVOKeyObserver *)keyObserver
244+
{
245+
id ancestorKey = [self dependencyKeyForObject: keyObserver.object
246+
key: keyObserver.key];
223247
GS_MUTEX_LOCK(_lock);
224-
if ([_existingDependentKeys containsObject:keypath])
248+
[_dependencyAncestorKeys removeObject: ancestorKey];
249+
GS_MUTEX_UNLOCK(_lock);
250+
}
251+
252+
/// Mark keypath as visited in the current dependency-expansion scope.
253+
- (BOOL) checkDependencyForCycle: (NSString *)keypath
254+
forNode: (_NSKVOKeyObserver *)keyObserver
255+
{
256+
NSString *dependentKey;
257+
NSString *unusedRemainder;
258+
id visitToken;
259+
260+
dependentKey = _NSKVCSplitKeypath(keypath, &unusedRemainder);
261+
visitToken = [self dependencyKeyForObject: keyObserver.object
262+
key: dependentKey];
263+
GS_MUTEX_LOCK(_lock);
264+
if ([_existingDependentKeys containsObject:visitToken])
225265
{
266+
BOOL isCycle = [_dependencyAncestorKeys containsObject: visitToken];
226267
GS_MUTEX_UNLOCK(_lock);
227-
return NO;
268+
// If it's on the current ancestor stack, treat as cycle and dedup.
269+
// If it's already visited but not on stack, allow expansion to continue.
270+
return !isCycle;
228271
}
229-
[_existingDependentKeys addObject:keypath];
272+
[_existingDependentKeys addObject:visitToken];
230273
GS_MUTEX_UNLOCK(_lock);
231274
return YES;
232275
}
233276

234-
- (void) popDependencyStack
277+
- (void) endDependencyExpansionScope
235278
{
236279
GS_MUTEX_LOCK(_lock);
237280
--_dependencyDepth;
238281
if (_dependencyDepth == 0)
239282
{
240283
[_existingDependentKeys release];
241284
_existingDependentKeys = nil;
285+
[_dependencyAncestorKeys release];
286+
_dependencyAncestorKeys = nil;
242287
}
243288
GS_MUTEX_UNLOCK(_lock);
244289
}
@@ -344,28 +389,40 @@ - (bool) isEmpty
344389
NSArray *affectedKeyObservers;
345390
NSMutableArray *dependentObservers;
346391

392+
affectedKeyObservers = keyObserver.affectedObservers;
393+
347394
/* affectedKeyObservers is the list of observers that must be notified
348395
* of changes. If we have descendants, we have to add ourselves to the
349396
* growing list of affected keys. If not, we must pass it along
350397
* unmodified. (This is a minor optimization: we don't need to signal
351-
* for our own reconstruction
352-
* if we have no subpath observers.)
353-
*/
354-
affectedKeyObservers = (keyObserver.restOfKeypath
355-
? ([keyObserver.affectedObservers arrayByAddingObject:keyObserver]
356-
?: [NSArray arrayWithObject:keyObserver])
357-
: keyObserver.affectedObservers);
358-
359-
[observationInfo pushDependencyStack];
360-
/* Don't allow our own key to be recreated.
361-
*/
362-
[observationInfo lockDependentKeypath:keyObserver.key];
398+
* for our own reconstruction if we have no subpath observers.)
399+
*/
400+
if (keyObserver.restOfKeypath)
401+
{
402+
if (affectedKeyObservers)
403+
{
404+
affectedKeyObservers =
405+
[affectedKeyObservers arrayByAddingObject:keyObserver];
406+
}
407+
else
408+
{
409+
affectedKeyObservers = [NSArray arrayWithObject:keyObserver];
410+
}
411+
}
363412

413+
[observationInfo beginDependencyExpansionScope];
414+
[observationInfo pushObserverToCurrentAncestorStack: keyObserver];
415+
/* Don't allow our own key to be recreated. */
416+
[observationInfo checkDependencyForCycle:keyObserver.key
417+
forNode:keyObserver];
418+
419+
/* The observers, which affect us */
364420
dependentObservers =
365421
[NSMutableArray arrayWithCapacity:[valueInfluencingKeys count]];
366422
for (NSString *dependentKeypath in valueInfluencingKeys)
367423
{
368-
if ([observationInfo lockDependentKeypath:dependentKeypath])
424+
if ([observationInfo checkDependencyForCycle:dependentKeypath
425+
forNode:keyObserver])
369426
{
370427
_NSKVOKeyObserver *dependentObserver
371428
= _addKeypathObserver(object, dependentKeypath,
@@ -379,7 +436,8 @@ - (bool) isEmpty
379436
}
380437
keyObserver.dependentObservers = dependentObservers;
381438

382-
[observationInfo popDependencyStack];
439+
[observationInfo popObserverFromCurrentAncestorStack: keyObserver];
440+
[observationInfo endDependencyExpansionScope];
383441
}
384442
}
385443
else
@@ -458,6 +516,7 @@ - (bool) isEmpty
458516
{
459517
_addNestedObserversAndOptionallyDependents(keyObserver, true);
460518
_addKeyObserver(keyObserver);
519+
} else {
461520
}
462521

463522
return keyObserver;
@@ -847,7 +906,8 @@ - (void) removeObserver: (id)observer forKeyPath:(NSString *)keyPath
847906
{
848907
_NSKVOObservationInfo *observationInfo
849908
= (__bridge _NSKVOObservationInfo *) [notifyingObject observationInfo];
850-
for (_NSKVOKeyObserver *keyObserver in [observationInfo observersForKey:key])
909+
NSArray<_NSKVOKeyObserver *> *observers = [observationInfo observersForKey:key];
910+
for (_NSKVOKeyObserver *keyObserver in observers)
851911
{
852912
_NSKVOKeypathObserver *keypathObserver;
853913

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#import <Foundation/Foundation.h>
2+
#import "ObjectTesting.h"
3+
4+
@class BNode;
5+
6+
@interface CNode : NSObject
7+
{
8+
NSInteger _f;
9+
NSObject *_d;
10+
BNode *_b;
11+
}
12+
@property (nonatomic, retain) NSObject *d;
13+
@property (nonatomic, retain) BNode *b;
14+
@property (nonatomic, assign) NSInteger f;
15+
@end
16+
@implementation CNode
17+
@synthesize d = _d;
18+
@synthesize b = _b;
19+
- (NSInteger) f
20+
{
21+
return _f;
22+
}
23+
- (void) setF: (NSInteger)f
24+
{
25+
BOOL notifyD = (self.d != nil);
26+
[self willChangeValueForKey: @"f"];
27+
if (notifyD)
28+
{
29+
[self willChangeValueForKey: @"d"];
30+
}
31+
_f = f;
32+
if (notifyD)
33+
{
34+
[self didChangeValueForKey: @"d"];
35+
}
36+
[self didChangeValueForKey: @"f"];
37+
}
38+
@end
39+
40+
@interface BNode : NSObject
41+
{
42+
CNode *_c;
43+
NSInteger _e;
44+
}
45+
@property (nonatomic, retain) CNode *c;
46+
@property (nonatomic, assign) NSInteger e;
47+
@end
48+
@implementation BNode
49+
@synthesize c = _c;
50+
@synthesize e = _e;
51+
@end
52+
53+
@interface ANode : NSObject
54+
{
55+
BNode *_b;
56+
CNode *_c;
57+
}
58+
@property (nonatomic, retain) BNode *b;
59+
@property (nonatomic, retain) CNode *c;
60+
@end
61+
@implementation ANode
62+
@synthesize b = _b;
63+
@synthesize c = _c;
64+
@end
65+
66+
@interface RootNode : NSObject
67+
{
68+
ANode *_a;
69+
}
70+
@property (nonatomic, retain) ANode *a;
71+
@property (nonatomic, readonly) BOOL x;
72+
@end
73+
@implementation RootNode
74+
@synthesize a = _a;
75+
+ (NSSet *) keyPathsForValuesAffectingX
76+
{
77+
return [NSSet setWithObjects: @"a.b.c.d", @"a.c.b.e", nil];
78+
}
79+
- (BOOL) x
80+
{
81+
return (self.a.b.c.d != nil) || (self.a.c.b.e > 0);
82+
}
83+
@end
84+
85+
@interface TestObserver : NSObject
86+
{
87+
@public
88+
NSUInteger calls;
89+
}
90+
@end
91+
@implementation TestObserver
92+
- (void) observeValueForKeyPath: (NSString *)keyPath
93+
ofObject: (id)object
94+
change: (NSDictionary *)change
95+
context: (void *)context
96+
{
97+
(void)keyPath;
98+
(void)object;
99+
(void)change;
100+
(void)context;
101+
calls++;
102+
}
103+
@end
104+
105+
int main(void)
106+
{
107+
CREATE_AUTORELEASE_POOL(pool);
108+
109+
#if !defined(clang)
110+
testHopeful = YES;
111+
#endif
112+
113+
// testcases here
114+
BNode *b = AUTORELEASE([BNode new]);
115+
b.e = 1;
116+
117+
CNode *cForA = AUTORELEASE([CNode new]);
118+
cForA.b = b;
119+
120+
CNode *cForB = AUTORELEASE([CNode new]);
121+
cForB.d = nil;
122+
cForB.f = 10;
123+
124+
ANode *a = AUTORELEASE([ANode new]);
125+
a.b = b;
126+
a.c = cForA;
127+
b.c = cForB;
128+
129+
RootNode *root = AUTORELEASE([RootNode new]);
130+
root.a = a;
131+
132+
TestObserver *obs = AUTORELEASE([TestObserver new]);
133+
[root addObserver: obs forKeyPath: @"x" options: 0 context: NULL];
134+
135+
obs->calls = 0;
136+
b.e = 2;
137+
PASS(obs->calls == 1, "updating e should trigger notification");
138+
139+
CNode *newCForB = AUTORELEASE([CNode new]);
140+
newCForB.d = AUTORELEASE([NSObject new]);
141+
newCForB.f = 100;
142+
obs->calls = 0;
143+
b.c = newCForB;
144+
PASS(obs->calls == 1,
145+
"updating c with a new object where d is non-nil should trigger notification");
146+
147+
obs->calls = 0;
148+
b.e = 3;
149+
PASS(obs->calls == 1, "updating e should still notify");
150+
151+
obs->calls = 0;
152+
newCForB.f = 101;
153+
PASS(obs->calls == 1, "updating f should notify");
154+
155+
obs->calls = 0;
156+
newCForB.d = nil;
157+
PASS(obs->calls == 1, "setting d to nil again should notify");
158+
159+
obs->calls = 0;
160+
newCForB.f = 102;
161+
PASS(obs->calls == 0, "updating f should not notify after d became nil");
162+
163+
obs->calls = 0;
164+
b.c = nil;
165+
PASS(obs->calls == 1, "setting c to nil should notify");
166+
167+
obs->calls = 0;
168+
b.e = 4;
169+
PASS(obs->calls == 1, "updating e should still notify");
170+
171+
[root removeObserver: obs forKeyPath: @"x"];
172+
173+
#if !defined(clang)
174+
testHopeful = NO;
175+
#endif
176+
177+
DESTROY(pool);
178+
return 0;
179+
}

0 commit comments

Comments
 (0)