Skip to content

Commit 6dd3fa4

Browse files
[iOSTextInput] fix potential dangling pointer access (flutter#26547)
1 parent c8e2660 commit 6dd3fa4

2 files changed

Lines changed: 127 additions & 59 deletions

File tree

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,9 @@ @implementation FlutterTextInputView {
505505
const char* _selectionAffinity;
506506
FlutterTextRange* _selectedTextRange;
507507
CGRect _cachedFirstRect;
508+
// The view has reached end of life, and is no longer
509+
// allowed to access its textInputDelegate.
510+
BOOL _decommissioned;
508511
}
509512

510513
@synthesize tokenizer = _tokenizer;
@@ -535,6 +538,7 @@ - (instancetype)init {
535538
_returnKeyType = UIReturnKeyDone;
536539
_secureTextEntry = NO;
537540
_accessibilityEnabled = NO;
541+
_decommissioned = NO;
538542
if (@available(iOS 11.0, *)) {
539543
_smartQuotesType = UITextSmartQuotesTypeYes;
540544
_smartDashesType = UITextSmartDashesTypeYes;
@@ -545,6 +549,7 @@ - (instancetype)init {
545549
}
546550

547551
- (void)configureWithDictionary:(NSDictionary*)configuration {
552+
NSAssert(!_decommissioned, @"Attempt to reuse a decommissioned view, for %@", configuration);
548553
NSDictionary* inputType = configuration[kKeyboardType];
549554
NSString* keyboardAppearance = configuration[kKeyboardAppearance];
550555
NSDictionary* autofill = configuration[kAutofillProperties];
@@ -596,6 +601,23 @@ - (UITextContentType)textContentType {
596601
return _textContentType;
597602
}
598603

604+
- (id<FlutterTextInputDelegate>)textInputDelegate {
605+
return _decommissioned ? nil : _textInputDelegate;
606+
}
607+
608+
// Declares that the view has reached end of life, and
609+
// is no longer allowed to access its textInputDelegate.
610+
//
611+
// UIKit may retain this view (even after it's been removed
612+
// from the view hierarchy) so that it may outlive the plugin/engine,
613+
// in which case _textInputDelegate will become a dangling pointer.
614+
615+
// The text input plugin needs to call decommision when it should
616+
// not have access to its FlutterTextInputDelegate any more.
617+
- (void)decommision {
618+
_decommissioned = YES;
619+
}
620+
599621
- (void)dealloc {
600622
[_text release];
601623
[_markedText release];
@@ -778,7 +800,8 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
778800

779801
- (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
780802
if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
781-
[_textInputDelegate performAction:FlutterTextInputActionNewline withClient:_textInputClient];
803+
[self.textInputDelegate performAction:FlutterTextInputActionNewline
804+
withClient:_textInputClient];
782805
return YES;
783806
}
784807

@@ -819,7 +842,7 @@ - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)t
819842
break;
820843
}
821844

822-
[_textInputDelegate performAction:action withClient:_textInputClient];
845+
[self.textInputDelegate performAction:action withClient:_textInputClient];
823846
return NO;
824847
}
825848

@@ -1062,9 +1085,9 @@ - (CGRect)firstRectForRange:(UITextRange*)range {
10621085
return _cachedFirstRect;
10631086
}
10641087

1065-
[_textInputDelegate showAutocorrectionPromptRectForStart:start
1066-
end:end
1067-
withClient:_textInputClient];
1088+
[self.textInputDelegate showAutocorrectionPromptRectForStart:start
1089+
end:end
1090+
withClient:_textInputClient];
10681091
// TODO(cbracken) Implement.
10691092
return CGRectZero;
10701093
}
@@ -1097,21 +1120,21 @@ - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
10971120
}
10981121

10991122
- (void)beginFloatingCursorAtPoint:(CGPoint)point {
1100-
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart
1101-
withClient:_textInputClient
1102-
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1123+
[self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart
1124+
withClient:_textInputClient
1125+
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
11031126
}
11041127

11051128
- (void)updateFloatingCursorAtPoint:(CGPoint)point {
1106-
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1107-
withClient:_textInputClient
1108-
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1129+
[self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1130+
withClient:_textInputClient
1131+
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
11091132
}
11101133

11111134
- (void)endFloatingCursor {
1112-
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1113-
withClient:_textInputClient
1114-
withPosition:@{@"X" : @(0), @"Y" : @(0)}];
1135+
[self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1136+
withClient:_textInputClient
1137+
withPosition:@{@"X" : @(0), @"Y" : @(0)}];
11151138
}
11161139

11171140
#pragma mark - UIKeyInput Overrides
@@ -1139,9 +1162,11 @@ - (void)updateEditingState {
11391162
};
11401163

11411164
if (_textInputClient == 0 && _autofillId != nil) {
1142-
[_textInputDelegate updateEditingClient:_textInputClient withState:state withTag:_autofillId];
1165+
[self.textInputDelegate updateEditingClient:_textInputClient
1166+
withState:state
1167+
withTag:_autofillId];
11431168
} else {
1144-
[_textInputDelegate updateEditingClient:_textInputClient withState:state];
1169+
[self.textInputDelegate updateEditingClient:_textInputClient withState:state];
11451170
}
11461171
}
11471172

