Remix.run Logo
▲ strenholme 3 hours ago

I recently had a heated but productive discussion about undefined behavior here.

As per the linked article:

“There are currently about 100 instances of undefined behavior in the C standard, but the in-progress C2y draft has removed 45 of them.”

I wonder how they handle the specific case of uninitialized but allocated memory.

Let’s look at something which will result in undefined behavior in C99: [1]

  #include<stdio.h>
  #include<stdint.h>
  #include<stdlib.h>
  #define b(z) for(c=0;c<z;c++)
  uint32_t c,e[42],f[42],g=19,h
  =13,n[45],i,j,k;void m(){j=0;
  b(12)f[c+c%3*h]^=e[c+1];b(g){
  i=c*7%g;k=e[i++];k^=e[i%g]|~e
  [(i+1)%g];j=j+c;n[c]=n[c+g]=k
  >>j%32|k<<-j%32;}for(i=39;i--
  ;f[i+1]=f[i])e[i]=n[i]^n[i+1]
  ^n[i+4];b(3)e[c+h]^=f[c*h]=f[
  c*h+h];*e^=1;}int main(int c,
  char**v){char*q=malloc(2);if(
  q==0)return 0;q[0]&=31;q[0]|=
  64;q[1]=0;for(;;m()){b(3){
  for(j=0;j<4;){f[c*h]^=k=(*q?
  255&*q:1)<<8*j++;e[c+16]^=k;
  if(!*q++){b(18)m();b(2){j=c;
  b(4)printf("%02x",(e[1+j%2]
  >>8*c)&255);c=j;if(c%2)m();}
  puts("");return 0;}}}}}
The key part of the above brick of code is this:

  char *q=malloc(2);
  if(q==0)return 0;
  q[0]&=31;
  q[0]|=64;
  q[1]=0;
Here, we see that q[0] is an allocated but undefined byte. As per C99, this results in undefined behavior, however 20 years ago this was a good trick to get kinda-randomish bytes to use as a possible entropy source.

Someone claimed that the above brick of code will compile in newer versions of clang such that, since the complex cryptographic pseudo random number generator code depends on uninitialized but allocated memory, the entire cryptographic operation isn’t performed.

So I tested it against multiple versions of GCC and clang; I also tested it against TCC for good measure.

In all cases, with all levels of optimization, the cryptographic routine ran. I even ran it against clang 23. In cygwin, it was a randomish but consistent byte (except for clang at a higher level of optimization, at which point the uninitialized byte had a value of 0); in Ubuntu 26, the uninitialized memory consistently had a value of 0 (in tcc/gcc/clang).

I am hoping the up and coming C2y spec has very clearly defined behavior when using unintialized memory (ideally where it will work but the bytes can have any values).

Naturally, I have updated my code to no longer use uninitialized memory as a source of entropy. 20 years ago, MacOS didn’t support clock_gettime() with nanosecond resolution, so that wasn’t a portable way to get pseudo-random bits; these days clock_gettime() is universal across modern development environments, and it provides pretty good entropy (along with using /dev/urandom in *NIX, which isn’t in POSIX but is widely supported, as well as CryptGenRandom() in the legacy Win32 port). [2]

[1] Said person said the appendices to C99 aren’t authoritative, but if something is in the spec, including in the appendices, it’s authoritative.

[2] I don’t blindly trust /dev/urandom to always make really hard to guess pseudo-random bits, because my code is open source, and, as such, doesn’t just compile in Linux. It often times will be compiled in embedded systems, and even Linux has had at times issues with /dev/urandom on Raspberry Pis.

[3] I would also like to see uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, and int64_t mandated. They exist in C99, but aren’t mandated, even though every real world compiler from this century supports all of the above types. Yes, I know about _BitInt(8/16/32/64/128/etc.) but a compiler from 2004—and yes I still use one to make win32 binaries—doesn’t support these new C23 datatypes.

▲jcranmer 3 hours ago | parent | next [-]

> [1] Said person said the appendices to C99 aren’t authoritative, but if something is in the spec, including in the appendices, it’s authoritative.

That's not true. There is a difference between normative text and informative text. Informative text is not authoritative, and you were citing an appendix that is labeled as informative.

> I am hoping the up and coming C2y spec has very clearly defined behavior when using unintialized memory (ideally where it will work but the bytes can have any values).

It won't. Uninitialized memory can't have "very clearly defined behavior" without breaking essentially every single implementation, and WG14 is very loth to break existing implementations.

▲rallyrascal 17 minutes ago | parent | next [-]

How would it break existing implementations?

Having accessing uninitialized memory be UB is just insane, there's only two possible sane implementations - potential trap for caps based systems (or for a static analyzer) or you get a pseudo random value i.e 'whatever happened to be there'. So just make it implementation defined.

▲afdbcreid 3 hours ago | parent | prev [-]

> It won't. Uninitialized memory can't have "very clearly defined behavior" without breaking essentially every single implementation, and WG14 is very loth to break existing implementations.

You can initialize with some bit pattern. This is what was accepted for C++ (but only for certain types of memory).

▲vbezhenar 2 hours ago | parent [-]

That's huge performance loss. Imagine doing useless initialization of multi-megabyte chunk of memory. Not acceptable for C programs.

In fact I hate that C mandates static variables being initialized to zero. This is dumb. When I'm writing for MCU, that's useless cycles spent at the power on. Thankfully it's possible to fix with linker tricks, but it should not be an issue in the first place.

▲afdbcreid 37 minutes ago | parent | next [-]

I agree it is, and like the other commenter said even C++ only did it for some things. I was only saying it is not breaking.

▲aw1621107 an hour ago | parent | prev [-]

In the case of C++ initialization is (currently) only done for automatic variables, so you're not likely to be zero-initializing multiple megabytes of stack. There's also [[indeterminate]] for opting out of initialization, though that has to be done per variable.

▲afdbcreid 3 hours ago | parent | prev | next [-]

It is quite hard to come with a reliable example, but (https://c.godbolt.org/z/56bfYszEb):

    int* p = malloc(sizeof(int));
    int v = *p;
    if (!(v < 0 || v == 0 || v > 0)) {
        exit(1);
    }
This compiles into `exit(1)` in Clang under -O3.
▲kstrauser 2 hours ago | parent [-]

How? That doesn’t seem like it should be possible. Why is that?

▲robinsonb5 an hour ago | parent | next [-]

As I understand it, the compiler "knows" the range of valid values for a variable (for instance, if an int was promoted from an unsigned char, it knows it doesn't need to bother with testing for negative values). If the value came from uninitialised malloc'ed memory, the compiler knows that there are no valid values less than zero, no valid values directly equal to zero, and no valid values greater than zero - thus all three tests fail, and the overall negated test always succeeds!

▲afdbcreid 36 minutes ago | parent [-]

The reasoning is roughly correct but range checking isn't what const-folding the comparisons into `false` (rather it's a special rule for LLVM's `undef`).

▲afdbcreid an hour ago | parent | prev [-]

Because it invokes UB.

▲fragmede 3 hours ago | parent | prev [-]

Interesting. x86_64 has RDRAND for entropy (and RDSEED to seed). Oh I guess there's also ARM these days but I bet they got one too.

▲strenholme 3 hours ago | parent [-]

I write code which runs in embedded spaces, so a lot of ARM and even RISC-V. So I have a simple, portable source of pseudo-random numbers which will give good random numbers on a potato, which means rolling my own.