简体   繁体   中英

Overloading new/delete operators on ubuntu 22.04, libc 2.35 and calling SDL2 SDL_CreateRenderer causes segmentation fault

I have my own memory manager in linux (ubuntu) in order to detect memory leaks or memory corruption in my c/c++ code.

To use my own memory manager just I've overloaded new/delete global operators like that,

void *operator new (size_t _size){
      return malloc(_size);
}

void operator delete(void *_pointer){
   free(_pointer);
}

Code 1.1

But in order to add some pointer information, I reserve additional memory for header metadata, for instance, to save allocator type and allocated size in this struct,

typedef struct{
    size_t size;
   ALLOCATOR_TYPE allocator_type
}PointerHeaderInformation;

where ALLOCATOR_TYPE is,

typedef enum{
    UNKNOWN_ALLOCATOR=0,
    NEW_ALLOCATOR,
    NEW_ALLOCATOR_WITH_BRACETS,
    MAX_ALLOCATE_TYPES
}ALLOCATOR_TYPE;

Code 1.2

So I have modified new operator seen on code 1.1 to allocate pointer and setup header information as we can see in the following code,

void *operator new(size_t _size){
    // set size for request _size and header information
    size_t size=sizeof(HeaderPointerInformation)+_size;
    
    // here the pointer allocation
    char *pointer=(char *)malloc(size);
    if(pointer==NULL){
       throw std::bad_alloc();
    }
    // setup header information
    ((HeaderPointerInformation *)(pointer))->type_allocator=NEW_ALLOCATOR;
    ((HeaderPointerInformation *)(pointer))->size=_size;

    // returns the pointer + sizeof(HeaderPointerInformation)
    return pointer+sizeof(HeaderPointerInformation);
}

Code 1.3

As we see at code 1.3, the it returns the pointer allocated + sizeof(HeaderPointerInformation) to not overwrite header information.

On the other side, when we want to free allocated pointer using our custom allocator, it has to get the header pointer first, check the type of allocator and free the pointer using free function. For instance for overloaded delete operator,

void  operator  delete(void  *pointer) noexcept{

    // check NULL
    if(pointer == NULL){
        return;
    }

    // get header information
    HeaderPointerInformation *header=(HeaderPointerInformation *)((char *)pointer-SIZEOF_ALIGNED_HEADER);

    // check type allocator
    if(header->type_allocator != NEW_ALLOCATOR){
        fprintf(stderr,"pointer was not allocated as 'new' allocator\n");
    }

    // header is the base of allocated pointer so it's freed
    free(header);
}

Code 1.4

I experimenting an extrange segmentation fault that it happens in combination with SDL2 and my overloaded new/delete operators when I try to create a renderer using SDL_CreateRenderer. SDL_CreateRenderer performs a series of calls until last known line 70 dl-init.c before the segmentation fault in llvm::Regex::Regex() happens as we can see in the following image.

在此处输入图像描述

I have never had any problems with previous ubuntu distributions, 22.04, 18.04, etc. but in the recent ubuntu distribution 22.04, which it comes with libc 2.35, so something was changed in the libc.

This segmentation fault it has to do with the overloaded operators new/delete operators because if I not using overloaded operators the problem is gone.

I have created a very simple code that produces this segmentation fault,

#include    <SDL2/SDL.h>
#include    <stdlib.h>
#include    <stdio.h>
#include    <stddef.h>
#include    <new>
#include    <cstddef>

// type of allocators
typedef enum{
    UNKNOWN_ALLOCATE=0,
    NEW_ALLOCATOR,
    NEW_ALLOCATOR_WITH_BRACETS,
    MAX_ALLOCATE_TYPES
}ALLOCATOR_TYPE;

// metadata information included in the pointer allocated
typedef  struct{
    char    type_allocator;
    int     size;
}HeaderPointerInformation;

void *newPointer(size_t _size, ALLOCATOR_TYPE _allocator_type){
    size_t size=sizeof(HeaderPointerInformation)+_size;
    char *pointer=(char *)malloc(size);
    if(pointer==NULL){
        throw std::bad_alloc();
    }

    ((HeaderPointerInformation *)(pointer))->type_allocator=_allocator_type;
    ((HeaderPointerInformation *)(pointer))->size=size;

    // returns the pointer base (allocated pointer with offset of metadata information)
    return pointer+sizeof(HeaderPointerInformation);
}

