链表程序中的双重释放问题

Posted

技术标签:

【中文标题】链表程序中的双重释放问题【英文标题】:Double free problem in Linked List program 【发布时间】:2022-01-21 23:33:45 【问题描述】:

我的免费使用有问题。 当我在 main.c 中仅释放 l 时,没有使用限制功能,它可以。但是如果我使用限制功能并释放 l_limit,存在问题:free(): double free 在 tcache 2 中检测到并且 valgrind 不满意。 你能帮我修复免费的错误吗? :)

最小的可重现示例:

#include <stdlib.h>
#include <stdio.h>

typedef void* gpointer;

struct cell_s 
    gpointer ptr_value;
    struct cell_s *next;
;

typedef struct cell_s cell_t;

typedef cell_t* adr; // address

struct list_s 
    cell_t *head;
    int size;
;

typedef struct list_s list_t;

typedef void (*list_gfree)(gpointer data);
typedef void (*list_gprint)(gpointer data);

cell_t* create_cell(gpointer v) 
    cell_t *c = malloc(sizeof(cell_t));
    c->next = NULL;
    c->ptr_value = v;

    return c;


void destroy_int(gpointer data) 
    free(data);


void print_int(gpointer data) 
    int *ptr_value = (int *)data;
    printf("%d - ", *ptr_value);


list_t* list_create() 
    list_t *l = malloc(sizeof(list_t));

    l->head = NULL;
    l->size = 0;

    return l;


void list_insert_in_head(list_t *l, gpointer element) 
    adr address_c = create_cell(element);

    address_c->next = l->head;
    l->head = address_c;

    ++l->size;


void list_insert_next(list_t *l, gpointer element, adr address) 
    adr address_c = create_cell(element);

    if (l->head == NULL) 
        list_insert_in_head(l, element);
     else 
        address_c->next = address->next;
        address->next = address_c;
    

    ++l->size;
 

void list_remove_in_head(list_t *l, list_gfree ft_destroy) 
    if (l->head != NULL) 
        adr tmp = l->head->next;
        
        ft_destroy(l->head->ptr_value);
        l->head->ptr_value = NULL;
        
        ft_destroy(l->head);
        l->head= tmp;

        --l->size;
    


void list_remove_after(list_t *l, adr address, list_gfree ft_destroy) 
    if (l->head->next == NULL) 
        printf("Use list_remove_in_head function\n");
     else if (address != NULL) 
        adr tmp = address->next->next;
        
        ft_destroy(address->next->ptr_value);
        address->next->ptr_value = NULL;
        
        ft_destroy(address->next);
        
        address->next = tmp;

        --l->size;
    


void list_destroy(list_t *l, list_gfree ft_destroy) 
    adr current = l->head;

    while(current != NULL) 
        adr tmp = current;

        current = current->next;
        
        ft_destroy(tmp->ptr_value);
        tmp->ptr_value = NULL;
        
        ft_destroy(tmp);
    

    free(l);


void list_print(list_t *l, list_gprint ft_print) 
    adr current = l->head;

    while (current != NULL) 
        ft_print(current->ptr_value);
        current = current->next;
    

    printf("\n");



list_t* limit(list_t *l, int n) 
    list_t *l_limit = list_create();

    adr current = l->head;

    list_insert_in_head(l_limit, current->ptr_value);

    current = current->next;

    adr current_addr_l_limit = l_limit->head;
       
    int count = 1;

    if (n < l->size) 
        while (count < n && current != NULL) 
            ++count;
            
            list_insert_next(l_limit, current->ptr_value, current_addr_l_limit);

            current = current->next;            
            current_addr_l_limit = current_addr_l_limit->next;
        
     else 
        while (current != NULL) 
            list_insert_next(l_limit,  current->ptr_value, current_addr_l_limit);

            current = current->next;            
            current_addr_l_limit = current_addr_l_limit->next;
        
    

    return l_limit;


int main(void) 
    list_t *l = list_create();

    int *ptr_int = (int *)malloc(sizeof(int));
    *ptr_int = 4;
    list_insert_in_head(l, ptr_int);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    int *ptr_int_2 = (int *)malloc(sizeof(int));
    *ptr_int_2 = 7;
    list_insert_in_head(l, ptr_int_2);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    int *ptr_int_3 = (int *)malloc(sizeof(int));
    *ptr_int_3 = 100;
    list_insert_next(l, ptr_int_3, l->head);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    list_t *l_limit = limit(l, 2);
    printf("\nLIMIT 2 \n");
    list_print(l_limit, print_int);
    printf("\n");

    list_remove_in_head(l, destroy_int);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    list_remove_after(l, l->head, destroy_int);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    list_remove_after(l, l->head, destroy_int);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);

    int *ptr_int_4 = (int *)malloc(sizeof(int));
    *ptr_int_4 = 447;
    list_insert_next(l, ptr_int_4, l->head);
    list_print(l, print_int);
    printf("Size : %d\n", l->size);
    
    list_destroy(l_limit, destroy_int);
    list_destroy(l, destroy_int);

