Bug 209504

Summary: Event listeners registered with 'once' option may get garbage collected too soon
Product: WebKit Reporter: Chris Dumez <cdumez>
Component: BindingsAssignee: Chris Dumez <cdumez>
Status: RESOLVED FIXED    
Severity: Normal CC: ap, cdumez, esprehn+autocc, ews-watchlist, ggaren, hi, joepeck, kangil.han, keith_miller, mark.lam, msaboff, rniwa, saam, tsavell, tzagallo, webkit-bug-importer, ysuzuki
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
See Also: https://bugs.webkit.org/show_bug.cgi?id=209445
Bug Depends on:    
Bug Blocks: 209552    
Attachments:
Description Flags
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch
none
Patch none

Description Chris Dumez 2020-03-24 14:55:56 PDT
Event listeners registered with 'once' option may get garbage collected too soon, before they get a chance to run.
Comment 1 Chris Dumez 2020-03-24 14:56:11 PDT
<rdar://problem/60541567>
Comment 2 Chris Dumez 2020-03-24 15:46:33 PDT
Created attachment 394427 [details]
Patch
Comment 3 Saam Barati 2020-03-24 15:48:42 PDT
Comment on attachment 394427 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394427&action=review

> Source/WebCore/dom/EventTarget.cpp:334
> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;

why not optional?
Comment 4 Saam Barati 2020-03-24 15:50:01 PDT
Comment on attachment 394427 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394427&action=review

> Source/WebCore/ChangeLog:20
> +        No new tests, covered by http/tests/inspector/network/har/har-page.html which crashes
> +        with aggressive GC.

do we have a version of this test that runs with aggressive GC already checked in?
Comment 5 Chris Dumez 2020-03-24 15:52:36 PDT
Comment on attachment 394427 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394427&action=review

>> Source/WebCore/ChangeLog:20
>> +        with aggressive GC.
> 
> do we have a version of this test that runs with aggressive GC already checked in?

I mean we have this test which gets run on the stress GC bot and currently crashes, thus the radar. Is there a way to make a single test use aggressive GC on the regular bots?

>> Source/WebCore/dom/EventTarget.cpp:334
>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
> 
> why not optional?

Why is Optional better?
Comment 6 Chris Dumez 2020-03-24 15:53:59 PDT
Comment on attachment 394427 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394427&action=review

>>> Source/WebCore/dom/EventTarget.cpp:334
>>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
>> 
>> why not optional?
> 
> Why is Optional better?

