mktmp: improve our temp file creation - #6207
Conversation
| *seed ^= (((uint64_t)((uintptr_t)printf)) << SEED_SHIFT); | ||
| *seed ^= (((uint64_t)((uintptr_t)errno))); | ||
|
|
||
| *seed ^= ((uint64_t)git__timer()); |
There was a problem hiding this comment.
Given that this one returns a double I'm thinking that we could possibly get some more bits out of it either by multiplying with some constant to get some of the sub-second information into the seed or write it to a union {double, uint64_t} to get access to all the bits.
There was a problem hiding this comment.
🤔 Isn't double 64-bit on all data models?
There was a problem hiding this comment.
The idea with using a union would be to write the double and read it as an integer to get all 64 bits into the seed.
It could be interesting to do this with the loadavg values too.
Technically double can be other sizes if __STDC_IEC_559__ isn't defined but I haven't seen this outside of some 8-bit processors where double is equivalent to float. I don't think that is something we need to take into consideration.
There was a problem hiding this comment.
Oh, yes, I see what you're saying. Apologies; I never work with floats and so I was just thinking about it as a big hunk of bytes. Indeed a simple cast here will drop a lot of the data we wanted in this first place.
| *seed ^= (((uint64_t)((uintptr_t)getseed)) << SEED_SHIFT); | ||
| *seed ^= (((uint64_t)((uintptr_t)seed))); | ||
| *seed ^= (((uint64_t)((uintptr_t)printf)) << SEED_SHIFT); | ||
| *seed ^= (((uint64_t)((uintptr_t)errno))); |
There was a problem hiding this comment.
I'm thinking that getseed and printf are the same for all threads here and probably doesn't add much. (And because of some microcontrollers with separate address spaces for functions and data C99 doesn't allow casting function pointers to data pointers.)
Are seed and errno at unique addresses for each thread?
Otherwise &tv should give a pointer to a stack variable.
There was a problem hiding this comment.
I wasn't aiming for uniqueness across threads - we'll seed once for the library at git_libgit2_init time for all threads - I was aiming for uniqueness across process invocations to avoid a replay attack. But for pthread, that will be a noop on anything that doesn't do ASLR, which I actually assumed was more common than it is - my Mac doesn't do ASLR, at least not by default. 🤔
And that should have been &errno, not errno. Meh.
In any case, I think that I got a bit belt-and-suspenders here; certainly I can remove the function pointers here for broader compatibility, especially since it's not the macOS and Linux cases that are going to fall into this function.
There was a problem hiding this comment.
Sorry, I got a bit of a tunnel vision here. The cases that doesn't have either getentropy or /dev/urandom probably lacks most other security functions.
There was a problem hiding this comment.
I do, too, at this point. This feels like a lot of code for "temp file in a place that we own", but there are always surprises.
| *seed |= ((uint64_t)kerneltime.dwLowDateTime << 32); | ||
| *seed |= ((uint64_t)kerneltime.dwHighDateTime); | ||
| *seed |= ((uint64_t)usertime.dwLowDateTime); | ||
| *seed |= ((uint64_t)usertime.dwHighDateTime << 32); |
There was a problem hiding this comment.
Is this supposed to be |= for these 6?
It "feels" like they were supposed to be ^=
There was a problem hiding this comment.
Meh, I actually meant to compose the high and low bits into a single 64 bit number and then xor that.
a72d260 to
004af89
Compare
|
LGTM An alternative to the shift/xor deal for the seed generation could have been to just feed the values into one of the SHA algorithms and let it deal with shuffling the bits around, but that is probably overkill. |
Yeah, I thought about that - and I actually think that it's a good followup when we have SHA-256. If we seeded with 256 bits instead of 64, we could easily pull 256 bits out of the system's entropy store, and then feed system state into SHA-256 as a fallback. This would let us drop the intermediate shiftmix64. But I didn't want to deal with that when we live in a SHA-1 only world, and I didn't really want to merge even the implementation bits of SHA-256 until we get v1.4.0 out the door. Even this feels like a lot of changes at the last minute for v1.4.0 (which I wanted to get out... last month... 😢 ) but it does feel safe "enough". |
Introduce `git_rand`, a PRNG based on xoroshiro256**, a fast, all-purpose pseudo-random number generator: https://prng.di.unimi.it The PRNG will be seeded by the system's entropy store when possible, falling back to current time and system data (pid, uptime, etc). Inspiration for this was taken from libressl, but since our PRNG is not used for cryptographic purposes (and indeed currently only generates a unique temp file name that is written in a protected directory), this should be more than sufficient. Our implementation of xoroshiro256** was taken almost strictly from the original author's sources, but was tested against PractRand to ensure that there were no foolish mistranslations: ``` RNG_test using PractRand version 0.94 RNG = RNG_stdin64, seed = unknown test set = core, folding = standard (64 bit) rng=RNG_stdin64, seed=unknown length= 256 megabytes (2^28 bytes), time= 2.9 seconds no anomalies in 210 test result(s) rng=RNG_stdin64, seed=unknown length= 512 megabytes (2^29 bytes), time= 6.2 seconds no anomalies in 226 test result(s) rng=RNG_stdin64, seed=unknown length= 1 gigabyte (2^30 bytes), time= 12.7 seconds no anomalies in 243 test result(s) rng=RNG_stdin64, seed=unknown length= 2 gigabytes (2^31 bytes), time= 25.4 seconds no anomalies in 261 test result(s) rng=RNG_stdin64, seed=unknown length= 4 gigabytes (2^32 bytes), time= 50.6 seconds no anomalies in 277 test result(s) rng=RNG_stdin64, seed=unknown length= 8 gigabytes (2^33 bytes), time= 104 seconds no anomalies in 294 test result(s) ```
`mktemp` on mingw is exceedingly deficient, using a single monotonically increasing alphabetic character and the pid. We need to use our own random number generator for temporary filenames.
We have our own temporary file creation function now in `git_futils_mktmp`, remove the others since they may be terrible on some platforms.
4bdb0d2 to
b933c14
Compare
|
Shipping it to unblock our CI |
Hey, why are our CI builds taking four hours?
I was hasty about merging #6178 and ignored @boretrk's sage advice about adding randomness to
mktemp. It turns out that mingw64'smktempis... just terrible. It's a single alphabetic character (a-z) followed by the process id. Which meant that in our test where we add a bunch of data to the repository in different threads caused temp file races against each other. At some point when 26 threads had temp files, we would end up in a big loop retrying new filenames, but having exhausted them, we would just sit in that loop for a while until one of the threads that had a temp file would eventually finish.(Or we deadlocked, and never ended up writing anything at all, and the lack of error checking simply means that the failures were ignored.)
Okay, so what are we going to do about it?
This PR gets rid of our reliance on
mktempandmkstemp, a) because they might suck, and b) it's not what we want anyway. We're not writing temp files into writable directories (/tmp) and need something that gives us a mode0600file. Instead, we're writing temp files into repositories and need something that respectsumask.Regrettably, this is not entirely trivial. We do not want to use
rand, because we cannot guarantee that the process calling us has ever calledsrand. And we cannot callsrandourselves, because the process calling us may have called srand and depends on a particular sequence number. (Test harnesses and fuzzers will oftensrandwith a random seed, and report that seed so that if there's a failure, it's reproducible by using the same seed in subsequent runs).So this PR:
uint64_t) instead of very academic (the code in my paper is along int, what do you mean portable?). This isgit_rand_next, and I've run the output against PractRand to ensure that there were no copy-pasta errors.git_rand_seedwhich will seed the PRNG. We do this by pulling 64 bits out of the system's entropy pool (CryptGenRandomon Windows, orgetentropyon *BSD and Linux). If we're on a non-Windows system andgetentropyis not available (or fails), we'll fall back to trying to read from/dev/urandom. If none of those work, we'll use the system time (hopefully with a resolution better than a second) and xor with some system values like load average, uptime, process ID, etc. This strategy was largely borrowed from libressl, but not as aggressive, since we're putting a temp file in a directory that we control, not sending your credit card numbers across the internet.mktempfunction to use our new PRNG and use it consistently.Notes:
/tmpor world-writable directories.getentropy()case, hard in thetime(NULL) ^ getpid()case. It's probably fine for our needs./cc @boretrk