]> git.proxmox.com Git - mirror_spl.git/commitdiff
random_get_pseudo_bytes() need not provide cryptographic strength entropy
authorRichard Yao <ryao@gentoo.org>
Fri, 11 Jul 2014 22:36:28 +0000 (18:36 -0400)
committerBrian Behlendorf <behlendorf1@llnl.gov>
Wed, 17 Feb 2016 17:49:09 +0000 (09:49 -0800)
Perf profiling of dd on a zvol revealed that my system spent 3.16% of
its time in random_get_pseudo_bytes(). No SPL consumers need
cryptographic strength entropy, so we can reduce our overhead by
changing the implementation to utilize a fast PRNG.

The Linux kernel did not export a suitable PRNG function until it
exported get_random_int() in Linux 3.10. While we could implement an
autotools check so that we use it when it is available or even try to
access the symbol on older kernels where it is not exported using the
fact that it is exported on newer ones as justification, we can instead
implement our own pseudo-random data generator. For this purpose, I have
written one based on a 128-bit pseudo-random number generator proposed
in a paper by Sebastiano Vigna that itself was based on work by the late
George Marsaglia.

http://vigna.di.unimi.it/ftp/papers/xorshiftplus.pdf

Profiling the same benchmark with an earlier variant of this patch that
used a slightly different generator (roughly same number of
instructions) by the same author showed that time spent in
random_get_pseudo_bytes() dropped to 0.06%. That is a factor of 50
improvement. This particular generator algorithm is also well known to
be fast:

http://xorshift.di.unimi.it/#speed

The benchmark numbers there state that it runs at 1.12ns/64-bits or 7.14
GBps of throughput on an Intel Core i7-4770 in what is presumably a
single-threaded context. Using it in `random_get_pseudo_bytes()` in the
manner I have will probably not reach that level of performance, but it
should be fairly high and many times higher than the Linux
`get_random_bytes()` function that we use now, which runs at 16.3 MB/s
on my Intel Xeon E3-1276v3 processor when measured by using dd on
/dev/urandom.

Also, putting this generator's seed into per-CPU variables allows us to
eliminate overhead from both spin locks and CPU memory barriers, which
is NUMA friendly.

We could have alternatively modified consumers to use something like
`gethrtime() % 3` as suggested by both Matthew Ahrens and Tim Chase, but
that has a few potential problems that this approach avoids:

1. Switching to `gethrtime() % 3` in hot code paths today requires
diverging from illumos-gate and does nothing about potential future
patches from illumos-gate that call our slow `random_get_pseudo_bytes()`
in different hot code paths. Reimplementing `random_get_pseudo_bytes()`
with a per-CPU PRNG avoids both of those things entirely, which means
less work for us in the future.

2.  Looking at the code that implements `gethrtime()`, I think it is
unlikely to be faster than this per-CPU PRNG implementation of
`random_get_pseudo_bytes()`. It would be best to go with something fast
now so that there is no point in revisiting this from a performance
perspective.

3. `gethrtime() % 3` can vary in behavior from system to system based on
kernel version, architecture and clock source. In comparison, this
per-CPU PRNG is about ~40 lines of code in `random_get_pseudo_bytes()`
that should behave consistently across all systems regardless of kernel
version, system architecture or machine clock source. It is unlikely
that we would ever need to revisit this per-CPU PRNG while the same
cannot be said for `gethrtime() % 3`.

4. `gethrtime()` uses CPU memory barriers and maybe atomic instructions
depending on the clock source, so replacing `random_get_pseudo_bytes()`
with `gethrtime()` in hot code paths could still require a future person
working on NUMA scalability to reimplement it anyway while this per-CPU
PRNG would not by virtue of using neither CPU memory barriers nor atomic
instructions. Note that I did not check various clock sources for the
presence of atomic instructions. There is simply too much code to read
and given the drawbacks versus this per-cpu PRNG, there is no point in
being certain.

5. I have heard of instances where poor quality pseudo-random numbers
caused problems for HPC code in ways that took more than a year to
identify and were remedied by switching to a higher quality source of
pseudo-random numbers. While filesystems are different than HPC code, I
do not think it is impossible for us to have instances where poor
quality pseudo-random numbers can cause problems. Opting for a well
studied PRNG algorithm that passes tests for statistical randomness over
changing callers to use `gethrtime() % 3` bypasses the need to think
about both whether poor quality pseudo-random numbers can cause problems
and the statistical quality of numbers from `gethrtime() % 3`.