void*  operator  new(size_t  _size) {
    return newPointer(_size,NEW_ALLOCATOR);
}
//--------------------------------------------------------------------------------------------
void*  operator  new[](size_t  _size) {
    return  newPointer(_size,NEW_ALLOCATOR_WITH_BRACETS);
}
//--------------------------------------------------------------------------------------------
void  operator  delete(void  *pointer) noexcept{

    // return if pointer==NULL
    if(pointer == NULL){
       return;
    }

    // get header information
    HeaderPointerInformation *header=(HeaderPointerInformation *)((char *)pointer-sizeof(HeaderPointerInformation));

    // check type allocator
    if(header->type_allocator != NEW_ALLOCATOR){
        fprintf(stderr,"pointer was not allocated as 'new' allocator\n");
    }

    // header is the base of allocated pointer so it's freed
    free(header);
}
//--------------------------------------------------------------------------------------------
void  operator  delete[](void  *pointer)  noexcept{

    // return if pointer==NULL
    if(pointer == NULL){
       return;
    }

    // get header information
    HeaderPointerInformation *header=(HeaderPointerInformation *)((char *)pointer-sizeof(HeaderPointerInformation));

    // check type allocator
    if(header->type_allocator != NEW_ALLOCATOR_WITH_BRACETS){
        fprintf(stderr,"pointer was not allocated as 'new []' allocator\n");
    }

    // header is the base of allocated pointer so it's freed
    free(header);
}

