setBigTimeout

(evanhahn.com)

59 points | by cfj a day ago ago

48 comments

  • n2d4 4 hours ago

    The default behaviour of setTimeout seems problematic. Could be used for an exploit, because code like this might not work as expected:

        const attackerControlled = ...;
        if (attackerControlled < 60_000) {
          throw new Error("Must wait at least 1min!");
        }
    
        setTimeout(() => {
          console.log("Surely at least 1min has passed!");
        }, attackerControlled);
    
    
    The attacker could set the value to a comically large number and the callback would execute immediately. This also seems to be true for NaN. The better solution (imo) would be to throw an error, but I assume we can't due to backwards compatibility.
    • arghwhat 4 hours ago

      A scenario where an attacker can control a timeout where having the callback run sooner than one minute later would lead to security failures, but having it set to run days later is perfectly fine and so no upper bound check is required seems… quite a constructed edge case.

      The problem here is having an attacker control a security sensitive timer in the first place.

      • a_cardboard_box 3 hours ago

        The exploit could be a DoS attack. I don't think it's that contrived to have a service that runs an expensive operation at a fixed rate, controlled by the user, limited to 1 operation per minute.

        • lucideer 2 hours ago

          > I don't think it's that contrived to have a service that runs an expensive operation at a fixed rate, controlled by the user

          Maybe not contrived but definitely insecure by definition. Allowing user control of rates is definitely useful & a power devs will need to grant but it should never be direct control.

          • shawnz an hour ago

            Can you elaborate on what indirect control would look like in your opinion?

            No matter how many layers of abstraction you put in between, you're still eventually going to be passing a value to the setTimeout function that was computed based on something the user inputted, right?

            If you're not aware of these caveats about extremely high timeout values, how do any layers of abstraction in between help you prevent this? As far as I can see, the only prevention is knowing about the caveats and specifically adding validation for them.

            • lucideer 24 minutes ago

              > that was computed

              Or comes from a set of known values. This stuff isn't that difficult.

              This doesn't require prescient knowledge of high timeout edge cases. It's generally accepted good security practice to limit business logic execution based on user input parameters. This goes beyond input validation & bounds on user input (both also good practice but most likely to just involve a !NaN check here), but more broadly user input is data & timeout values are code. Data should be treated differently by your app than code.

              To generalise the case more, another common case of a user submitting a config value that would be used in logic would be string labels for categories. You could validate against a known list of categories (good but potentially expensive) but whether you do or not it's still good hygiene to key the user submitted string against a category hashmap or enum - this cleanly avoids using user input directly in your executing business logic.

    • swatcoder 3 hours ago

      That's just terrible input validation and has nothing to do with setTimeout.

      If your code would misbehave outside a certain range of values and you're input might span a larger range, you should be checking your input against the range that's valid. Your sample code simply doesn't do that, and that's why there's a bug.

      That the bug happens to involve a timer is irrelevant.

      • tourist2d 2 hours ago

        > That's just terrible input validation and has nothing to do with setTimeout.

        Except for the fact that this behaviour is surprising.

        > you should be checking your input against the range that's valid. Your sample code simply doesn't do that, and that's why there's a bug.

        Indeed, so why doesn't setTimeout internally do that?

        • drdaeman 2 hours ago

          > Indeed, so why doesn't setTimeout internally do that?

          Given that `setTimeout` is a part of JavaScript's ancient reptilian brain, I wouldn't be surprised it doesn't do those checks just because there's some silly compatibility requirement still lingering and no one in the committees is brave enough to make a breaking change.

          (And then, what should setTimeout do if delay is NaN? Do nothing? Call immediately? Throw an exception? Personally I'd prefer it to throw, but I don't think there's any single undeniably correct answer.)

          Given the trend to move away from the callbacks, I wonder why there is no `async function sleep(delay)` in the language, that would be free to sort this out nicely without having to be compatible with stuff from '90s. Or something like that.

    • wging 2 hours ago

      In nodejs you at least get a warning along with the problematic behavior:

          Welcome to Node.js v22.7.0.
          Type ".help" for more information.
          > setTimeout(() => console.log('reached'), 3.456e9)
          Timeout { <contents elided> }
          > (node:64799) TimeoutOverflowWarning: 3456000000 does not fit into a 32-bit signed integer.
          Timeout duration was set to 1.
          (Use `node --trace-warnings ...` to show where the warning was created)
          reached
      
      I'm surprised to see that setTimeout returns an object - I assume at one point it was an integer identifying the timer, the same way it is on the web. (I think I remember it being so at one point.)
      • augusto-moura 2 hours ago

        It returns an object for a long time now, I might say it was always like this actually. Don't know about very old versions

    • sfvisser 4 hours ago

      Don’t ever use attacker controlled data directly in your source code without validation. Don’t blame setTimeout for this, it’s impolite!

      • n2d4 4 hours ago

        The problem is the validation. You'd expect you just have to validate a lower bound, but you also have to validate an upper bound.

  • yifanl 4 hours ago

    If we're pedantic, this doesn't actually do what's advertised, this would be waiting X timeouts worth of event cycles rather than just the one for a true Big timeout, assuming the precision matters when you're stalling a function for 40 days.

    • keithwhor 4 hours ago

      I haven’t looked at the code but it’s fairly likely the author considered this? eg the new timeout is set based on the delta of Date.now() instead of just subtracting the time from the previous timeout.

      • yifanl 4 hours ago

        No, it pretty much just does exactly that.

            const subtractNextDelay = () => {
              if (typeof remainingDelay === "number") {
                remainingDelay -= MAX_REAL_DELAY;
              } else {
                remainingDelay -= BigInt(MAX_REAL_DELAY);
              }
            };
        • keithwhor 2 hours ago

          Oh yikes. Yeah; not ideal.

      • gnachman an hour ago

        That wouldn't very well because Date.now() isn't monotonic.

    • ballenf an hour ago

      Each subtracted timeout is a 25 day timer, so any accumulated error would be miniscule. In your example there would a total of 2 setTimeouts called, one 25 day timer and one 15 day. I think the room for error with this approach is smaller and much simpler than calculating the date delta and trying to take into account daylight savings, leap days, etc. (but I don't know what setTimeout does with those either).

      Or maybe I'm missing your point.

  • darepublic 3 hours ago

    instead of chaining together shorter timeouts, why not calculate the datetime of the delay and then invoke via window.requestAnimationFrame (by checking the current date ofc).

    • augusto-moura 3 hours ago

      Are you suggesting checking the date every frame vs scheduling long task every once in a long while? Can't tell if it is ironic or not, I'm sorry (damn Poe's law). But assuming not, it would be a lot more computationaly expensive to do that, timeouts are very optmized and they "give back" on the computer resources while in the meantime

      • darepublic an hour ago

        No irony intended I can be this dumb. Your point did occur to me as I posted, was just grasping at straws for a "clean" solution

  • yesco 3 hours ago

    > In most JavaScript runtimes, this duration is represented as a 32-bit signed integer

    I thought all numbers in JavaScript were basically some variation of double precision floating points, if so, why is setTimeout limited to a smaller 32bit signed integer?

    If this is true, then if I pass something like "0.5", does it round the number when casting it to an integer? Or does it execute the callback after half a millisecond like you would expect it would?

    • arp242 3 hours ago

      You're correct about JS numbers. It works like this presumably because the implementation is written in C++ or the like and uses an int32 for this, because "25 days ought to be enough for everyone".

      • drdaeman 2 hours ago

        I thought most non-abandoned C/C++ projects have long switched to time_t or similar. 2038 is not that far in the future.

        • andrewmcwatters 5 minutes ago

          2038 is even "now" if you're calculating futures.

    • blixt 3 hours ago

      JS numbers technically have 53 bits for integers (mantissa) but all binary operators turns it into a 32-bit signed integer. Maybe this is related somehow to the setTimeout limitation. JavaScript also has the >>> unsigned bit shift operator so you can squeeze that last bit out of it if you only care about positive values: ((2*32-1)>>>0).toString(2).length === 32

      • tubs 2 hours ago

        I assume by binary you mean logical? A + b certainly does not treat either side as 32bit.

        • blixt an hour ago

          Sorry, I meant bitwise operators, such as: ~ >> << >>> | &

  • issafram 4 hours ago

    I wish that I could actually see the code. I understand that it's chaining timeouts, but the git site is just garbage

  • sjaak 4 hours ago

    What is the use-case for such a function?

    • echoangle 4 hours ago

      Make a joke and have something to write a blogpost about, while letting your readers learn something new.

    • keithwhor 4 hours ago

      Off the top of my head, a cron scheduler for a server that reads from a database and sets a timeout upon boot. Every time the server is reboot the timeouts are reinitialized (fail safe in case of downtime). If upon boot there’s a timeout > 25 days it’ll get executed immediately which is not the behavior you want.

      • hinkley 2 hours ago

        This should be an interval with a lookup.

        Every five seconds check for due dates sooner than 10 seconds from now and schedule them.

        The longer a delay the higher the odds the process exits without finishing the work.

      • skykooler 3 hours ago

        Why would you do that in JS rather than just using cron for it?

    • bgirard 4 hours ago

      Not having your timeout fire unexpectedly instantly is a good use-case IMO.

  • miiiiiike 4 hours ago

    Got hit with this one a few months ago.

    • graypegg 4 hours ago

      Just out of curiosity, what was the use case for a really long timeout? Feels like most if not all long timeouts would be best served with some sort of "job" you could persist, rather than leaving it in the event queue.

      • cout 4 hours ago
        • graypegg 4 hours ago

          To be fair, this will be fixed by browsers when it's within spitting distance of the scale of numbers setTimeout is normally used with. (not huge numbers) Like, if it's close enough that setTimeout(() => {}, 5000) will stop working a month later, that would be a major failure on the browser vendor's part. Much too close for comfort.

          But I totally understand it not being a priority if the situation is: setTimeout(() => {}, 500000000) not working in X years.

    • BillyTheKing 4 hours ago

      this is the thing with JS and TS - the types and stuff, it's all good until you realise that all integers are basically int 52 (represented as float 64, with 52 bits for the fraction).

      Yes, it's nice and flexible - but also introduces some dangerous subtle bugs.

      • 8n4vidtmkvmk 4 hours ago

        2^53-1 I thought.

        And no, they're not all that. There's a bunch that are 2^32 such as this timeout, apparently, plus all the bit shift operations.

      • vhcr 4 hours ago

        Not ALL integers are 52 bit, BigInts were added on ECMAScript 2020.