Go Wiki: LinuxKernelSignalVectorBug

はじめに

Goプログラムによって次のようなメッセージが表示されたため、このページにたどり着いた場合

runtime: note: your Linux kernel may be buggy
runtime: note: see https://go.dokyumento.jp/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以降でコンパイルされた場合にのみ存在します。

このバグは、Linuxカーネルバージョン5.2.x(任意の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プログラムを実行して、失敗するかどうかを確認してください。失敗した場合、カーネルはバグがあります。新しいカーネルにアップグレードする必要があります。このバグの回避策はありません。

Go 1.14でビルドされたGoプログラムは、mlockシステムコールを使用してシグナルスタックページをメモリにロックすることで、このバグを軽減しようとします。これは、シグナルスタックページがフォールトインされる必要がある場合にのみバグが発生するため機能します。ただし、このmlockの使用は失敗する可能性があります。次のようなメッセージが表示された場合

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

errno 12ENOMEMとも呼ばれる)は、プログラムがロックできるメモリ量にシステムが制限を設定したため、mlockが失敗したことを意味します。制限を増やすことができれば、プログラムは成功する可能性があります。これはulimit -lを使用して行われます。Dockerコンテナでプログラムを実行する場合、-ulimit memlock=67108864オプションを指定してDockerを呼び出すことで制限を増やすことができます。

mlockの制限を増やすことができない場合は、Goプログラムを実行するときに環境変数GODEBUG=asyncpreemptoff=1を設定することで、バグがプログラムに干渉する可能性を低くすることができます。ただし、これはプログラムがメモリ破損に苦しむ可能性を低くするだけです(プログラムが受信するシグナルの数を減らすためです)。バグは依然として存在し、メモリ破損が発生する可能性があります。

質問はありますか?

メーリングリストgolang-nuts@googlegroups.com、または質問に記載されている任意のGoフォーラムで質問してください。

詳細

このバグがGoプログラムにどのように影響し、どのように検出および理解されたかの詳細については、#35777および#35326を参照してください。


このコンテンツはGo Wikiの一部です。