Given a CSPRNG (arc4random, using ChaCha20 keystream), one want to use rejection sampling to get random number on a smaller range avoiding modulo bias (arc4random_uniform()).
OpenBSD's arc4random_uniform() implementation draws a 32bits value and tests its fitness, rejecting values that don't fit.
It's not the only possible implementation. In Efficiently Generating a Number in a Range, M.E. O'Neill provides various other ways to generate random number within a range while avoiding the modulo bias.
In particular, there's this so called Bitmask with Rejection (Unbiased) — Apple's Method. Following the link to the actual Apple's implementation, one can see it has another trick:
/*
* Calculate a uniformly distributed random number less than upper_bound
* avoiding "modulo bias".
*
* Uniformity is achieved by trying successive ranges of bits from the random
* value, each large enough to hold the desired upper bound, until a range
* holding a value less than the bound is found.
*/
uint32_t
arc4random_uniform(uint32_t upper_bound)
{
if (upper_bound < 2)
return 0;
// find smallest 2**n -1 >= upper_bound
int zeros = __builtin_clz(upper_bound);
int bits = CHAR_BIT * sizeof(uint32_t) - zeros;
uint32_t mask = 0xFFFFFFFFU >> zeros;
do {
uint32_t value = arc4random();
// If low 2**n-1 bits satisfy the requested condition, return result
uint32_t result = value & mask;
if (result < upper_bound) {
return result;
}
// otherwise consume remaining bits of randomness looking for a satisfactory result.
int bits_left = zeros;
while (bits_left >= bits) {
value >>= bits;
result = value & mask;
if (result < upper_bound) {
return result;
}
bits_left -= bits;
}
} while (1);
}
Instead of drawing a new value if the current one doesn't fit the range, the value is shifted to discard any bits involved in the fit test, until the value fits or there's not enough unused bit left.
This is especially useful if the CSPRNG is costly, for example, if it requires a system call to generate one value, reducing the amount of time it's invoked, while algorithmically complex, might improve arc4random_uniform() overall speed.
And now my question: given a 32bits value generated by a CSPRNG, given the CSPRNG output is indiscernible from a random stream, that every value is as likely as any other, why discarding every tested bits ?
- Is it possible to shift the value by one bit at a time and test the resulting value against the range ?
do {
uint32_t value = arc4random();
for (int bits_left = 32; bits_left >= bits ; bits_left--) {
uint32_t result = value & mask;
if (result < upper_bound) {
return result;
value = value >> 1; // shift right by one bit
}
} while (1);
- Is it possible to rotate the value by one bit at a time and test the resulting value against the range ?
do {
uint32_t value = arc4random();
for (int i = 0; i < 32; i++) {
uint32_t result = value & mask;
if (result < upper_bound) {
return result;
value = ror32(value, 1); // rotate right by one bit
}
} while (1);
Are those optimization safe to build arc4random_uniform ?
If not, on what basis ?
arc4random_uniform()
implementation: see sourceware.org/pipermail/libc-alpha/2022-August/141184.html $\endgroup$