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

使用getline解析配置文件时随机出现内存泄漏问题求助

问题描述

使用getline解析配置文件时,随机出现内存泄漏,已确认在所有执行分支中释放了fileconfig_parser中初始分配的line内存,但泄漏仍随机发生。泄漏来自getline分配的内存,AddressSanitizer检测日志如下:

=================================================================
==1561==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 120 byte(s) in 1 object(s) allocated from:
    #0 0x7fad2918a808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144
    #1 0x7fad28f0e543 in _IO_getdelim /build/glibc-SzIz7B/glibc-2.31/libio/iogetdelim.c:62
    #2 0x7ffde0365e1f  ([stack]+0x1ee1f)

SUMMARY: AddressSanitizer: 120 byte(s) leaked in 1 allocation(s).

相关代码如下:

#define _GNU_SOURCE
#include "parser.h"

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

struct global parse_global(char *line, FILE *file)
{
    char *log_file = NULL;
    bool log = false;
    char *pid_file = NULL;

    size_t len;
    ssize_t nread;
    char *saveptr = NULL;

    while ((nread = getline(&line, &len, file)) != -1
           && strcmp(line, "[[vhosts]]\n") != 0)
    {
        char *arg_name = strtok_r(line, " =", &saveptr);
        char *arg_value = strtok_r(NULL, " =\n", &saveptr);

        if (strcmp(arg_name, "log_file") == 0)
        {
            log_file = strdup(arg_value);
        }
        else if (strcmp(arg_name, "log") == 0)
        {
            if (strcmp(arg_value, "true") == 0)
            {
                log = true;
            }
            else
            {
                log = false;
            }
        }
        else if (strcmp(arg_name, "pid_file") == 0)
        {
            pid_file = strdup(arg_value);
        }
    }

    return global_init(log_file, log, pid_file);
}

struct vhost parse_vhost(char *line, FILE *file)
{
    char *server_name = "my_server";
    char *port = NULL;
    char *ip = NULL;
    char *root_dir = NULL;
    char *default_file = NULL;

    size_t len;
    ssize_t nread;
    char *saveptr = NULL;

    while ((nread = getline(&line, &len, file)) != -1
           && strcmp(line, "\n") != 0)
    {
        char *arg_name = strtok_r(line, " =", &saveptr);
        char *arg_value = strtok_r(NULL, " =\n", &saveptr);

        if (strcmp(arg_name, "server_name") == 0)
        {
            server_name = strdup(arg_value);
        }
        else if (strcmp(arg_name, "port") == 0)
        {
            port = strdup(arg_value);
        }
        else if (strcmp(arg_name, "ip") == 0)
        {
            ip = strdup(arg_value);
        }
        else if (strcmp(arg_name, "root_dir") == 0)
        {
            root_dir = strdup(arg_value);
        }
        else if (strcmp(arg_name, "default_file") == 0)
        {
            default_file = strdup(arg_value);
        }
    }
    
    return vhost_init(server_name, port, ip, root_dir, default_file);
}

void free_global(struct global g)
{
    free(g.pid_file);
    free(g.log_file);
}

void free_vhost(struct vhost v)
{
    free(v.server_name);
    free(v.port);
    free(v.ip);
    free(v.root_dir);
    free(v.default_file);
}

struct server *fileconfig_parser(char *file)
{
    FILE *f = fopen(file, "r");
    if (!f)
    {
        perror("cannot open file");
        return NULL;
    }

    char *line = NULL;
    size_t len = 0;

    ssize_t nread = getline(&line, &len, f);
    if (nread == -1 || strcmp(line, "[global]\n") != 0)
    {
        free(line);
        perror("invalid file");
        fclose(f);
        return NULL;
    }

    struct global global = parse_global(line, f);
    if (global.pid_file == NULL)
    {
        free_global(global);
        fclose(f);
        free(line);
        return NULL;
    }

    struct vhost vhost = parse_vhost(line, f);
    if (vhost.server_name == NULL || vhost.port == NULL || vhost.ip == NULL
        || vhost.root_dir == NULL)
    {
        free_global(global);
        free_vhost(vhost);
        fclose(f);
        free(line);
        return NULL;
    }

    struct server *server = server_init(global, vhost);

    free(line);
    fclose(f);

    return server;
}
问题原因