6. `gethrtime()` calls `getrawmonotonic()`, which uses seqlocks. This is
probably not a huge issue, but anyone using kgdb would never be able to
step through a seqlock critical section, which is not a problem either
now or with the per-CPU PRNG:

https://en.wikipedia.org/wiki/Seqlock

The only downside that I can see is that this code's memory requirement
is O(N) where N is NR_CPUS, versus the current code and `gethrtime() %
3`, which are O(1), but that should not be a problem. The seeds will use
64KB of memory at the high end (i.e `NR_CPU == 4096`) and 16 bytes of
memory at the low end (i.e. `NR_CPU == 1`).  In either case, we should
only use a few hundred bytes of code for text, especially since
`spl_rand_jump()` should be inlined into `spl_random_init()`, which
should be removed during early boot as part of "Freeing unused kernel
memory". In either case, the memory requirements are minuscule.

Signed-off-by: Richard Yao <ryao@gentoo.org>
Signed-off-by: Brian Behlendorf <behlendorf1@llnl.gov>
Signed-off-by: Tim Chase <tim@chase2k.com>
Closes #372

include/sys/random.h
module/spl/spl-generic.c

index 2bf581f2651f600e2f3a978ceea38346ba0ace51..64f70ee52f4a000d5543b6438267d64724c55af5 100644 (file)
@@ -35,11 +35,6 @@ random_get_bytes(uint8_t *ptr, size_t len)
        return 0;
 }
 
-static __inline__ int
-random_get_pseudo_bytes(uint8_t *ptr, size_t len)
-{
-       get_random_bytes((void *)ptr,(int)len);
-       return 0;
-}
+extern int random_get_pseudo_bytes(uint8_t *ptr, size_t len);
 
 #endif /* _SPL_RANDOM_H */
index dc3e74aa5042a42fbcc4b4093727308a42503454..88a0fcc512ad4ba11b03084d154ae5c307e3232d 100644 (file)
@@ -41,6 +41,8 @@
 #include <sys/kstat.h>
 #include <sys/file.h>
 #include <linux/ctype.h>
+#include <sys/disp.h>
+#include <sys/random.h>
 #include <linux/kmod.h>
 #include <linux/math64_compat.h>
 #include <linux/proc_compat.h>
@@ -56,6 +58,112 @@ MODULE_PARM_DESC(spl_hostid, "The system hostid.");
 proc_t p0 = { 0 };
 EXPORT_SYMBOL(p0);
 
