Go Wiki: LinuxKernelSignalVectorBug

簡介

如果您到達此頁面是因為看見 Go 程式列印出類似這樣的訊息,

runtime: note: your Linux kernel may be buggy
runtime: note: see https://go.dev.org.tw/wiki/LinuxKernelSignalVectorBug
runtime: note: mlock workaround for kernel bug failed with errno <number>

則表示您使用的是可能含有錯誤的 Linux 核心。此核心錯誤可能會在您的 Go 程式中造成記憶體損壞,並導致您的 Go 程式崩潰。

如果您了解程式崩潰的原因,那麼您可以忽略此頁面。

否則,本頁面將會說明核心錯誤為何,並提供一個您可以用來檢查您的核心是否含有錯誤的 C 程式碼。

錯誤說明

在 Linux 核心 5.2 版本引入了錯誤:如果將信號傳送給執行緒,而且傳送信號需要錯誤執行緒信號堆疊的頁面,那麼從信號傳回至程式的過程中 AVX YMM 暫存器可能會損壞。如果程式正在執行使用 YMM 暫存器的函式,該函式可能會發生異常行為。

此錯誤只會在配備 x86 處理器的系統中發生,且會影響以任何語言編寫的程式。此錯誤只會影響接收信號的程式,在接收信號的程式中,使用替代信號堆疊的程式比較容易受到影響。此錯誤只會影響使用 YMM 暫存器的程式。特別是在 Go 程式中,此錯誤通常會造成記憶體損壞,因為 Go 程式主要使用 YMM 暫存器來執行將一個記憶體緩衝區複製到另一個記憶體緩衝區。

此錯誤 已回報給 Linux 核心開發人員。它已快速修正。錯誤修正並未回溯至 Linux 核心 5.2 系列。此錯誤已於 Linux 核心版本 5.3.15、5.4.2、以及 5.5 及更新版本中修正。

如果核心是使用 GCC 9 或更新版本編譯,才會出現此錯誤。

錯誤出現在任何 x 值的原始 Linux 核心版本 5.2.x、5.3.0 至 5.3.14,以及 5.4.0 與 5.4.1。然而,許多配送那些核心版本的發行版實際上已回溯修正程式 (非常小)。而有些發行版仍使用 GCC 8 編譯核心,這種情況下核心不會有錯誤。

換句話說,即使您的核心在容易受攻擊的範圍內,仍有很大的機率不會遭此錯誤影響。

錯誤測試

若要測試您的核心是否包含錯誤,您可以執行以下 C 程式碼 (點擊「詳細資料」以查看程式碼)。在有錯誤的核心上,程式碼將幾乎立刻失敗。在沒有錯誤的核心上,程式碼將執行 60 秒,然後以狀態 0 退出。

// Build with: gcc -pthread test.c
//
// This demonstrates an issue where AVX state becomes corrupted when a
// signal is delivered where the signal stack pages aren't faulted in.
//
// There appear to be three necessary ingredients, which are marked
// with "!!!" below:
//
// 1. A thread doing AVX operations using YMM registers.
//
// 2. A signal where the kernel must fault in stack pages to write the
//    signal context.
//
// 3. Context switches. Having a single task isn't sufficient.

##include <errno.h>
##include <signal.h>
##include <stdio.h>
##include <stdlib.h>
##include <string.h>
##include <unistd.h>
##include <pthread.h>
##include <sys/mman.h>
##include <sys/prctl.h>
##include <sys/wait.h>

static int sigs;

static stack_t altstack;
static pthread_t tid;

static void die(const char* msg, int err) {
  if (err != 0) {
    fprintf(stderr, "%s: %s\n", msg, strerror(err));
  } else {
    fprintf(stderr, "%s\n", msg);
  }
  exit(EXIT_FAILURE);
}

void handler(int sig __attribute__((unused)),
             siginfo_t* info __attribute__((unused)),
             void* context __attribute__((unused))) {
  sigs++;
}

void* sender(void *arg) {
  int err;

  for (;;) {
    usleep(100);
    err = pthread_kill(tid, SIGWINCH);
    if (err != 0)
      die("pthread_kill", err);
  }
  return NULL;
}

void dump(const char *label, unsigned char *data) {
  printf("%s =", label);
  for (int i = 0; i < 32; i++)
    printf(" %02x", data[i]);
  printf("\n");
}

