问题 HOWTO确定代码是否在信号处理程序上下文中运行?


我刚刚发现有人正在调用 - 从信号处理程序 - 一个绝对不是我编写的异步信号安全函数。而且,当然,我受到了责备(尽管我的文档中有警告)。 (同一个程序员从他的信号处理程序中调用各种非异步信号安全函数。叹气。)

那么,现在我很好奇:如何避免这种情况再次发生?我希望能够轻松确定我的代码是否在信号处理程序上下文中运行(语言是C,但解决方案不适用于任何语言吗?):

int myfunc( void ) {
    if( in_signal_handler_context() ) { return(-1) }
    // rest of function goes here
    return( 0 );
}

这是在Linux下。 希望这不是一个简单的答案,否则我会觉得自己像个白痴。


10648
2018-01-28 20:27


起源

了解。然而,当程序员第一次遇到信号的概念时,总会发生这种事情。猜猜从信号处理程序强行调用C ++对象的析构函数会发生什么? - smcdow


答案:


显然,较新的Linux / x86(可能是因为某些2.6.x内核)调用了信号处理程序 vdso。你可以利用这个事实给毫无疑问的世界造成以下可怕的黑客攻击:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>

#include <unistd.h>

uintmax_t vdso_start = 0;
uintmax_t vdso_end = 0;             /* actually, next byte */

int check_stack_for_vdso(uint32_t *esp, size_t len)
{
    size_t i;

    for (i = 0; i < len; i++, esp++)
            if (*esp >= vdso_start && *esp < vdso_end)
                    return 1;

    return 0;
}

void handler(int signo)
{
    uint32_t *esp;

    __asm__ __volatile__ ("mov %%esp, %0" : "=r"(esp));
    /* XXX only for demonstration, don't call printf from a signal handler */
    printf("handler: check_stack_for_vdso() = %d\n", check_stack_for_vdso(esp, 20));
}

void parse_maps()
{
    FILE *maps;
    char buf[256];
    char path[7];
    uintmax_t start, end, offset, inode;
    char r, w, x, p;
    unsigned major, minor;

    maps = fopen("/proc/self/maps", "rt");
    if (maps == NULL)
            return;

    while (!feof(maps) && !ferror(maps)) {
            if (fgets(buf, 256, maps) != NULL) {
                    if (sscanf(buf, "%jx-%jx %c%c%c%c %jx %u:%u %ju %6s",
                                    &start, &end, &r, &w, &x, &p, &offset,
                                    &major, &minor, &inode, path) == 11) {
                            if (!strcmp(path, "[vdso]")) {
                                    vdso_start = start;
                                    vdso_end = end;
                                    break;
                            }
                    }
            }
    }

    fclose(maps);

    printf("[vdso] at %jx-%jx\n", vdso_start, vdso_end);
}

int main()
{
    struct sigaction sa;
    uint32_t *esp;

    parse_maps();
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_handler = handler;
    sa.sa_flags = SA_RESTART;

    if (sigaction(SIGUSR1, &sa, NULL) < 0) {
            perror("sigaction");
            exit(1);
    }

    __asm__ __volatile__ ("mov %%esp, %0" : "=r"(esp));
    printf("before kill: check_stack_for_vdso() = %d\n", check_stack_for_vdso(esp, 20));

    kill(getpid(), SIGUSR1);

    __asm__ __volatile__ ("mov %%esp, %0" : "=r"(esp));
    printf("after kill: check_stack_for_vdso() = %d\n", check_stack_for_vdso(esp, 20));

    return 0;
}

SCNR。


7
2018-01-28 22:29



我不知道从vdso调用信号处理程序。你能指点参考吗?无论如何,我喜欢这个黑客。很多。把它卷成一个不透明的库很容易。诀窍是确保在任何信号处理程序之前调用parse_maps()。 - smcdow
我能找到的最佳参考是 lxr.free-electrons.com/source/arch/x86/kernel/... - ninjalj
但第320行的评论看起来很有趣: lxr.free-electrons.com/source/arch/x86/kernel/... - ninjalj
我们正在运行RHEL-5发行版 - Linux-2.6.18,所以也许我可以使用它。我将在下周写一个测试用例(这实际上意味着我将复制并粘贴你的代码以查看它的作用:-)。谢谢。 - smcdow
为“给毫无疑问的世界带来以下可怕的黑客”+1 - Benjamin Gruenbaum


如果我们可以假设您的应用程序不使用手动阻止信号 sigprocmask() 要么 pthread_sigmask(),这很简单:获取当前的线程ID(tid)。打开 /proc/tid/status 并获取值 SigBlk 和 SigCgtAND 那两个价值观。如果结果那样 AND 非零,那么该线程当前正在从信号处理程序内部运行。我自己测试了这个并且它有效。


3
2018-04-25 21:32



