调用fclose()时malloc分配内存大小被覆盖的原因及相关疑问
问题现象
开发PNG解码器时频繁触发malloc断言错误,例如:
malloc.c:2617: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.
[IHDR] Data: RGBA
malloc(): invalid next size (unsorted)
排查中发现调用fclose()后,通过自定义函数getAllocLen获取的内存分配长度会减少1,且仅在使用fseek(fp, 0, SEEK_SET)或rewind(fp)重置文件指针时出现该问题;先关闭文件再分配内存重新读取则无此现象。
复现代码(异常场景)
#include <stdio.h> #include <stdlib.h> #include <errno.h> void checkFP(FILE* fp) { if(!fp) { printf("couldn't open file\n"); switch(errno) { case 2: printf("File not found\n"); break; case 13: printf("No perms to open file\n"); break; } exit(1); } } unsigned getAllocLen(char * p) { return *(unsigned*)(p-8); } int main() { unsigned len = 0, orig = 0, l = 0; char *buffer, *head; FILE *fp = fopen("test_1.c", "r"); checkFP(fp); fseek(fp, 0, SEEK_END); len = ftell(fp); l = len; buffer = malloc(len); head = buffer; orig = getAllocLen(buffer); fseek(fp, 0, SEEK_SET); while(l) { size_t read_bytes = fread(head, 1, l, fp); head += read_bytes; l -= read_bytes; } if( fclose(fp) ) { printf("Error when closing File\n"); exit(2); } if(orig != getAllocLen(buffer)) { printf("Uh Ohh Memory corrupted(%p, original len = %u,current len = %u)\n",buffer, orig, getAllocLen(buffer)); free(buffer); exit(3); } printf("%s\n",buffer); free(buffer); return 0; }
正常场景代码
#include <stdio.h> #include <stdlib.h> #include <errno.h> void checkFP(FILE* fp) { if(!fp) { printf("couldn't open file\n"); switch(errno) { case 2: printf("File not found\n"); break; case 13: printf("No perms to open file\n"); break; } exit(1); } } unsigned getAllocLen(char * p) { return *(unsigned*)(p-8); } int main() { unsigned len = 0, orig = 0, l = 0; char *buffer, *head; FILE *fp = fopen("test_1.c", "r"); checkFP(fp); fseek(fp, 0, SEEK_END); len = ftell(fp), l = len; if( fclose(fp) ) { printf("Error when closing file\n"); exit(2); } buffer = malloc(len); head = buffer; orig = getAllocLen(buffer); fp = fopen("test_1.c", "r"); checkFP(fp); while(l) { size_t read_bytes = fread(head, 1, l, fp); head += read_bytes; l -= read_bytes; } if( fclose(fp) ) { printf("Error when closing File\n"); exit(2); } if(orig != getAllocLen(buffer)) { printf("Uh Ohh Memory corrupted(%p, original len = %u,current len = %u)\n",buffer, orig, getAllocLen(buffer)); free(buffer); exit(3); } printf("%s\n",buffer); free(buffer); return 0; }
核心原因
直接访问malloc内部元数据是未定义行为
getAllocLen函数通过p-8访问malloc分配的内存块头部元数据,这完全依赖于特定malloc实现的内存布局(比如glibc的malloc会在返回指针前存储块大小等信息)。不同编译器、libc版本甚至编译选项都会改变这个布局,这种操作没有任何标准保证,本身就会导致不可预测的结果。内存越界写入破坏元数据
- 代码中用
fseek(fp, 0, SEEK_END)+ftell(fp)获取文件长度,但在文本模式("r")下,系统会自动转换换行符(比如Windows下\r\n转为\n),ftell返回的是转换后的字符数,而非实际文件字节数。当你用这个长度分配内存并读取时,实际读取的字节数可能超过分配的内存大小,导致越界写入。 - 即使长度计算准确,
malloc(len)分配的内存没有预留\0终止符的空间,后续printf("%s", buffer)会从buffer开始一直读取到\0,必然越界访问,破坏相邻的内存区域——而malloc的元数据通常就存储在返回指针的相邻位置,这直接导致了你看到的“分配长度减少1”的异常。
- 代码中用
fclose触发的缓冲区同步放大问题
当文件以文本模式打开时,fclose会刷新文件缓冲区,这个过程中如果之前的读取已经造成了内存越界,可能刚好触发元数据的可见性变化,让你通过getAllocLen观察到异常;而先关闭文件再分配内存的场景中,内存分配和文件操作的内存区域没有重叠,所以不会出现元数据被破坏的情况。
为什么内存改变后调用free未报错?
内存破坏的影响是随机的:
- 你破坏的元数据可能没有影响到
free函数的关键校验逻辑(比如块大小的合法性检查); - 或者破坏的位置在当前内存块的非关键区域,暂时没有触发断言或崩溃。
但这只是“运气好”,本质上还是未定义行为,后续的内存操作随时可能触发崩溃、数据损坏等严重问题。
修复建议
- 禁止直接访问malloc内部元数据:永远不要依赖libc的私有内存布局,若需要跟踪内存分配大小,自己维护一个长度变量即可。
- 用二进制模式打开文件:处理PNG等二进制文件时,必须用
"rb"模式打开,避免换行符转换导致的长度计算错误。 - 分配内存时预留终止符空间:如果要将读取的内容作为字符串使用,分配内存时要
malloc(len + 1),并在读取后手动添加buffer[len] = '\0'。 - 用更可靠的方式获取文件大小:可以使用
stat或fstat函数获取准确的文件字节数,避免fseek+ftell在文本模式下的缺陷。
内容的提问来源于stack exchange,提问作者berry acer