核心问题在于parse_global和parse_vhost的参数传递方式:

  • fileconfig_parser中创建的line指针是以值传递的方式传入两个解析函数的,这意味着函数内部的line是局部变量,仅初始指向外部line的内存,但二者并非同一个指针。
  • 在解析函数的getline(&line, &len, file)调用中,当当前内存不足以容纳新行时,getline会分配新的堆内存,并修改局部的line指针指向新内存,但这个新指针不会同步回fileconfig_parser中的外部line。
  • 函数返回后,这些在解析函数内部由getline新分配的内存地址丢失,无法被释放,从而导致泄漏。
  • 泄漏的随机性来自getline的内存分配策略:只有当现有内存不足以容纳新行时才会分配新内存;若所有行都能被现有内存容纳,就不会触发泄漏。
修复方案

需要将line和len以指针的指针(char/size_t*)**传递给解析函数,确保getline修改的是fileconfig_parser中的原始指针,这样后续free(line)就能释放所有由getline分配的内存。同时修复server_name字符串字面量被free的潜在未定义行为。

1. 修改parse_global函数

struct global parse_global(char **line, size_t *len, FILE *file)
{
    char *log_file = NULL;
    bool log = false;
    char *pid_file = NULL;

    ssize_t nread;
    char *saveptr = NULL;

    while ((nread = getline(line, len, file)) != -1
           && strcmp(*line, "[[vhosts]]\n") != 0)
    {
        char *arg_name = strtok_r(*line, " =", &saveptr);
        char *arg_value = strtok_r(NULL, " =\n", &saveptr);

        if (strcmp(arg_name, "log_file") == 0)
        {
            log_file = strdup(arg_value);
        }
        else if (strcmp(arg_name, "log") == 0)
        {
            log = strcmp(arg_value, "true") == 0;
        }
        else if (strcmp(arg_name, "pid_file") == 0)
        {
            pid_file = strdup(arg_value);
        }
    }

    return global_init(log_file, log, pid_file);
}

2. 修改parse_vhost函数

struct vhost parse_vhost(char **line, size_t *len, FILE *file)
{
    char *server_name = NULL; // 初始化为NULL,避免释放字符串字面量
    char *port = NULL;
    char *ip = NULL;
    char *root_dir = NULL;
    char *default_file = NULL;

    ssize_t nread;
    char *saveptr = NULL;

    while ((nread = getline(line, len, file)) != -1
           && strcmp(*line, "\n") != 0)
    {
        char *arg_name = strtok_r(*line, " =", &saveptr);
        char *arg_value = strtok_r(NULL, " =\n", &saveptr);

        if (strcmp(arg_name, "server_name") == 0)
        {
            server_name = strdup(arg_value);
        }
        else if (strcmp(arg_name, "port") == 0)
        {
            port = strdup(arg_value);
        }
        else if (strcmp(arg_name, "ip") == 0)
        {
            ip = strdup(arg_value);
        }
        else if (strcmp(arg_name, "root_dir") == 0)
        {
            root_dir = strdup(arg_value);
        }
        else if (strcmp(arg_name, "default_file") == 0)
        {
            default_file = strdup(arg_value);
        }
    }
    
    return vhost_init(server_name, port, ip, root_dir, default_file);
}

3. 调整vhost_init处理默认值

struct vhost vhost_init(char *server_name, char *port, char *ip, char *root_dir, char *default_file) {
    struct vhost v;
    // 若server_name为NULL,分配默认值的堆内存,确保可以安全释放
    v.server_name = server_name ? server_name : strdup("my_server");
    v.port = port;
    v.ip = ip;
    v.root_dir = root_dir;
    v.default_file = default_file;
    return v;
}

4. 修改fileconfig_parser中的调用

struct server *fileconfig_parser(char *file)
{
    FILE *f = fopen(file, "r");
    if (!f)
    {
        perror("cannot open file");
        return NULL;
    }

    char *line = NULL;
    size_t len = 0;

    ssize_t nread = getline(&line, &len, f);
    if (nread == -1 || strcmp(line, "[global]\n") != 0)
    {
        free(line);
        perror("invalid file");
        fclose(f);
        return NULL;
    }

    struct global global = parse_global(&line, &len, f);
    if (global.pid_file == NULL)
    {
        free_global(global);
        fclose(f);
        free(line);
        return NULL;
    }

    struct vhost vhost = parse_vhost(&line, &len, f);
    if (vhost.server_name == NULL || vhost.port == NULL || vhost.ip == NULL
        || vhost.root_dir == NULL)
    {
        free_global(global);
        free_vhost(vhost);
        fclose(f);
        free(line);
        return NULL;
    }

    struct server *server = server_init(global, vhost);

    free(line);
    fclose(f);

    return server;
}
总结

通过指针传递line和len,确保getline修改的是外部原始指针,后续free(line)就能释放所有getline分配的内存,解决随机泄漏问题。同时调整server_name的初始化逻辑,避免释放字符串字面量导致的未定义行为。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 03:31:05