Live data from Hacker News

Defer: Resource cleanup in C with GCCs magic

oshub.org

51–60 of 99 posts

Re: Defer: Resource cleanup in C with GCCs magic

#51
post #37

Earlier quoted context omitted.

If you check for null pointer before you dereference, then no the compiler cannot elide the check. If you check after dereferencing it, yes it can. But in this case why would you not check before dereferencing? It's the only UB-free choice.

Yes, it can. Why would you be checking the pointer for nullptr after you have dereferenced it? It makes no sense at all, so, compiler indeed can elide the nullptr check before dereferencing the ptr exactly because it is free to _always_ assume that the program is free of UB. To be more precise GCC says "eliminate useless checks for null pointers" and what I am saying that you can never be sure what in your code ended…

> Why would you be checking the pointer for nullptr after you have dereferenced it? It makes no sense at all

Right. It's UB. And that's why the optimization in question is about removing that check. The only reason the optimization is valid for a C compiler to do, is that it can assume dereferencing a null pointer lands you in UB land.

I'm sorry, either you are terrible at trying to explain things, or you have thoroughly misunderstood what all this is about. GCC cannot, under any circumstances or with any flags, remove an "if (ptr == NULL)" that happens before dereferencing the pointer.

What this flag is about, and what the kernel bug you mentioned (at least I think you're referring to this one) is about, was a bug that went "int foo = ptr->some_field; […] if (ptr == NULL) { return -EINVAL; }". And GCC removed the post-deref null pointer check, thus making the bug exploitable.

From the help text:

> if a pointer is checked after it has already been dereferenced, it cannot be null.

after. Only applies after. A check before dereferencing can never be removed by the compiler.

Obviously.

Re: Defer: Resource cleanup in C with GCCs magic

#52
post #49
post #36

Earlier quoted context omitted.

But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily.

A null pointer is not a valid pointer in a predominant number of systems in existence. If malloc (3) has returned a NULL, *ptr will cause a SIGSEGV. Embedded systems are an exception, though. They may not have a MMU, and in such a case the operation will succeed.

1. No, dereferencing a null pointer will not "cause a sigsegv". It causes UB. In practice, in unix user space, yes it'll probably be SIGSEGV. 2. A null pointer is not a valid pointer: Yeah… Once again my question was "But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily."

This code will NEVER deference a null pointer. Not under any compiler, not with any compiler options:

    if (ptr != NULL) { *ptr = 0; }
> A null pointer is not a valid pointer in a predominant number of systems in existence.

No, that's not quite pedantically accurate. A null pointer is not a valid pointer in the C programming language. Address zero may or may not be, that's outside the scope of the C language. Which is why embedded and kernel work sometimes has to be very careful here.

> They may not have a MMU, and in such a case the operation will succeed.

Lack of MMU does not mean address zero is valid. It definitely* doesn't make a null pointer valid. In fact, a null pointer may not point to address zero.

Re: Defer: Resource cleanup in C with GCCs magic

#53
post #32

Earlier quoted context omitted.

> on top of C. If we're referring to the "C is a subset of C++" / "C++ is a superset of C" idea, then this just hasn't been the case for some time now, and the two continue to diverge. It came up recently, so I'll link to a previous comment on it ( https://news.ycombinator.com/item?id=45268696 ). I did reply to that with a few of the other current/future ways C is proposing/going to diverge even further from C++, sin…

> this just hasn't been the case for some time now Which I find sad actually. The idea of C++ as a superset of C is really powerful, especially when mixing C and C++. A while ago I had a C project (firmware for a microcontroller) and wanted to bake the version and the compilation time into the firmware. I didn't find a way to do this in plain C, but in C++ you can initialize a global struct and it gets statically lin…

> wanted to bake the version and the compilation time into the firmware. I didn't find a way to do this in plain C, but in C++ you can initialize a global struct and it gets statically linked into the output. This didn't even use constexpr, just preprocessor trickery.

I might be misunderstanding here, but if you are okay with preprocessor trickery, then it's doable.

I do this routinely in the Makefile, which (very tediously) generates a build_info module (header and implementation) that is linked into the final binary: https://github.com/lelanthran/skeleton-c/blob/8e04bed2654dac...

Re: Defer: Resource cleanup in C with GCCs magic

#54
post #37

Earlier quoted context omitted.

If you check for null pointer before you dereference, then no the compiler cannot elide the check. If you check after dereferencing it, yes it can. But in this case why would you not check before dereferencing? It's the only UB-free choice.

Yes, it can. Why would you be checking the pointer for nullptr after you have dereferenced it? It makes no sense at all, so, compiler indeed can elide the nullptr check before dereferencing the ptr exactly because it is free to _always_ assume that the program is free of UB. To be more precise GCC says "eliminate useless checks for null pointers" and what I am saying that you can never be sure what in your code ended…

> Yes, it can.

I don't think so. If it could, then this code would reliably crash:

    char *mystr = strdup (oldstr);
    if (mystr)
        *mystr = 0; // Truncate string
That never crashes.

Re: Defer: Resource cleanup in C with GCCs magic

#55
post #6

I don't understand why people insist on simulating a poor substitute for RAII with a feature that is itself almost decent RAII. > If malloc fails and returns NULL, the cleanup function will still be called, and there’s no simple way to add a guard inside free_ptr. free(NULL) is a no-op, this is a non-issue. I don't know what's so hard about a single if statement anyway even if this were an issue.

`free(NULL);` will crash on some platforms that gcc supports, I believe.

Then it's in violation of the C standard, at least as of C11 (I didn't check C99 or C89).