I guess because we would not need to heap-allocate then?
Comment 7 Saam Barati 2020-03-24 15:54:18 PDT
(In reply to Chris Dumez from comment #6)
> Comment on attachment 394427 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=394427&action=review
> 
> >>> Source/WebCore/dom/EventTarget.cpp:334
> >>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
> >> 
> >> why not optional?
> > 
> > Why is Optional better?
> 
> I guess because we would not need to heap-allocate then?

Not heap allocated
Comment 8 Saam Barati 2020-03-24 15:55:01 PDT
(In reply to Saam Barati from comment #7)
> (In reply to Chris Dumez from comment #6)
> > Comment on attachment 394427 [details]
> > Patch
> > 
> > View in context:
> > https://bugs.webkit.org/attachment.cgi?id=394427&action=review
> > 
> > >>> Source/WebCore/dom/EventTarget.cpp:334
> > >>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
> > >> 
> > >> why not optional?
> > > 
> > > Why is Optional better?
> > 
> > I guess because we would not need to heap-allocate then?
> 
> Not heap allocated

Sorry, was responding to your first comment. Yes, it's not heap allocated :-)
Comment 9 Chris Dumez 2020-03-24 15:56:06 PDT
(In reply to Saam Barati from comment #8)
> (In reply to Saam Barati from comment #7)
> > (In reply to Chris Dumez from comment #6)
> > > Comment on attachment 394427 [details]
> > > Patch
> > > 
> > > View in context:
> > > https://bugs.webkit.org/attachment.cgi?id=394427&action=review
> > > 
> > > >>> Source/WebCore/dom/EventTarget.cpp:334
> > > >>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
> > > >> 
> > > >> why not optional?
> > > > 
> > > > Why is Optional better?
> > > 
> > > I guess because we would not need to heap-allocate then?
> > 
> > Not heap allocated
> 
> Sorry, was responding to your first comment. Yes, it's not heap allocated :-)

Ok, will fix this now. Let me know if you know how to make a single test use aggressive GC.
Comment 10 Saam Barati 2020-03-24 16:00:28 PDT
(In reply to Chris Dumez from comment #9)
> (In reply to Saam Barati from comment #8)
> > (In reply to Saam Barati from comment #7)
> > > (In reply to Chris Dumez from comment #6)
> > > > Comment on attachment 394427 [details]
> > > > Patch
> > > > 
> > > > View in context:
> > > > https://bugs.webkit.org/attachment.cgi?id=394427&action=review
> > > > 
> > > > >>> Source/WebCore/dom/EventTarget.cpp:334
> > > > >>> +        std::unique_ptr<ListenerGCProtector> listenerGCProtector;
> > > > >> 
> > > > >> why not optional?
> > > > > 
> > > > > Why is Optional better?
> > > > 
> > > > I guess because we would not need to heap-allocate then?
> > > 
> > > Not heap allocated
> > 
> > Sorry, was responding to your first comment. Yes, it's not heap allocated :-)
> 
> Ok, will fix this now. Let me know if you know how to make a single test use
> aggressive GC.

Yeah, at the top of the test, you could do something like we have in other tests:

```
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"><!-- webkit-test-runner [ jscOptions=--slowPathAllocsBetweenGCs=5 ] -->
```

I haven't verified this works myself, so you might want to ensure the test fails without your fix. If that specific option doesn't work, maybe a combination of other options could work better
Comment 11 Chris Dumez 2020-03-24 16:34:46 PDT
Created attachment 394436 [details]
Patch
Comment 12 Chris Dumez 2020-03-24 16:38:04 PDT
Created attachment 394438 [details]
Patch
Comment 13 Saam Barati 2020-03-24 16:44:12 PDT
Comment on attachment 394438 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394438&action=review

r=me

> Source/WebCore/dom/EventTarget.cpp:93
> +    RefPtr<EventTarget> m_target;
> +    RefPtr<RegisteredEventListener> m_listener;

why not ref like you had before?
Comment 14 Saam Barati 2020-03-24 16:47:41 PDT
Comment on attachment 394438 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394438&action=review

> Source/WebCore/dom/EventTarget.cpp:76
> +        if (!m_target)
> +            return;

if you do below, this branch can be removed

> Source/WebCore/dom/EventTarget.cpp:89
> +    ListenerGCProtector(const ListenerGCProtector&) = delete;
> +    ListenerGCProtector& operator=(const ListenerGCProtector&&) = delete;
> +    ListenerGCProtector(ListenerGCProtector&&) = default;
> +    ListenerGCProtector& operator=(ListenerGCProtector&&) = default;

I think you can just use .emplace instead of using "operator=" in your code below, and all of these can be delete or left unspecified
Comment 15 Geoffrey Garen 2020-03-24 17:12:07 PDT
Comment on attachment 394438 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394438&action=review

> Source/WebCore/dom/EventTarget.cpp:341
> +        Optional<ListenerGCProtector> listenerGCProtector;

Are we just trying to keep the function alive within the scope of this function? If so, I think this might be overkill. We can just call ensureStillAliveHere at the end of the function; or create an RAII EnsureStillAliveScope for this purpose.
Comment 16 Chris Dumez 2020-03-24 17:23:58 PDT
(In reply to Geoffrey Garen from comment #15)
> Comment on attachment 394438 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=394438&action=review
> 
> > Source/WebCore/dom/EventTarget.cpp:341
> > +        Optional<ListenerGCProtector> listenerGCProtector;
> 
> Are we just trying to keep the function alive within the scope of this
> function? If so, I think this might be overkill. We can just call
> ensureStillAliveHere at the end of the function; or create an RAII
> EnsureStillAliveScope for this purpose.

Yes, we basically just want to extend the lifetime of the function for the scope. I went the visitor way because this is what I understand. ensureStillAliveHere() may be another way to fix this but I don't understand it :) I have asked Yusuke to take a look and comment.
Comment 17 Yusuke Suzuki 2020-03-24 17:24:19 PDT
Comment on attachment 394438 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394438&action=review

> Source/WebCore/ChangeLog:13
> +        In EventTarget::innerInvokeEventListeners, if the listener we're about to call is a one-time
> +        listener (has 'once' flag set), we would first unregister the event listener and then call
> +        it, as per the DOM specification. However, once unregistered, the event listener is no longer
> +        visited for GC purposes and its internal JS Function may get garbage collected before we get
> +        a chance to call it.

Nice find.

>> Source/WebCore/dom/EventTarget.cpp:341
>> +        Optional<ListenerGCProtector> listenerGCProtector;
> 
> Are we just trying to keep the function alive within the scope of this function? If so, I think this might be overkill. We can just call ensureStillAliveHere at the end of the function; or create an RAII EnsureStillAliveScope for this purpose.

Yes. I think we should do something like this would be enough and efficient :)

EnsureStillAliveScope wrapper;
EnsureStillAliveScope function;

if (registeredListener->isOnce()) {
    wrapper = ...->wrapper();
    function = ...->function(); // In some way.
}
...
if (registeredListener->isPassive())
    event.setInPassiveListener(false);
Comment 18 Chris Dumez 2020-03-24 17:30:11 PDT
(In reply to Yusuke Suzuki from comment #17)
> Comment on attachment 394438 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=394438&action=review
> 
> > Source/WebCore/ChangeLog:13
> > +        In EventTarget::innerInvokeEventListeners, if the listener we're about to call is a one-time
> > +        listener (has 'once' flag set), we would first unregister the event listener and then call
> > +        it, as per the DOM specification. However, once unregistered, the event listener is no longer
> > +        visited for GC purposes and its internal JS Function may get garbage collected before we get
> > +        a chance to call it.
> 
> Nice find.
> 
> >> Source/WebCore/dom/EventTarget.cpp:341
> >> +        Optional<ListenerGCProtector> listenerGCProtector;
> > 
> > Are we just trying to keep the function alive within the scope of this function? If so, I think this might be overkill. We can just call ensureStillAliveHere at the end of the function; or create an RAII EnsureStillAliveScope for this purpose.
> 
> Yes. I think we should do something like this would be enough and efficient
> :)
> 
> EnsureStillAliveScope wrapper;
> EnsureStillAliveScope function;
> 
> if (registeredListener->isOnce()) {
>     wrapper = ...->wrapper();
>     function = ...->function(); // In some way.
> }
> ...
> if (registeredListener->isPassive())
>     event.setInPassiveListener(false);

Hmm, Ok. I will try this alternative tomorrow and confirm it fixes the crash too. Not sure if it is really less work / code, we'll see.
Comment 19 Chris Dumez 2020-03-24 17:42:34 PDT
Created attachment 394450 [details]
Patch
Comment 20 Chris Dumez 2020-03-24 17:43:15 PDT
(In reply to Chris Dumez from comment #19)
> Created attachment 394450 [details]
> Patch

Uploaded this clean version of the patch reviewed by Saam, in case the alternative approach does not end up working out.
Comment 21 Alexey Proskuryakov 2020-03-24 21:12:45 PDT
Comment on attachment 394450 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394450&action=review

> LayoutTests/http/tests/inspector/network/har/har-page-aggressive-gc.html:1
> +<!DOCTYPE html><!-- webkit-test-runner [ jscOptions=--slowPathAllocsBetweenGCs=5 ] -->

How slow does this make the test? Not sure where I would draw the line, but we don’t want stress tests run as part of normal test suite in general.
Comment 22 Alexey Proskuryakov 2020-03-24 23:36:04 PDT
Also, would this crash in a more reduced test when slowPathAllocsBetweenGCs is used?
Comment 23 Chris Dumez 2020-03-25 08:39:49 PDT
(In reply to Alexey Proskuryakov from comment #22)
> Also, would this crash in a more reduced test when slowPathAllocsBetweenGCs
> is used?

I tried the following test case but have not been able to reproduce it with a simple test case:

<!DOCTYPE html><!-- webkit-test-runner [ jscOptions=--slowPathAllocsBetweenGCs=5 ] -->
<html>
<body>
<p>This test passes if it does not crash or time out.</p>
<script>
let messagesReceived = 0;
if (window.testRunner) {
    testRunner.dumpAsText();
    testRunner.waitUntilDone();
}

function handleMessageReceived()
{
    messagesReceived++
    if (messagesReceived == 500) {
        if (window.testRunner)
            testRunner.notifyDone();
        return;
    }

    sendMessage();
}

function sendMessage()
{
    window.addEventListener("message", (messageEvent) => { handleMessageReceived(); }, { capture: true, once: true });
    window.postMessage("foo", "*");
}

onload = () => {
    sendMessage();
}

</script>
</body>
</html>
Comment 24 Chris Dumez 2020-03-25 10:01:46 PDT
Created attachment 394510 [details]
Patch
Comment 25 Yusuke Suzuki 2020-03-25 10:49:33 PDT
Comment on attachment 394510 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394510&action=review

r=me

> Source/JavaScriptCore/runtime/JSCJSValue.h:653
> +class EnsureStillAlive {

Let's put WTF_FORBID_HEAP_ALLOCATION, WTF_MAKE_NONCOPYABLE, and WTF_MAKE_NONMOVABLE.
Comment 26 Chris Dumez 2020-03-25 10:59:06 PDT
Created attachment 394518 [details]
Patch
Comment 27 Saam Barati 2020-03-25 11:20:38 PDT
Comment on attachment 394518 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394518&action=review

> Source/JavaScriptCore/runtime/JSCJSValue.h:654
> +class EnsureStillAlive {

can we call this EnsureStillAliveScope or EnsureIsLiveScope?
Comment 28 Keith Miller 2020-03-25 11:21:20 PDT
Comment on attachment 394518 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=394518&action=review

> Source/JavaScriptCore/runtime/JSCJSValue.h:654
> +class EnsureStillAlive {

Can we call this EnsureAliveOnStack? It also seems like it would be useful to add a comment about were you would use this.
Comment 29 Chris Dumez 2020-03-25 11:24:23 PDT
(In reply to Keith Miller from comment #28)
> Comment on attachment 394518 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=394518&action=review
> 
> > Source/JavaScriptCore/runtime/JSCJSValue.h:654
> > +class EnsureStillAlive {
> 
> Can we call this EnsureAliveOnStack? It also seems like it would be useful
> to add a comment about were you would use this.

Can a JSC person help me come up with this comment. I'd be happy to rename the object and add the comment before landing.
Comment 30 Geoffrey Garen 2020-03-25 11:40:29 PDT
> > Can we call this EnsureAliveOnStack? It also seems like it would be useful
> > to add a comment about were you would use this.
> 
> Can a JSC person help me come up with this comment. I'd be happy to rename
> the object and add the comment before landing.

Let's call this "EnsureStillAliveScope", since the function it calls is named "ensureStillAlive..." and it is bad when one thing becomes two.

For a comment, I would say:

Use EnsureStillAliveScope when you need to remove a JavaScript object from the DOM and then use it in the same scope. For example, a 'once' event listener needs to be removed from the DOM and then fired.
Comment 31 Geoffrey Garen 2020-03-25 11:44:56 PDT
Or maybe:

Use EnsureStillAliveScope when you have a data structure that includes GC pointers, and you need to remove it from the DOM and then use it in the same scope. For example, a 'once' event listener needs to be removed from the DOM and then fired.
Comment 32 Chris Dumez 2020-03-25 11:46:55 PDT
(In reply to Geoffrey Garen from comment #31)
> Or maybe:
> 
> Use EnsureStillAliveScope when you have a data structure that includes GC
> pointers, and you need to remove it from the DOM and then use it in the same
> scope. For example, a 'once' event listener needs to be removed from the DOM
> and then fired.

In case it influences your decision, I am planning to add the following operator to this class in a follow-up so that I can use it in the generated JS DOM bindings:

operator JSValue() const { return m_value; }
Comment 33 Keith Miller 2020-03-25 11:51:31 PDT
(In reply to Geoffrey Garen from comment #31)
> Or maybe:
> 
> Use EnsureStillAliveScope when you have a data structure that includes GC
> pointers, and you need to remove it from the DOM and then use it in the same
> scope. For example, a 'once' event listener needs to be removed from the DOM
> and then fired.

It might also be worth mentioning that you'll need this a GC object points to a malloc'd value and you want to use the malloced value after the last use of the GC'd object.
Comment 34 Chris Dumez 2020-03-25 11:54:34 PDT
Created attachment 394521 [details]
Patch
Comment 35 Chris Dumez 2020-03-25 11:56:04 PDT
(In reply to Chris Dumez from comment #32)
> (In reply to Geoffrey Garen from comment #31)
> > Or maybe:
> > 
> > Use EnsureStillAliveScope when you have a data structure that includes GC
> > pointers, and you need to remove it from the DOM and then use it in the same
> > scope. For example, a 'once' event listener needs to be removed from the DOM
> > and then fired.
> 
> In case it influences your decision, I am planning to add the following
> operator to this class in a follow-up so that I can use it in the generated
> JS DOM bindings:
> 
> operator JSValue() const { return m_value; }

If we go with a "*Scope" naming, then maybe it should not be an operator but rather an explicit value() getter?
Comment 36 Geoffrey Garen 2020-03-25 12:14:19 PDT
> > operator JSValue() const { return m_value; }
> 
> If we go with a "*Scope" naming, then maybe it should not be an operator but
> rather an explicit value() getter?

Sure, I think that would look nice.

I guess it's also OK to drop "Scope". I was mostly interested in making the "ensure still alive" naming consistent.
Comment 37 Chris Dumez 2020-03-25 12:41:26 PDT
Comment on attachment 394521 [details]
Patch

Looks like the output of some tests have changed. I will investigate.
Comment 38 Chris Dumez 2020-03-25 12:55:20 PDT
Created attachment 394530 [details]
Patch
Comment 39 EWS 2020-03-25 14:05:46 PDT
Attachment 394530 [details] does not apply
Comment 40 Chris Dumez 2020-03-25 14:15:10 PDT
Created attachment 394544 [details]
Patch
Comment 41 Chris Dumez 2020-03-25 14:16:20 PDT
Comment on attachment 394544 [details]
Patch

Clearing flags on attachment: 394544

Committed r259009: <https://trac.webkit.org/changeset/259009>
Comment 42 Chris Dumez 2020-03-25 14:16:23 PDT
All reviewed patches have been landed.  Closing bug.
Comment 43 Truitt Savell 2020-03-26 09:37:41 PDT
The new test http/tests/inspector/network/har/har-page-aggressive-gc.html

is timing out on Mac wk1

History:
https://results.webkit.org/?suite=layout-tests&test=http%2Ftests%2Finspector%2Fnetwork%2Fhar%2Fhar-page-aggressive-gc.html
Comment 44 Chris Dumez 2020-03-26 09:43:00 PDT
(In reply to Truitt Savell from comment #43)
> The new test http/tests/inspector/network/har/har-page-aggressive-gc.html
> 
> is timing out on Mac wk1
> 
> History:
> https://results.webkit.org/?suite=layout-
> tests&test=http%2Ftests%2Finspector%2Fnetwork%2Fhar%2Fhar-page-aggressive-gc.
> html

Ok, I will investigate. It is a copy of http/tests/inspector/network/har/har-page.html but with aggressive GC enabled. So if http/tests/inspector/network/har/har-page.html is not timing out, then the test is likely just slow because of aggressive GC. Maybe I can make the GC less aggressive or we can mark the test as slow?
Comment 45 Chris Dumez 2020-03-26 09:52:41 PDT
(In reply to Chris Dumez from comment #44)
> (In reply to Truitt Savell from comment #43)
> > The new test http/tests/inspector/network/har/har-page-aggressive-gc.html
> > 
> > is timing out on Mac wk1
> > 
> > History:
> > https://results.webkit.org/?suite=layout-
> > tests&test=http%2Ftests%2Finspector%2Fnetwork%2Fhar%2Fhar-page-aggressive-gc.
> > html
> 
> Ok, I will investigate. It is a copy of
> http/tests/inspector/network/har/har-page.html but with aggressive GC
> enabled. So if http/tests/inspector/network/har/har-page.html is not timing
> out, then the test is likely just slow because of aggressive GC. Maybe I can
> make the GC less aggressive or we can mark the test as slow?

Made gc less aggressive on the test via <https://trac.webkit.org/changeset/259048>.