Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EventTarget.removeEventListener called from within listener callback results in index out of bounds access #61

Open
aplr opened this issue Aug 9, 2023 · 0 comments

Comments

@aplr
Copy link

aplr commented Aug 9, 2023

If EventTarget.removeEventListener is called from within the listeners callback, this may result in an access to an array index which does not exist any more when calling listeners in a loop:

for (let i = 0; i < listeners.length; ++i) {
const listener = listeners[i]
// Skip if removed.
if (isRemoved(listener)) {
continue
}
// Remove this listener if has the `once` flag.
if (isOnce(listener) && removeListenerAt(list, i, !cow)) {
// Because this listener was removed, the next index is the
// same as the current value.
i -= 1
}
// Call this listener with the `passive` flag.
eventData.inPassiveListenerFlag = isPassive(listener)
invokeCallback(listener, this, event)
eventData.inPassiveListenerFlag = false
// Stop if the `event.stopImmediatePropagation()` method was called.
if (eventData.stopImmediatePropagationFlag) {
break
}
}

As this library is used as a ponyfill in vercel's edge-runtime this becomes evident when using libraries which rely on removing event listeners in the listener callback. One instance of this case is documented in the following issue:

connectrpc/connect-es#749

Insights

When calling dispatchEvent, the list of listeners consists of two items:

listeners = [
  { callback: [function onAbort], flags: 0 },
  { callback: [function onAbort], flags: 4 }
]

In the first iteration, the callback removes the first listener from list.listeners. However, the local, deconstructed listeners still points to the old list with 2 items. This becomes a problem in the second iteration, as it's a "once" listener, which the code tries to remove from list.listeners. As the list only contains one listener, the operation fails as it's working on a undefined value.

Possible solution

I don't know if this case should work, or this library is still maintained. However, it's used by nextjs and I'm surprised it did not become evident before.

I did not dig too deep, but one thing I've noticed is when always referencing the listeners list by list.listeners the issue does not arise. However, I don't know if this messes up other parts of the listener invoke logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant