由递归和 realloc 引发的 double free 错误分析

774 阅读2分钟

double free 简介:

Double free errors occur when free() is called more than once with the same memory address as an argument. -owasp

从字面意思,double free 就是对同一地址,释放了超过一次,而这并不一定意味着我们在代码中写了两次 free(),极有可能是某些库方法自身使用了 free();

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

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */

void gen(int pos, int n, int open, int close, int *returnSize, char **r, char *str) {
  if (close == n) {
    (*returnSize)++;
    r = (char **)realloc(r, (*returnSize + 1) * sizeof(char *)); //(1)
    str[2 * n] = '\0';
    r[*returnSize - 1] = (char *)malloc((2 * n + 1) * sizeof(char));
    r[*returnSize - 1] = strcpy(r[*returnSize - 1], str);
    return;
  } else {
    if (open > close) {
      str[pos] = ')';
      gen(pos + 1, n, open, close + 1, returnSize, r, str); //(2)
    }
    if (open < n) {
      str[pos] = '(';
      gen(pos + 1, n, open + 1, close, returnSize, r, str); //(3)
    }
  }
}

char **generateParenthesis(int n, int *returnSize) {
  *returnSize = 0;
  if (n <= 0) {
    return NULL;
  }
  char *str = (char *)malloc(2 * n * sizeof(char) + 1);
  char **result = (char **)malloc(sizeof(char *));
  gen(0, n, 0, 0, returnSize, result, str);
  free(str);
  return result;
}

int main(void) {
  int num = 3;
  int size = 0;
  char **r = generateParenthesis(num, &size);
  for (size_t i = 0; i < size; ++i) {
    printf("%zu : %s\n", i, r[i]);
  }
  return 0;
}

原因

上述代码在运行时会报 double free,只有在 return 前写了 free(str),这里不可能会重复调用,于是只有realloc()可能导致这个错误。在realloc过程中,如果是扩展内存区域,会返回新指针,并释放原始指针。

观察 (1)和(2),在一次递归中,传入的r为同一地址,在(1)中,由于执行了realloc,所以会被释放,返回新的地址。

而当(1)执行完毕,(2)中仍然是之前的地址,再次执行realloc,便会遇到 double free 问题。原因找到了,那么应该怎么解决呢。

修复

当使用递归时,尤其是树递归,若要重复访问某一地址,则应该避免此地址收到更改。这时我们便可以献上三重指针大法(之前变量为二重指针),代码可以修改为:

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

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */

void gen(int pos, int n, int open, int close, int *returnSize, char ***r, char *str){
  if (close == n){
    (*returnSize) ++;
    *r = (char **)realloc(*r, (*returnSize + 1) * sizeof(char *));
    str[2*n] = '\0';
   ( *r)[*returnSize - 1] = (char *)malloc((2*n + 1)*sizeof(char));
   ( *r)[*returnSize - 1] = strcpy((*r)[*returnSize - 1] , str);
    return;
  }else{
    if (open > close){
      str[pos] = ')';
      gen(pos+1, n, open, close + 1 ,returnSize, r, str);
    }
    if (open < n){
      str[pos] = '(';
      gen(pos+1, n, open+1, close, returnSize, r, str); 
    }
  }

}

char ** generateParenthesis(int n, int* returnSize){
  *returnSize = 0;
  if (n <= 0){
    return NULL;
  }
  char * str = (char *)malloc(2*n*sizeof(char)+1);
  char ** result = (char **)malloc(sizeof(char *));
  gen(0, n, 0, 0, returnSize, &result, str);
  free(str);
  return result;
}

int main(void) {
  int num = 3;
  int size = 0;
  char ** r = generateParenthesis(num, &size);
  for (size_t i =0; i<size; ++i){
    printf("%zu : %s\n", i, r[i]);
  }
  return 0;
}

观察,我们将 r = (char **)realloc(r, (*returnSize + 1) * sizeof(char *)); 改为 *r = (char **)realloc(*r, (*returnSize + 1) * sizeof(char *));,并在传参时,为char ***,虽然*r的值仍然会因为realloc而被多次修改,但是每次递归我们都传入同样的r,便保证了引用一致的同时不会触发 double free 问题。