void doAVX(void) {
  unsigned char input[32];
  unsigned char output[32];

  // Set input to a known pattern.
  for (int i = 0; i < sizeof input; i++)
    input[i] = i;
  // Mix our PID in so we detect cross-process leakage, though this
  // doesn't appear to be what's happening.
  pid_t pid = getpid();
  memcpy(input, &pid, sizeof pid);

  while (1) {
    for (int i = 0; i < 1000; i++) {
      // !!! Do some computation we can check using YMM registers.
      asm volatile(
        "vmovdqu %1, %%ymm0;"
        "vmovdqa %%ymm0, %%ymm1;"
        "vmovdqa %%ymm1, %%ymm2;"
        "vmovdqa %%ymm2, %%ymm3;"
        "vmovdqu %%ymm3, %0;"
        : "=m" (output)
        : "m" (input)
        : "memory", "ymm0", "ymm1", "ymm2", "ymm3");
      // Check that input == output.
      if (memcmp(input, output, sizeof input) != 0) {
        dump("input ", input);
        dump("output", output);
        die("mismatch", 0);
      }
    }

    // !!! Release the pages of the signal stack. This is necessary
    // because the error happens when copy_fpstate_to_sigframe enters
    // the failure path that handles faulting in the stack pages.
    // (mmap with MMAP_FIXED also works.)
    //
    // (We do this here to ensure it doesn't race with the signal
    // itself.)
    if (madvise(altstack.ss_sp, altstack.ss_size, MADV_DONTNEED) != 0)
      die("madvise", errno);
  }
}

void doTest() {
  // Create an alternate signal stack so we can release its pages.
  void *altSigstack = mmap(NULL, SIGSTKSZ, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  if (altSigstack == MAP_FAILED)
    die("mmap failed", errno);
  altstack.ss_sp = altSigstack;
  altstack.ss_size = SIGSTKSZ;
  if (sigaltstack(&altstack, NULL) < 0)
    die("sigaltstack", errno);

  // Install SIGWINCH handler.
  struct sigaction sa = {
    .sa_sigaction = handler,
    .sa_flags = SA_ONSTACK | SA_RESTART,
  };
  sigfillset(&sa.sa_mask);
  if (sigaction(SIGWINCH, &sa, NULL) < 0)
    die("sigaction", errno);

  // Start thread to send SIGWINCH.
  int err;
  pthread_t ctid;
  tid = pthread_self();
  if ((err = pthread_create(&ctid, NULL, sender, NULL)) != 0)
    die("pthread_create sender", err);

  // Run test.
  doAVX();
}

void *exiter(void *arg) {
  sleep(60);
  exit(0);
}

int main() {
  int err;
  pthread_t ctid;

  // !!! We need several processes to cause context switches. Threads
  // probably also work. I don't know if the other tasks also need to
  // be doing AVX operations, but here we do.
  int nproc = sysconf(_SC_NPROCESSORS_ONLN);
  for (int i = 0; i < 2 * nproc; i++) {
    pid_t child = fork();
    if (child < 0) {
      die("fork failed", errno);
    } else if (child == 0) {
      // Exit if the parent dies.
      prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
      doTest();
    }
  }

  // Exit after a while.
  if ((err = pthread_create(&ctid, NULL, exiter, NULL)) != 0)
    die("pthread_create exiter", err);

  // Wait for a failure.
  int status;
  if (wait(&status) < 0)
    die("wait", errno);
  if (status == 0)
    die("child unexpectedly exited with success", 0);
  fprintf(stderr, "child process failed\n");
  exit(1);
}

該如何做

如果您的核心版本在可能包含錯誤的範圍內,請執行上述 C 程式碼以查看它是否會失敗。如果會失敗,則您的核心有錯誤。您應該升級至較新的核心。此錯誤無解決方法。

使用 1.14 編譯的 Go 程式碼會嘗試使用 mlock 系統呼叫將訊號堆疊頁鎖定至記憶體中,以減輕錯誤。這樣做是因為只有在訊號堆疊頁必須有錯誤時,錯誤才會發生。然而,此 mlock 用法可能失敗。如果您看到訊息

runtime: note: mlock workaround for kernel bug failed with errno 12

errno 12 (也稱為 ENOMEM) 表示 mlock 失敗,因為系統已設定程式碼可以鎖定的記憶體量上限。如果您能提高上限,程式碼可能會成功。請使用 ulimit -l 執行此動作。在 docker 容器中執行程式碼時,您可以透過使用選項 -ulimit memlock=67108864 呼叫 docker 來提升上限。

如果您無法提升 mlock 上限,您可以透過在執行 Go 程式碼時設定環境變數 GODEBUG=asyncpreemptoff=1,讓錯誤不太可能干擾您的程式碼。然而,這只會讓您的程式碼比較不容易遭受記憶體毀損 (因為它會減少您的程式碼會接收到的訊號數量)。錯誤仍然存在,記憶體毀損仍可能發生。

有問題嗎?

請在 golang-nuts@googlegroups.com 的郵件清單中提出問題,或在 Questions 中說明的任何 Go 論壇中提出問題。

詳細資料

若要查看錯誤如何影響 Go 程式碼,以及它是如何被偵測並了解的更多詳細資料,請參閱 #35777#35326


這項內容是 Go Wiki 的一部分。