openlang

memset before free passes at default flags, fails with OPT=-O2

I'm working on a small C credential holder called vault, built in WSL. vault_release() is supposed to wipe the secret and then free it:

void vault_release(vault_entry *e)
{
    if (!e)
        return;

    memset(e->secret, 0, e->secret_len);
    free(e->secret);
    e->secret = NULL;
    free(e);
}

The test, tests/wipe_check.c, saves the secret pointer, calls vault_release(), then reads the freed block from byte 16 on. Yes, that's UB. It's a throwaway test and I only want to see what's left in memory. Don't lecture me about it.

Plain make check passes. make release-check does a clean and then make check OPT=-O2, and that one fails. The bytes after the first 16 are still the original secret, and only the first 16 look clobbered. I'm skipping those 16 in the test anyway, since glibc puts its own bookkeeping there.

The memset is right there on the whole length, and I've read this function three times. My plain target should already be optimizing, since that's the default, so I don't see how the same source gives two results. Either glibc's free() writes stuff back into the block, or my -O2 target builds something different from the plain one. Neither explanation really makes sense to me.

I haven't looked at the generated assembly yet. What am I missing here?

The conversation continues 6

Follow the reasoning
  1. Permalink

    I'd disagree with the premise, the default isn't optimising. GCC and Clang both give you -O0 unless something says otherwise, so unless your Makefile sets OPT, plain make check is an unoptimised build, and there the memset call is emitted as written.

    At -O2 the compiler sees that e->secret is freed straight after the memset, with no read in between, so the store is dead and the call goes. The abstract machine can't observe it. That also explains why only the first 16 bytes differ, as that's glibc's own bookkeeping.

    Compare the -S output of both builds and the call will be gone. Use explicit_bzero or C23 memset_explicit instead.

  2. Permalink

    Okay, lathe is right and I had it backwards. The GCC docs are plain about it: without an -O option you get -O0. So my "plain" target was never optimizing, and the two targets really are different builds of the same source.

    Most optimizations are completely disabled at -O0 or if an -O level is not set on the command line

    https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

    That also makes the 16 bytes a non-mystery. Those are glibc's free-list pointers, so free() writing there was never in question. I was skipping them in the test anyway.

    kiln, the cppreference page doesn't say what you think it does. memset is an ordinary function call, and 5.1.2.3 only requires the observable behavior of the abstract machine. A store into memory that is freed with no read in between isn't observable, so the compiler can drop it. "Always runs" is folklore, not what the standard promises.

    I still haven't read the -S output for both builds, and I'm going to diff them before I touch the code. After that, explicit_bzero goes in, and memset_explicit once my toolchain has C23. Really should have checked the default before posting.

  3. Permalink

    Before I swap in explicit_bzero: is the memset through a volatile function pointer trick actually guaranteed by the standard, or do compilers just happen to respect it? I'd really rather not replace one piece of folklore with another.

Other sides of the same question

  1. -race reports DATA RACE even though ack is an unbuffered receive. Is time.After to blame?

    Is it possible that the race detector is wrong here, or that I misunderstand what the ack gives me? This is pushgate, a small…

    Replies 4Latest activity go
  2. HMAC mismatch on one webhook only (the one with accents in the name)

    I wrote a small Node checker (plain JS, no build step) that replays saved webhook deliveries and verifies the HMAC-SHA256 bef…

    Replies 7Latest activity typescriptjavascript
  3. Same overflow check, different error branch at -O0 vs -O2

    Hostile pack with count=65536 and rec_size=32768. At -O0 I get "pack too large". At -O2 I get "out of memory" instead. Same s…

    Replies 6Latest activity c