rdrand

Last week (at the time of writing, anyway), Ars Technica reported a serious bug in AMD’s implementation of rdrand, an instruction that helps you generate random numbers. Apparently, on (some) Ryzen 3000, 0xfff…ff is “random”.

I recently got an AMD Ryzen 9 3900x, and I wondered if I have the bug as well.

Let’s first write a routine to read the random numbers with rdrand. This instruction sets the carry flag to let us know if the value returned is random enough, so that we can retry if it isn’t. GCC/Gas/Masm assembly lets us easily interface with this instruction:

#ifndef __module__rrand__
#define __module__rrand__

#include <cstdint>
#include <utility>

using rand_t=std::pair<uint64_t,uint64_t>;

rand_t rd_rand();


#endif
// __module__rrand__

With an assembly file:

_Z7rd_randv:
.LFB82:
        .cfi_startproc

        xor rdx,rdx

.retry:

        inc rdx
        rdrand rax

        jnc .retry

        ## rax,rdx returns a std::pair<uint64_t,uint64_t>
        ret

        .cfi_endproc

This function automagically assigns two registers (%0 and %1), one with the random value, the other with the number of tries (1 if it succeeds right away). A quick test shows us that it (seems to) work:

int main()
 {
  uint64_t counts[64]={0};

  std::cout << std::hex << std::setfill('0');
  for (int i=0;i<100000000;i++)
   {
    rand_t t=rd_rand();

    std::cout
     << std::dec << t.second
     << '\t'
     << std::hex << std::setw(16) << t.first << std::endl;
   }

  return 0;
 }

It prints:

Tries   Values
1       848d44f6fabf8aa5
1       36b3c5953104ea25
1       e5312e8cfdba8d62
1       c55a698dcd5ec5fd
1       a7bf76560b22d56a
1       207c2fa54beea397
1       ae27f2a5a9263b83
1       a829dd4aabd41b8f
1       a8e3d747de951a02
1       f9781bda02545036
1       410cc2263d1c8001
1       f8ebf3fad61c1d6b
1       70a76df3f4759e4e
1       f5aabcfa42b4824d
1       fcdd1260c56027ec

Well, that’s not fff...ff, but is it random?

Changing the above code to count the number of times each bits is set to either 0 or 1:

int main()
 {
  uint64_t counts[64]={0};

  for (int i=0;i<1000000;i++)
   {
    rand_t t=rd_rand();

    for (int i=0;i<64;i++)
     counts[i]+=((t.first>>i)&1);
   }

  // display

  for (int i=0;i<64;i++)
   std::cout << counts[i] << std::endl;

  return 0;
 }

We take those result and plot them (in gnumeric, for example):

It seems that my AMD R9 3900x doesn’t suffer from that defect.

*
* *

At first, I tried to use GCC inline assembly:

rand_t rd_rand()
 {
  uint64_t x,r;
  asm(".intel_syntax noprefix;"
      "    xor %1,%1;"
      ".retry%=:"
      "    inc %1;"
      "    rdrand %0;"
      "    jnc .retry%=;"
      ".att_syntax;"
      : "=R"(x),"=R"(r) // %0, %1
      : // no input
      : // auto clobber
      );

  return {x,r};
 }

But the compiler kept optimizing the function away, with weird results. Sometimes working, sometimes not… depending on whether or not I used both members of the pair. GCC inline assembly always gave me trouble.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: