nm-random-utils: always generate good random bytes and prioritize getrandom support

The current mess of code seems like a hodgepodge of complex ideas,
partially copied from systemd, but then subtly different, and it's a
mess. Let's simplify this drastically.

First, assume that getrandom() is always available. If the kernel is too
old, we have an unoptimized slowpath for still supporting ancient
kernels, a path that should be removed at some point. If getrandom()
isn't available and the fallback path doesn't work, the system has much
larger problems, so just crash. This should basically never happen.
getrandom() and having randomness available in general is a critical
system API that should be expected to be available on any functioning
system.

Second, assume that the rng is initialized, so that asking for random
numbers should never block. This is virtually always true on modern
kernels. On ancient kernels, it usually becomes true. But, more
importantly, this is not the responsibility of various daemons, even
ones that run at boot. Instead, this is something for the kernel and/or
init to ensure.

Putting these together, we adopt new behavior:

- First, try getrandom(..., ..., 0). The 0 flags field means that this
  call will only return good random bytes, not insecure ones.

- If this fails for some reason that isn't ENOSYS, crash.

- If this fails due to ENOSYS, poll on /dev/random until 1 byte is
  available, suggesting that subsequent reads from the rng will almost
  have good random bytes. If this fails, crash. Then, read from
  /dev/urandom. If this fails, crash.

We don't bother caching when getrandom() returns ENOSYS. We don't apply
any other fancy optimizations to the slow fallback path. We keep that as
barebones and minimal as we can. It works. It's for ancient kernels. It
should be removed soon. It's not worth spending cycles over. Instead,
the goal is to eventually reduce all of this down to a simple boring
call to getrandom(..., ..., 0).

https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/2127
This commit is contained in:
Jason A. Donenfeld
2025-02-05 03:06:06 +01:00
committed by Lubomir Rintel
parent 2fd34e1dec
commit c627bbea4c
6 changed files with 71 additions and 438 deletions

View File