输出:

4 - 
Size : 1
7 - 4 - 
Size : 2
7 - 100 - 4 - 
Size : 3

LIMIT 2 
7 - 100 - 

100 - 4 - 
Size : 2
100 - 
Size : 1
Use list_remove_in_head function.
100 - 
Size : 1
100 - 447 - 
Size : 2
free(): double free detected in tcache 2

执行: (-g -fsanitize=address)

=================================================================
==16065==ERROR: AddressSanitizer: attempting double-free on 0x602000000070 in thread T0:
    #0 0x7f8b09173517 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
    #1 0x55ad7141f365 in destroy_int /home/zzz/zzz/main2.c:34
    #2 0x55ad7141fa5b in list_destroy /home/zzz/zzz/main2.c:112
    #3 0x55ad714203a9 in main /home/zzz/zzz/main2.c:211
    #4 0x7f8b08ec4fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #5 0x7f8b08ec507c in __libc_start_main_impl ../csu/libc-start.c:409
    #6 0x55ad7141f204 in _start (/home/zzz/zzz/main+0x1204)

0x602000000070 is located 0 bytes inside of 4-byte region [0x602000000070,0x602000000074)
freed by thread T0 here:
    #0 0x7f8b09173517 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
    #1 0x55ad7141f365 in destroy_int /home/zzz/zzz/main2.c:34
    #2 0x55ad7141f6ea in list_remove_in_head /home/antoine/progc/main2.c:77
    #3 0x55ad714200f5 in main /home/zzz/zzz/main2.c:193
    #4 0x7f8b08ec4fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

previously allocated by thread T0 here:
    #0 0x7f8b09173867 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x55ad7141feed in main /home/zzz/zzz/main2.c:176
    #2 0x7f8b08ec4fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: double-free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127 in __interceptor_free
==16065==ABORTING

Valgrind

==16161== Invalid free() / delete / delete[] / realloc()
==16161==    at 0x484621F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x10921F: destroy_int (in /home/zzz/zzz/main2)
==16161==    by 0x1094C2: list_destroy (in /home/zzz/zzz/main2)
==16161==    by 0x109918: main (in /home/zzz/zzz/main2)
==16161==  Address 0x4a97570 is 0 bytes inside a block of size 4 free'd
==16161==    at 0x484621F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x10921F: destroy_int (in /home/zzz/zzz/main2)
==16161==    by 0x10939C: list_remove_in_head (in /home/antoine/progc/main2)
==16161==    by 0x1097CA: main (in /home/zzz/zzz/main2)
==16161==  Block was alloc'd at
==16161==    at 0x4843839: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x1096B7: main (in /home/zzz/zzz/main2)
==16161== 
==16161== Invalid free() / delete / delete[] / realloc()
==16161==    at 0x484621F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x10921F: destroy_int (in /home/zzz/zzz/main2)
==16161==    by 0x1094C2: list_destroy (in /home/zzz/zzz/main2)
==16161==    by 0x10992E: main (in /home/zzz/zzz/main2)
==16161==  Address 0x4a97610 is 0 bytes inside a block of size 4 free'd
==16161==    at 0x484621F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x10921F: destroy_int (in /home/zzz/zzz/main2)
==16161==    by 0x1094C2: list_destroy (in /home/zzz/zzz/main2)
==16161==    by 0x109918: main (in /home/zzz/zzz/main2)
==16161==  Block was alloc'd at
==16161==    at 0x4843839: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==16161==    by 0x109715: main (in /home/zzz/zzz/main2)
==16161== 
==16161== 
==16161== HEAP SUMMARY:
==16161==     in use at exit: 0 bytes in 0 blocks
==16161==   total heap usage: 13 allocs, 15 frees, 1,168 bytes allocated
==16161== 
==16161== All heap blocks were freed -- no leaks are possible
==16161== 
==16161== For lists of detected and suppressed errors, rerun with: -s
==16161== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

【问题讨论】:

