← Back to context

Comment by jstanley

5 days ago

This is not the first RNG bug on Zen 2, I recall after I first got mine that some application or other would quit immediately at startup because rdrand always returned -1, i.e. all 1s. It was fixed with a microcode update.

Do we now learn that they fixed "always generate all 1s" with "never generate all 0s"??

EDIT: I've been unable to reproduce the problem on my CPU, FWIW. It's a Ryzen 5 3600.

EDIT2: OK, update, I can reproduce it with rdrand16, rdrand32 is fine but rdrand16 can never generate all 0s. So my CPU does have this problem!

I can reproduce it too with rdrand16 on Zen2.

But it looks like the rdrand16 instruction can produce zeros just fine, it just sets CF=0 erroneously (indicating an error and that the user program should retry).

So keep that in mind when you try to reproduce it too and use some abstraction that could implement retries internally.

  • Funny. Look up errata AMD-SB-7055: RDSEED Failure on AMD “Zen 5” Processors.

    Zen 5 rdrand16/32 return zero with CF=1 on entropy exhaustion and their recommended approach directly leads to the issue you observed: treat all-zero result of rdseed as if cf=0 (failure) and re-roll the dice, effectively recreating the zen 1/zen 2 issue all over again!

    They say this might be addressed by a future microcode update… meaning there’s a chance they’ll just patch it to do just that in software. Maybe that’s how they got into this mess in the first place?

    Also, am I a complete idiot or is asserting the relative distribution of a mere 64k possible results a rather easy black box validation test that I would’ve assumed they’d be doing? When I used to write cycle-accurate emulators in the past, that would have been an obvious test to include. This isn’t some arcane instruction no one uses or a really complicated case with deep dependency and/or timing issues; it’s like getting rdtsc wrong.

    • I was going to suggest exactly that, if you're got an RNG, or pretty much anything else for that matter, you need the ability to return some sort of things-went-wrong-somewhere indicator value, and presumably AMD is using 0 to do this. Yes, there's also the CF, but the caller may not be checking that, particularly if it's being done from a HLL.

      Has anyone checked whether it can return ~0, (signed) -1, the traditional error-return value?

      1 reply →

  • Good observation, that seems like the most likely explanation. Do you ever see "true" CF=0 (with nonzero arg) or did they just take the lazy approach?

    • No, CF=0 occurences seem to be happen frequently and uniformely distributed like valid results at ~1/65536, not clustered. Under a minute-long all-core load CF=0 always produces zero, but that's to be expected according to the manual.

      Here are some stats:

          Rounds (N): 1000000000
          Failed (F): 15312
          Valid  (V): 999984688
          N/65536: 15258.789
          V/65536: 15258.555
          Failed, result was zero: 15312
          Failed, result non-zero: 0
          Bucket value for      0: 15312
          Bucket value for      1: 15290
          Bucket value for  65535: 15223
          Min bucket value: 14670
          Max bucket value: 15835
      

      I used this C program to collect them:

          #include <stdio.h>
          #include <stdint.h>
          #include <stdbool.h>
      
          const size_t N = 1000000000; // 1e9
      
          struct rdrand16_result {
              uint16_t n;
              bool ok;
          };
      
          static inline struct rdrand16_result rdrand16()
          {
              struct rdrand16_result result;
              __asm__ __volatile__( "rdrand %0" : "=r" (result.n), "=@ccc" (result.ok) );
              return result;
          }
      
          int main()
          {
              size_t buckets[0xFFFF + 1] = { 0 };
              size_t notok = 0, notok_zero = 0, notok_nonz = 0;
              for (size_t i = 0; i < N; ++i) {
                  struct rdrand16_result result = rdrand16();
                  ++buckets[result.n];
                  if (! result.ok) {
                      ++notok;
                      notok_zero += result.n == 0;
                      notok_nonz += result.n != 0;
                  }
              }
              size_t max = 0, min = N;
              for (size_t i = 0; i <= 0xFFFF; ++i) {
                  size_t n = buckets[i];
                  min = n < min ? n : min;
                  max = n > max ? n : max;
              }
              printf("Rounds (N): %zu\n", N);
              printf("Failed (F): %zu\n", notok);
              printf("Valid  (V): %zu\n", N - notok);
              printf("N/65536: %.3f\n", (double)N / 65536);
              printf("V/65536: %.3f\n", (double)(N - notok) / 65536);
              printf("Failed, result was zero: %zu\n", notok_zero);
              printf("Failed, result non-zero: %zu\n", notok_nonz);
              printf("Bucket value for      0: %zu\n", buckets[0]);
              printf("Bucket value for      1: %zu\n", buckets[1]);
              printf("Bucket value for  65535: %zu\n", buckets[0xFFFF]);
              printf("Min bucket value: %zu\n", min);
              printf("Max bucket value: %zu\n", max);
              return 0;
          }

      1 reply →

    return 4 # Determined by fair dice roll.

Zen 4 reporting in. I'm unable to reproduce it (7840U).

   $ ./a.out | rg '\b\-?\d\b' | sort -n | uniq -c
   15281 -2
   15192 -1
   15273 0
   15243 1
   15269 2

I used the GCC intrinsic ( _rdrand16_step ),

    #include <immintrin.h>
    
    short rdrand16() {     // gcc -mrdrnd
        short ret;
        while (1 != _rdrand16_step(&ret)) { }    
        return ret;
    }

If I remember correctly, we had a setting in every Linux server we owned to remove CPU as a RNG seeder for the kernel because of those bugs with AMD CPUs.

I.e., we had `random.trust_cpu=off nordrand` in `GRUB_CMDLINE_LINUX`.

  • Adding bad randomness can't degrade good randomness, can it?

    I thought the kernel would not replace anything just because it adds a potentially bad source.

    E.g. if you have rand source A, and xor it with rand source B, then you get, at worst, the best of A and B,

    • Careful, there is two different things going on here:

      a) whether you use the maybe-entropy provided by the CPU (and/or the bootloader)

      b) whether you credit that maybe-entropy towards your tracking of whether the pool should be considered sufficiently seeded

      random.trust_cpu/random.trust_bootloader configures b).

      nordrand has been removed from the kernel as it had become overloaded by meaning both a) and b)

      Under most circumstances, a) is harmless. You mostly want that off when the CPU exhibits some performance hiccups when asked.

      Under some circumstances, b) is outright dangerous. Some applications can work without seeded pool at some slightly reduced performance, but could be made to fail miserably if they had been made to believe that the pool was seeded yet it was not. This happens with hash tables when you skip some of the accounting because it seems no longer relevant. It really would not be relevant, once even a determined attacker should be unable to reliably trigger the worst-case-performance.

      1 reply →

    • As far as I know that is correct; the kernel was written in a way such that one bad source doesn’t poison the pool. Still, if you know one source is bad, might as well take it out.

      4 replies →

    • > E.g. if you have rand source A, and xor it with rand source B, then you get, at worst, the best of A and B,

      With the assumption that sources A and B are independent from each other.

      2 replies →

    • this is generally true however if an adversary is able to control a source it becomes dangerous if they can preview the results or inspect the other sources.

      7 replies →

Does rdrand32 and then taking the lowest 16 bits of its result yield any zeroes?

Basically I'm wondering if it's a bug in the version of the instruction that writes to a 16-bit reg, or a bug in the underlying RNG

  • Yes it does. rdrand32()%65535 was my first attempt, and generated zeroes at about the expected rate, that's why I initially erroneously thought my CPU did not have this problem.

Even if you reproduce the issue, it is not a proof it can't generate a zero - just that it's very unlikely.

To prove it, we'd need to examine the chip and its microcode.