Skip to content
This repository was archived by the owner on Dec 8, 2021. It is now read-only.

Commit f533447

Browse files
authored
fix: workaround a libstdc++ on random_device (#208)
On fairly recent versions of libstdc++ the `std::random_device` class fails when used by multiple threads: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94087 We workaround this problem by detecting the version of the standard C++ library, and if it is `libstdc++` and it is a version after 20200128 then we use `rdrand` as the token value to initialize `std::random_device`, which does not have the problem with multiple threads.
1 parent c26a613 commit f533447

File tree

3 files changed

+100
-24
lines changed

3 files changed

+100
-24
lines changed

google/cloud/internal/random.cc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,66 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/internal/random.h"
16+
#include <algorithm>
17+
#include <limits>
1618

1719
namespace google {
1820
namespace cloud {
1921
inline namespace GOOGLE_CLOUD_CPP_NS {
2022
namespace internal {
23+
std::vector<unsigned int> FetchEntropy(std::size_t desired_bits) {
24+
// We use the default C++ random device to generate entropy. The quality of
25+
// this source of entropy is implementation-defined:
26+
// http://en.cppreference.com/w/cpp/numeric/random/random_device/random_device
27+
// However, in all the platforms we care about, this seems to be a reasonably
28+
// non-deterministic source of entropy.
29+
//
30+
// On Linux with libstdc++ (the most common library on Linux) is based on
31+
// either `/dev/urandom`, or (if available) the RDRAND [1], or the RDSEED [1]
32+
// CPU instructions.
33+
//
34+
// On Linux with libc++ the default seems to be using `/dev/urandom`, but the
35+
// library could have been compiled [2] to use `getentropy(3)` [3],
36+
// `arc4random()` [4], or even `nacl` [5]. It does not seem like the library
37+
// uses the RDRAND or RDSEED instructions directly. In any case, it seems that
38+
// all the choices are good entropy sources.
39+
//
40+
// With MSVC the documentation says that the numbers are not deterministic,
41+
// and cryptographically secure, but no further details are available:
42+
// https://docs.microsoft.com/en-us/cpp/standard-library/random-device-class
43+
//
44+
// On macOS the library is libc++ implementation so the previous comments
45+
// apply.
46+
//
47+
// One would want to simply pass a `std::random_device` to the constructor for
48+
// the random bit generators, but the C++11 approach is annoying, see this
49+
// critique for the details:
50+
// http://www.pcg-random.org/posts/simple-portable-cpp-seed-entropy.html
51+
//
52+
// [1]: https://en.wikipedia.org/wiki/RDRAND
53+
// [2]: https://github.com/llvm-mirror/libcxx/blob/master/src/random.cpp
54+
// [3]: http://man7.org/linux/man-pages/man3/getentropy.3.html
55+
// [4]: https://linux.die.net/man/3/arc4random
56+
// [5]: https://en.wikipedia.org/wiki/NaCl_(software)
57+
//
58+
#if defined(__GLIBCXX__) && __GLIBCXX__ >= 20200128
59+
// Workaround for a libstd++ bug:
60+
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94087
61+
// we cannot simply use `rdrand` everywhere because this is library and
62+
// version specific, i.e., other standard C++ libraries do not support
63+
// `rdrand`, and even older versions of libstdc++ do not support `rdrand`.
64+
std::random_device rd("rdrand");
65+
#else
66+
std::random_device rd;
67+
#endif // defined(__GLIBCXX__) && __GLIBCXX__ >= 20200128
68+
69+
auto constexpr kWordSize = std::numeric_limits<unsigned int>::digits;
70+
auto const n = (desired_bits + kWordSize - 1) / kWordSize;
71+
std::vector<unsigned int> entropy(n);
72+
std::generate(entropy.begin(), entropy.end(), [&rd]() { return rd(); });
73+
return entropy;
74+
}
75+
2176
std::string Sample(DefaultPRNG& gen, int n, std::string const& population) {
2277
std::uniform_int_distribution<std::size_t> rd(0, population.size() - 1);
2378

google/cloud/internal/random.h

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_RANDOM_H
1717

1818
#include "google/cloud/version.h"
19-
#include <algorithm>
2019
#include <random>
2120

2221
namespace google {
2322
namespace cloud {
2423
inline namespace GOOGLE_CLOUD_CPP_NS {
2524
namespace internal {
25+
/**
26+
* Retrieve at least @p desired_bits of entropy from `std::random_device`.
27+
*/
28+
std::vector<unsigned int> FetchEntropy(std::size_t desired_bits);
29+
2630
// While std::mt19937_64 is not the best PRNG ever, it is fairly good for
2731
// most purposes. Please read:
2832
// http://www.pcg-random.org/
@@ -35,34 +39,14 @@ using DefaultPRNG = std::mt19937_64;
3539
*/
3640
template <typename Generator>
3741
Generator MakePRNG() {
38-
// We use the default C++ random device to generate entropy. The quality of
39-
// this source of entropy is implementation-defined. The version in
40-
// Linux with libstdc++ (the most common library on Linux) it is based on
41-
// either `/dev/urandom`, or (if available) the RDRAND CPU instruction.
42-
// http://en.cppreference.com/w/cpp/numeric/random/random_device/random_device
43-
// On Linux with libc++ it is also based on `/dev/urandom`, but it is not
44-
// known if it uses the RDRND CPU instruction (though `/dev/urandom` does).
45-
// On Windows the documentation says that the numbers are not deterministic,
46-
// and cryptographically secure, but no further details are available:
47-
// https://docs.microsoft.com/en-us/cpp/standard-library/random-device-class
48-
// One would want to simply pass this object to the constructor for the
49-
// generator, but the C++11 approach is annoying, see this critique for
50-
// the details:
51-
// http://www.pcg-random.org/posts/simple-portable-cpp-seed-entropy.html
52-
// Instead we will do a lot of work below.
53-
std::random_device rd;
54-
5542
// We need to get enough entropy to fully initialize the PRNG, that depends
5643
// on the size of its state. The size in bits is `Generator::state_size`.
5744
// We convert that to the number of `unsigned int` values; as this is what
5845
// std::random_device returns.
59-
auto const S =
60-
Generator::state_size *
61-
(Generator::word_size / std::numeric_limits<unsigned int>::digits);
46+
auto const desired_bits = Generator::state_size * Generator::word_size;
6247

6348
// Extract the necessary number of entropy bits.
64-
std::vector<unsigned int> entropy(S);
65-
std::generate(entropy.begin(), entropy.end(), [&rd]() { return rd(); });
49+
auto const entropy = FetchEntropy(desired_bits);
6650

6751
// Finally, put the entropy into the form that the C++11 PRNG classes want.
6852
std::seed_seq seq(entropy.begin(), entropy.end());

google/cloud/internal/random_test.cc

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414

1515
#include "google/cloud/internal/random.h"
1616
#include <gmock/gmock.h>
17+
#include <future>
18+
#include <vector>
1719

1820
using namespace google::cloud::internal;
1921

20-
TEST(BenchmarksRandom, Basic) {
22+
TEST(Random, Basic) {
2123
// This is not a statistical test for PRNG, basically we want to make
2224
// sure that MakeDefaultPRNG uses different seeds, or at least creates
2325
// different series:
@@ -29,3 +31,38 @@ TEST(BenchmarksRandom, Basic) {
2931
std::string s1 = gen_string();
3032
EXPECT_NE(s0, s1);
3133
}
34+
35+
#if GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS
36+
/**
37+
* @test verify that multiple threads can call MakeDefaultPRNG() simultaneously.
38+
*
39+
* This test verifies our code works around a bug in libstdc++:
40+
* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94087
41+
* When this bug is triggered, the standard library throws an exception and
42+
* the test would just crash (or not compile, as we use EXPECT_NO_THROW). It is
43+
* simpler to compile the test only when exceptions are enabled.
44+
*/
45+
TEST(Random, Threads) {
46+
auto constexpr kNumWorkers = 64;
47+
auto constexpr kIterations = 100;
48+
std::vector<std::future<int>> workers(kNumWorkers);
49+
50+
std::generate_n(workers.begin(), workers.size(), [&] {
51+
return std::async(std::launch::async, [&] {
52+
for (auto i = 0; i != kIterations; ++i) {
53+
auto g = MakeDefaultPRNG();
54+
(void)g();
55+
}
56+
return kIterations;
57+
});
58+
});
59+
60+
int count = 0;
61+
for (auto& f : workers) {
62+
SCOPED_TRACE("testing with worker " + std::to_string(count++));
63+
int result = 0;
64+
EXPECT_NO_THROW(result = f.get());
65+
EXPECT_EQ(result, kIterations);
66+
}
67+
}
68+
#endif // GOOGLE_CLOUD_CPP_HAVE_EXCEPTIONS

0 commit comments

Comments
 (0)