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

Bug in timer initialisation steps: the id's of cleared but still live timers can be reused #10387

Closed
majaha opened this issue Jun 3, 2024 · 5 comments

Comments

@majaha
Copy link
Contributor

majaha commented Jun 3, 2024

What is the issue with the HTML Standard?

The timer initialisation steps have a bug related to how timers that have been cancelled are represented, and how new timer IDs can be generated that match cleared (but still existing) timers. This is best illustrated with an example, consider these things happening in order:

  • there are two functions, A() and B()
  • setTimeout(A, 1000) is called
  • step 2 of the timer initialisation steps is executed, giving the timer a non-zero ID. It receives the value 103.
  • step 12 is executed, adding the key 103 to the map of active timers
  • clearTimeout(103) is called, removing the key 103 from the map of active timers
  • setTimeout(B, 2000) is called
  • step 2 of the timer initialisation steps is executed, giving the timer a non-zero ID that is not in the map of active timers. It receives the ID 103, which it could do because that key has been removed from the map.
  • After 1000 milliseconds, the task created in setTimeout(A, 1000), step 8, is executed.
  • In step 8.1, the ID of 103 exists in the map of active timers, and so function A() is executed, even though it was cancelled.

I can see two ways of attacking this problem:

  1. Stop step 2 from generating IDs of cleared timers, perhaps by moving cleared IDs to a new map of cleared timers. Then the IDs would be removed from the map of cleared timers once the task executes, perhaps in an "else" after step 8.1.
  2. Try to make run steps after a timeout directly cancellable somehow. I assume this is closer to how web browsers actually do it, but I'm not sure how to implement that in specification-ese.
@annevk
Copy link
Member

annevk commented Jun 10, 2024

Well spotted!

How about instead of removing the entry, we set its value to null? And then update all the call sites to account for that. This is similar to your second map idea I think, but seems easier to get right.

cc @domenic

@domenic
Copy link
Member

domenic commented Jun 24, 2024

It feels like whatwg/infra#396 might be related here, although I'm not sure it's exactly the same thing.

How Chromium's code does it is it keeps a "next timer ID" variable. That seems relatively simple and still allows us to clear the map entries. It also reduces implementation-defined behavior.

@majaha
Copy link
Contributor Author

majaha commented Jul 7, 2024

Ah okay, that's good info. That's also pretty much how Firefox does it.

By the way, I spotted some Undefined Behaviour in that Chromium code (signed int overflow), which I've sent in a patch for.

The Firefox code is also technically broken, because the value can overflow and use the ID 0, or reuse ID's. (The webIDL types should probably also be changed to unsigned long).

@domenic
Copy link
Member

domenic commented Jul 8, 2024

Nice! Do you want to work on a spec PR to incorporate a good algorithm here?

majaha added a commit to majaha/html that referenced this issue Jul 23, 2024
whatwg#10387

Previously, it had been possible for timer IDs to be reused
after having been cleared by clearTimeout(), which caused
problems when the Run Steps After a Timeout callback
was eventually executed.

The problem was fixed by setting the value of cleared timeouts
in the map of active timers to null, which prevents the IDs
from being reused until after the relevent algorithm steps
have actually executed.

It may have been better to make the timeouts directly
cancellable somehow, instantly releasing their timer IDs,
as that is much closer to how real real browsers implement
clearTimeout(), and the difference is technically observable
if you somehow manage to exhaust all the timeout IDs.
However, I couldn't work out how to do it that way in the
specification language.
@majaha
Copy link
Contributor Author

majaha commented Jul 23, 2024

Alright, I've had a bash at fixing the spec.
#10505

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

No branches or pull requests

3 participants