您需要进程ID(PID),而不是线程ID。这样做将涉及调用非异步信号安全功能,禁止编写自己的功能。 - mgarey
@mgarey好点,确保你只使用异步信号安全功能,如open()和read()系统调用,这是绝对可行的。但是,如果程序是多线程的,那么你必须使用与pid不同的tid。在信号处理程序内部是特定于线程的状态。 - David Yeager
大卫,你说得对 - 我站得更正了。 - mgarey


有两种正确的方法可以解决这个问题:

  • 让你的同事停止做错事。祝老板好运,尽管......

  • 使您的功能重新进入并保持异步安全。如有必要,提供具有不同签名的功能(例如使用广泛使用的功能) *_r 命名约定)以及状态保存所必需的附加参数。

至于 不正确的 这样做的方法,在Linux上使用GNU libc就可以使用了 backtrace() 和朋友一起浏览你的函数的调用者列表。它的  容易上手,安全或便携,但它可能会持续一段时间:

/*
 * *** Warning ***
 *
 * Black, fragile and unportable magic ahead
 *
 * Do not use this, lest the daemons of hell be unleashed upon you
 */
int in_signal_handler_context() {
        int i, n;
        void *bt[1000];
        char **bts = NULL;

        n = backtrace(bt, 1000);
        bts = backtrace_symbols(bt, n);

        for (i = 0; i < n; ++i)
                printf("%i - %s\n", i, bts[i]);

        /* Have a look at the caller chain */
        for (i = 0; i < n; ++i) {
                /* Far more checks are needed here to avoid misfires */
                if (strstr(bts[i], "(__libc_start_main+") != NULL)
                        return 0;
                if (strstr(bts[i], "libc.so.6(+") != NULL)
                        return 1;
        }

        return 0;
}


void unsafe() {
        if (in_signal_handler_context())
                printf("John, you know you are an idiot, right?\n");
}

在我看来,它可能会更好 放弃 而不是被迫写代码 这个


0
2018-01-28 21:34



我刚试过 backtrace(),它只是不起作用: __libc_start_main 处于信号处理环境的内部和外部。 - P Shved
正如我所提到的,它是 不 很容易做对。你必须找到两种情况之间的回溯的差异并使用它。例如,对于我的测试我假设在到达之前没有libc函数会在回溯中 main(),除非它是信号处理代码。你的回溯在每种情况下都是什么样的? - thkala
两条评论:(1)我担心它可能是这样的。 (2)不是鼻涕,但printf(3)不是异步信号安全功能。你必须使用write(2)。 - 可以在此处找到异步信号安全功能列表(至少是我通常所指的列表): pubs.opengroup.org/onlinepubs/009695399/functions/... - smcdow
这就是我避免试图给出直接答案的原因。正如@thkala所提议的那样,你要求的是一个黑客攻击。因此,找到外交解决方案可能更好。 - Judge Maygarden
@smcdow:我非常清楚 printf() 不是异步安全 - 但是使用它的功能(即你的功能)也不是:-) - thkala


你可以用一些东西来解决问题 SIGALTSTACK。设置一个替代信号堆栈,以一些异步安全的方式获取堆栈指针,如果在备用堆栈中继续,否则abort()。


0
2018-01-28 21:51



我想到了类似的东西,但我不认为我可以保证在从信号处理程序调用函数之前我已经设置了备用堆栈。我还应该重申,我的函数绝对不应该从信号处理程序中调用,文档也是如此。 - smcdow
有一种更简单的方法。 sigaltstack 如果您已经在备用堆栈上运行并尝试对其进行更改,则需要返回错误,因此您可以尝试调用它并查看调用是否失败。 - R..


我想你需要做以下事情。这是一个复杂的解决方案,它不仅结合了编码方面的最佳实践,还结合了软件工程方面的最佳实践!

  1. 说服你的老板,信号处理程序的命名约定是一件好事。例如,建议a 匈牙利表示法,并告诉它在微软使用它取得了巨大成功。 因此,所有信号处理程序都将从一开始 sighnd, 喜欢 sighndInterrupt
  2. 检测信号处理上下文的函数将执行以下操作:
    1. 得到 backtrace()
    2. 看看它中的任何函数是否以 sighnd...。如果确实如此,那么恭喜你,你就是一个信号处理者!
    3. 否则,你不是。
  3. 尽量避免使用 吉米 在同一家公司。你知道,“只有一个”。

0
2018-01-28 21:53



我们将让一名看门人通过代码库并报告从信号处理程序调用的任何和所有函数。我已经畏缩了。 - smcdow
顺便说一下,@ smcdow可以使用静态分析工具。但话说回来,你必须注释每个信号处理程序,这使得提出的解决方案不再复杂。 :-) - P Shved
前段时间有一个Valgrind工具来寻找信号处理程序问题,它被称为番红花。 - ninjalj
@ninjalj,Valgrind是一个动态分析工具,而不是静态分析工具。无论如何,我刚刚设计了一种静态的方法,但这是非常偏离主题的。 - P Shved


对于在-O2或更好(istr)优化的代码,我们发现需要添加-fno-omit-frame-pointer

否则gcc将优化堆栈上下文信息


0
2018-01-10 22:12