about summary refs log tree commit diff
path: root/src/libnm-glib-aux/nm-random-utils.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-08-12 19:21:11 +0200
committerMichael Biebl <biebl@debian.org>2022-08-12 19:21:11 +0200
commit6accbd3ec0e42d8633bbde4d47ed7bfe854e7e0b (patch)
treea77a0617213a5183a4de43824e3fbae9779e9b8e /src/libnm-glib-aux/nm-random-utils.c
parentf7c750061dab327e638c0129cfafd5b2215b3a2e (diff)
New upstream version 1.38.4 upstream/1.38.4
Diffstat (limited to 'src/libnm-glib-aux/nm-random-utils.c')
-rw-r--r--src/libnm-glib-aux/nm-random-utils.c340
1 files changed, 245 insertions, 95 deletions
diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c
index 3a8ba64f..93eee7c4 100644
--- a/src/libnm-glib-aux/nm-random-utils.c
+++ b/src/libnm-glib-aux/nm-random-utils.c
@@ -10,6 +10,7 @@
 #include <fcntl.h>
 #include <sys/auxv.h>
 #include <sys/syscall.h>
+#include <poll.h>
 
 #if USE_SYS_RANDOM_H
 #include <sys/random.h>
@@ -34,18 +35,121 @@
 #define GRND_INSECURE 0x04
 #endif
 
-#if !HAVE_GETRANDOM && defined(SYS_getrandom)
-static int
+#if !HAVE_GETRANDOM
+static ssize_t
 getrandom(void *buf, size_t buflen, unsigned flags)
 {
+#if defined(SYS_getrandom)
     return syscall(SYS_getrandom, buf, buflen, flags);
+#else
+    errno = ENOSYS;
+    return -1;
+#endif
 }
-#undef HAVE_GETRANDOM
-#define HAVE_GETRANDOM 1
 #endif
 
 /*****************************************************************************/
 
+static ssize_t
+_getrandom(void *buf, size_t buflen, unsigned flags)
+{
+    static int have_getrandom = TRUE;
+    ssize_t    l;
+    int        errsv;
+
+    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;
@@ -72,7 +176,8 @@ typedef struct _nm_packed {
         guint8  u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
         guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
     } rand_vals;
-    GRand *rand;
+    guint8 rand_vals_getrandom[16];
+    gint64 rand_vals_timestamp;
 } BadRandState;
 
 static void
@@ -106,18 +211,7 @@ _bad_random_init_seed(BadRandSeed *seed)
         memcpy(&seed->auxval, p_at_random, 16);
     }
 
-#if HAVE_GETRANDOM
-    {
-        ssize_t r;
-
-        /* This is likely to fail, because we already failed a moment earlier. Still, give
-         * it a try. */
-        r = getrandom(seed->getrandom_buf,
-                      sizeof(seed->getrandom_buf),
-                      GRND_INSECURE | GRND_NONBLOCK);
-        (void) r;
-    }
-#endif
+    _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();
@@ -168,6 +262,10 @@ _bad_random_bytes(guint8 *buf, gsize n)
             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;
 
@@ -197,103 +295,155 @@ _bad_random_bytes(guint8 *buf, gsize n)
     }
 }
 
+/*****************************************************************************/
+
 /**
- * nm_utils_random_bytes:
+ * nm_random_get_bytes_full:
  * @p: the buffer to fill
  * @n: the number of bytes to write to @p.
+ * @out_high_quality: (allow-none) (out): whether the returned
+ *   random bytes are of high quality.
  *
- * Uses getrandom() or reads /dev/urandom to fill the buffer
- * with random data. If all fails, as last fallback it uses
- * GRand to fill the buffer with pseudo random numbers.
- * The function always succeeds in writing some random numbers
- * to the buffer. The return value of FALSE indicates that the
- * obtained bytes are probably not of good randomness.
- *
- * Returns: whether the written bytes are good. If you
- * don't require good randomness, you can ignore the return
- * value.
- *
- * Note that if calling getrandom() fails because there is not enough
- * entropy (at early boot), the function will read /dev/urandom.
- * Which of course, still has low entropy, and cause kernel to log
- * a warning.
+ * - 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().
  */
-gboolean
-nm_utils_random_bytes(void *p, size_t n)
+void
+nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality)
 {
     int      fd;
     int      r;
-    gboolean has_high_quality = TRUE;
-    guint8  *buf              = p;
+    gboolean has_high_quality;
+    ssize_t  l;
 
-    g_return_val_if_fail(p, FALSE);
-    g_return_val_if_fail(n > 0, FALSE);
+    if (n == 0) {
+        NM_SET_OUT(out_high_quality, TRUE);
+        return;
+    }
 
-#if HAVE_GETRANDOM
-    {
-        static gboolean have_syscall = TRUE;
-
-        if (have_syscall) {
-            ssize_t r2;
-            int     errsv;
-
-            r2 = getrandom(buf, n, GRND_NONBLOCK);
-            if (r2 >= 0) {
-                if ((size_t) r2 == n)
-                    return TRUE;
-
-                /* no or partial read. There is not enough entropy.
-                 * Fill the rest reading with the fallback code and remember
-                 * that some bits are not high quality. */
-                nm_assert((size_t) r2 < n);
-                buf += r2;
-                n -= r2;
-
-                /* At this point, we don't want to read /dev/urandom, because
-                 * the entropy pool is low (early boot?), and asking for more
-                 * entropy causes kernel messages to be logged.
-                 *
-                 * Note that we fall back to _bad_random_bytes(), which (among others) seeds
-                 * itself with g_rand_new(). That also will read /dev/urandom, but as
-                 * we do that only once, we don't care. But in general, we are here in
-                 * a situation where we want to avoid reading /dev/urandom too much. */
-                goto out_bad_random;
-            }
-            errsv = errno;
-            if (errsv == ENOSYS) {
-                /* no support for getrandom(). We don't know whether
-                 * we /dev/urandom will give us good quality. Assume yes. */
-                have_syscall = FALSE;
-            } else if (errsv == EAGAIN) {
-                /* No entropy. We avoid reading /dev/urandom. */
-                goto out_bad_random;
-            } else {
-                /* Unknown error, likely no entropy. We'll read /dev/urandom below, but we don't
-                 * have high-quality randomness. */
-                has_high_quality = FALSE;
+    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;
             }
         }
     }
-#endif
 
-fd_open:
+again_open:
     fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
     if (fd < 0) {
         if (errno == EINTR)
-            goto fd_open;
-        goto out_bad_random;
+            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;
+        }
     }
-    r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE);
-    nm_close(fd);
-    if (r >= 0)
-        return has_high_quality;
 
-out_bad_random:
     /* we failed to fill the bytes reading from /dev/urandom.
-     * Fill the bits using our pseudo random numbers.
-     *
-     * We don't have good quality.
+     * Fill the bits using our fallback approach (which obviously
+     * cannot give high quality random).
      */
-    _bad_random_bytes(buf, n);
-    return FALSE;
+    _bad_random_bytes(p, n);
+    NM_SET_OUT(out_high_quality, FALSE);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_random_get_crypto_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)
+{
+    nm_auto_close int fd = -1;
+    ssize_t           l;
+    int               r;
+
+    if (n == 0)
+        return 0;
+
+    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);
 }