int main(int argc, char *argv[]){

    printf(
        "align(std::max_align_t):%i\n"
        "sizeof(HeaderPointerInformation):%i\n"
        ,(int)(alignof(std::max_align_t))
        ,(int)(sizeof(HeaderPointerInformation))
   );

    //--------------------------------------------------
    // BEGIN: test that overloaded new/delete works as expected
    int *i=new int;
    int *ii=new int[2];

    delete i;
    delete [] ii;
    // END: test that overloaded new/delete works as expected
    //--------------------------------------------------

    if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr,"Unable to init video subsystem: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    SDL_Window *window = SDL_CreateWindow("test",50,50,100,100,0);

    if (!window) {
        fprintf(stderr,"Unable to create window: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    // Calling SDL_CreateRenderer it causes segmentation fault at llvm::Regex::Regex
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_WINDOW_SHOWN | SDL_RENDERER_ACCELERATED);

    if (!renderer) {
        fprintf(stderr,"Unable to create renderer: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    if(renderer != NULL){
        SDL_DestroyRenderer(renderer);
    }

    if(window != NULL){
        SDL_DestroyWindow(window);
    }
}

Code 1.5

As we can see in the code 1.5, first there're delete/new operator implementations, then in the main code it performs a test using overloaded operators new/delete that check its functionallity and the rest is just SDL2 initialization.

To compile just as simple as follow,

g++ main.cpp -Ipath_include_sdl2 -Lpath_lib_sdl2 -lSDL2 -lm -o main

Note: You have to provide path_include_sdl2 and path_lib_sdl2 if necessary


Why to NOT usign address sanitizer (aka ASAN) tool that comes with gnu compiler itself as is suggested by some comments?

I don't know yet if I'll switch using ASAN, but in principle I won't becasue my code is also compiles on windows through mingw and AFAIK mingw ASAN is not implemented yet.


Edit(s) with regard to user17732522 (comments) I have change code 1.5,

  • Fixed point 2, so on overloaded new and new[] operators I added conditional to throw sdt::bad_alloc if resulting of pointer after doing malloc is NULL,

void *newPointer(size_t _size, ALLOCATOR_TYPE _allocator_type){

    ...

    char *pointer=(char *)malloc(size);
    if(pointer==NULL){
        throw std::bad_alloc();
    }

    ...
}

  • Fixed point 5, so on overloaded delete and delete[] operators, it has changed noexcept(true) by noexcept ,

    void  operator  delete(void  *pointer) noexcept{
      ...
    }
            
    void  operator  delete[](void  *pointer)  noexcept{
      ...
    }

  • Fixed point 6, it has changed free((void *)header) by free(header)

  • Fixed point 7, on overloaded function delete it has added conditional that returns if pointer passed is NULL


    void  operator  delete(void  *pointer) noexcept{
           // check NULL
           if(pointer == NULL){
               return;
           }
           ...
    }

    void  operator  delete[](void  *pointer) noexcept{
           // check NULL
           if(pointer == NULL){
               return;
           }
           ...
    }
            
    void  operator  delete[](void  *pointer)  noexcept{
      ...
    }

Multiple problems:

  1. You do not make sure that the pointer you return from your operator new replacement is aligned at least as strictly as __STDCPP_DEFAULT_NEW_ALIGNMENT__ , which this overload must always guarantee. Even if malloc is guaranteed to return suitably aligned memory, your offset may change the alignment of the pointer that you return. But malloc may also not align sufficiently. malloc is guaranteed to align at least to alignof(std::max_align_t) which may be smaller than __STDCPP_DEFAULT_NEW_ALIGNMENT__ .

  2. The operator new overload you are replacing must not return a null pointer. You need to check the result of malloc and if it is a null pointer you must throw std::bad_alloc . Because you are dereferencing the pointer inside your implementation that is even more directly important.

Less severe problems:

  1. The way you access the header structure are aliasing violations. Your header structure is an implicit-lifetime type, so you don't necessarily need to new it explicitly, however you need to std::launder your pointer to access it. This would be enough for operator new , however I don't think there is any strictly standard-conforming way to implement the operator delete to be able to access the header, so I guess you need to rely on the compiler behaving "reasonably" here anyway.

  2. If you replace operator new and operator delete for the overloads without alignment parameter, you probably also want to replace those with std::align_val_t parameter, in case the program allocates memory with alignment requirements stricter than __STDCPP_DEFAULT_NEW_ALIGNMENT__ .

  3. noexcept(true) on the deallocation functions is redundant. They are noexcept(true) by default. Also this is canonically written as noexcept , not noexcept(true) .

  4. The void* cast in free((void *)header); is redundant. It works implicitly (and you are using that in the return statement of operator new ).

(Edit) Additional severe issue:

  1. The operator delete overloads you are replacing here must accept null pointer values as argument. Your current implementation does not since it immediately does pointer arithmetic on the pointer. You need to check for a null pointer and return from the function in that case before trying to access the header.

Also in context of what you are trying to do: There are already tools which provide the functionality you are trying to implement, for example ASAN and valgrind.

As user17732522 pointed out the pointer returned it has to be correctly aligned in blocks as alignof(std::max_align_t) that in my machine is 16 .

In the case of overloaded operator new, after allocate pointer through malloc (that returns perfectly and aligned pointer) I return a pointer as pointer base with an offset of sizeof(HeaderPointerInformation) that in case is 8 so it returns an unaligned pointer .

So to solve this issue is has to calcule a valid offset transforming sizeof(HeaderPointerInformation) as multiple of alignof(std::max_align_t) like it describes the following line,

#define SIZEOF_ALIGNED_HEADER       (sizeof(HeaderPointerInformation)/BLOCK_ALIGNMENT+1)*BLOCK_ALIGNMENT

Where indeed BLOCK_ALIGMENT is defined as,

#define BLOCK_ALIGNMENT             alignof(std::max_align_t)

Finally I present the modified code 1.5 of overloaded functions returning/getting the pointer with the right offset,


#define BLOCK_ALIGNMENT             alignof(std::max_align_t)
#define SIZEOF_ALIGNED_HEADER       (sizeof(HeaderPointerInformation)/BLOCK_ALIGNMENT+1)*BLOCK_ALIGNMENT


void *newPointer(size_t _size, ALLOCATOR_TYPE _allocator_type){
    size_t size=SIZEOF_ALIGNED_HEADER+_size;

    char *pointer=(char *)malloc(size);

    ...

    // returns the pointer base (allocated pointer with offset of metadata information)
    return pointer+SIZEOF_ALIGNED_HEADER;
}


void  operator  delete(void  *pointer) noexcept{

    ...

    // get header information
    HeaderPointerInformation *header=(HeaderPointerInformation *)((char *)pointer-SIZEOF_ALIGNED_HEADER);

    ...
    
    // header is the base of allocated pointer so it's freed
    free(header);
}

void  operator  delete[](void  *pointer)  noexcept{

    ...

    // get header information
    HeaderPointerInformation *header=(HeaderPointerInformation *)((char *)pointer-SIZEOF_ALIGNED_HEADER);
        
    ...
    
    // header is the base of allocated pointer so it's freed
    free(header);
}


The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM