Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Incompatible pointer type warning with pointer-to-pointer types when passing char** to void** function parameter

I'm trying to implement a secure-free function that erases the allocated memory, frees it and then also sets the pointer to the allocated region to NULL so the pointer cannot be reused after-free and cannot be double-freed with the same function. To achieve this I'm using a pointer-to-pointer parameter, which allows me to overwrite the pointer to the allocated memory.

The issue is GCC complaining about incompatible pointer types ("but it works on my machine"); I did not expect such a warning. My understanding was that any pointer can be implicitly cast to void*, thus I'm guessing also the address of a pointer could be cast to void**.

In the meantime I rewrote secure_free() as a macro, which solves the warning, but I would like to know why the compiler is complaining.

File securefree.c

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

#define STRING_BUFFER_LEN 10

/**
 * Securely erases a heap-allocated memory section, frees it and sets its
 * pointer to NULL to avoid use-after-free and double-free.
 */
static void secure_free(void** p_p_data, size_t length_in_bytes)
{
    if (p_p_data == NULL || *p_p_data == NULL)
    { return; }
    memset(*p_p_data, 0, length_in_bytes);
    free(*p_p_data);
    *p_p_data = NULL;
}

int main(void)
{
    // Allocate some data
    char* my_string = calloc(STRING_BUFFER_LEN, sizeof(char));
    if (my_string == NULL) { return 1; }
    // Use the allocated space in some way
    my_string[0] = 'a';
    my_string[1] = 'b';
    // Free using the dedicated function
    secure_free(&my_string, STRING_BUFFER_LEN);
    return 0;
}

Compiling with GCC (Rev6, Built by MSYS2 project, 10.2.0):

$ gcc securefree.c -o securefree
securefree.c: In function 'main':
securefree.c:29:17: warning: passing argument 1 of 'secure_free' from incompatible pointer type [-Wincompatible-pointer-types]
   29 |     secure_free(&my_string, STRING_BUFFER_LEN);
      |                 ^~~~~~~~~~
      |                 |
      |                 char **
securefree.c:11:32: note: expected 'void **' but argument is of type 'char **'
   11 | static void secure_free(void** p_p_data, size_t length_in_bytes)
      |                         ~~~~~~~^~~~~~~~

EDIT: the macro version looks like this

#define secure_free_macro(ptr, len) if ((ptr) != NULL) { \
        memset((ptr), 0, (len)); free(ptr); (ptr) = NULL; }
like image 734
Matjaž Avatar asked Sep 06 '25 03:09

Matjaž


2 Answers

What you're trying to do cannot be done portably, because different pointer types can have different representations; and to assign the null pointer to the value, you must cast the pointer-to-pointer first to a pointer to the effective type of the actual pointer variable - which is impossible.

What you can do however is use a macro, it is as good as any, and much simpler to use:

#define secure_free(x) (free(x), (x) = 0)

This works without &.

like image 134

C lets any pointer be implicitly cast to void* as an explicit exception. Note, that void and char are not compatible types. Thus void*, char*, void** and char** are not compatible as well. That is why compiler emits a warning.

To bypass this issue change the function signature to use void*:

void secure_free(void* ptr, size_t length_in_bytes) {
   void **p_p_data = (void**)ptr;
   ...
}

To add protection that the argument is a pointer to a pointer one could use a macro:

#define secure_free(x,s) ((void)sizeof **(x), secure_free((x), (s)))
  • Expression **(x) will not compile is x were not a pointer to a pointer.
  • sizeof prevent evaluation of x in **(x) to avoid side effects
  • (void) silence the compiler about complaining on unused value
  • comma operator (X,Y), return only value of the Y, which is return value of secure_free(...)
  • using the same name for macro as for function allows to expand secure_free as macro only if it used as a function. This allows to use secure_free as a pointer to a function

Extra note. In the code

    memset(*p_p_data, 0, length_in_bytes);
    free(*p_p_data);

The compiler will likely optimize out memset(). I suggest casting to volatile void * to force the compiler to generate clearing code.

Edit

Additionally, one may have clear the content with a loop because memset discards volatile qualifier.

like image 43
tstanisl Avatar answered Sep 07 '25 20:09

tstanisl