← Back to context

Comment by 0x000xca0xfe

5 days ago

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?

    • > Yes, there's also the CF, but the caller may not be checking that, particularly if it's being done from a HLL.

      You can’t call a CPU instruction from a high-level language. You would either use inline assembly or call a library function.

      Either way, not handling CF=0 would be a bug (in your code or in the library function)

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;
        }

    • > but that's to be expected according to the manual

      Confusingly, the AMD programming manual (Rev. 3.38 - July 2026) only explicitly states this ("that the result is always zero when CF=0") in the description of RDSEED, but the Intel SDM mentions this in the description of both instructions.