Skip to content

2 min read中文

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 [0,2321][0,2^{32}-1], providing 32 bits of randomness.

However, 2322^{32} is not a very large number. We ran a test backup that iterated over 10610^6 values in 1.751.75 seconds, so we would expect to be able to iterate over all 2322^{32} cases in 2.092.09 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 232e\frac{2^{32}}e numbers, including 77 and 1313, 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.

girl

Image source backup