> The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs. Otherwise, if the argument does not match a pointer earlier returned by a memory management function, or if the space has been deallocated by a call to free or realloc, the behavior is undefined.

Emphasis mine

Re: Defer: Resource cleanup in C with GCCs magic

#56

Earlier quoted context omitted.

Not having RAII is precisely the reason I prefer C over C++ or Rust. I WANT to be able to separate allocation from initialization. I'm currently working with Arduino code and the API is a mess. Everything has a second set of manual constructor/destructor, which bypasses type-safety entirely. All only to shoehorn having existing, but uninitialized objects into C++.

Both C++ and Rust allow that? Having niche behaviour not be the default makes sense, but both know it's needed and therefore allow it? (C++ lets you malloc and then placement new (just casting the pointer like C does is UB, but it's being fixed for trivial types) and Rust has both plain alloc and Box >) There are a lot of other reasons not to use them, but yours is a made up strawman.

This isn't what people are talking about, you aren't understanding the problem

With RAII you need to leave everything in an initialized state unless you are being very very careful - which is why MaybeUninit is always surrounded by unsafe

    {
        Foo f;
    }

f must be initialized here, it cannot be left uninitialized

    std::vector my_vector(10000);
EVERY element in my_vector must be initialized here, they cannot be left uninitialized, there is no workaround

Even if I just want a std::vector to use as a buffer, I can't - I need to manually malloc with `(uint8_t)malloc(sizeof(uint8_t)*10000)` and fill that

So what if the API I'm providing needs a std::vector? well, I guess i'm eating the cost of initializing 10000 objects, pull them into cache + thrash them out just to do it all again when I memcpy into it

This is just one example of many

another one:

with raii you need copy construction, operator=, move construction, move operator=. If you have a generic T, then using `=` on T might allocate a huge amount of memory, free a huge amount of memory, or none of the above. in c++ it could execute arbitrary code

If you haven't actually used a language without RAII for an extended period of time then you just shouldn't bother commenting. RAII very clearly has its downsides, you should be able to at least reason about the tradeoffs without assuming your terrible strawman argument represents the other side of the coin accurately

Re: Defer: Resource cleanup in C with GCCs magic

#57
post #18

Nested functions are cool, although not supported by clang. However they rely on Trampolines: https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html And trampolines need executable stack: > The use of trampolines requires an executable stack, which is a security risk. To avoid this problem, GCC also supports another strategy: using descriptors for nested functions. Under this model, taking the address of a nested fun…

They only need trampolines when they access their local environment and you take their address. Without optimization a trampoline was generated whenever an address was taken, but I recently changed this in the development version of GCC to only do this when needed, so hopefully in the next released version you will not get a trampoline for many more cases. Here, there is no address being taken anyway, so you do not get a trampoline.

(and I hope we get a solution without trampolines for the remaining cases as well)

Re: Defer: Resource cleanup in C with GCCs magic

#58
post #52
post #49

Earlier quoted context omitted.

A null pointer is not a valid pointer in a predominant number of systems in existence. If malloc (3) has returned a NULL, *ptr will cause a SIGSEGV. Embedded systems are an exception, though. They may not have a MMU, and in such a case the operation will succeed.

1. No, dereferencing a null pointer will not "cause a sigsegv". It causes UB. In practice, in unix user space, yes it'll probably be SIGSEGV. 2. A null pointer is not a valid pointer: Yeah… Once again my question was "But when would it not be a valid pointer, and yet also not a null pointer? A null pointer we can check for easily." This code will NEVER deference a null pointer. Not under any compiler, not with any co…

A zero (0, not NULL!) pointer is a valid pointer in C/C++. It is not a UB, and it means one simple thing: «give me the contents of a memory cell (a byte, a word, a long word etc) at the address of 0». Old hardware designs used the address of 0 to store a jump address of the system boot-up sequence (i.e. firmware), and I personally wrote the code in C to inspect / use it in the unpriviledged hardware mode.

The prevailing number of modern systems do not map the very first virtual (the emphasis is on virtual) memory page (the one that starts from zero) into the process address space for pragmatic reasons – an attempt to dereference a zero pointer is most assuredly a defect in the application. Therefore, an attempt to dereference a zero pointer always results in a page fault due to the zeroeth memory page not being present in the process' address space, which is always a SIGSEGV in a UNIX.

Embdedded systems that do not have a MMU will allow *ptr where «ptr» is zero to proceed happily. Some (not all) systems may even have a system specific or a device register mapped at the address being 0.

You are conflating several unrelated things, and there is no pedantry involved – it is a very simple matter with nothing else to debate.

Re: Defer: Resource cleanup in C with GCCs magic

#59
post #35

Earlier quoted context omitted.

> RAII doesn't make sense without initialization. Rust has RAII and does not have constructors.

Rust mandates that every field in a user-defined type is initialized at once. How do you propose to retrofit that into C without "constructors"?

C has had designated initializers since C99, if you want you can initialise every struct field at once.

Re: Defer: Resource cleanup in C with GCCs magic

#60
Slightly off-topic, but:

The fact that go "lifts" the deferred statement out of the block is just another reason in the long list of reasons that go shouldn't exist.

Not only is there no protection against data-races (in a language all about multithreading), basically no static checking for safety, allocation and initialization is easy to mess up, but also defer just doesn't work as it does in C++, Rust, Zig, and any other language that implements similar semantics.

What a joke.

Post reply on HN