Skip to content

mktmp: improve our temp file creation - #6207

Merged
ethomson merged 5 commits into
mainfrom
ethomson/prng
Feb 12, 2022
Merged

mktmp: improve our temp file creation#6207
ethomson merged 5 commits into
mainfrom
ethomson/prng

Conversation

@ethomson

@ethomson ethomson commented Feb 8, 2022

Copy link
Copy Markdown
Member

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's mktemp is... 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 mktemp and mkstemp, 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 mode 0600 file. Instead, we're writing temp files into repositories and need something that respects umask.

Regrettably, this is not entirely trivial. We do not want to use rand, because we cannot guarantee that the process calling us has ever called srand. And we cannot call srand ourselves, because the process calling us may have called srand and depends on a particular sequence number. (Test harnesses and fuzzers will often srand with 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:

  1. Introduces the xoroshiro256** pseudo-random number generator. xoroshiro256** is fast and simple, and has the uncommon attribute of being easy to reason about code (64 bit types are uint64_t) instead of very academic (the code in my paper is a long int, what do you mean portable?). This is git_rand_next, and I've run the output against PractRand to ensure that there were no copy-pasta errors.
  2. Introduces git_rand_seed which will seed the PRNG. We do this by pulling 64 bits out of the system's entropy pool (CryptGenRandom on Windows, or getentropy on *BSD and Linux). If we're on a non-Windows system and getentropy is 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.
  3. Update our mktemp function to use our new PRNG and use it consistently.

Notes:

  1. I'm not a cryptanalyst, but I feel confident about xoroshiro256** based on its adoption by Java.
  2. Even if I didn't, we don't need to worry too much about attacks here, since we're only ever writing within the repository and our goal is race/collision-avoidance. That said, we should probably not use this in /tmp or world-writable directories.
  3. We put a mutex around our state boxes for thread safety. It's probably fine for our needs. We could, I guess, try to pull it into a thread-local, but then we'd need to re-seed each thread independently and try to ensure that they were separate seeds. This is easy in the getentropy() case, hard in the time(NULL) ^ getpid() case. It's probably fine for our needs.

/cc @boretrk

@ethomson ethomson added the v1.4.0 label Feb 8, 2022
Comment thread src/rand.c Outdated
*seed ^= (((uint64_t)((uintptr_t)printf)) << SEED_SHIFT);
*seed ^= (((uint64_t)((uintptr_t)errno)));

*seed ^= ((uint64_t)git__timer());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Isn't double 64-bit on all data models?

@boretrk boretrk Feb 8, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/rand.c Outdated
*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)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/rand.c Outdated
*seed |= ((uint64_t)kerneltime.dwLowDateTime << 32);
*seed |= ((uint64_t)kerneltime.dwHighDateTime);
*seed |= ((uint64_t)usertime.dwLowDateTime);
*seed |= ((uint64_t)usertime.dwHighDateTime << 32);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be |= for these 6?
It "feels" like they were supposed to be ^=

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh, I actually meant to compose the high and low bits into a single 64 bit number and then xor that.

@boretrk

boretrk commented Feb 9, 2022

Copy link
Copy Markdown
Contributor

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.

@ethomson

ethomson commented Feb 9, 2022

Copy link
Copy Markdown
Member Author

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".

Edward Thomson and others added 5 commits February 9, 2022 09:41
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.
@ethomson
ethomson merged commit 4467bd6 into main Feb 12, 2022
@ethomson
ethomson deleted the ethomson/prng branch February 12, 2022 14:09
@ethomson

Copy link
Copy Markdown
Member Author

Shipping it to unblock our CI

@ethomson ethomson added the bug label Feb 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants