There's Math.random(), and then there's Math.random() (2015)
v8.dev
v8.dev
https://stackoverflow.com/questions/9550796/why-is-google-ch... https://issues.chromium.org/issues/40404440
I came across it because of some tests I had for a statistics library I was working on. There was a test that created an array with 100k random values that shouldn't have had any collisions, but I consistently got multiple collisions, only in Chrome.
> Safari, Firefox, and IE were all already using more robust PRNGs for Math.random
This wasn't true at the time. I actually don't remember what Safari was doing in 2012 so I won't say that one for sure, but Firefox and IE were both using garbage RNGs as well, they were probably just generating across the full (or at least more of the) (0, 1] double range so didn't produce the collisions you saw in your testing.
[1] https://medium.com/@betable/tifu-by-using-math-random-f1c308...
My recollection of that time working on that stats library was that Chrome took many shortcuts to max out things like SunSpider. There were other Chrome specific workarounds I had to implement to address what I considered bugs, but they were definitely in lightly defined areas like `Math.random()`.
I couldn't find the original bug I file, but the discussion on it specifically noted that they could just copy the Firefox one (which was noted to be an improvement over Chrome's) but they decided on Won't Fix until someone more important than me drew attention to it. It was trivially fixed within a week once that happened.
The reason the initial bug report [0] was marked as WAI but the Medium blog post got a lot more traction is very simple and much more human.
When the initial issue was filed to the chromium bug tracker, it was routed to Chrome's security team. From a security perspective, Math.random() provides no guarantees about cryptographic safety, so naturally it was marked as WAI – nobody from the V8 team actually saw this issue. One of the suggestions "3. Make crypto.random(size)" was acted upon though, and so crypto.getRandomValues() was introduced in Chrome 11, about 10 months after the issue report. In terms of specifying and introducing new Web APIs, this was incredibly fast.
After the Medium blog post was published, someone filed an issue directly to the V8 project. It did not question the spec compliance, but pointed out that the PRNG quality could be better. That nerd-sniped me into researching this topic and after reading a few papers, I implemented the fix with xorshift128+. I'm thankful that my team lead allowed me to set aside some time to work on this even though it was not on our project roadmap.
Performance regressions that could affect our benchmark performance was of course part of the consideration, but there were many options to avoid a performance regression. In V8, crossing the boundary between machine code compiled from JS to C++ runtime builtins is fairly expensive. I could have either ported xorshift128+ to assembly so that this boundary crossing was not necessary, or I could have amortized the boundary crossing cost by buffering multiple random values. I chose the latter because porting to assembly is error prone and I would have to do this for each platform. There are better options in V8 nowadays, by expressing the algorithm in an intermediate representation, but back then that was not available.
[0] https://g-issues.chromium.org/issues/40404440 [1] https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getR... [2] https://g-issues.chromium.org/issues/40643979
Anyway, I like how the openbsd folk fixed the old C rand family of functions. they made their seed functions do nothing and tied the generators to a strong kernel random source, that is, they made rand actually random. If your application actually depends on rand as a hash function they provided the old behavior under srand_deterministic(3)
"Standards insist that this interface return deterministic results. Unsafe usage is very common, so OpenBSD changed the subsystem to return non-deterministic results by default."
makes me laugh every time.
That is a phenomenally stupid thing to do. There is legitimate need for reproducible pseudo-random sequences derived from a known random seed. If you break seeding that can't be done. This is how fuzz testing with generated inputs works.
Whereas lots of programs use rand as a source of general-purpose random numbers, but get the seeding wrong, and so suffer from various problems. This change fixes those programs.
[1] https://pubs.opengroup.org/onlinepubs/009696699/functions/ra...
Fucking things up for people who understand how PRNGs work just to fix things for people who don't seems backwards; things like this is why configure scripts take over a minute to run. I might be okay with the non-determinism in rand() if srand isn't ever called, but intentionally ignoring the stated intent of the program just pisses me off.
This is the path Go took for its math/rand library eventually: if you don't seed the RNG, it's a CSPRNG, but if you do seed it, it's deterministic.
However, there is lots of "advice" out there to seed your RNG with "random" values like the current time to make it "more secure" (and to be fair, in some cases, you may get sufficient entropy for the purpose at hand by doing this, but in most cases, you won't get enough and you'll spend it very fast). So a call to srand may indicate that you know what you're doing and want determinism and understand the consequences, but it doesn't necessarily mean that.
Good point. srand(time(NULL)) is a common, yet nondeterministic, seed. The caller clearly doesn't expect rand() to generate a particular sequence of numbers, so srand() could check whether the given seed looks like a time_t close to the current time() and, if it does, then use a proper CSPRNG instead.
> The following code defines a pair of functions that could be incorporated into applications wishing to ensure that the same sequence of numbers is generated across different machines.
And then gives code which does not use rand/srand. The intent of the spec is very clearly not for it to be portably reproducible.
Another example of when you need it, is games. I want to store in the game state, that on Day 32 of being in this cave, there is randomly a cache of diamonds if you mine the east wall. If I generate this random number dynamically, then I permit save-scumming behaviors which may distract from the artistic points of my game. So I basically want a random seed and a hash function, hashing (seed, day, activity, location, attempt_number) to see if something is successful.
Correct, but rand() / srand() aren't an effective way of providing for that need. The state of the RNG is process-global; it rapidly becomes nondeterministic in multithreaded applications, let alone if a library you're using calls rand() internally. And, as mentioned upthread, the algorithm is implementation-defined, and isn't guaranteed to remain the same over time.
Whereas, when you don't know what you're doing, the default should not be a footgun.
The appropriate solution is to not break anything and use tooling that flags use of banned symbols that can have negative safety or security implications.
In those cases, they provide deterministic operation under a different set of functions so nobody does anything as stupid as using deterministic RNG as encryption
I think it would have been better to remove rand and srand, and provide two replacement functions with different names, or require all users to pick one using a #define.
However, it is the reason that (what feels like) every single C function has a footnote, along the lines of "*except on FordBAT OS, where mkdir doesn't create a directory, all floats are inf, and negatives are positive -- because it seemed convenient"
edit: they all fall into their own loops. I forget the term for it, but the pattern is, unless I've made an arithmetic mistake (likely!):
1,3,5,15,1...
2,6,10,14,2...
4,12,4...
7,9,11,(1,3,5,15,1...)
8,8...
14,7,9,11,(1,3,5,15,1...)
def advance_state(state):
state ^= state << 1
state ^= state >> 3
state ^= state << 10
return stateI also definitely wasn't up for working with 8-digit binary numbers on my graph paper. What I consider may be weird, but it's not that weird :P
(But I did do a little reading about LFSRs, and I wonder if the amount the numbers are shifted by is related to the stuff about arithmetic fields & groups & such... maybe I'll look into it later, but statistically speaking, probably not :) )
uint64_t a = s[0];
uint64_t b = s[1];
s[0] = b;
a ^= a << 23;
a ^= a >> 18;
a ^= b;
a ^= b >> 5;
s[1] = a;
return a + b;
https://stackoverflow.com/a/34432126Hypothetically, you could use this to vectorize/parallelize computation of xorshift128+ output sequences in large batches, but I'm not sure anyone's ever bothered to do this because the naive sequential method is so cheap.
How does this differ cryptographically from algorithms like MWC1616 and xorshift128+ in backend applications? How can a random number generator be vulnerable to attacks in real life?
Does it really matter which random number generator you use on a webpage? It's already considered an insecure environment anyway (that's why we have server-side validation).
So a PRNG needs "seeding". You need some value to put in at the start that is different every time. Using the time on the computer results in a different seed on each run, and so a different number sequence each time.
If you need random numbers for cryptography (very good quality random numbers are important for lots of crypto processes) then you need to do more - they need to be completely unpredictable. If you just used the time, someone could seed a copy of the RNG with the same time, and predict the randomness sequence. So what we want is a seed that comes from actual randomness (also called "entropy"). That entropy has to come from somewhere - ideally some hardware source using some statistically random physical effect.
This can be easy to bump into in fairly trivial cases.
For example, initializing by time alone can be problematic if you spawn N threads at once which all do their own seeding. Then they can all reach the seeding process at the same time, relative to the clock used for the seeding.
I had that problem with one of my earlier ray tracers, and so had to add add the thread id into the mix to seed the PRNGs.
You do want a deterministic PRNG (for repeatability of the simulation). But if you seed with time alone, and spawn 100 parallel jobs, you will see repeats or near-repeats which will bias the Monte Carlo averages resulting from your simulation ensemble.
In ancient times, you could xor in the PID and the IP address of the host to ensure no seed overlap. Nowadays there are other ways to introduce entropy to the seed, e.g. with dedicated system calls or /dev/random.
Using timestamp in seconds and process id for example would be easy enough to guess. There was some good work about a decade ago to provide more usable apis to get a quality seed from the OS (getentropy, getrandom), so using a self built seed is pretty suboptimal unless you have a case where you need to repeat the sequence later (recording the seed from the OS is also an option)
The CSPRNG has only made it practically impossible to predict future outputs from past outputs alone; however, if I can guess the seed, I can predict every output past, present, and future. CSPRNGs aren't magic and have to be used properly, and in any security-sensitive context, properly means with a truly random seed. The best CSPRNG in the world cannot create entropy out of thin air. It can, however, stretch a good source of entropy very far. There are cases when using a CSPRNG with a low-quality seed is appropriate, but the minimum acceptable entropy in the seed depends on the application, not on the RNG.
There are some cases in crypto where a secret that has even a single bit biased more than epsilon will break a whole scheme. CSPRNGs don't tend to work that way.
However, the quality of a modern CSPRNG doesn't save you from a low-entropy seed. A couple biased bits in a 128-bit seed may not be a serious issue (but why take the chance?!), however good entropy estimation is a difficult task which is best left to the operating system. Modern operating systems will ingest sources of hardware randomness and seed their system-level CSPRNGs, which should then be used directly by application software (for key material) or as seeds to local CSPRNGs (for less security-sensitive but more performance-sensitive needs). Do not play around with seed entropy just to be clever.
In the absence of a full-fledged operating system, then things like high-precision boot timers can be used for seeds, but only because there's nothing better available. The lack of good randomness is a real problem in resource-constrained environments, though usually there are easier-to-exploit vulnerabilities that get hit first.
> How does this differ cryptographically from algorithms like MWC1616 and xorshift128+ in backend applications?
That question doesn’t make any sense. The current timestamp would be a seed to initialise the PRNG, which can be MWC or xorshift.
> Does it really matter which random number generator you use on a webpage?
That is application dependent. If you’re coding a roulette wheel no, if you’re coding an E2E chat yes.
> It's already considered an insecure environment anyway (that's why we have server-side validation).
That is also application dependent. The client is considered insecure from the server pov because it is entirely under user control, so the user can mess with anything. But if it is acting entirely on behalf of the user then that can be the point and by design.
In the example above, you don’t care that a user fucks up their own crypto, but you do care that the crypto holds for the average user.
Even the qualitative difference between two non-CS PRNG can make or break a project due to too small a state space or short a cycle.
Or a prominent example are games.
The whole idea of sharing "cool minecraft worlds" or "seeds for cool runs in Balatro/Slay the Spire/..." are possible due to careful use of PRNGs in those games for world generation.
In fact, games often run at least 2 RNGs and possibly more in a multiplayer setting involving networking and encryption of that. You want some kind of "World Generation seed", which is usually a PRNG players can enter funny seeds like "CAFEBABE" into to share worlds. However, you want to protect this PRNG, because otherwise player action changes the world generation.
So you end up with more RNGs of some kind, for example for AI actions, particle movements. And depending on how these RNGs are used, you might link different subsystems together, allowing AI manipulation and such. It's a very interesting thing to think about and design.
If you include nanoseconds in your key then you have more entropy, of course, but timestamps are guessable, so it's risky.
> How does this differ cryptographically from algorithms like MWC1616 and xorshift128+ in backend applications? How can a random number generator be vulnerable to attacks in real life?
In the early days of online poker some people made a killing by figuring out all the possible hands from the PRNGs being used and then they could just start a game and look up the hands they'd been dealt in their database.
This only becomes a problem when people assume this isn't possible (for example, using it to pick lottery numbers) - this is more likely to happen on the server-side than the client-side (and v8 runs on both), but it can be security-relevant in client-only apps too (consider an insecure "password generator" app).
People typically seed a PRNG with the current time. The actual output sequence is still created with a determinstic algorithm. These two things are really orthogonal.
Bad PRNGs repeat early and have predictable patterns, as such they form a special attack vector.
Whether that is really an issue for any given application, depends on the application (e.g. how shortlived the crypto is and how bad decryption acter the fact is). But if you are asking that question it likely means you don't know enough knowledge to roll your own crypto unless you wanna be in a world of pain.
Just because PRNGs appear unpredictable to you does not mean they aren't.
If you really wanna go the route, at least add a salt, so something like
Hash(application_secret_long_and_random + timestamp_nanosecond_precision)I'm sure some other runtimes don't do it, too.
// Global PRNG: set Math.random.
seedrandom('hello.', { global: true });
console.log(Math.random()); // Always 0.9282578795792454
I found this useful when using worker threads to 'render' a construction by slices that included some randomness (that should be the same for each thread)For new/clean code it's better to just import a seeded random function for clarity.
There's Math.random(), and then there's Math.random() (2015) - https://news.ycombinator.com/item?id=38188421 - Nov 2023 (49 comments)
There's Math.random(), and then there's Math.random() - https://news.ycombinator.com/item?id=10751396 - Dec 2015 (106 comments)
- Linear congruential generator[2]:
xₜ = a xₜ₋₁ + c mod m
- Mersenne Twister[3]: too complicated for a few lines
- Subtract with carry[3]: k < p
carryₜ = [xₜ₋ₖ - xₜ₋ₚ - carryₜ₋₁ < 0]
xₜ = xₜ₋ₖ - xₜ₋ₚ - carryₜ₋₁ mod 2ⁿ
All three algorithms are fairly popular.(Please excuse the unidiomatic index and constant names, I'm at the mercy of unicode)
[1]: https://en.cppreference.com/w/cpp/header/random
[2]: https://en.wikipedia.org/wiki/Linear_congruential_generator