You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调用fclose()时malloc分配内存大小被覆盖的原因及相关疑问

malloc内存元数据异常问题:fclose后分配长度变化的原因解析

问题现象

开发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;
}

核心原因

  1. 直接访问malloc内部元数据是未定义行为
    getAllocLen函数通过p-8访问malloc分配的内存块头部元数据,这完全依赖于特定malloc实现的内存布局(比如glibc的malloc会在返回指针前存储块大小等信息)。不同编译器、libc版本甚至编译选项都会改变这个布局,这种操作没有任何标准保证,本身就会导致不可预测的结果。

  2. 内存越界写入破坏元数据

    • 代码中用fseek(fp, 0, SEEK_END) + ftell(fp)获取文件长度,但在文本模式("r")下,系统会自动转换换行符(比如Windows下\r\n转为\n),ftell返回的是转换后的字符数,而非实际文件字节数。当你用这个长度分配内存并读取时,实际读取的字节数可能超过分配的内存大小,导致越界写入。
    • 即使长度计算准确,malloc(len)分配的内存没有预留\0终止符的空间,后续printf("%s", buffer)会从buffer开始一直读取到\0,必然越界访问,破坏相邻的内存区域——而malloc的元数据通常就存储在返回指针的相邻位置,这直接导致了你看到的“分配长度减少1”的异常。
  3. fclose触发的缓冲区同步放大问题
    当文件以文本模式打开时,fclose会刷新文件缓冲区,这个过程中如果之前的读取已经造成了内存越界,可能刚好触发元数据的可见性变化,让你通过getAllocLen观察到异常;而先关闭文件再分配内存的场景中,内存分配和文件操作的内存区域没有重叠,所以不会出现元数据被破坏的情况。

为什么内存改变后调用free未报错?

内存破坏的影响是随机的:

  • 你破坏的元数据可能没有影响到free函数的关键校验逻辑(比如块大小的合法性检查);
  • 或者破坏的位置在当前内存块的非关键区域,暂时没有触发断言或崩溃。
    但这只是“运气好”,本质上还是未定义行为,后续的内存操作随时可能触发崩溃、数据损坏等严重问题。

修复建议

  • 禁止直接访问malloc内部元数据:永远不要依赖libc的私有内存布局,若需要跟踪内存分配大小,自己维护一个长度变量即可。
  • 用二进制模式打开文件:处理PNG等二进制文件时,必须用"rb"模式打开,避免换行符转换导致的长度计算错误。
  • 分配内存时预留终止符空间:如果要将读取的内容作为字符串使用,分配内存时要malloc(len + 1),并在读取后手动添加buffer[len] = '\0'。
  • 用更可靠的方式获取文件大小:可以使用stat或fstat函数获取准确的文件字节数,避免fseek+ftell在文本模式下的缺陷。

内容的提问来源于stack exchange,提问作者berry acer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 10:25:57