@@ -1259,8 +1284,6 @@ - (void)enableActiveViewAccessibility {
12591284
@end
12601285

12611286
@interface FlutterTextInputPlugin ()
1262-
@property(nonatomic, strong) FlutterTextInputView* reusableInputView;
1263-
12641287
// The current password-autofillable input fields that have yet to be saved.
12651288
@property(nonatomic, readonly)
12661289
NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
@@ -1278,11 +1301,11 @@ - (instancetype)init {
12781301
self = [super init];
12791302

12801303
if (self) {
1281-
_reusableInputView = [[FlutterTextInputView alloc] init];
1282-
_reusableInputView.secureTextEntry = NO;
12831304
_autofillContext = [[NSMutableDictionary alloc] init];
1284-
_activeView = [_reusableInputView retain];
12851305
_inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
1306+
// Initialize activeView with a dummy view to keep tests
1307+
// passing.
1308+
_activeView = [[FlutterTextInputView alloc] init];
12861309
}
12871310

12881311
return self;
@@ -1291,7 +1314,6 @@ - (instancetype)init {
12911314
- (void)dealloc {
12921315
[self hideTextInput];
12931316
_activeView.textInputDelegate = nil;
1294-
[_reusableInputView release];
12951317
[_activeView release];
12961318
[_inputHider release];
12971319
[_autofillContext release];
@@ -1398,14 +1420,14 @@ - (void)triggerAutofillSave:(BOOL)saveEntries {
13981420
if (saveEntries) {
13991421
// Make all the input fields in the autofill context visible,
14001422
// then remove them to trigger autofill save.
1401-
[self cleanUpViewHierarchy:YES clearText:YES];
1423+
[self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
14021424
[_autofillContext removeAllObjects];
14031425
[self changeInputViewsAutofillVisibility:YES];
14041426
} else {
14051427
[_autofillContext removeAllObjects];
14061428
}
14071429

1408-
[self cleanUpViewHierarchy:YES clearText:!saveEntries];
1430+
[self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
14091431
[self addToInputParentViewIfNeeded:_activeView];
14101432
}
14111433

@@ -1414,9 +1436,11 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14141436
// Hide all input views from autofill, only make those in the new configuration visible
14151437
// to autofill.
14161438
[self changeInputViewsAutofillVisibility:NO];
1439+
1440+
// Update the current active view.
14171441
switch (autofillTypeOf(configuration)) {
14181442
case FlutterAutofillTypeNone:
1419-
self.activeView = [self updateAndShowReusableInputView:configuration];
1443+
self.activeView = [self createInputViewWith:configuration];
14201444
break;
14211445
case FlutterAutofillTypeRegular:
14221446
// If the group does not involve password autofill, only install the
@@ -1431,7 +1455,6 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14311455
isPasswordRelated:YES];
14321456
break;
14331457
}
1434-
14351458
[_activeView setTextInputClient:client];
14361459
[_activeView reloadInputViews];
14371460

@@ -1441,35 +1464,37 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur
14411464
// them to free up resources and reduce the number of input views in the view
14421465
// hierarchy.
14431466
//
1444-
// This is scheduled on the runloop and delayed by 0.1s so we don't remove the
1467+
// The garbage views are decommissioned immediately, but the removeFromSuperview
1468+
// call is scheduled on the runloop and delayed by 0.1s so we don't remove the
14451469
// text fields immediately (which seems to make the keyboard flicker).
14461470
// See: https://github.com/flutter/flutter/issues/64628.
1447-
[self performSelector:@selector(collectGarbageInputViews) withObject:nil afterDelay:0.1];
1471+
[self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
14481472
}
14491473

1450-
// Updates and shows an input field that is not password related and has no autofill
1451-
// hints. This method re-configures and reuses an existing instance of input field
1452-
// instead of creating a new one.
1453-
// Also updates the current autofill context.
1454-
- (FlutterTextInputView*)updateAndShowReusableInputView:(NSDictionary*)configuration {
1474+
// Creates and shows an input field that is not password related and has no autofill
1475+
// hints. This method returns a new FlutterTextInputView instance when called, since
1476+
// UIKit uses the identity of `UITextInput` instances (or the identity of the input
1477+
// views) to decide whether the IME's internal states should be reset. See:
1478+
// https://github.com/flutter/flutter/issues/79031 .
1479+
- (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
14551480
// It's possible that the configuration of this non-autofillable input view has
14561481
// an autofill configuration without hints. If it does, remove it from the context.
14571482
NSString* autofillId = autofillIdFromDictionary(configuration);
14581483
if (autofillId) {
14591484
[_autofillContext removeObjectForKey:autofillId];
14601485
}
1461-
1462-
[_reusableInputView configureWithDictionary:configuration];
1463-
[self addToInputParentViewIfNeeded:_reusableInputView];
1464-
_reusableInputView.textInputDelegate = _textInputDelegate;
1486+
FlutterTextInputView* newView = [[FlutterTextInputView alloc] init];
1487+
[newView configureWithDictionary:configuration];
1488+
[self addToInputParentViewIfNeeded:newView];
1489+
newView.textInputDelegate = _textInputDelegate;
14651490

14661491
for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
14671492
NSString* autofillId = autofillIdFromDictionary(field);
14681493
if (autofillId && autofillTypeOf(field) == FlutterAutofillTypeNone) {
14691494
[_autofillContext removeObjectForKey:autofillId];
14701495
}
14711496
}
1472-
return _reusableInputView;
1497+
return [newView autorelease];
14731498
}
14741499

14751500
- (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
@@ -1547,11 +1572,21 @@ - (UIView*)keyWindow {
15471572
return _inputHider.subviews;
15481573
}
15491574

1550-
// Removes every installed input field, unless it's in the current autofill
1551-
// context. May remove the active view too if includeActiveView is YES.
1575+
// Decommisions (See the "decommision" method on FlutterTextInputView) and removes
1576+
// every installed input field, unless it's in the current autofill context.
1577+
//
1578+
// The active view will be decommisioned and removed from its superview too, if
1579+
// includeActiveView is YES.
15521580
// When clearText is YES, the text on the input fields will be set to empty before
15531581
// they are removed from the view hierarchy, to avoid triggering autofill save.
1554-
- (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText {
1582+
// If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
1583+
// will be delayed by 0.1s so we don't remove the text fields immediately (which seems
1584+
// to make the keyboard flicker).
1585+
// See: https://github.com/flutter/flutter/issues/64628.
1586+
1587+
- (void)cleanUpViewHierarchy:(BOOL)includeActiveView
1588+
clearText:(BOOL)clearText
1589+
delayRemoval:(BOOL)delayRemoval {
15551590
for (UIView* view in self.textInputViews) {
15561591
if ([view isKindOfClass:[FlutterTextInputView class]] &&
15571592
(includeActiveView || view != _activeView)) {
@@ -1560,16 +1595,17 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText {
15601595
if (clearText) {
15611596
[inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
15621597
}
1563-
[view removeFromSuperview];
1598+
[inputView decommision];
1599+
if (delayRemoval) {
1600+
[inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
1601+
} else {
1602+
[inputView removeFromSuperview];
1603+
}
15641604
}
15651605
}
15661606
}
15671607
}
15681608

1569-
- (void)collectGarbageInputViews {
1570-
[self cleanUpViewHierarchy:NO clearText:YES];
1571-
}
1572-
15731609
// Changes the visibility of every FlutterTextInputView currently in the
15741610
// view hierarchy.
15751611
- (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
@@ -1582,7 +1618,12 @@ - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
15821618
}
15831619

15841620
// Resets the client id of every FlutterTextInputView in the view hierarchy
1585-
// to 0. Called when a new text input connection will be established.
1621+
// to 0.
1622+
// Called before establishing a new text input connection.
1623+
// For views in the current autofill context, they need to
1624+
// stay in the view hierachy but should not be allowed to
1625+
// send messages (other than autofill related ones) to the
1626+
// framework.
15861627
- (void)resetAllClientIds {
15871628
for (UIView* view in self.textInputViews) {
15881629
if ([view isKindOfClass:[FlutterTextInputView class]]) {

0 commit comments

Comments
 (0)