Hacker News new | ask | show | jobs
by btrask 2030 days ago
Here's a trick that will actually help produce more secure and reliable programs.

    *outArg = myPtr; myPtr = NULL;
    free(aPtr); aPtr = NULL;
Set your pointers to null when you free them! Set them to null when you transfer ownership! Stop leaving dangling pointers everywhere!

Some people say they like dangling pointers because they want their program to crash if something is freed when they don't expect it to be. Good! Do this:

    assert(ptr);
There are also many more tricks you can do once you start nulling pointers. You can use const to mark pointers that you don't own and thus can't free. You can check that buffers are all zero before you free them to catch memory leaks (this requires zeroing out other fields too of course).

Please, null out your pointers and stop writing (most) use-after-free bugs!

3 comments

Better then

#define ZFREE(p) do { free(p); p = NULL; } while(0)

You can even do

  #define free(p) do { free(p); p = NULL; } while(0)
And when you want to call the original free:

  (free)(p);
The preprocessor will not substitute this occurence of free.
C n00b here - why the `do {} while(0)`? Couldn't you just do something like `#define free(p) { free(p); p = NULL; }`?
Semi-experienced C user here, I believe the anonymous block is perfectly adequate here. No idea why they are wrapping it in a single instance do loop, unless they’re unaware of block scoping or I’m unaware of some UB here.
do {} while(0) is a common idiom for macros in C, because it consumes the trailing semicolon, which a bare {} block doesn't do.

    if(x) MACRO();
    else something();
expands to

    if(x) { ... }; // Error!
    else something();
Here's the actual macro I (sometimes) use:

    #define FREE(ptrptr) do { \
        __typeof__(ptrptr) const __x = (ptrptr); \
        free(*__x); *__x = NULL; \
    } while(0)
There might be a better way of doing it though. Also, __typeof__() obviously isn't standard C.

Edit to add: I've honestly been moving away from using a macro and just putting both statements on one line like in the OP. For something so simple, using a macro seems like overkill.

What's the benefit of that over nn3's version?
It'll only evaluate the pointer once. It's possible to make this a function though, that might be preferable
Good point. But it seems like it would require usage like this:

    int* p = malloc(sizeof(int));
    FREE(&p);
What if we instead define the macro like this:

    #define FREE(ptr) do { \
        __typeof__(ptr)* const __x = &(ptr); \
        free(*__x); *__x = NULL; \
    } while(0)
Then make usage slightly shorter, as well as more similar to free():

    int* p = malloc(sizeof(int));
    FREE(p);
Taking a pointer-to-pointer is intentional to make it clear that the pointer will be modified. That's actually the most important difference from nn3's version IMHO.
I tried making it a plain function at one point but ran into some weirdness around using void * * with certain arguments (const buffers?). You don't want to accept plain void * because it's too easy to pass a pointer instead of a pointer to a pointer. Using a macro is (ironically) more type safe.

Maybe someone else could figure out how to do it properly, since I'd definitely prefer a function.

Your approach requires extra checks, though, which are easy to forget. Also, NULL is not guaranteed to be the stored as zeros, plus padding is going to make your life annoying.
Well, dangling pointers are also easy to forget... Yes, it requires some discipline. Good code requires discipline, doesn't it?

The trick of checking that buffers are zeroed is purely a debugging tool, so it's okay if it doesn't work on some platforms. And if you allocate with calloc(), the padding will be zeroed for you. It's actually very rare that you will have to call memset() with this technique.

> Good code requires discipline, doesn't it?

This is like the most clichéd way of saying “my code has security vulnerabilities” that there is. I have yet to see code that has remained secure solely on the “discipline” of programmers remembering to check things.

> The trick of checking that buffers are zeroed is purely a debugging tool, so it's okay if it doesn't work on some platforms.

Fair.

> And if you allocate with calloc(), the padding will be zeroed for you.

It might get unzeroed if you work with the memory.

All code is full of vulnerabilites. If you say your code isn't, then I'm sure it is. I just do the best I can to keep the error rate as low as possible. But it's a rate, and it's never zero.

Also, it's not just about vulns in security-critical code. It's also about ordinary bugs. Why not be a little more careful? It won't hurt.

> It might get unzeroed if you work with the memory.

Maybe, but it isn't very common. I'm not sure when the C standard allows changing padding bytes, but in practice the compilers I've used don't seem to do it. And again, it's just a debugging aid, if it causes too much trouble on some platform, just turn it off.

It’s better to have automatic checks than rely on programmers being careful enough to remember to add them. For padding: this probably happens more on architectures that don’t do unaligned accesses very well.
> I have yet to see code that has remained secure solely on the “discipline” of programmers remembering to check things.

That's not what the parent comment said.

I’m not sure what it could have said after saying that programmers should have disciple after I mentioned that their thing required extra checks to work.
Parent said: "Good code requires discipline, doesn't it?"

