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

WebAssembly C程序malloc随机崩溃问题排查求助

问题:WebAssembly打包工具处理大型项目时malloc崩溃

我正在开发一个用C和少量JavaScript编写的JavaScript/Web模块打包工具。该工具处理小型项目完全正常,但处理文件较多的复杂项目时,会随机在某个函数的malloc调用处崩溃。此前遇到过由程序其他部分内存泄漏/错误导致的malloc和free报错,但目前未发现明显异常。

报错信息

RuntimeError: memory access out of bounds
    at dlmalloc (wasm://wasm/007a3c2a:wasm-function[339]:0x7db59)
    at ReadDataFromFile (wasm://wasm/007a3c2a:wasm-function[114]:0x4cfd8)
    at BundleFile (wasm://wasm/007a3c2a:wasm-function[128]:0x55e36)
    at BundleFiles (wasm://wasm/007a3c2a:wasm-function[130]:0x6b9fb)
    at /mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/Build/CFunctions.js:985:22
    at Object.ccall (/mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/Build/CFunctions.js:5328:22)
    at /mnt/c/Users/redstarbird/Documents/Projects/ArrowStack/src/ArrowPack.js:104:24

崩溃函数代码

char EMSCRIPTEN_KEEPALIVE *ReadDataFromFile(char *path)
{ // 返回文件内容
    printf("path: %s\n", path);
    FILE *filePTR = fopen(path, "r");

    if (filePTR == NULL)
    {
        printf("Error opening file %s\n", path);
        return NULL;
    }
    fseek(filePTR, 0, SEEK_END);      // 定位到文件末尾
    long int length = ftell(filePTR); // 获取文件长度
    fseek(filePTR, 0, SEEK_SET);      // 回到文件开头

    char *buffer = malloc(length + 1); // 崩溃发生在此处
    if (buffer == NULL)
    {
        printf("Error creating buffer\n");
        exit(1);
    }

    int currentChar = 0;
    do
    {
        if (feof(filePTR))
        {
            break;
        }
        buffer[currentChar] = fgetc(filePTR);
        currentChar++;
    } while (1);
    fclose(filePTR);
    buffer[currentChar - 1] = '\0';
    return buffer;
}

项目编译命令

emcc --no-entry -s INITIAL_MEMORY=128mb -Wl,--export=__heap_base -Wl,--export=__data_end -Wl,--export=malloc -Wl,--export=free -sENVIRONMENT=node --profiling -sRUNTIME_DEBUG=1 -fsanitize=undefined -sLLD_REPORT_UNDEFINED -g3 -sSTACK_OVERFLOW_CHECK=2 -sASSERTIONS=2 src/Main.c src/C/cJSON/cJSON.c src/DependencyGraph/DependencyGraph.c ./src/C/StringRelatedFunctions.c ./src/Regex/RegexFunctions.c ./src/DependencyGraph/FindDependencies.c ./src/SettingsSingleton/settingsSingleton.c ./src/C/ProblemHandler.c ./src/C/TextColors.c ./src/C/FileHandler.c ./src/C/IntFunctions.c ./src/Minifiers/HTMLMinifier.c ./src/C/FileTypesHandler.c ./src/C/Stack.c ./src/C/BundleFiles.c ./src/C/ProgressBar.c ./src/C/StringShiftHandler.c ./src/Minifiers/JSMinifier.c -s EXPORT_ES6=0 -s MODULARIZE -s USE_ES6_IMPORT_META=0 -s EXPORTED_RUNTIME_METHODS=["ccall"] -s NODERAWFS=1 -sBINARYEN=1 -sEXIT_RUNTIME=1 -sALLOW_MEMORY_GROWTH -o Build/CFunctions.js

WebAssembly初始化JavaScript代码

#!/usr/bin/env node
"use strict";
// js wrapper for arrowpack for NPM
const fs = require("fs");
const path = require("path");
const chalk = require("chalk");
const settingsSingleton = require("./SettingsSingleton/settingsSingleton");
const DirFunctions = require("./js/DirFunctions");
const wasm_exec = require("../Build/wasm_exec.js");
const CFunctionFactory = require("../Build/CFunctions.js");
const go = new Go();
// const Sleep = require("../src/js/Sleep");
const { mkdirIfNotExists } = require("./js/DirFunctions.js");

var StartTime = performance.now();

const argv = require("yargs/yargs")(process.argv.slice(2))
    .option("c", {
        alias: "config-path",
        describe: "Path to config file if not in working directory",
        type: "string"
    })
    .help().argv;

var CONFIG_FILE_NAME = "ArrowPack-config.json"

if (argv.c) { CONFIG_FILE_NAME = path.join(argv.c, CONFIG_FILE_NAME) } else { console.log("no custom file thingy"); }

var rawconfigData = null;
if (fs.existsSync(CONFIG_FILE_NAME)) { rawconfigData = fs.readFileSync(CONFIG_FILE_NAME, "utf8"); }
var temp;
const Settings = new settingsSingleton(rawconfigData);
if (Settings.getValue("largeProject") === false) {
    temp = DirFunctions.RecursiveWalkDir(Settings.getValue("entry")); // eventually add pluginAPI event here
} else {
    let RecursiveWalkDirWASM = fs.readFileSync("../Build/RecursiveWalkDir.wasm"); const { WebAssembly } = require("wasi");
    let compiledWalkDirWASM = WebAssembly.compile(wasm);
    let InstanceWalkDirWASM = WebAssembly.instantiate(compiledWalkDirWASM);
    const { InstanceWalkDirWASMExports } = instance;
    temp = InstanceWalkDirWASMExports.walk_dir(Settings.getValue("entry"));
}

var WalkedFiles = temp.Files;
var WalkedDirs = temp.Directories;
console.log(WalkedDirs);
if (WalkedDirs) {
    WalkedDirs.forEach(Dir => {
        console.log(chalk.red(Dir));
        var tempDir = Settings.getValue("exit") + Dir.substring(Settings.getValue("entry").length);

        console.log(chalk.yellowBright(tempDir));
        DirFunctions.mkdirIfNotExists(tempDir);
        //DirFunctions.mkdirIfNotExists(Dir);
    });
}
DirFunctions.mkdirIfNotExists("ARROWPACK_TEMP_PREPROCESS_DIR");
var AbsoluteFilesCharLength = 0;
var WrappedWalkedFiles = "";
if (WalkedFiles && WalkedFiles.length > 0) { // Paths are wrapped into one string because passing array of strings from JS to C is complicated
    WalkedFiles.forEach(FilePath => { WrappedWalkedFiles += FilePath + "::"; console.log(chalk.bold.blue(FilePath)); AbsoluteFilesCharLength += FilePath.length; });

    console.log(chalk.red(WalkedFiles.length));

    var StructsPointer;

    CFunctionFactory().then((CFunctions) => {
        CFunctions._CheckWasm();
        CFunctions._InitFileTypes();
        for (let k in Settings.settings) {
            if (CFunctions.ccall(
                "SendSettingsString",
                "number",
                ["string"],
                [k]
            ) != 1) {
                throw "Error sending Wasm settings string: " + k;
            }
            console.log(chalk.bold.blue(k));
            // Gives time to apply settings
            if (CFunctions.ccall(
                "SendSettingsString",
                "number",
                ["string"],
                [Settings.settings[k].toString()]
            ) != 1) {
                throw "Error sending Wasm settings string: " + Settings.settings[k];
            }
            console.log(chalk.bold.blue(Settings.settings[k]));
            // Also gives time to apply settings
        }
        var Success;
        // StructsPointer = CFunctions._CreateTree(allocateUTF8(WrappedWalkedFiles), WalkedFiles.length, AbsoluteFilesCharLength); // Need to get this working eventually for faster speed but couldn't work out allocateUTF8
        StructsPointer = CFunctions.ccall(
            "CreateGraph",
            "number",
            ["string", "number"],
            [WrappedWalkedFiles, WalkedFiles.length]
        );

        Success = CFunctions.ccall(
            "BundleFiles",
            "number",
            ["number"],
            [StructsPointer]
        );

        if (Success === 1 || Success === 0) {
            fs.rm("ARROWPACK_TEMP_PREPROCESS_DIR", { recursive: true }, (err) => {
                if (err) { console.error(err); } else {
                    console.log("Sucessfully removed temporary preprocess directory");
                }
            });
            DirFunctions.DeleteDirectory(); //CFunctions.ccall("PrintTimeTaken", "void", ["number", "number"], [StartTime, performance.now()]); // Not working for some reason
            console.log(chalk.magentaBright("Bundling files completed in " + (performance.now() - StartTime) / 1000 + " seconds"));

        }
    });
}

排查方向与解决方案

  1. 校验文件长度有效性

    • ftell可能因文件无法seek、权限问题返回-1,此时length + 1会溢出为超大负数,导致malloc请求非法内存。添加前置检查:
      if (length == -1) {
          fclose(filePTR);
          printf("Failed to get file length for %s\n", path);
          return NULL;
      }
      
    • 文本模式"r"会转换换行符,导致ftell返回值与实际读取字节数不一致,改用二进制模式"rb"打开文件避免转换。
  2. 修复读取循环逻辑

    • 当前循环在fgetc返回EOF后仍会执行一次赋值,导致currentChar越界。修改为:
      int currentChar = 0;
      int ch;
      while ((ch = fgetc(filePTR)) != EOF && currentChar < length) {
          buffer[currentChar++] = ch;
      }
      buffer[currentChar] = '\0';
      
    • 添加currentChar < length限制,防止文件读取过程中被修改导致长度超出预期。
  3. 启用内存调试工具

    • 添加编译参数-s MALLOC_DEBUG=1,启用dlmalloc调试模式,输出详细内存错误信息。
    • 替换-fsanitize=undefined为-fsanitize=address,利用AddressSanitizer检测堆越界、重复free等内存破坏问题。
  4. 调整WebAssembly内存配置

    • 尝试增大初始内存,比如-s INITIAL_MEMORY=256mb,同时确认-s ALLOW_MEMORY_GROWTH正常工作,避免内存耗尽。
  5. 检查JS与C交互的内存管理

    • 确保所有从C返回的内存(如ReadDataFromFile的buffer)在JS中调用CFunctions._free释放,避免内存泄漏或堆空间污染。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 06:45:30