0

This post shows a way to use void**s to eliminate a category of bugs related to dangling pointers (use after free, double free, etc):

void freep(void **p) {
    if (p) {
        free(*p);
        *p = NULL;
    }
}

I tried it out with the following driver code:

#include <stdlib.h>
#include <string.h>

#define free(x) freep(x)

int main(void) {
    char *s = malloc(5);
    strcpy(s, "hehe");

    char **ss = &s;

    free(s);
    free(ss);
    free(&s);
}

As noted in the post and its comments, it is technically in violation of the C standard - compiled with -Wall -Wextra -pedantic -std=c17, I get warnings regarding passing char* and char** type parameters to a void** type parameters.

My question is, can it be made to not violate the C standard, while still achieving its goal of avoiding dangling pointers by ensuring the user can't forget to set a pointer to NULL after free()-ing it?

Thank you for your time.

  • 1
    Implement it as a macro? – Scott Hunter Jun 29 '23 at 14:19
  • 1
    Your code doesn't have any allocations so it shouldn't `free` anything. Also, freeing `x` and freeing `*x` are completely different things, so your `#define` will lead to massive errors. – interjay Jun 29 '23 at 14:19
  • Apologies, I was focusing on demonstrating the fact that passing a `char*`/`char**` type parameter to a `void**` type parameter raised a compiler warning. – 404 Name Not Found Jun 29 '23 at 14:32
  • 2
    OT: You should read https://stackoverflow.com/a/1879469/4386427 – Support Ukraine Jun 29 '23 at 14:32
  • `free(ss);` and `free(&s);` were leftovers from my experimentations regarding what I can pass to a `void**` type parameter - since I can pass any pointer to a `void*` type parameter, I thought I might be able to pass any pointer / double pointer to `void**`. I thought I'd keep it around, since I was focusing on the types I'm passing around, rather than the semantics of what can be `free()`'d. – 404 Name Not Found Jun 29 '23 at 14:40

4 Answers4

2

freep can only set a void * to NULL. You presumably want to set a variety of pointer types (including char *) to NULL.

The only way freep would work is if you did

void *p = s;
freep( &p );
s = p;

Of course, that's ridiculous.

An ordinary function won't do because different pointer types can have different sizes and layouts. Solution:

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

free( s );

Warning: The above evaluates the argument expression twice. It's better to use a name that makes it clear this is a macro rather than overridding free.

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

SAFE_FREE( s );
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • 'SAFE_FREE(cond ? ptr1 : ptr2);' gives "error: lvalue required as left operand of assignment" in expansion of macro ‘SAFE_FREE’ – yvs2014 Jun 29 '23 at 16:01
  • @yvs2014 Garbage In, Garbage Out. Use `if ( cond ) SAFE_FREE( ptr1 ); else SAFE_FREE( ptr2 );` (And all the more reason not to call it `free`.) – ikegami Jun 29 '23 at 16:06
  • "if ( cond ) SAFE_FREE( ptr1 ); else SAFE_FREE( ptr2 );" it means that one cannot automatically replace free() with SAVE_FREE() without editing code. And it's possible to find another reasons against macros, what I can remember it's undetectable redirection made with macros for example for autoconf's AC_SEARCH_LIBS(fn,...), like with res_xxx on MacOS – yvs2014 Jun 29 '23 at 16:12
  • @yvs2014 Re "*it means that one cannot automatically replace free() with SAVE_FREE() without editing code*", And that's why it isn't called `free`. I've already explained they're not equivalent, and suggested using a different name for that very reason. – ikegami Jun 29 '23 at 16:15
  • "that's why it's called free" got it:) then maybe it's a good reason for "UNSAFE_FREE" naming – yvs2014 Jun 29 '23 at 16:18
  • @yvs2014, Sorry, That was supposed to be "isn't called". Already fixed. /// I didn't come up with the name `SAFE_FREE`. (It's used by MS, IIRC.) It's safer than a normal `free` because you get a protection violation if you use the freed pointer. That's its reason for being. – ikegami Jun 29 '23 at 16:19
2

The only generic object pointer type that C supports is void*. This special ability of void* does not apply recursively to void**. Passing a char** to a function expecting a void** is invalid C like the compiler told you with a warning/error.

Therefore the only correct use of the freep function in your question is this:

char *s = malloc(5);
void* vptr = s;
freep(&vptr);

And then we can see why freep is problematic - the bad void** API means that s is still a dangling pointer because only vptr points to null now. Just forgot about this function, it was a bad idea and poorly implemented.

A call for sanity is to forget all about that function and instead write readable standard C:

free(s);
s = NULL;

There exists no reason why you can't write this instead of designing some bad API bloat function as an abstraction layer over 2 lines of readable C code.

A macro would work too, but it's good practice to avoid mysterious macros over well-known standard C.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • I agree replacing a two-liner with a macro is not the greatest idea, but for this question I'm putting that aside and focusing more on improving the use case of `void**`s as shown in the original question. – 404 Name Not Found Jun 29 '23 at 15:18
  • @404NameNotFound The correct approach is probably not to build a dedicated abstraction layer on top of the low-level malloc/free, but rather to build an abstraction layer around them while writing the application-specific code. That's how C programs are normally written. – Lundin Jun 30 '23 at 06:36
1

Something like this might do the job:

#define freep(p) {free(p); p=NULL;}
Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
  • 1
    macros can be tricky if they're applied directly, like 'if (cond) free(ptr); else smth;' works, but 'if (cond) freep(ptr); else smth;' doesn't – yvs2014 Jun 29 '23 at 14:43
1

As a special case, C specifies that there are automatic conversions between void * and all other object-pointer types. This is not any kind of pattern, but rather a provision specifically for type void *. That is, there is no analogous special case for void **.

You can manually (via typecast) convert among different object pointer types, such as between char ** and void **. Some compilers may even provide such conversions implicitly, which constitutes a language extension. But writing through such a converted pointer violates the strict aliasing rule, producing undefined behavior.*

Overall, no, you cannot make a function of type void f(void **) serve as a general-purpose function for freeing memory and setting pointers to NULL. Alternatives include:

  • use a macro instead:

    #define free(p) do { free(p); p = NULL; } while (0)
    
  • pass an additional argument that conveys the pointer's actual target type:

    enum target_type { VOID, CHAR, INT, DOUBLE };
    void freep(void *pp, enum target_type type) {
        switch (type) {
            case VOID: {
                void **p = (void **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case CHAR: {
                char **p = (char **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case INT: {
                int **p = (int **) pp;
                free(*p);
                *p = NULL;
                break;
            }
            case DOUBLE: {
                double **p = (double **) pp;
                free(*p);
                *p = NULL;
                break;
            }
        }
    }
    

    Of course, this requires you to choose in advance which pointer types are supported, so it is not altogether general. But if you want something like this then it might be a good use case for X macros to generate the enum definition and matching function implementation without so much boilerplate.

  • In principle, you could also use a type-generic macro as a front end to multiple type-specific free-and-nullify functions, but I don't see much reason to prefer that over both the preceding options. If you're ok with a macro, then the former is much simpler, and if you're not then you're not.


* One might argue that writing a char * through a void ** -- *voidpp = NULL -- is an allowed case, given that char * and void * are required to have the same representation and alignment requirement, and are generally intended to be interchangeable. But even if one accepts that, it is (another) special case. That does not address your question in its full generality.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157