无关:为什么在调用只需要void*free 时将void* 转换为int* @Ted Lyngmo 明确说明 int * 在函数中被释放。使用 struct student,我会做 destroy_student 并将 void* 转换为 student* 我觉得destroy_int这个名字已经够清楚了。无论如何,如果您首先在 destroy_int 中添加 printf("free %p\n", data); fflush(stdout);,您将看到两次相同的地址。 @Ted Lyngmo 是的,你是对的。我用你的 printf 和没有演员表两次看到相同的地址。我删除演员表并编辑我的原始消息。 您的示例看起来并不真正最小。您是否复制并粘贴了 Valgrind 的完整消息?也许您可以启用更多详细信息?作为valgrind 的替代方案,您可以使用选项-g -fsanitize=address,undefined 使用GCC 编译程序。显然,您在list_remove_in_head /home/main.c:77 调用的destroy_int /home/main.c:34 中释放了相同的内存,然后又在list_destroy /home/main.c:112 调用的destroy_int /home/main.c:34 中释放了相同的内存,所以我猜list_remove_in_head 有问题。 【参考方案1】:

你的极限函数是:

list_t* limit(list_t *l, int n) 
list_t *l_limit = list_create();

adr current = l->head;

list_insert_in_head(l_limit, current->ptr_value);//Creates a new cell but uses the same ptr_value!

current = current->next;

adr current_addr_l_limit = l_limit->head;
   
int count = 1;

if (n < l->size) 
    while (count < n && current != NULL) 
        ++count;
        
        list_insert_next(l_limit, current->ptr_value, current_addr_l_limit); //reuses the same ptr_value!

        current = current->next;            
        current_addr_l_limit = current_addr_l_limit->next;
    
 else 
    while (current != NULL) 
        list_insert_next(l_limit,  current->ptr_value, current_addr_l_limit);

        current = current->next;            
        current_addr_l_limit = current_addr_l_limit->next;
    


return l_limit;

从源列表向l_limits 中插入元素时,您会创建新单元格,但不会创建新元素! 所以原始列表中的单元格使用与新列表中的单元格相同的ptr_value

因此,当您销毁第二个列表时,您会尝试释放相同的 ptr_values:

void list_destroy(list_t *l, list_gfree ft_destroy) 
adr current = l->head;

while(current != NULL) 
    adr tmp = current;

    current = current->next;
    
    ft_destroy(tmp->ptr_value);//When this is called for the second list, you access the same pointer as for the first list!
    tmp->ptr_value = NULL;
    
    ft_destroy(tmp);


free(l);

要解决此问题,您可以制作存储在 limit 中“ptr_values”中的对象的实际副本,但这需要您知道存储在 ptr_value 中的值的类型:

list_t* limit(list_t *l, int n) 
list_t *l_limit = list_create();

adr current = l->head;
gpointer buff = malloc(sizeof(int));//replace int by the correct type
if (buff == NULL)
   exit(-1);
*((int*)buff) = *((int*)current->ptr_value);
list_insert_in_head(l_limit, buff);

current = current->next;

adr current_addr_l_limit = l_limit->head;
   
int count = 1;

if (n < l->size) 
    while (count < n && current != NULL) 
        ++count;
        gpointer buff = malloc(sizeof(int));//replace int by the correct type
        if (buff == NULL)
          exit(-1);
        *buff = *current->ptr_value;
        list_insert_next(l_limit, buff, current_addr_l_limit);

        current = current->next;            
        current_addr_l_limit = current_addr_l_limit->next;
    
 else 
    while (current != NULL) 
        list_insert_next(l_limit,  current->ptr_value, current_addr_l_limit);

        current = current->next;            
        current_addr_l_limit = current_addr_l_limit->next;
    


return l_limit;

或者,您可以创建一个特殊的列表析构函数,它不会释放单元格的内容,但这感觉有点奇怪。

【讨论】:

【参考方案2】:

您在main 中使用malloc 为数据元素分配内存,并将指针存储在您的列表中。

稍后您调用free 获取列表管理功能中的数据,例如list_destroylist_remove_in_head

函数limit 创建一个新列表并将指向原始列表中数据的指针存储在新列表中。然后,您将拥有指向不同列表中数据元素的重复(或多个)指针(在本例中为 ll_limit),当列表管理函数释放数据元素的内存时,这将导致双(或多个) )free.

当您的函数可以创建指向同一内存对象的重复或多个指针时,在main 中分配内存的概念,或更一般地在列表管理函数的调用者中并在函数内释放它的概念不起作用。

【讨论】:

以上是关于链表程序中的双重释放问题的主要内容,如果未能解决你的问题,请参考以下文章

free():调用赋值运算符时在 tcache 2 中检测到双重释放

如何使用 Xcode 检测对象的双重释放?

检测到 glibc - 在 C 程序中释放(int ** 类型)时出现双重释放或损坏消息

双重释放自动释放的对象不会崩溃

MySQL 重启使用 MySQL C API 导致双重释放或损坏

我的析构函数是不是给了我这个错误:*** `./main' 中的错误:双重释放或损坏(fasttop):?