Skip to content

[bug] 带参函数内 f():m() 链式方法调用导致编译器 SIGSEGV (exit 139) #36

Closed

Description

@esrrhs

描述

在带参数的函数体内,把「函数调用的结果」直接作为方法调用的接收者(即 f():m(...) 这种链式写法),代码生成阶段会直接 SIGSEGV(exit 139 / signal 11)。

关键:跨包不是必要条件,同包函数、文件内 local function 一样崩。真正条件是「接收者是一个函数调用表达式」+「所在函数带参数」。

因为是编译期崩溃,哪怕这个函数永远不会被调用也会崩,会直接导致整个服务起不来。

最小复现

lua/a.lua:

package "PkgA"
local m = container.map()
function Init()
    m:set("v", 42)
    return 0
end
function Map()
    return m
end

lua/main.lua:

package "main"

function init()
    PkgA.Init()
    ok_no_param()
    workaround(0)
    return 0
end

-- OK:无参函数里链式调用没问题
function ok_no_param()
    local x = PkgA.Map():get("v")
    print("ok_no_param = ", tostring(x))
    return 0
end

-- SIGSEGV:同样一句,放进带参数的函数就崩(哪怕这个函数永远不会被调用)
function crash_with_param(a)
    local x = PkgA.Map():get("v")
    print("crash_with_param = ", tostring(x))
    return 0
end

-- 规避:拆成两步,先赋给局部变量再调方法
function workaround(a)
    local m = PkgA.Map()
    local x = m:get("v")
    print("workaround = ", tostring(x))
    return 0
end

function update() return 1 end
function fini() return 0 end

运行:

flua lua --once     # -> exit 139

注释掉 crash_with_param 后重跑,会正常打印 ok_no_param / workaround 并退出 0。

触发条件矩阵(已二分确认)

场景 结果
无参函数体内 Pkg.Fn():get(k) ok
带参函数体内 Pkg.Fn():get(k)(参数 1/2/3 个都崩) SIGSEGV
带参函数体内 Pkg.Fn():set(k, v) 作为语句 SIGSEGV
带参函数体内表构造器里 { v = Pkg.Fn():get(k) } SIGSEGV
带参函数体内同包函数 Fn():m() SIGSEGV
带参函数体内文件内 local 函数 lf():m() SIGSEGV
带参函数体内拆两步 local m = Pkg.Fn() 再 m:get(k) ok
带参函数体内只调用 Pkg.Fn() 不链式 ok
带参函数体内对变量 m:get(k) ok

崩溃现场

日志停在 start CompileFile lua/main.lua,还没进 JIT(TCC/GCC)阶段,所以是 CGen 阶段的问题:

* thread #1, stop reason = EXC_BAD_ACCESS (code=1, address=0x3f)
  frame #0: libfakelua`std::__1::basic_string<...>::__is_long[abi:nqe220106](this=<unavailable>) const
    -> 2153	    return __rep_.__s.__is_long_;

即对一个近乎空指针的 std::string 取 __is_long()。lldb 展不开外层栈帧(Release 构建 + 内联),所以没能定位到具体 C++ 行号。

相关代码位置(供参考)

src/compile/c_gen.cpp 的 CGen::CompileFunctioncall(),约 3371–3378 行:当方法名非空时先求接收者;接收者是函数调用时走 CompilePrefixexp() 再递归回 CompileFunctioncall():

std::string method_obj_tmp;
if (!fc->Name().empty()) {
    // Lua:先求 prefix(接收者),再求参数。obj():f(arg()) 必须先跑 obj()。
    std::string obj_expr = var ? CompileVar(var) : CompilePrefixexp(pe_pre_ptr);
    method_obj_tmp = std::format("flua_obj_{}", tmp_var_counter_++);
    func_temp_decls_ << "    CVar " << method_obj_tmp << ";\n";
    Out() << GenTab() << method_obj_tmp << " = " << obj_expr << ";\n";
}

怀疑与「函数带参数时 func_temp_decls_ / Out() 的写入时机或作用域状态」有关(无参函数不崩),但没有进一步验证。

规避写法

-- 崩
local x = Pkg.Fn():get("k")

-- 不崩
local m = Pkg.Fn()
local x = m:get("k")

或者让包内函数直接返回需要的标量,避免把对象暴露出去再链式调用。

环境

  • fakelua e4582bd(master)
  • macOS 15 arm64,Release 构建(-DFAKELUA_BUILD_TESTS=OFF -DFAKELUA_BUILD_BENCHMARKS=OFF)

实际影响

我是在给麻将服务端(Lua 版)加临时统计接口时撞到的——在带 3 个参数的 HTTP 分发函数 OnRequest(typ, connid, req) 里写了 MajiangAi.QStat():get("hit"),服务启动时直接 139。因为宿主固定调用无参的 init/update/fini,这类写法平时不容易碰到,但一旦出现在业务函数里就是整个进程起不来,且没有任何 Lua 侧报错信息,排查成本较高。

Activity

  1. esrrhs commented on Oct 17, 2026

    @esrrhs
    OwnerAuthor

    已定位并修复,PR: #37

    根因不在 CGen,而在类型推断阶段:TypeInferencer::CheckCallNodeChange 对带参函数体里的每个 FunctionCall 都假定被调者是简单变量名,f():m() 的被调者是 FunctionCall 节点,dynamic_pointer_cast<SyntaxTreeVar> 返回空指针,而 -DNDEBUG 恒开使 DEBUG_ASSERT 被编译掉 → 空指针上调 GetName()(崩溃地址 0x3f)。

    「函数带参数」是必要条件,因为 CheckCallNodeChange 只在 FindMathParamIndices(数学参数发现)里被调用,而它只对形参非空的函数运行——这也解释了 (...) 变参同样会崩。

    修复是把它和另外两处同类假设(ResolveCallReturnType、CGen::TryCompileNativeSpecCallExpr)的断言换成真实提前返回,并用 fc->Name() 排除方法调用,语义上是保守退化(少做数学特化),不改变行为。

    你提到的规避写法(先赋给局部变量再调方法)现在不再是必需的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions