C++ | How to Safely Initialize std::mt19937
Analyzes the randomness weaknesses of seeding std::mt19937 with std::random_device and the default std::seed_seq, and shows how to initialize it correctly using /dev/urandom.
Machine-translated from the Chinese original.
Background
Before I read C++ Seeding Surprises backup, I always used the following code to initialize std::mt19937:
std::mt19937 mtr(std::random_device{}());
The problems
std::random_device might not be random at all
Older versions of MinGW’s GCC implemented it deterministically; see std::random_device not working properly backup. This has since been fixed in MinGW GCC 9.2.
std::random_device’s range is not large enough
The range of std::random_device is the same as unsigned int, which in my environment is , providing 32 bits of randomness.
However, is not a very large number. We ran a test backup that iterated over values in seconds, so we would expect to be able to iterate over all cases in hours.
As is well known, knowing the seed lets you predict the entire random sequence. So a 32-bit seed is far from sufficient for use cases that need high security.
Use cases that need high security should use a cryptographically secure pseudorandom number generator backup.
std::seed_seq’s implementation is not trustworthy
In the code above, we only give std::mt19937 a single 32-bit integer, but the state of a Mersenne Twister contains 624 32-bit integers (see Wikipedia backup), so the standard library uses std::seed_seq to expand the state.
However, std::seed_seq’s implementation has a problem.
When seeding with a 32-bit integer, roughly numbers, including and , can never occur as the first number produced by std::mt19937. If you run the code below, the function send_detailed_tracking_info_secretly will never be called.
std::mt19937 mtr(std::random_device{}());
if (mtr() == 7) /* lucky seven! you get to send in a report */
{
send_detailed_tracking_info_secretly();
}
The fix
You can do a simple fix by referring to this answer backup on Stack Overflow:
using Generator = std::mt19937;
Generator mtr = ([]() {
static std::array<Generator::result_type, Generator::state_size> data;
static std::random_device rd;
std::generate(std::begin(data), std::end(data), std::ref(rd));
static std::seed_seq seq(std::begin(data), std::end(data));
static Generator mtr{seq};
return mtr;
})();
The following code reads random numbers directly from /dev/urandom:
using Generator = std::mt19937_64;
Generator mtr = ([]() -> Generator {
using iv_type = Generator::result_type;
constexpr size_t iv_length = Generator::state_size;
constexpr size_t iv_size = sizeof(iv_type) * iv_length;
iv_type *iv = (iv_type *)std::malloc(iv_size);
std::FILE *fin = std::fopen("/dev/urandom", "rb");
[[maybe_unused]] size_t unused = std::fread(iv, 1, iv_size, fin);
std::fclose(fin);
std::seed_seq seq = std::seed_seq(iv, iv + iv_length);
std::free(iv);
return Generator{seq};
})();
On the source of entropy
On Linux, good randomness can be obtained by reading /dev/urandom; on Windows you can use BCryptGenRandom backup instead.
On the amount of entropy
To initialize std::seed_seq, I read 2496B of data from /dev/urandom.
Improvements
Since std::seed_seq and /dev/urandom work well for my use case, I chose to use them.
If you are not satisfied with std::seed_seq, see Developing a seed_seq Alternative backup.
For issues with std::random_device, see Everything You Never Wanted to Know about C++’s random_device backup.
If you want to implement your own random_device, see Simple Portable C++ Seed Entropy backup.
