SEI CERT C安全编码-信号(SIG)

来源:互联网 发布:python中tile函数 编辑:程序博客网 时间:2024/04/27 18:14

信号 (SIG)

SIG30-C 在信号处理程序中仅调用异步安全函数

在信号处理程序中仅调用异步安全函数。对于严格限制的程序,只有C标准库函数的abort(),_Exit(),quick_exit()和signal()可以安全地在信号处理程序中被调用。
在C标准的7.14.1.1的第5段[ ISO/IEC 9899:2011 ]指出,如果不是因为调用 abort() 或 raise() 函数而产生信号,该行为是 不确定的,如果

…信号处理程序调用标准库中除 abort函数, _Exit函数,quick_exit函数或signal函数的第一个参数等于信号对应的数值时,都会触发信号处理函数。

实施过程中,可以定义一系列额外的异步安全函数。这些函数也可以在信号处理程序内被调用。此限制适用于库函数以及应用函数。
根据C Rationale的7.14.1.1 [ C99 Rationale 2003 ]

当信号发生时,程序的正常控制流程中断。如果信号处理程序对应的信号发生时,会触发该信号处理程序。当它执行完成时,程序在信号发生时的地方继续执行。如果在信号发生时,一个库函数正在被执行,而信号处理函数运行时会触发这个库函数,这种设计会导致问题。

一般来说,从信号处理程序中调用I / O函数是不安全的。程序员需要确保在信号处理函数中使用的函数是所有执行代码中是异步安全的。

不合规代码示例

在这个不合规的例子中,信号处理程序通过函数log_message()调用C标准库函数fprintf()和free()。这两个函数都不是异步安全的。

#include <signal.h>#include <stdio.h>#include <stdlib.h>enum { MAXLINE = 1024 };char *info = NULL;void log_message(void) {  fputs(info, stderr);}void handler(int signum) {  log_message();  free(info);  info = NULL;}int main(void) {  if (signal(SIGINT, handler) == SIG_ERR) {    /* Handle error */  }  info = (char *)malloc(MAXLINE);  if (info == NULL) {    /* Handle Error */  }  while (1) {    /* Main loop program code */    log_message();    /* More program code */  }  return 0;}

合规方案

信号处理程序应尽可能简洁,比较理想的是标志设置采用无条件判断并返回。此合规方案中,给volatile sig_atomic_t类型变量设置标志位并返回; main函数再调用log_message()和free()函数:

#include <signal.h>#include <stdio.h>#include <stdlib.h>enum { MAXLINE = 1024 };volatile sig_atomic_t eflag = 0;char *info = NULL;void log_message(void) {  fputs(info, stderr);}void handler(int signum) {  eflag = 1;}int main(void) {  if (signal(SIGINT, handler) == SIG_ERR) {    /* Handle error */  }  info = (char *)malloc(MAXLINE);  if (info == NULL) {    /* Handle error */  }  while (!eflag) {    /* Main loop program code */    log_message();    /* More program code */  }  log_message();  free(info);  info = NULL;  return 0;}

不合规代码示例(longjmp())

如果信号处理程序是在非异步安全 函数内被调用,那么在信号处理函数内部调用 longjmp() 函数可能导致未定义的行为。因此,不应该在信号处理程序内调用longjmp()或POSIX siglongjmp()函数。
这不合规代码示例类似于一个在老版本Sendmail [VU#834865 ] 中的漏洞 。它打算在main() 循环中执行代码,并记录一些数据。一旦接收到 SIGINT,程序跳出循环,记录错误,并终止。
然而,攻击者可以利用这个不合规代码,他在log_message()的第二个if语句前面产生一个SIGINT信号 。结果是longjmp() 切换程序返回main(),log_message()再次被调用时。但是,第一个if 语句在这次将不会被执行(因为中断的原因,buf 没有被设置为NULL),程序将写入buf0引用的无效内存空间中。

#include <setjmp.h>#include <signal.h>#include <stdlib.h>enum { MAXLINE = 1024 };static jmp_buf env;void handler(int signum) {  longjmp(env, 1);}void log_message(char *info1, char *info2) {  static char *buf = NULL;  static size_t bufsize;  char buf0[MAXLINE];  if (buf == NULL) {    buf = buf0;    bufsize = sizeof(buf0);  }  /*   * Try to fit a message into buf, else reallocate   * it on the heap and then log the message.   */  /* Program is vulnerable if SIGINT is raised here */  if (buf == buf0) {    buf = NULL;  }}int main(void) {  if (signal(SIGINT, handler) == SIG_ERR) {    /* Handle error */  }  char *info1;  char *info2;  /* info1 and info2 are set by user input here */  if (setjmp(env) == 0) {    while (1) {      /* Main loop program code */      log_message(info1, info2);      /* More program code */    }  } else {    log_message(info1, info2);  }  return 0;}

合规方案

在此合规方案中,longjmp()调用被移除; 取而代之的是,信号处理程序设置一个错误标志:

#include <signal.h>#include <stdlib.h>enum { MAXLINE = 1024 };volatile sig_atomic_t eflag = 0;void handler(int signum) {  eflag = 1;}void log_message(char *info1, char *info2) {  static char *buf = NULL;  static size_t bufsize;  char buf0[MAXLINE];  if (buf == NULL) {    buf = buf0;    bufsize = sizeof(buf0);  }  /*   * Try to fit a message into buf, else reallocate   * it on the heap and then log the message.   */  if (buf == buf0) {    buf = NULL;  }}int main(void) {  if (signal(SIGINT, handler) == SIG_ERR) {    /* Handle error */  }  char *info1;  char *info2;  /* info1 and info2 are set by user input here */  while (!eflag) {    /* Main loop program code */    log_message(info1, info2);    /* More program code */  }  log_message(info1, info2);  return 0;}

不合规代码示例(raise())

在这个不合规的代码示例中,int_handler() 函数用于执行和SIGINT相关的任务,然后触发SIGTERM。但是,在信号处理函数中调用raise() 函数,这是一个未定义的行为。

#include <signal.h>#include <stdlib.h>void term_handler(int signum) {  /* SIGTERM handler */}void int_handler(int signum) {  /* SIGINT handler */  if (raise(SIGTERM) != 0) {    /* Handle error */  }}int main(void) {  if (signal(SIGTERM, term_handler) == SIG_ERR) {    /* Handle error */  }  if (signal(SIGINT, int_handler) == SIG_ERR) {    /* Handle error */  }  /* Program code */  if (raise(SIGINT) != 0) {    /* Handle error */  }  /* More code */  return EXIT_SUCCESS;}

合规方案

在这个合规方案中, int_handler()直接调用term_handler()而不是触发SIGTERM:

#include <signal.h>#include <stdlib.h>void term_handler(int signum) {  /* SIGTERM handler */}void int_handler(int signum) {  /* SIGINT handler */  /* Pass control to the SIGTERM handler */  term_handler(SIGTERM);}int main(void) {  if (signal(SIGTERM, term_handler) == SIG_ERR) {    /* Handle error */  }  if (signal(SIGINT, int_handler) == SIG_ERR) {    /* Handle error */  }  /* Program code */  if (raise(SIGINT) != 0) {    /* Handle error */  }  /* More code */  return EXIT_SUCCESS;}

执行细节

POSIX

下表来自POSIX标准[IEEE Std 1003.1:2013],定义了一系列异步信号安全的函数。应用程序可以没有限制的在信号处理函数中触发这些函数。

_Exit() fexecve() posix_trace_event() _exit() fork() pselect() abort() fstat() pthread_kill() accept() fstatat() pthread_self() access() fsync() pthread_sigmask() aio_error() ftruncate() raise() aio_return() futimens() read() aio_suspend() getegid() readlink() alarm() geteuid() readlinkat() bind() getgid() recv() cfgetispeed() getgroups() recvfrom() cfgetospeed() getpeername() recvmsg() cfsetispeed() getpgrp() rename() cfsetospeed() getpid() renameat() chdir() getppid() rmdir() chmod() getsockname() select() chown() getsockopt() sem_post() clock_gettime() getuid() send() close() kill() sendmsg() connect() link() sendto() creat() linkat() setgid() dup() listen() setpgid() dup2() lseek() setsid() execl() lstat() setsockopt() execle() mkdir() setuid() execv() mkdirat() shutdown() execve() mkfifo() sigaction() faccessat() mkfifoat() sigaddset() fchdir() mknod() sigdelset() fchmod() mknodat() sigemptyset() fchmodat() open() sigfillset() fchown() openat() sigismember() fchownat() pause() signal() fcntl() pipe() sigpause() fdatasync() poll() sigpending()

考虑到信号,所有不在上述表格中的函数都可以认为是不安全的。在信号存在的情况下,所有POSIX函数被信号处理函数调用或被中断,都会表现得和定义一样。只有一个例外,当一个信号中断了一个不安全的函数且信号处理函数调用了一个不安全的函数,这个行为是不确定的。
C标准,7.14.1.1第4段[ISO/IEC 9899:2011]陈述到:

如果一个信号是因为调用了abort或raise函数而产生的,信号处理函数不应该调用raise函数

然而,在POSIX [IEEE Std 1003.1:2013]的signal函数的描述中写到:

这个限制不适用于POSIX应用程序,因为POSIX.1-2008需要raise函数是异步安全的。
同时参考未确定行为131。

风险评估

在信号处理函数中触发一个非异步安全的函数会产生未定义的行为。

规则 严重性 可能性 修补成本 优先级 等级 SIG30-C 高 很可能 中 P18 L1
相关脆弱性

关于不恰当的信号处理所导致的软件脆弱性的全貌,可以参看Michal
Zalewski的论文《Delivering Signals for Fun and Profit》[Zalewski 2001]
CERT 脆弱性条目 VU #834865,“Sendmail signal I/O race condition,”描述了违反这个条目所导致的脆弱性。另外一个非常有名的在信号处理函数中使用longjmp函数导致的严重脆弱性是wu-ftpd 2.4 [Greenman 1997]。有效用户ID在一个信号处理函数中被设置0。如果第二个信号中断了第一个,调用了longjmp函数,在没有降低用户权限的情况下将程序返回到主线程中。这些被扩大的权限被用于进一步的攻击。

SIG31-C 不要在信号处理程序中访问共享对象

在信号处理程序中访问或修改共享对象可能会导致竞争,使数据处于不一致状态。本规则的两个例外(C标准,5.1.2.3,第5段),读写免锁原子对象和volatile sig_atomic_t类型变量。在信号处理程序中访问任何其他类型的对象是未定义的行为。
在DCL22-C中描述了需要volatile关键字的原因:对无法缓存的数据使用volatile类型。
sig_atomic_t类型是一个整数型对象,即使在异步中断的情况下也可以作为原子实体访问。虽然提供一些保障,但sig_atomic_t类型是由实现定义的。从SIG_ATOMIC_MIN到SIG_ATOMIC_MAX范围内的整数值可以安全地存储到该类型的变量中。此外,如果sig_atomic_t是一个有符号整数类型,SIG_ATOMIC_MIN必须不小于−127且SIG_ATOMIC_MAX不大于127。否则,SIG_ATOMIC_MIN必须为0且SIG_ATOMIC_MAX必须不大于255。在头文件

不合规代码示例

在此不合规代码示例中,SIGINT信号已被投递通过更新err_msg来记录。该err_msg变量是一个字符指针,而不是volatile sig_atomic_t类型的变量。

#include <signal.h>#include <stdlib.h>#include <string.h>enum { MAX_MSG_SIZE = 24 };char *err_msg;void handler(int signum) {  strcpy(err_msg, "SIGINT encountered.");}int main(void) {  signal(SIGINT, handler);  err_msg = (char *)malloc(MAX_MSG_SIZE);  if (err_msg == NULL) {    /* Handle error */  }  strcpy(err_msg, "No errors yet.");  /* Main code loop */  return 0;}

合规解决方案(写volatile sig_atomic_t)

这合规方案中,为了有最大的可移植性,信号处理程序应该只能做无条件判断地volatile sig_atomic_t类型变量设置并返回。

#include <signal.h>#include <stdlib.h>#include <string.h>enum { MAX_MSG_SIZE = 24 };volatile sig_atomic_t e_flag = 0;void handler(int signum) {  e_flag = 1;}int main(void) {  char *err_msg = (char *)malloc(MAX_MSG_SIZE);  if (err_msg == NULL) {    /* Handle error */  }  signal(SIGINT, handler);  strcpy(err_msg, "No errors yet.");  /* Main code loop */  if (e_flag) {    strcpy(err_msg, "SIGINT received.");  }  return 0;}

合规方案(无锁原子访问)

这合规方案中,信号处理程序可以引用静态或线程存储持续期内的无锁原子对象:

#include <signal.h>#include <stdlib.h>#include <string.h>#include <stdatomic.h>#ifdef __STDC_NO_ATOMICS__#error "Atomics are not supported"#elif ATOMIC_INT_LOCK_FREE == 0#error "int is never lock-free"#endifatomic_int e_flag = ATOMIC_VAR_INIT(0);void handler(int signum) {  e_flag = 1;}int main(void) {  enum { MAX_MSG_SIZE = 24 };  char err_msg[MAX_MSG_SIZE];#if ATOMIC_INT_LOCK_FREE == 1  if (!atomic_is_lock_free(&e_flag)) {    return EXIT_FAILURE;  }#endif  if (signal(SIGINT, handler) == SIG_ERR) {    return EXIT_FAILURE;  }  strcpy(err_msg, "No errors yet.");  /* Main code loop */  if (e_flag) {    strcpy(err_msg, "SIGINT received.");  }  return EXIT_SUCCESS;}

例外

SIG31-C-EX1:C标准的7.14.1.1第5段[ ISO / IEC 9899:2011 ]有一个errno的特殊例外,signal()函数调用会返回SIG_ERR信号,这会允许errno取一个不确定值(参见ERR32-C 不要依赖errno的不确定值)

风险评估

在一个信号处理函数中访问或修改共享对象会导致正在访问的数据处于不一致的状态。 Michal Zalewski的论文 “Delivering Signals for Fun and Profit” [Zalewski 2001] 提供了一些因违反该条目和其他信号处理原则的脆弱性例子。

规则 严重性 可能性 修补成本 优先级 等级 SIG31-C 高 很可能 高 P9 L2

SIG34-C 不要从可中断的信号处理程序中调用signal()

信号处理程序不应该再次声明自己要处理信号的欲望。再次声明通常在非持久性平台上完成,非持久性平台指的是,在接收到信号后,并在调用信号处理程序之前,将信号的处理程序重置为SIG_DFL 。在这些条件下调用signal()会呈现竞争条件。(参见SIG01-C了解有关信号处理程序持久性的具体实现细节。)
只有当信号处理程序不需要是异步安全的,信号处理程序才可以调用signal()(即,如果所有相关信号都被屏蔽,使得处理程序不能被中断)。

不合规代码示例(POSIX)

在非持久性平台上,此不合规代码示例包含一个竞争窗口,从主机环境复位信号开始,并在处理程序调用signal()时结束。在该期间,发送到程序的第二个信号将触发信号的默认行为,因此无法完成信号和处理函数的再次绑定,从而破坏其持续行为。
如果环境是持久的(即,当接收到信号时不重置处理程序),则在handler()函数内调用signal()是冗余的。

#include <signal.h>void handler(int signum) {  if (signal(signum, handler) == SIG_ERR) {    /* Handle error */  }  /* Handle signal */}void func(void) {  if (signal(SIGUSR1, handler) == SIG_ERR) {    /* Handle error */  }}

合规方案(POSIX)

对于持久性平台,从信号处理程序中调用函数signal()来重新绑定是不必要的:

#include <signal.h>void handler(int signum) {  /* Handle signal */}void func(void) {  if (signal(SIGUSR1, handler) == SIG_ERR) {    /* Handle error */  }}

合规方案(POSIX)

POSIX定义了sigaction()函数,它以类似signal()的方式为信号分配处理程序,但允许调用者明确地设置持久性。因此,该sigaction()函数可用于消除非持久性平台上的竞争窗口:

#include <signal.h>#include <stddef.h>void handler(int signum) {  /* Handle signal */}void func(void) {  struct sigaction act;  act.sa_handler = handler;  act.sa_flags = 0;  if (sigemptyset(&act.sa_mask) != 0) {    /* Handle error */  }  if (sigaction(SIGUSR1, &act, NULL) != 0) {    /* Handle error */  }}

在这个例子中的处理程序虽然不调用signal(),它之所以可以安全地这样做,是因为信号被屏蔽,处理程序不能被中断。如果为相同的处理程序安装了多个信号,则必须在act.sa_mask中明确得屏蔽这些信号,以确保处理程序不会被中断,因为系统仅屏蔽正在被投递的信号。
POSIX建议新的应用程序使用sigaction()而不是signal()。但该sigaction()函数未被C标准定义,并且在某些平台(包括Windows)上不受支持。

合规方案(Windows)

在Windows平台上没有安全的方式实现持久的信号处理程序行为,因此不应该尝试。如果设计取决于这种行为,并且设计不能改变,则可能有必要在完成适当的风险分析之后声明与该规则的偏离。
其原因是Windows是如上所述的非持久平台。在调用当前处理程序函数之前,Windows将重置等待下个相同信号的处理程序为SIG_DFL。如果处理程序调用signal()以重新安装本身,仍然会有一个竞争窗口。在处理程序的开始和调用signal()之间可能会产生信号,这将调用默认行为,而不是所需的处理程序。

例外

SIG34-C-EX1:对于具有持久信号处理程序的实现,处理程序可以安全地修改其自身信号的行为。行为修改包括忽略信号,重置为默认行为,并使信号由不同的处理程序处理。处理程序重新声明它的绑定也是安全的,但不必要。
以下代码示例将信号处理程序重置为系统的默认行为:

#include <signal.h>void handler(int signum) {#if !defined(_WIN32)  if (signal(signum, SIG_DFL) == SIG_ERR) {    /* Handle error */  }#endif  /* Handle signal */}void func(void) {  if (signal(SIGUSR1, handler) == SIG_ERR) {    /* Handle error */  }}

风险评估

在非持久性平台上,两个紧密连着的信号会触发一个竞争窗口,会引起信号的默认行为,即使处理函数试图覆盖这个默认行为。

规则 严重性 可能性 修补成本 优先级 等级 SIG34-C 低 不太可能 低 P3 L3

SIG35-C 不要从处理计算异常的信号处理程序中返回

根据C标准,7.14.1.1 [ISO/IEC 9899:2011],因为一个计算异常的原因而执行信号处理程序在返回时(即,其入参值是SIGFPE,SIGILL,SIGSEGV,或者任何对应这样异常的应用定义值),其行为是未定义的。(参见未定义的行为130)
可移植操作系统接口(POSIX)基本规范的第7个问题 [IEEE Std 1003.1:2013],把SIGBUS加入到计算异常信号处理程序的列表:

不是由kill(),sigqueue()或raise()触发的SIGBUS,SIGFPE,SIGILL,或SIGSEGV信号所引起的信号处理函数在正常返回时,进程的行为是未定义的。
无论信号是怎么产生的,都不要从SIGFPE,SIGILL,SIGSEGV,或其他计算异常对应的自定义值(如POSIX系统中的SIGBUS)中返回。

不合规代码示例

在这个不合规的代码示例中,如果denom等于0,除法运算具有未定义的行为,并可能对程序产生SIGFPE信号.(见INT33-C。确保除法和余数运算不会导致除零错误)。

#include <errno.h>#include <limits.h>#include <signal.h>#include <stdlib.h>volatile sig_atomic_t denom;void sighandle(int s) {  /* Fix the offending volatile */  if (denom == 0) {    denom = 1;  }}int main(int argc, char *argv[]) {  if (argc < 2) {    return 0;  }  char *end = NULL;  long temp = strtol(argv[1], &end, 10);  if (end == argv[1] || 0 != *end ||      ((LONG_MIN == temp || LONG_MAX == temp) && errno == ERANGE)) {    /* Handle error */  }  denom = (sig_atomic_t)temp;  signal(SIGFPE, sighandle);  long result = 100 / (long)denom;  return 0;}

在和一些代码编译后,如果输入0,这个不合规的代码示例将会无限循环。
在遵循所有其他信号处理规则的情况在,即使SIGFPE的处理函数尝试修复错误条件,程序仍然不能如预期那样工作。

合规解决方案

唯一可移植的离开SIGFPE,SIGILL或SIGSEGV处理函数的安全方法,就是触发abort(),quick_exit(),_Exit()。在SIGFPE的例子中,默认行为是异常终止,因此,用户的处理函数是不需要的

#include <errno.h>#include <limits.h>#include <signal.h>#include <stdlib.h>int main(int argc, char *argv[]) {  if (argc < 2) {    return 0;  }  char *end = NULL;  long denom = strtol(argv[1], &end, 10);  if (end == argv[1] || 0 != *end ||      ((LONG_MIN == denom || LONG_MAX == denom) && errno == ERANGE)) {    /* Handle error */  }  long result = 100 / denom;  return 0;}
实现细节

一些实现为程序定义了针对这些信号处理函数退出的有用的行为。Solaris提供了sigfpe函数专门用于程序能从SIGFPE处理函数中安全的返回。Oracle为SIGTRAP,SIGBUS和SIGEMT提供了平台特定的计算异常。最后,GNU的ligsigsegv库利用SIGSEGV处理函数返回的能力,在用户模式下实现了页面级的内存管理。

风险评估

从计算异常信号处理程序中返回是未定义的行为。

规则 严重性 可能性 修补成本 优先级 等级 SIG35-C 低 不太可能 高 P1 L3
原创粉丝点击