antony@notes:~/container$ cat "在-Alpine-Linux-上修復-Gemini-CLI-Segfault.md"
在 Alpine Linux 上修復 Gemini CLI Segfault
在 Alpine Linux 上修復 Gemini CLI Segfault
環境:Alpine Linux 3.23、x86_64、Node.js(apk 套件版) 症狀:Gemini CLI 在互動模式下 thinking 後直接退出,看似當機卻沒錯誤訊息 解法:11 行 C code 的 LD_PRELOAD shim,補上 musl 缺少的 2 個 glibc 符號
TL;DR(只想看解法的請從這裡開始)
如果你已經確認是同樣問題,直接跳到 完整修復步驟。
如果想了解問題定位過程,請從 症狀 開始讀。
症狀
在 Alpine Linux 上用 apk add nodejs npm 裝好 Node 後,用 npm 安裝 Gemini CLI:
npm install -g @google/gemini-cli啟動互動模式並嘗試讓 Gemini 執行 shell 指令:
gemini輸入任何會觸發 shell tool 的 prompt,例如:
請執行 shell 指令: openspec new change testGemini 顯示「Thinking…」幾秒後,直接退回 shell prompt,沒有任何錯誤訊息。
更詭異的:
gemini -p "echo hello"(headless 模式)不會 crash,但會出現Tool "run_shell_command" not found警告- 互動模式才 crash,且每次都 crash
除錯過程
Step 1:啟用 core dump
Gemini crash 時什麼錯誤訊息都沒有,所以第一步是讓系統在 crash 時把記憶體狀態完整保存下來,後續才能分析。
# 允許 core dump 沒有大小限制
ulimit -c unlimited
# 設定 core dump 寫到 /tmp,檔名包含程序名跟 PID
sudo sysctl -w kernel.core_pattern=/tmp/core.%e.%p注意:
ulimit -c只在當前 shell 有效。如果開新 terminal 要重設一次。
Step 2:重現 crash 並確認 core dump 有產生
gemini進去後輸入會觸發 crash 的 prompt(任何要跑 shell 的都行):
請執行 shell 指令: ls等它 thinking 後直接退出,然後檢查:
$ ls -la /tmp/core.* 2>/dev/null
-rw------- 1 bigred bigred 877506560 May 4 17:22 /tmp/core.MainThread.12320看到一個 877 MB 的 core dump,程序名為 MainThread(Node.js 22+ 對 main thread 的命名)。
這個 core dump 確認了兩件事:
- Crash 是真的 SIGSEGV(系統有寫 core dump,代表是異常終止)
- 記憶體用量正常(877 MB 對 Node 程式不算大,排除記憶體不足)
Step 3:從 core dump 找出載入的 native modules
.node 檔案是 Node.js 的 native binding(C++ 編譯的模組)。crash 多半發生在這類模組,所以先列出 crash 當下載入了哪些:
$ readelf -n /tmp/core.MainThread.12320 2>/dev/null | grep "\.node$"
.../node_modules/@github/keytar/build/Release/keytar.node
.../node_modules/@lydell/node-pty-linux-x64/pty.node兩個嫌疑犯:
keytar.node(處理系統 keychain)pty.node(處理 PTY,讓 Gemini 能跑 shell 指令)
Step 4:檢查 .node 檔案的符號解析
$ ldd /opt/node-musl/lib/node_modules/@google/gemini-cli/node_modules/@github/keytar/build/Release/keytar.node
/lib/ld-musl-x86_64.so.1 (0x7feffb852000)
libsecret-1.so.0 => /usr/lib/libsecret-1.so.0
libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0
...
# 沒有 "symbol not found" → keytar 沒問題$ ldd /opt/node-musl/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-linux-x64/pty.node
/lib/ld-musl-x86_64.so.1 (0x7fd30e512000)
libutil.so.1 => /lib/ld-musl-x86_64.so.1
...
Error relocating .../pty.node: napi_call_threadsafe_function: symbol not found
Error relocating .../pty.node: napi_get_cb_info: symbol not found
Error relocating .../pty.node: napi_create_object: symbol not found
Error relocating .../pty.node: __asprintf_chk: symbol not found
Error relocating .../pty.node: fcntl64: symbol not found
... (40+ 個 symbol not found)找到嫌疑犯:pty.node 有 40 多個 symbol 解析失敗。
Step 5:辨識真正的「致命」符號
過濾掉 N-API 相關的(那些是 ldd 假警報,實際 dlopen 時會從 node binary 解析):
$ ldd .../pty.node 2>&1 | grep "symbol not found" | grep -v napi_
__asprintf_chk
fcntl64只缺這兩個 glibc 符號:
__asprintf_chk:glibc_FORTIFY_SOURCE提供的硬化版 asprintffcntl64:glibc 32-bit 系統用的 64-bit file offset 版 fcntl(x86_64 上等同fcntl)
兩者 musl libc 都沒提供。
Step 6:確認 gcompat 也沒提供這兩個符號
$ sudo apk info gcompat | head -3
gcompat-1.1.0-r4 description:
GNU C Library compatibility layer for musl
$ nm -D /usr/lib/libgcompat.so.0 | grep -E "asprintf|fcntl"
# (空)連最新版 gcompat(已是最新 1.1.0-r4)都沒實作這兩個。
Step 7:確認 N-API 那 40 多個是假警報
驗證 Node binary 真的有 export N-API 符號:
$ nm -D /opt/node-musl/bin/node | grep napi_call_threadsafe_function
0000000000d24590 T napi_call_threadsafe_functionNode binary 確實 export 了 N-API 符號(T 代表 exported)。
ldd 顯示 napi_* not found 的原因是:.node 檔案不是設計來直接執行的,ldd 的 NEEDED libraries 列表裡沒有 node binary,所以找不到 N-API 符號。真正執行時是 dlopen 從 node main process 動態解析,不會出問題。
根因分析
用一個比喻說明
想像 Linux 系統就像一個工具箱出租店,租客(程式)上門時可以借各種工具來完成工作。
不同 Linux 發行版的工具箱規格不同:
- Ubuntu / Debian / RHEL 用「glibc 工具箱」—— 工具齊全、型號繁多、體積大
- Alpine 用「musl 工具箱」—— 精簡輕量、只放最常用的工具
Gemini CLI 裡有一個關鍵零件叫 pty.node(讓它能控制終端機),這個零件原本是給 Ubuntu 的 glibc 工具箱用的,它預期能借到 42 種特定工具:
- 其中 40 種(N-API 工具)是「Gemini 自己帶來的」—— 不是跟工具箱借,所以沒問題 ✓
- 剩下 2 種(
__asprintf_chk、fcntl64)只有 glibc 工具箱才有,Alpine 的 musl 工具箱沒放這兩個 ✗ - 連 Alpine 的「相容套件箱」(gcompat,號稱能模擬 glibc)也沒提供這兩個 ✗
結果:pty.node 載入到一半發現借不到工具,而 Gemini 對這個錯誤沒有處理好,整個程式直接當機(SIGSEGV)。
我們的修復就是自己做一個小工具盒(11 行 C code 的 shim),補上這兩個 musl 沒有的工具,讓 pty.node 能正常載入。
關鍵概念對照表
| 我用的詞 | 實際技術名詞 | 簡單解釋 |
|---|---|---|
| 工具箱 | C 標準函式庫(libc) | Linux 系統最底層的函式庫,所有程式都會用到 |
| glibc 工具箱 | glibc(GNU C Library) | Ubuntu / Debian / RHEL 等主流發行版用的 libc,功能多 |
| musl 工具箱 | musl libc | Alpine 用的 libc,功能精簡、體積小 |
| 相容套件箱 | gcompat | Alpine 提供的 glibc 相容層,但實作不完整 |
| 工具 | 動態連結符號(symbol) | 程式執行時需要從 libc 借用的函式名稱 |
| 關鍵零件 | native binding(.node 檔) | 用 C++ 編譯的 Node 模組,直接跟系統打交道 |
| 自製小工具盒 | LD_PRELOAD shim | 在程式啟動前先載入,補上缺失的函式 |
為什麼 headless 模式不會 crash,互動模式才會?
因為只有互動模式才會打開終端機。
- 互動模式:Gemini 要給你聊天介面、要能執行 shell 指令、要能即時顯示輸出 → 必須載入
pty.node→ 借不到工具 → 當機 - Headless 模式(
gemini -p "..."):純文字進、純文字出,不需要終端機 → 完全不載入pty.node→ 沒事
這也解釋了為什麼之前那麼難找原因:headless 跟互動只差一個小開關,但內部走的路徑天差地別。
那 N-API 那 40 個錯誤訊息是什麼?
那是 ldd 工具的「假警報」。
ldd 是設計來檢查「直接執行的程式」會借哪些工具,但 pty.node 不是直接執行,而是被 Node 用另一種機制(dlopen)動態載入。Node 載入時會把 N-API 那 40 個工具直接遞給 pty.node,所以實際上不會缺。
但 ldd 不知道有這層機制,看到 pty.node 沒明確標註「這 40 個工具會跟 Node 借」,就誤報成「缺 40 個符號」。
所以實際上只缺 2 個 glibc 符號,不是 42 個。這也是為什麼修復這麼簡單——只要補 2 個就好。
完整修復步驟
整個流程約 10 分鐘。
前置:確認你的環境符合條件
$ cat /etc/alpine-release
3.23.3
$ uname -m
x86_64
# 確認你的 Node 是 musl 編譯
$ ldd $(which node) | head -1
/lib/ld-musl-x86_64.so.1 (...)如果你不是 Alpine,或不是 musl Node,這篇修復可能不適用。
Step 1:換一個 Node.js binary(可選但推薦)
Alpine 的 apk add nodejs 是 Node 24,這個版本對某些 N-API 套件相容性差。換成 unofficial-builds 的 Node 22 LTS musl prebuild:
# 下載
NODE_VERSION=v22.11.0
cd /tmp
wget https://unofficial-builds.nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64-musl.tar.xz
# 解壓到 /opt
sudo mkdir -p /opt/node-musl
sudo tar -xJf node-${NODE_VERSION}-linux-x64-musl.tar.xz -C /opt/node-musl --strip-components=1
# 確認 libstdc++ 已裝(unofficial musl Node 需要這個)
sudo apk add libstdc++
# 切到新 Node
export PATH=/opt/node-musl/bin:$PATH
# 永久化
echo 'export PATH=/opt/node-musl/bin:$PATH' >> ~/.bashrc驗證:
$ which node
/opt/node-musl/bin/node
$ node --version
v22.11.0
$ node -e "console.log('N-API:', process.versions.napi)"
N-API: 9Step 2:用新 Node 安裝 Gemini CLI
/opt/node-musl/bin/npm install -g @google/gemini-cli預期輸出:
added 86 packages in 21sStep 3:確認 pty.node 缺哪些符號
ldd /opt/node-musl/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-linux-x64/pty.node 2>&1 \
| grep "symbol not found" \
| grep -v napi_預期輸出(只有兩個):
Error relocating .../pty.node: __asprintf_chk: symbol not found
Error relocating .../pty.node: fcntl64: symbol not found如果你看到的不只這兩個,代表你的環境跟我不同,後續 shim 可能要再加幾個函式。
Step 4:安裝 build toolchain
sudo apk add build-base預期看到:
(1/3) Installing fortify-headers (...)
(2/3) Installing patch (...)
(3/3) Installing build-base (...)Step 5:撰寫並編譯 LD_PRELOAD shim
建立 shim 目錄:
sudo mkdir -p /opt/glibc-shim寫 shim 程式:
sudo tee /opt/glibc-shim/shim.c > /dev/null <<'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <fcntl.h>
/*
* __asprintf_chk: glibc _FORTIFY_SOURCE 提供的 hardened asprintf
* musl 沒有對應實作,直接 forward 到 vasprintf
* (功能等價,只是不做 buffer overflow check,對 PTY 操作沒影響)
*/
int __asprintf_chk(char **strp, int flag, const char *fmt, ...) {
va_list ap;
int ret;
va_start(ap, fmt);
ret = vasprintf(strp, fmt, ap);
va_end(ap);
return ret;
}
/*
* fcntl64: glibc 32-bit 系統用的 64-bit file offset 版 fcntl
* x86_64 上 fcntl 行為等同 fcntl64,musl 只提供 fcntl
*/
int fcntl64(int fd, int cmd, ...) {
va_list ap;
void *arg;
va_start(ap, cmd);
arg = va_arg(ap, void *);
va_end(ap);
return fcntl(fd, cmd, arg);
}
EOF編譯:
sudo gcc -shared -fPIC -O2 \
-o /opt/glibc-shim/libglibc-shim.so \
/opt/glibc-shim/shim.c驗證 shim 提供了正確符號:
$ nm -D /opt/glibc-shim/libglibc-shim.so | grep -E "asprintf_chk|fcntl64"
0000000000001160 T __asprintf_chk
0000000000001210 T fcntl64驗證 LD_PRELOAD 後 pty.node 不再缺符號:
$ LD_PRELOAD=/opt/glibc-shim/libglibc-shim.so \
ldd /opt/node-musl/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-linux-x64/pty.node 2>&1 \
| grep "symbol not found" | grep -v napi_
# (空白,代表所有非 napi 符號都解決了)Step 6:用 wrapper script 永久化 LD_PRELOAD
不要每次都手打 LD_PRELOAD=...。把原本的 gemini symlink 換成一個 wrapper:
# 備份原本的 symlink
sudo mv /opt/node-musl/bin/gemini /opt/node-musl/bin/gemini.real
# 寫 wrapper
sudo tee /opt/node-musl/bin/gemini > /dev/null <<'EOF'
#!/bin/sh
# Gemini CLI launcher with glibc shim for Alpine/musl
# Required because @lydell/node-pty-linux-x64/pty.node references
# glibc-only symbols (__asprintf_chk, fcntl64) that musl/gcompat lacks.
export LD_PRELOAD=/opt/glibc-shim/libglibc-shim.so
exec /opt/node-musl/bin/node \
/opt/node-musl/lib/node_modules/@google/gemini-cli/bundle/gemini.js "$@"
EOF
sudo chmod +x /opt/node-musl/bin/geminiStep 7:驗證修復
gemini --version進互動模式測試:
gemini進 Gemini CLI 後,輸入會觸發 shell 的 prompt:
請執行 shell 指令: ls預期看到 Gemini 真的執行 ls 並回傳結果,而不是 thinking 後 crash 退出。
結果驗證範例
實際執行 Gemini 互動模式的成功輸出:
> 請執行 shell 指令: openspec new change test-with-shim
Creating New OpenSpec Change: Executing the requested openspec command.
✓ Shell openspec new change test-with-shim
✔ Created change 'test-with-shim' at openspec/changes/test-with-shim/
✓ ReadFolder openspec/changes/test-with-shim/ → Found 1 item(s).
✓ ReadFile openspec/.../.openspec.yaml
✓ Shell which openspec
/opt/node-musl/bin/openspec
✦ 已成功執行指令 openspec new change test-with-shim。已知限制
Headless 模式仍有問題
gemini -p "..." 在 headless 模式下,即使套用此 shim,仍會看到:
Error executing tool run_shell_command: Tool "run_shell_command" not found.這是不同問題,跟 musl/segfault 無關。Gemini CLI 在 headless 模式刻意限制工具白名單(見 Gemini CLI Issue #5382)。
workaround:用互動模式即可,本修復解決的就是互動模式問題。
Node 版本選擇
本文用 Node 22.11.0。理論上 Node 20.x 也可以(都有 unofficial musl prebuild)。Node 24 沒實測過,如果你想用 Node 24,記得驗證 N-API 9+ 支援。
Gemini CLI 升級後可能要重做
每次 npm install -g @google/gemini-cli@latest 後,如果 Gemini 換了 PTY 的相依套件,可能需要重新檢查缺失符號清單(回到 Step 3),補進 shim.c 重編。
參考資料
- Gemini CLI Issue #2251 — Segfault in musl libc distros
- Gemini CLI Issue #12878 — Optionalize node-pty with external backend
- nodejs/unofficial-builds — musl prebuilt Node
- Alpine Linux gcompat
致謝
整個除錯過程靠 strace、core dump、ldd、readelf、nm 一步步收斂問題範圍,從「Gemini 莫名 crash」收斂到「2 個 glibc 符號缺失」。最終解法只有 11 行 C code,但沒有前面的系統性除錯,做不出來。
工程除錯的價值不在解法本身,而在「定位問題的能力」。