@@ -988,9 +988,8 @@ announce_router(NMNDisc *ndisc)
/* Schedule next initial announcement retransmit. */
priv->send_ra_id =
g_timeout_add_seconds(nm_random_u64_range_full(NM_NDISC_ROUTER_ADVERT_DELAY,
NM_NDISC_ROUTER_ADVERT_INITIAL_INTERVAL,
FALSE),
g_timeout_add_seconds(nm_random_u64_range(NM_NDISC_ROUTER_ADVERT_DELAY,
NM_NDISC_ROUTER_ADVERT_INITIAL_INTERVAL),
(GSourceFunc) announce_router,
ndisc);
} else {
@@ -1024,9 +1023,10 @@ announce_router_initial(NMNDisc *ndisc)
/* Schedule the initial send rather early. Clamp the delay by minimal
* delay and not the initial advert internal so that we start fast. */
if (G_LIKELY(!priv->send_ra_id)) {
priv->send_ra_id = g_timeout_add_seconds(nm_random_u64_range(NM_NDISC_ROUTER_ADVERT_DELAY),
(GSourceFunc) announce_router,
ndisc);
priv->send_ra_id =
g_timeout_add_seconds(nm_random_u64_range(0, NM_NDISC_ROUTER_ADVERT_DELAY),
(GSourceFunc) announce_router,
ndisc);
}
}
@@ -1042,7 +1042,7 @@ announce_router_solicited(NMNDisc *ndisc)
nm_clear_g_source(&priv->send_ra_id);
if (!priv->send_ra_id) {
priv->send_ra_id = g_timeout_add(nm_random_u64_range(NM_NDISC_ROUTER_ADVERT_DELAY_MS),
priv->send_ra_id = g_timeout_add(nm_random_u64_range(0, NM_NDISC_ROUTER_ADVERT_DELAY_MS),
(GSourceFunc) announce_router,
ndisc);
}

View File

@@ -2834,10 +2834,7 @@ _host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
int base64_save = 0;
gsize len;
if (nm_random_get_crypto_bytes(rnd_buf, sizeof(rnd_buf)) < 0)
nm_random_get_bytes_full(rnd_buf, sizeof(rnd_buf), &success);
else
success = TRUE;
nm_random_get_bytes(rnd_buf, sizeof(rnd_buf));
/* Our key is really binary data. But since we anyway generate a random seed
* (with 32 random bytes), don't write it in binary, but instead create
@@ -2858,12 +2855,9 @@ _host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
secret_arr = _host_id_hash_v2(new_content, len, sha256_digest);
secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
success = TRUE;
if (!success)
nm_log_warn(LOGD_CORE,
"secret-key: failure to generate good random data for secret-key (use "
"non-persistent key)");
else if (nm_utils_get_testing()) {
if (nm_utils_get_testing()) {
/* for test code, we don't write the generated secret-key to disk. */
} else if (!nm_utils_file_set_contents(SECRET_KEY_FILE,
(const char *) new_content,
@@ -3738,7 +3732,7 @@ _hw_addr_eth_complete(struct ether_addr *addr,
nm_assert((ouis == NULL) ^ (ouis_len != 0));
if (ouis) {
oui = ouis[nm_random_u64_range(ouis_len)];
oui = ouis[nm_random_u64_range(0, ouis_len)];
g_free(ouis);
} else {
if (!nm_utils_hwaddr_aton(current_mac_address, &oui, ETH_ALEN))

View File

@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2017 Red Hat, Inc.
* Copyright (C) 2025 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
@@ -51,420 +52,85 @@ getrandom(void *buf, size_t buflen, unsigned flags)
/*****************************************************************************/
static ssize_t
_getrandom(void *buf, size_t buflen, unsigned flags)
getrandom_full(void *buf, size_t count, unsigned flags)
{
static int have_getrandom = TRUE;
ssize_t l;
int errsv;
ssize_t ret;
uint8_t *p = buf;
nm_assert(buflen > 0);
/* This calls getrandom() and either returns the positive
* success or an negative errno. ENOSYS means getrandom()
* call is not supported. That result is cached and we don't retry. */
if (!have_getrandom)
return -ENOSYS;
l = getrandom(buf, buflen, flags);
if (l > 0)
return l;
if (l == 0)
return -EIO;
errsv = errno;
if (errsv == ENOSYS)
have_getrandom = FALSE;
return -errsv;
}
static ssize_t
_getrandom_insecure(void *buf, size_t buflen)
{
static int have_grnd_insecure = TRUE;
ssize_t l;
/* GRND_INSECURE was added recently. We catch EINVAL
* if kernel does not support the flag (and cache it). */
if (!have_grnd_insecure)
return -EINVAL;
l = _getrandom(buf, buflen, GRND_INSECURE);
if (l == -EINVAL)
have_grnd_insecure = FALSE;
return l;
}
static ssize_t
_getrandom_best_effort(void *buf, size_t buflen)
{
ssize_t l;
/* To get best-effort bytes, we would use GRND_INSECURE (and we try that
* first). However, not all kernel versions support that, so we fallback
* to GRND_NONBLOCK.
*
* Granted, this is called from a fallback path where we have no entropy
* already, it's unlikely that GRND_NONBLOCK would succeed. Still... */
l = _getrandom_insecure(buf, buflen);
if (l != -EINVAL)
return l;
return _getrandom(buf, buflen, GRND_NONBLOCK);
}
static int
_random_check_entropy(gboolean block)
{
static gboolean seen_high_quality = FALSE;
nm_auto_close int fd = -1;
int r;
/* We come here because getrandom() gave ENOSYS. We will fallback to /dev/urandom,
* but the caller wants to know whether we have high quality numbers. Poll
* /dev/random to find out. */
if (seen_high_quality) {
/* We cache the positive result. Once kernel has entropy, we will get
* good random numbers. */
return 1;
}
fd = open("/dev/random", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0)
return -errno;
r = nm_utils_fd_wait_for_event(fd, POLLIN, block ? -1 : 0);
if (r <= 0) {
nm_assert(r < 0 || !block);
return r;
}
nm_assert(r == 1);
seen_high_quality = TRUE;
return 1;
}
/*****************************************************************************/
typedef struct _nm_packed {
uintptr_t heap_ptr;
uintptr_t stack_ptr;
gint64 now_bootime;
gint64 now_real;
pid_t pid;
pid_t ppid;
pid_t tid;
guint32 grand[16];
guint8 auxval[16];
guint8 getrandom_buf[20];
} BadRandSeed;
typedef struct _nm_packed {
guint64 counter;
union {
guint8 full[NM_UTILS_CHECKSUM_LENGTH_SHA256];
struct {
guint8 half_1[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
guint8 half_2[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
};
} sha_digest;
union {
guint8 u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
} rand_vals;
guint8 rand_vals_getrandom[16];
gint64 rand_vals_timestamp;
} BadRandState;
static void
_bad_random_init_seed(BadRandSeed *seed)
{
const guint8 *p_at_random;
int seed_idx;
GRand *rand;
/* g_rand_new() reads /dev/urandom too, but we already know that
* /dev/urandom fails to give us good randomness (which is why
* we hit the "bad random" code path). So this may not be as
* good as we wish, but let's hope that it it does something smart
* to give some extra entropy... */
rand = g_rand_new();
/* Get some seed material from a GRand. */
for (seed_idx = 0; seed_idx < (int) G_N_ELEMENTS(seed->grand); seed_idx++)
seed->grand[seed_idx] = g_rand_int(rand);
/* Add an address from the heap and stack, maybe ASLR helps a bit? */
seed->heap_ptr = (uintptr_t) ((gpointer) rand);
seed->stack_ptr = (uintptr_t) ((gpointer) &rand);
g_rand_free(rand);
/* Add the per-process, random number. */
p_at_random = ((gpointer) getauxval(AT_RANDOM));
if (p_at_random) {
G_STATIC_ASSERT(sizeof(seed->auxval) == 16);
memcpy(&seed->auxval, p_at_random, 16);
}
_getrandom_best_effort(seed->getrandom_buf, sizeof(seed->getrandom_buf));
seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
seed->now_real = g_get_real_time();
seed->pid = getpid();
seed->ppid = getppid();
seed->tid = nm_utils_gettid();
do {
ret = getrandom(p, count, flags);
if (ret < 0 && errno == EINTR)
continue;
else if (ret < 0)
return ret;
p += ret;
count -= ret;
} while (count);
return 0;
}
static void
_bad_random_bytes(guint8 *buf, gsize n)
dev_random_wait(void)
{
nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256);
static bool has_waited = false;
struct pollfd random_fd = {.events = POLLIN};
int ret;
nm_assert(n > 0);
/* We are in the fallback code path, where getrandom() (and /dev/urandom) failed
* to give us good randomness. Try our best.
*
* Our ability to get entropy for the CPRNG is very limited and thus the overall
* result will be bad randomness.
*
* Once we have some seed material, we combine GRand (which is not a cryptographically
* secure PRNG) with some iterative sha256 hashing. It would be nice if we had
* easy access to chacha20, but it's probably more cumbersome to fork those
* implementations than hack a bad CPRNG by using sha256 hashing. After all, this
* is fallback code to get *some* bad randomness. And with the inability to get a good
* seed, any CPRNG can only give us bad randomness. */
{
static BadRandState gl_state;
static GRand *gl_rand;
static GMutex gl_mutex;
NM_G_MUTEX_LOCKED(&gl_mutex);
if (G_UNLIKELY(!gl_rand)) {
union {
BadRandSeed d_seed;
guint32 d_u32[(sizeof(BadRandSeed) + 3) / 4];
} data = {
.d_u32 = {0},
};
_bad_random_init_seed(&data.d_seed);
gl_rand = g_rand_new_with_seed_array(data.d_u32, G_N_ELEMENTS(data.d_u32));
g_checksum_update(sum, (const guchar *) &data, sizeof(data));
nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
}
_getrandom_best_effort(gl_state.rand_vals_getrandom, sizeof(gl_state.rand_vals_getrandom));
gl_state.rand_vals_timestamp = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
while (TRUE) {
int i;
gl_state.counter++;
for (i = 0; i < G_N_ELEMENTS(gl_state.rand_vals.u32); i++)
gl_state.rand_vals.u32[i] = g_rand_int(gl_rand);
g_checksum_reset(sum);
g_checksum_update(sum, (const guchar *) &gl_state, sizeof(gl_state));
nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
/* gl_state.sha_digest.full and gl_state.rand_vals contain now our
* bad random values, but they are also the state for the next iteration.
* We must not directly expose that state to the caller, so XOR the values.
*
* That means, per iteration we can generate 16 bytes of bad randomness. That
* is suitable to initialize a random UUID. */
for (i = 0; i < (int) (NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2); i++) {
nm_assert(n > 0);
buf[0] = gl_state.sha_digest.half_1[i] ^ gl_state.sha_digest.half_2[i]
^ gl_state.rand_vals.u8[i];
buf++;
n--;
if (n == 0)
return;
}
}
}
}
/*****************************************************************************/
/**
* nm_random_get_bytes_full:
* @p: the buffer to fill
* @n: the number of bytes to write to @p.
* @out_high_quality: (out) (optional): whether the returned
* random bytes are of high quality.
*
* - will never block
* - will always produce some numbers, but they may not
* be of high quality.
* - Whether they are of high quality, you can know via @out_high_quality.
* - will always try hard to produce high quality numbers, and on success
* they are as good as nm_random_get_crypto_bytes().
*/
void
nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality)
{
int fd;
int r;
gboolean has_high_quality;
ssize_t l;
if (n == 0) {
NM_SET_OUT(out_high_quality, TRUE);
if (has_waited)
return;
random_fd.fd = open("/dev/random", O_RDONLY);
nm_assert(random_fd.fd >= 0);
for (;;) {
ret = poll(&random_fd, 1, -1);
if (ret == 1)
break;
nm_assert(ret == -1 && errno == EINTR);
}
g_return_if_fail(p);
again_getrandom:
l = _getrandom(p, n, GRND_NONBLOCK);
if (l > 0) {
if ((size_t) l == n) {
NM_SET_OUT(out_high_quality, TRUE);
return;
}
p = ((uint8_t *) p) + l;
n -= l;
goto again_getrandom;
}
/* getrandom() failed. Fallback to read /dev/urandom. */
if (l == -ENOSYS) {
/* no support for getrandom(). */
if (out_high_quality) {
/* The caller wants to know whether we have high quality. Poll /dev/random
* to find out. */
has_high_quality = (_random_check_entropy(FALSE) > 0);
} else {
/* The value doesn't matter in this case. It will be unused. */
has_high_quality = FALSE;
}
} else {
/* Any other failure of getrandom() means we don't have high quality. */
has_high_quality = FALSE;
if (l == -EAGAIN) {
/* getrandom(GRND_NONBLOCK) failed because lack of entropy. Retry with GRND_INSECURE. */
for (;;) {
l = _getrandom_insecure(p, n);
if (l > 0) {
if ((size_t) l == n) {
NM_SET_OUT(out_high_quality, FALSE);
return;
}
p = ((uint8_t *) p) + l;
n -= l;
continue;
}
/* Any error. Fallback to /dev/urandom. */
break;
}
}
}
again_open:
fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0) {
if (errno == EINTR)
goto again_open;
} else {
r = nm_utils_fd_read_loop_exact(fd, p, n, TRUE);
nm_close(fd);
if (r >= 0) {
NM_SET_OUT(out_high_quality, has_high_quality);
return;
}
}
/* we failed to fill the bytes reading from /dev/urandom.
* Fill the bits using our fallback approach (which obviously
* cannot give high quality random).
*/
_bad_random_bytes(p, n);
NM_SET_OUT(out_high_quality, FALSE);
nm_close(random_fd.fd);
has_waited = true;
}
/*****************************************************************************/
static ssize_t
dev_urandom_read_full(void *buf, size_t count)
{
nm_auto_close int fd = open("/dev/urandom", O_RDONLY);
nm_assert(fd >= 0);
return nm_utils_fd_read_loop_exact(fd, buf, count, FALSE);
}
/**
* nm_random_get_crypto_bytes:
* nm_random_get_bytes:
* @p: the buffer to fill
* @n: the number of bytes to fill
*
* - can fail (in which case a negative number is returned
* and the output buffer is undefined).
* - will block trying to get high quality random numbers.
*/
int
nm_random_get_crypto_bytes(void *p, size_t n)
void
nm_random_get_bytes(void *p, size_t n)
{
nm_auto_close int fd = -1;
ssize_t l;
int r;
ssize_t ret;
if (n == 0)
return 0;
ret = getrandom_full(p, n, 0);
if (ret == 0)
return;
nm_assert(ret == 0 || (ret == -1 && errno == ENOSYS));
nm_assert(p);
again_getrandom:
l = _getrandom(p, n, 0);
if (l > 0) {
if ((size_t) l == n)
return 0;
p = (uint8_t *) p + l;
n -= l;
goto again_getrandom;
}
if (l != -ENOSYS) {
/* We got a failure, but getrandom seems to be working in principle. We
* won't get good numbers. Fail. */
return l;
}
/* getrandom() failed with ENOSYS. Fallback to reading /dev/urandom. */
r = _random_check_entropy(TRUE);
if (r < 0)
return r;
if (r == 0)
return nm_assert_unreachable_val(-EIO);
fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0)
return -errno;
return nm_utils_fd_read_loop_exact(fd, p, n, FALSE);
dev_random_wait();
ret = dev_urandom_read_full(p, n);
nm_assert(ret == 0);
}
/*****************************************************************************/
guint64
nm_random_u64_range_full(guint64 begin, guint64 end, gboolean crypto_bytes)
nm_random_u64_range(guint64 begin, guint64 end)
{
gboolean bad_crypto_bytes = FALSE;
guint64 remainder;
guint64 maxvalue;
guint64 x;
guint64 m;
guint64 remainder;
guint64 maxvalue;
guint64 x;
guint64 m;
/* Returns a random #guint64 equally distributed in the range [@begin..@end-1].
*
* The function always set errno. It either sets it to zero or to EAGAIN
* (if crypto_bytes were requested but not obtained). In any case, the function
* will always return a random number in the requested range (worst case, it's
* not crypto_bytes despite being requested). Check errno if you care. */
/* Returns a random #guint64 equally distributed in the range [@begin..@end-1]. */
if (begin >= end) {
/* systemd's random_u64_range(0) is an alias for nm_random_u64().
@@ -483,19 +149,9 @@ nm_random_u64_range_full(guint64 begin, guint64 end, gboolean crypto_bytes)
maxvalue = G_MAXUINT64 - remainder;
do
if (crypto_bytes) {
if (nm_random_get_crypto_bytes(&x, sizeof(x)) < 0) {
/* Cannot get good crypto numbers. We will try our best, but fail
* and set errno below. */
crypto_bytes = FALSE;
bad_crypto_bytes = TRUE;
continue;
}
} else
nm_random_get_bytes(&x, sizeof(x));
nm_random_get_bytes(&x, sizeof(x));
while (x >= maxvalue);
out:
errno = bad_crypto_bytes ? EAGAIN : 0;
return begin + (x % m);
}

View File

@@ -6,15 +6,7 @@
#ifndef __NM_RANDOM_UTILS_H__
#define __NM_RANDOM_UTILS_H__
void nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality);
static inline void
nm_random_get_bytes(void *p, size_t n)
{
nm_random_get_bytes_full(p, n, NULL);
}
int nm_random_get_crypto_bytes(void *p, size_t n);
void nm_random_get_bytes(void *p, size_t n);
static inline guint32
nm_random_u32(void)
@@ -43,12 +35,6 @@ nm_random_bool(void)
return ch % 2u;
}
guint64 nm_random_u64_range_full(guint64 begin, guint64 end, gboolean crypto_bytes);
static inline guint64
nm_random_u64_range(guint64 end)
{
return nm_random_u64_range_full(0, end, FALSE);
}
guint64 nm_random_u64_range(guint64 begin, guint64 end);
#endif /* __NM_RANDOM_UTILS_H__ */

View File

@@ -197,10 +197,7 @@ test_nm_random(void)
if (begin >= end)
continue;
if (begin == 0 && nmtst_get_rand_bool())
x = nm_random_u64_range(end);
else
x = nm_random_u64_range_full(begin, end, nmtst_get_rand_bool());
x = nm_random_u64_range(begin, end);
g_assert_cmpuint(x, >=, begin);
g_assert_cmpuint(x, <, end);

View File

@@ -4113,7 +4113,7 @@ generate_wpa_key(char *key, size_t len)
int c;
do {
c = nm_random_u64_range_full(48, 122, TRUE);
c = nm_random_u64_range(48, 122);
/* skip characters that look similar */
} while (NM_IN_SET(c, '1', 'l', 'I', '0', 'O', 'Q', '8', 'B', '5', 'S')
|| !g_ascii_isalnum(c));
@@ -4136,7 +4136,7 @@ generate_wep_key(char *key, size_t len)
for (i = 0; i < 10; i++) {
int digit;
digit = nm_random_u64_range_full(0, 16, TRUE);
digit = nm_random_u64_range(0, 16);
key[i] = hexdigits[digit];
}
key[10] = '\0';