INI解析器多section场景触发segmentation fault崩溃排查
INI解析器段错误故障排查
故障现象
开发INI解析器,目标是将所有section存入数组供后续调用,目前SECTION_OPEN逻辑分支触发故障:调用ini_init()函数时,函数首行执行内存分配操作就触发Segmentation Fault崩溃。已尝试以下排查手段:
- 为malloc调用分配超过100000字节的多种不同大小内存空间,程序仍然触发崩溃
- 采用逐段注释代码后逐步取消注释的方式,定位触发崩溃的具体代码位置
测试文件
测试用test.ini内容如下:
; This is a valid comment ; Global variables work var = 1 var2 = 2 var3 = 3 [Test1] ; Section name = Primitive organization = PrimOS test = var another = test once_again=another_test ; Will check to see if variables work as intended [.Test2] ; Subsection ; This variable will crash another_organization = %name% ; This test will crash [Test3] ; Section another_name = "This is me"
关联代码
ini.h头文件
#ifndef SEAWARE_INI_H #define SEAWARE_INI_H #define MAX_HOLDING_CELL 255 #define MAX_LINE_LENGTH 255 // Syntax #define QUOTES '"' #define SECTION_OPEN '[' #define SUBSECTION '.' #define SECTION_CLOSE ']' #define VARIABLE_CALL '%' #define COMMENT ';' #define EQUALS '=' #define EQUALS_PTR "=" // Only one variableType for each variable struct variable { char * variableName; char * value; }; // The magic... struct INI { // If NULL, you are global char * sectionName; // Like [.Test] or [Test1.Test] struct INI ** subSections; // Self explanitory struct variable ** variables; int subSectionsPtr; int variablesPtr; struct INI * nextSection; struct INI * prevSection; }; struct INI * ini_init(); struct INI * interpret(struct INI * ini, char * filePath); #endif
ini.c实现文件
#include <ini.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <stdio.h> // Global functions // Sets up new struct INI * variable struct INI * ini_init() { // Crashes struct INI * newINI = malloc(sizeof(struct INI)); newINI->variablesPtr = 0; newINI->subSectionsPtr = 0; newINI->variables = malloc(sizeof(struct variable*)*(newINI->variablesPtr+1)); newINI->subSections = malloc(sizeof(struct INI *)*(newINI->subSectionsPtr+1)); *newINI->variables = NULL; *newINI->subSections = NULL; newINI->sectionName = NULL; newINI->nextSection = NULL; newINI->prevSection = NULL; return newINI; } // Interprets an INI file, with syntax defined in ../include/ini.h struct INI * interpret(struct INI * ini, char * filePath) { // Loop Variables char line[MAX_LINE_LENGTH]; FILE * iniFile = fopen(filePath, "r"); bool isSubsection = false; // Read line by line while(fgets(line, sizeof(line), iniFile)) { bool isComment = false; bool isQuoted = false; // A holding cell for all the bad defaults out there, stay safe out there... char holdingCell[MAX_HOLDING_CELL]; int holdingCellPtr = 0; // For each character in line for (int i = 0; i < strlen(line); i++) { printf("%c", line[i]); // Prevents memory leaks in holdingCell holdingCell[holdingCellPtr] = '\0'; // Interpret switch(line[i]) { // Move into section case SECTION_OPEN: { // If you are in a subsection, get out of it if (isSubsection == true) { ini = ini->prevSection; isSubsection = false; } struct INI * newSection = ini_init(); if (line[(i+1)]==SUBSECTION) { isSubsection = true; // Reallocate size of subSections array ini->subSections = realloc(ini->subSections, sizeof(struct INI *) * (ini->subSectionsPtr+1)); // Set new subsection and enter it newSection->prevSection = ini; ini->subSections[ini->subSectionsPtr] = newSection; ini->subSectionsPtr++; ini = ini->subSections[ini->subSectionsPtr]; i++; } else { // Create new section and go into it newSection->prevSection = ini; ini->nextSection = newSection; ini = ini->nextSection; ini->nextSection = NULL; } break; } // Set sectionName case SECTION_CLOSE: { ini->sectionName = malloc(sizeof(char) * strlen(holdingCell)); strcpy(ini->sectionName, holdingCell); break; } // Creates new variable and assigns name and value case EQUALS: { isQuoted = true; struct variable * newVar = malloc(sizeof(struct variable)*(ini->variablesPtr+6)); char * value = strtok(line, EQUALS_PTR); // Set variableName newVar->variableName = malloc(sizeof(char)*strlen(value)); strcpy(newVar->variableName, value); // After EQUALS value = strtok(NULL, EQUALS_PTR); // Set value newVar->value = malloc(sizeof(char)*strlen(value)); strcpy(newVar->value, value); printf("%s", value); // Add variable to ini field ini->variables = realloc(ini->variables, sizeof(struct variable*)*(ini->variablesPtr+1)); ini->variables[ini->variablesPtr] = newVar; ini->variablesPtr++; break; } // Skips to new line case COMMENT: { isComment = true; break; } // Gets names and values default: { holdingCell[holdingCellPtr] = line[i]; holdingCellPtr++; break; } } if (isComment == true || isQuoted == true) break; } } // Stop reading file fclose(iniFile); // Loop back to beginning while(ini->prevSection != NULL) { ini = ini->prevSection; } // You get what you get, stop throwing a fit return ini; }
根因定位
ini_init()里malloc直接崩,根本不是malloc本身的问题,是之前的代码已经把堆内存的元数据踩坏了,malloc遍历堆空闲链表的时候访问到非法地址才触发崩溃,具体问题点:
- 核心越界:子节跳转逻辑写错索引
处理子section的三行代码顺序逻辑错误:
给ini->subSections[ini->subSectionsPtr] = newSection; ini->subSectionsPtr++; ini = ini->subSections[ini->subSectionsPtr];subSectionsPtr自增后,直接取自增后索引的指针赋值给ini,这个位置的内存根本没初始化,是随机野值。后续所有基于这个野指针的内存操作都是非法写,直接破坏堆结构。
正确写法应该是跳转到刚存入的新节点,也就是自增前的索引位置:ini = ini->subSections[ini->subSectionsPtr - 1]; - 所有字符串分配没给结束符留空间
不管是section名、变量名还是变量值,分配内存时都只分配了strlen()返回的长度,没有额外+1字节存C语言字符串必需的\0结束符。strcpy执行时会把结束符写到分配的内存块外,属于堆越界写,同样会破坏堆元数据。 - 缺少基础校验逻辑
fopen打开文件后没判断返回值是否为NULL,文件打开失败时直接传给fgets会触发空指针访问- 所有
malloc/realloc的返回值没做判空处理,内存分配失败时直接访问会崩溃 - 写
holdingCell缓冲区时没做边界判断,行内容过长会直接越界写栈空间
- 冗余错误的内存分配
创建variable结构体时写了malloc(sizeof(struct variable)*(ini->variablesPtr+6)),实际只需要存1个variable结构体,分配sizeof(struct variable)即可,多余分配的内存虽然不会直接触发崩溃,但属于逻辑错误,后续操作不当很容易踩内存。
修复方向
- 修正子section跳转的索引错误,进入新创建的子节时取
subSectionsPtr-1位置的指针 - 所有字符串内存分配长度统一+1,预留
\0结束符位置 - 补充文件打开、内存分配的返回值校验,出错时直接返回并打印错误信息
- 给
holdingCell增加写入边界判断,写入前检查指针是否超过缓冲区长度,避免栈溢出
内容的提问来源于stack exchange,提问作者user18348624
相关产品推荐
相关产品推荐

