Skip to content

antony@notes:~/container$ cat "在-Alpine-Linux-上修復-Gemini-CLI-Segfault.md"

在 Alpine Linux 上修復 Gemini CLI Segfault

2026-05-04· container

在 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 test

Gemini 顯示「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 確認了兩件事:

  1. Crash 是真的 SIGSEGV(系統有寫 core dump,代表是異常終止)
  2. 記憶體用量正常(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 提供的硬化版 asprintf
  • fcntl64: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_function

Node 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_chkfcntl64)只有 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 libcAlpine 用的 libc,功能精簡、體積小
相容套件箱gcompatAlpine 提供的 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: 9

Step 2:用新 Node 安裝 Gemini CLI

/opt/node-musl/bin/npm install -g @google/gemini-cli

預期輸出:

added 86 packages in 21s

Step 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/gemini

Step 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 重編。


參考資料


致謝

整個除錯過程靠 strace、core dump、ldd、readelf、nm 一步步收斂問題範圍,從「Gemini 莫名 crash」收斂到「2 個 glibc 符號缺失」。最終解法只有 11 行 C code,但沒有前面的系統性除錯,做不出來。

工程除錯的價值不在解法本身,而在「定位問題的能力」。