You retort: "I have yet to see code that has remained secure solely on the “discipline” of programmers remembering to check things."

I think that is a dishonest misrepresentation of what the parent comment said, isn't it?

> NULL is not guaranteed to be the stored as zeros

Is that a real issue, though?

> padding is going to make your life annoying

Just memset?

>> NULL is not guaranteed to be the stored as zeros

> Is that a real issue, though?

Of course, it's not, but that's one of those factoids that everyone learns at some point and feels like needing to rub it into everyone else's face assuming that these poor schmucks are as oblivious to it as they once were. A circle of life and all that.

Forgive me for encouraging the adoption of portable, compliant code to those who may not otherwise be aware of it. If you want to assume all the world’s an x86 that’s great but you should at least know what part of your code is going to be wrong elsewhere.
Name a single platform where NULL is not 0.
AMD GPUs use a non-zero NULL pointer[0].

[0] https://reviews.llvm.org/D26196

Interesting. And it isn't even always non-zero; sometimes it's 32-bit -1, sometimes it's 32-bit 0 and sometimes it's 64-bit 0:

https://llvm.org/docs/AMDGPUUsage.html#address-spaces

NULL is required to be stored as all zeros on POSIX systems.
Please don’t get me wrong but these precautions sound like you are sweeping problems under the carpet which will come out one day back again. It sounds like you have ownership issues in the design and trying to hide ‘possible future bugs’.

Do you use sanitizers for use-after free bugs? I see many people still don’t use them even though sanitizers have become very good in the last 5-6 years

It's defensive coding. Do you think defensive driving is 'sweeping problems under the carpet'? (It is, but it's still useful...)

I use every tool at my disposal. Sanitizers, static analyzers... and also not leaving dangling pointers in the first place. Why would I do anything less? It doesn't cost anything except a little effort.

Take a look at this recent HN link: https://www.radsix.com/dashboard1/ . Look at all those use-after-free bugs. Even if it only happens 1% or 0.01% of the time... It's a huge class of bugs in C code. Why not take such a simple step?

If it works for you, then it is okay. It is not ‘a little effort’ for me to worry about someone else might use this pointer mistakenly, so I need to think about that all the time. It shifts my focus from problem solving to preventing future undefined behavior bugs. These bugs in the link, I don’t know C++, it is a big language which does a lot of things automatically, so it is already scary for me :) Maybe that is it, I write C server side code mostly(database) with very well defined ownership rules. Things are a bit more straightforward compared to any c++ project I believe. I just checked again, we don’t have any use-after free bugs in the bug history, probably that is because of %100 branch coverage test suite + fuzzing + sanitizers. So I rather adding another test to the suite than doing defensive programming. It is a personal choice I guess.
Generally, it is considered preferable to find problems as early as possible. If a program fails to compile or quickly crashes (because of a failed assertion), then I consider that better than having to unit test and fuzz test your code to find that particular problem.

As an added benefit the code also becomes more robust in the production environment, if there are use cases you failed to consider -- 100% branch coverage does not guarantee that there are none!

> Generally, it is considered preferable to find problems as early as possible.

Whole heartedly agree.

> If a program fails to compile or quickly crashes (because of a failed assertion), then I consider that better than having to unit test and fuzz test your code to find that particular problem.

This confuses me. My typical order would be:

fails to compile > unit test > quick crash at runtime > slow crash at runtime (fuzzing)

I am curious to understand why we differ there.

Every problem can be solved in many different ways. If you think you've already got use-after-free bugs under control, then more power to you! You absolutely have to concentrate your effort on whatever your biggest problems are.

But I'll also say that if you don't have any use-after-free bugs in the history of a large C codebase... you might not even be on the lookout for them? I still have them sometimes, mainly when it comes to multiple ownership. And those are just the ones I found eventually.

So yes, different strokes for different folks, but if you make the effort to incorporate tricks like this into your "unconscious" coding style, the ongoing effort is pretty minimal. Even if you decide this trick isn't worth it, there are countless others that you might find worthwhile. I'm always on the lookout for better ways of doing things.

I meant no use-after-free bugs in production, otherwise we find a lot in development with daily tests etc. but looks like we catch them pretty effectively. It works good for us but doesn’t mean it’ll work for all other projects, so yeah I can imagine myself applying such tricks to a project some time, especially when you jump to another project which has messy code, you become paranoid and start to ‘fix’ possible crash scenarios proactively :)
Big reason for defensive coding like nulling pointers is to make the code fail hard when someone messes up when they make a change. One can imagine the sort of hell unleashed if later the code is changed to make use of a dangling pointer. That's often the type of bug that slips through testing and ends up causing rare unexplained crashes/corruption in shipped code. Worse it can take multiple iterations of changes to finally expose the bug.
This makes UAF easier to detect but double-free impossible to detect. I would consider that to be worse than not doing anything at all, especially since it isn't amenable to modern tooling that is much better at catching these issues than hand-rolled defensive coding.