Go Wiki:LinuxKernelSignalVectorBug
简介
如果您是因为 Go 程序打印的类似以下消息而访问此页面
runtime: note: your Linux kernel may be buggy
runtime: note: see https://golang.ac.cn/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 的 vanilla 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
来降低此错误干扰程序的可能性。但是,这只会降低程序遭受内存损坏的可能性(因为它减少了程序将接收到的信号数量)。此错误仍然存在,并且仍然可能发生内存损坏。
问题?
请在邮件列表 [email protected] 或如问题中所述的任何 Go 论坛上提问。
详细信息
要查看有关此错误如何影响 Go 程序以及如何检测和理解它的更多详细信息,请参阅#35777和#35326。
此内容是Go Wiki的一部分。