+/*
+ * Xorshift Pseudo Random Number Generator based on work by Sebastiano Vigna
+ *
+ * "Further scramblings of Marsaglia's xorshift generators"
+ * http://vigna.di.unimi.it/ftp/papers/xorshiftplus.pdf
+ *
+ * random_get_pseudo_bytes() is an API function on Illumos whose sole purpose
+ * is to provide bytes containing random numbers. It is mapped to /dev/urandom
+ * on Illumos, which uses a "FIPS 186-2 algorithm". No user of the SPL's
+ * random_get_pseudo_bytes() needs bytes that are of cryptographic quality, so
+ * we can implement it using a fast PRNG that we seed using Linux' actual
+ * equivalent to random_get_pseudo_bytes(). We do this by providing each CPU
+ * with an independent seed so that all calls to random_get_pseudo_bytes() are
+ * free of atomic instructions.
+ *
+ * A consequence of using a fast PRNG is that using random_get_pseudo_bytes()
+ * to generate words larger than 128 bits will paradoxically be limited to
+ * `2^128 - 1` possibilities. This is because we have a sequence of `2^128 - 1`
+ * 128-bit words and selecting the first will implicitly select the second. If
+ * a caller finds this behavior undesireable, random_get_bytes() should be used
+ * instead.
+ *
+ * XXX: Linux interrupt handlers that trigger within the critical section
+ * formed by `s[1] = xp[1];` and `xp[0] = s[0];` and call this function will
+ * see the same numbers. Nothing in the code currently calls this in an
+ * interrupt handler, so this is considered to be okay. If that becomes a
+ * problem, we could create a set of per-cpu variables for interrupt handlers
+ * and use them when in_interrupt() from linux/preempt_mask.h evaluates to
+ * true.
+ */
+static DEFINE_PER_CPU(uint64_t[2], spl_pseudo_entropy);
+
+/*
+ * spl_rand_next()/spl_rand_jump() are copied from the following CC-0 licensed
+ * file:
+ *
+ * http://xorshift.di.unimi.it/xorshift128plus.c
+ */
+
+static inline uint64_t
+spl_rand_next(uint64_t *s) {
+       uint64_t s1 = s[0];
+       const uint64_t s0 = s[1];
+       s[0] = s0;
+       s1 ^= s1 << 23; // a
+       s[1] = s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5); // b, c
+       return (s[1] + s0);
+}
+
+static inline void
+spl_rand_jump(uint64_t *s) {
+       static const uint64_t JUMP[] = { 0x8a5cd789635d2dff, 0x121fd2155c472f96 };
+
+       uint64_t s0 = 0;
+       uint64_t s1 = 0;
+       int i, b;
+       for(i = 0; i < sizeof JUMP / sizeof *JUMP; i++)
+               for(b = 0; b < 64; b++) {
+                       if (JUMP[i] & 1ULL << b) {
+                               s0 ^= s[0];
+                               s1 ^= s[1];
+                       }
+                       (void) spl_rand_next(s);
+               }
+
+       s[0] = s0;
+       s[1] = s1;
+}
+
+int
+random_get_pseudo_bytes(uint8_t *ptr, size_t len)
+{
+       uint64_t *xp, s[2];
+
+       ASSERT(ptr);
+
+       xp = get_cpu_var(spl_pseudo_entropy);
+
+       s[0] = xp[0];
+       s[1] = xp[1];
+
+       while (len) {
+               union {
+                       uint64_t ui64;
+                       uint8_t byte[sizeof (uint64_t)];
+               }entropy;
+               int i = MIN(len, sizeof (uint64_t));
+
+               len -= i;
+               entropy.ui64 = spl_rand_next(s);
+
+               while (i--)
+                       *ptr++ = entropy.byte[i];
+       }
+
+       xp[0] = s[0];
+       xp[1] = s[1];
+
+       put_cpu_var(spl_pseudo_entropy);
+
+       return (0);
+}
+
+
+EXPORT_SYMBOL(random_get_pseudo_bytes);
+
 #if BITS_PER_LONG == 32
 /*
  * Support 64/64 => 64 division on a 32-bit platform.  While the kernel
@@ -502,6 +610,44 @@ spl_kvmem_init(void)
        return (rc);
 }
 
+/*
+ * We initialize the random number generator with 128 bits of entropy from the
+ * system random number generator. In the improbable case that we have a zero
+ * seed, we fallback to the system jiffies, unless it is also zero, in which
+ * situation we use a preprogrammed seed. We step forward by 2^64 iterations to
+ * initialize each of the per-cpu seeds so that the sequences generated on each
+ * CPU are guaranteed to never overlap in practice.
+ */
+static void __init
+spl_random_init(void)
+{
+       uint64_t s[2];
+       int i;
+
+       get_random_bytes(s, sizeof (s));
+
+       if (s[0] == 0 && s[1] == 0) {
+               if (jiffies != 0) {
+                       s[0] = jiffies;
+                       s[1] = ~0 - jiffies;
+               } else {
+                       (void) memcpy(s, "improbable seed", sizeof (s));
+               }
+               printk("SPL: get_random_bytes() returned 0 "
+                   "when generating random seed. Setting initial seed to "
+                   "0x%016llx%016llx.", cpu_to_be64(s[0]), cpu_to_be64(s[1]));
+       }
+
+       for (i = 0; i < NR_CPUS; i++) {
+               uint64_t *wordp = per_cpu(spl_pseudo_entropy, i);
+
+               spl_rand_jump(s);
+
+               wordp[0] = s[0];
+               wordp[1] = s[1];
+       }
+}
+
 static void
 spl_kvmem_fini(void)
 {
@@ -514,6 +660,8 @@ spl_init(void)
 {
        int rc = 0;
 
+       spl_random_init();
+
        if ((rc = spl_kvmem_init()))
                goto out1;