嵌入 Lua 腳本的宿主程式面臨一大風險:使用者編寫的腳本可能包含死循環,導致整個應用程式凍結。由於停機問題不可判定,無法靜態分析所有死循環。工業界通用方案是執行時間監控 + 超時強制中止。本文實作一個跨執行緒的 Lua 腳本超時保護機制。
lua_pcall 捕獲錯誤lua_State* g_lua = nullptr; // Lua 虛擬機狀態
volatile unsigned g_begin = 0; // 腳本開始執行時間 (ms)
volatile long g_counter = 0; // 執行計數器:奇數=執行中,偶數=閒置
volatile long g_check = 0; // 監控執行緒採樣時的計數器快照
int run_user_script(int nargs, int nresults, int errfunc) {
g_begin = GetTickCount(); // 記錄開始時間
_InterlockedIncrement(&g_counter); // 計數器變奇數 → 標記執行中
int err = lua_pcall(g_lua, nargs, nresults, errfunc);
_InterlockedIncrement(&g_counter); // 計數器變偶數 → 標記結束
return err; // 正常返回或捕獲 luaL_error
}
關鍵點:必須使用 lua_pcall 而非 lua_call,才能捕獲 luaL_error 拋出的異常。
void check_script_timeout() {
g_check = g_counter; // 快照當前計數器
// 若非執行中 (偶數),直接返回
if ((g_check & 0x1) == 0) return;
// 超過 1000ms 判定超時
if (GetTickCount() - g_begin > 1000) {
int mask = LUA_MASKCALL | LUA_MASKRET | LUA_MASKLINE | LUA_MASKCOUNT;
lua_sethook(g_lua, timeout_hook, mask, 1);
}
}
void timeout_hook(lua_State* L, lua_Debug* ar) {
lua_sethook(L, nullptr, 0, 0); // 移除 hook,避免重複觸發
// 雙重檢查:確認仍在執行同一個腳本
if (g_check == g_counter) {
luaL_error(L, "script timeout"); // 拋出 Lua 錯誤,被 pcall 捕獲
}
}
避免「檢查旗標 → 旗標變更 → 執行動作」的競態條件。原子增量操作保證:
even → odd → eveng_check 時,若為奇數則確定在執行中g_check == g_counter,排除 hook 注入延遲導致的誤判LUA_MASKCOUNT 配合 count=1 確保每條 VM 指令都會觸發 hook,最快偵測到超時並中止。若僅用 LUA_MASKLINE,死循環若無換行則永不觸發。
若不想多執行緒,可在主迴圈中定期呼叫 check_script_timeout(),但 hook 會頻繁觸發影響效能。適用於低效能需求場景。
kernel32.lib,Linux: -lpthread)GetTickCount() 為 Windows API,跨平台請改用 std::chrono::steady_clocklua_sethook 新版 API此機制以 ~1ms 精度 偵測死循環,零侵入式 不需修改使用者腳本,透過 lua_pcall + lua_sethook + 原子計數器三重保障,實現生產級的 Lua 腳本沙箱超時保護。實務部署建議:超時閾值可配置、記錄超時堆疊資訊供事後分析、考慮記憶體/CPU 配額限制作為纵深防禦。