Repository navigation
[bug] 带参函数内 f():m() 链式方法调用导致编译器 SIGSEGV (exit 139) #36
Description
描述
在带参数的函数体内,把「函数调用的结果」直接作为方法调用的接收者(即 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
endlua/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
esrrhs commentedon Oct 17, 2026
已定位并修复,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()排除方法调用,语义上是保守退化(少做数学特化),不改变行为。你提到的规避写法(先赋给局部变量再调方法)现在不再是必需的。
fix: stop crashing on f():m() inside parameterised functions (#36) (#37)