Changeset 273141 in webkit


Ignore:
Timestamp:
Feb 19, 2021 9:29:11 AM (3 years ago)
Author:
Chris Dumez
Message:

Norton Safe Web extension is causing crashes / hangs under [WKRemoteObjectEncoder encodeObject:forKey:]
https://bugs.webkit.org/show_bug.cgi?id=222172

Reviewed by Alex Christensen.

The extension appears to be trying to send a JSValue that is a DOM Node. WebKit makes the following
call to convert it into a NSDictionary:
[[JSValue valueWithJSValueRef:value inContext:[JSContext contextWithJSGlobalContextRef:JSContextGetGlobalContext(context)]] toObject]

JSC very aggressively iterates over all of the properties of the DOM Node and recursively ends up
converting the whole DOM tree with all their properties. This leads to a lot of cycles to as JSC
maintains the JSObject <-> NSObject identity during the conversion (Each time the JSDocument is
serialized, the same NSDictionary* pointer is used to represent it).

The logic introduced in r270559 to detect cycles was flawed because it relied on a NSSet of
NSObject* and [NSSet containsObject:] to detect the cycles. The issue is that [NSSet containsObject:]
doesn't do a simple pointer comparison but instead calls [NSObject isEqual:] which is very
expensive for types like NSDictionary and leads to trouble when the dictionary contains a cycle.
To address this I replaced the NSSet with a WTF::HashSet<NSObject *> so that key lookup ends up
doing a simple pointer comparison.

Even after the previous fix, the extension would still cause massive hangs because it would take
a very long time to try and encode the whole DOM tree with all the properties of each Node (even
without cycles). To address this, we now abort encoding when detecting a cycle instead of encoding
an empty object to break the cycle.

After this change, Safari becomes usable with this extension again. However, there are still much
shorter hangs that occur due to the converting of the JSNode into a JSDictionary via
[JSValue toObject]. We should probably improve this in a follow-up.

Easy way to reproduce the crash / hang:

  1. Install Norton Safe Web & Norton Password Manager extension (may require a subscription)
  2. Make sure the extensions are activated and turned on by clicking on their icons next to the URL bar
  3. Go to https://bugs.webkit.org/attachment.cgi?id=420530&action=edit
  4. Click on the combo box next to "Review" -> Hang / Crash

No new tests, covered by WebKit.RemoteObjectRegistry API test.

  • Shared/API/Cocoa/WKRemoteObjectCoder.mm:

(-[WKRemoteObjectEncoder init]):
(encodeInvocationArguments):
(encodeObject):

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebKit/ChangeLog

    r273139 r273141  
     12021-02-19  Chris Dumez  <cdumez@apple.com>
     2
     3        Norton Safe Web extension is causing crashes / hangs under [WKRemoteObjectEncoder encodeObject:forKey:]
     4        https://bugs.webkit.org/show_bug.cgi?id=222172
     5
     6        Reviewed by Alex Christensen.
     7
     8        The extension appears to be trying to send a JSValue that is a DOM Node. WebKit makes the following
     9        call to convert it into a NSDictionary:
     10        `[[JSValue valueWithJSValueRef:value inContext:[JSContext contextWithJSGlobalContextRef:JSContextGetGlobalContext(context)]] toObject]`
     11
     12        JSC very aggressively iterates over all of the properties of the DOM Node and recursively ends up
     13        converting the whole DOM tree with all their properties. This leads to a lot of cycles to as JSC
     14        maintains the JSObject <-> NSObject identity during the conversion (Each time the JSDocument is
     15        serialized, the same NSDictionary* pointer is used to represent it).
     16
     17        The logic introduced in r270559 to detect cycles was flawed because it relied on a NSSet of
     18        NSObject* and [NSSet containsObject:] to detect the cycles. The issue is that [NSSet containsObject:]
     19        doesn't do a simple pointer comparison but instead calls [NSObject isEqual:] which is very
     20        expensive for types like NSDictionary and leads to trouble when the dictionary contains a cycle.
     21        To address this I replaced the NSSet with a WTF::HashSet<NSObject *> so that key lookup ends up
     22        doing a simple pointer comparison.
     23
     24        Even after the previous fix, the extension would still cause massive hangs because it would take
     25        a very long time to try and encode the whole DOM tree with all the properties of each Node (even
     26        without cycles). To address this, we now abort encoding when detecting a cycle instead of encoding
     27        an empty object to break the cycle.
     28
     29        After this change, Safari becomes usable with this extension again. However, there are still much
     30        shorter hangs that occur due to the converting of the JSNode into a JSDictionary via
     31        [JSValue toObject]. We should probably improve this in a follow-up.
     32
     33        Easy way to reproduce the crash / hang:
     34        1. Install Norton Safe Web & Norton Password Manager extension (may require a subscription)
     35        2. Make sure the extensions are activated and turned on by clicking on their icons next to the
     36           URL bar
     37        3. Go to https://bugs.webkit.org/attachment.cgi?id=420530&action=edit
     38        4. Click on the combo box next to "Review" -> Hang / Crash
     39
     40        No new tests, covered by WebKit.RemoteObjectRegistry API test.
     41
     42        * Shared/API/Cocoa/WKRemoteObjectCoder.mm:
     43        (-[WKRemoteObjectEncoder init]):
     44        (encodeInvocationArguments):
     45        (encodeObject):
     46
    1472021-02-19  Kimmo Kinnunen  <kkinnunen@apple.com>
    248
  • trunk/Source/WebKit/Shared/API/Cocoa/WKRemoteObjectCoder.mm

    r272936 r273141  
    6464
    6565    API::Dictionary* _currentDictionary;
    66     RetainPtr<NSMutableSet> _objectsBeingEncoded; // Used to detect cycles.
     66    HashSet<NSObject *> _objectsBeingEncoded; // Used to detect cycles.
    6767}
    6868
     
    7474    _rootDictionary = API::Dictionary::create();
    7575    _currentDictionary = _rootDictionary.get();
    76     _objectsBeingEncoded = adoptNS([[NSMutableSet alloc] init]);
    7776
    7877    return self;
     
    249248            [invocation getArgument:&value atIndex:i];
    250249
    251             encodeToObjectStream(encoder, value);
     250            @try {
     251                encodeToObjectStream(encoder, value);
     252            } @catch (NSException *e) {
     253                RELEASE_LOG_ERROR(IPC, "WKRemoteObjectCode::encodeInvocationArguments: Exception caught when trying to encode an argument of type ObjC Object");
     254            }
     255
    252256            break;
    253257        }
     
    310314        [NSException raise:NSInvalidArgumentException format:@"-classForCoder returned nil for %@", object];
    311315
    312     if ([encoder->_objectsBeingEncoded containsObject:object]) {
     316    if (encoder->_objectsBeingEncoded.contains(object)) {
    313317        RELEASE_LOG_FAULT(IPC, "WKRemoteObjectCode::encodeObject: Object of type '%{private}s' contains a cycle", class_getName(object_getClass(object)));
    314         @try {
    315             // Try to encode a newly initialized object instead.
    316             object = adoptNS([[[object class] alloc] init]).autorelease();
    317         } @catch (NSException *e) {
    318             [NSException raise:NSInvalidArgumentException format:@"Object of type '%s' contains a cycle", class_getName(object_getClass(object))];
    319         }
    320     }
    321 
    322     [encoder->_objectsBeingEncoded addObject:object];
     318        [NSException raise:NSInvalidArgumentException format:@"Object of type '%s' contains a cycle", class_getName(object_getClass(object))];
     319        return;
     320    }
     321
     322    encoder->_objectsBeingEncoded.add(object);
    323323    auto exitScope = makeScopeExit([encoder, object] {
    324         [encoder->_objectsBeingEncoded removeObject:object];
     324        encoder->_objectsBeingEncoded.remove(object);
    325325    });
    326326
  • trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/RemoteObjectRegistry.mm

    r273062 r273141  
    151151        @try {
    152152            [object takeDictionary:dictionaryWithCycle completionHandler:^(NSDictionary* value) {
    153                 NSString *name = [value objectForKey:@"name"];
    154                 EXPECT_WK_STREQ(@"root", name);
    155                 NSDictionary *child = [value objectForKey:@"child"];
    156                 EXPECT_TRUE(!!child);
    157                 NSString* childName = [child objectForKey:@"name"];
    158                 EXPECT_WK_STREQ(@"foo", childName);
    159                 NSNumber *childValue = [child objectForKey:@"value"];
    160                 EXPECT_EQ(1, [childValue integerValue]);
    161                 // We should have encoded parent as an empty NSDictionary.
    162                 NSDictionary *childParent = [child objectForKey:@"parent"];
    163                 EXPECT_TRUE(!!childParent);
    164                 EXPECT_EQ(0U, [childParent count]);
     153                EXPECT_TRUE(!value.count);
    165154                isDone = true;
    166155            }];
Note: See TracChangeset for help on using the changeset viewer.