fgets(),信号(EINTR)和输入数据完整性

时间:2019-06-02 11:18:18

标签: c signals posix stdio libc

fgets()用于读取某些字符串,直到发生EOF\n为止。例如,这对于读取文本配置文件非常方便,但是存在一些问题。

首先,在传递信号的情况下,它可能返回EINTR,因此应对此进行循环检查。

第二个问题要严重得多:至少在glibc中,它将返回EINTR并丢失所有已读取的数据,以防它们在中间传送。这不太可能发生,但是我认为这可能是某些守护程序中一些复杂漏洞的根源。

在信号上设置SA_RESTART标志似乎有助于避免此问题,但是我不确定它是否涵盖了所有平台上的所有可能情况。是吗?

如果没有,有办法完全避免这个问题吗?

如果否,看来fgets()不可用于读取守护程序中的文件,因为它可能会导致随机数据丢失。

示例代码:

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

static  char buf[1000000];
static volatile int do_exit = 0;
static void int_sig_handle(int signum) { do_exit = 1; }

void try(void) {
  char * r;
  int err1, err2;
  size_t len;

  memset(buf,1,20); buf[20]=0;
  r = fgets(buf, sizeof(buf), stdin);
  if(!r) {
    err1 = errno;
    err2 = ferror(stdin);
    printf("\n\nfgets()=NULL, errno=%d(%s), ferror()=%d\n", err1, strerror(err1), err2);
    len = strlen(buf);
    printf("strlen()=%u, buf=[[[%s]]]\n", (unsigned)len, buf);
  } else if(r==buf) {
    err1 = errno;
    err2 = ferror(stdin);
    len = strlen(buf);
    if(!len) {
      printf("\n\nfgets()=buf, strlen()=0, errno=%d(%s), ferror()=%d\n", err1, strerror(err1), err2);
    } else {
      printf("\n\nfgets()=buf, strlen()=%u, [len-1]=0x%02X, errno=%d(%s), ferror()=%d\n",
        (unsigned)len, (unsigned char)(buf[len-1]), err1, strerror(err1), err2);
    }
  } else {
    printf("\n\nerr\n");
  }
}

int main(int argc, char * * argv) {
  struct sigaction sa;
  sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sa.sa_handler = int_sig_handle;
  sigaction(SIGINT, &sa, NULL);

  printf("attempt 1\n");
  try();
  printf("\nattempt 2\n");
  try();
  printf("\nend\n");
  return 0;
}

此代码可用于测试“尝试1”中间的信号传递,并确保此后部分读取的数据完全丢失。

如何测试:

  1. 使用strace运行程序
  2. 输入一些行(不要按Enter),按Ctrl + D,请参阅read()系统调用,其中已包含一些数据
  3. 发送SIGINT
  4. 请参阅fread()返回NULL,“尝试2”并输入一些数据,然后按Enter键。
  5. 它将打印第二个输入的数据,但不会在任何地方第一个打印

FreeBSD 11 libc:相同的行为

FreeBSD 8 libc:第一次尝试返回部分读取的数据并设置ferror()和errno

编辑:根据@John Bollinger的建议,我在NULL返回后添加了转储缓冲区。结果:

glibc和FreeBSD 11 libc:缓冲区包含部分读取的数据,但不是NULL-TERM,因此获取其长度的唯一方法是在调用fgets()之前清除整个缓冲区,这看起来不像预期的用途

FreeBSD 8 libc:仍然可以正确返回以空值结尾的部分读取的数据

2 个答案:

答案 0 :(得分:3)

stdio在中断信号处理程序的情况下确实不能合理使用

根据ISO C 11 7.21.7.2 fgets函数,第3段:

  

如果成功,fgets函数将返回s。如果遇到文件末尾并且没有字符读入数组,则数组的内容保持不变,并返回空指针。如果在操作过程中发生读取错误,则数组内容不确定并且返回空指针。

EINTR是读取错误,因此返回后数组的内容不确定。

从理论上讲,可以为fgets指定行为 ,您可以通过在调用之前适当地设置缓冲区来从操作过程中的错误中有意义地恢复,因为您知道fgets不会写'\n',只是作为空终止之前的最后一个字符(类似于将fgets与嵌入式NUL一起使用的技术)。但是,并没有指定这种方式,也没有类似的方式来处理其他scanf这样的stdio函数,这些函数没有位置存储状态来在EINTR之后恢复它们。

真的,信号只是做事的倒退,而打断信号则是倒退的工具,充满了比赛条件和其他令人不快且无法解决的极端情况。如果您想以一种安全,现代的方式进行此类操作,则可能需要一个线程来通过管道或套接字转发标准输入,并关闭信号处理程序中管道或套接字的写入端,以便从中读取程序的一部分会得到EOF。

答案 1 :(得分:2)

  

首先,在传递信号的情况下,它可能返回EINTR,因此应该   包裹着循环检查。

当然,您的意思是fgets()将返回NULL并将<{>将errno 设置为EINTR。是的,这是可能的,不仅对于fgets(),甚至对于一般的stdio功能,这种情况都是可能的-I / O领域的许多功能以及其他功能可能都表现出这种现象。可能会阻止程序外部事件的大多数POSIX函数可能会因EINTR和各种特定于函数的关联行为而失败。这是编程和操作环境的特征。

  

第二个问题要严重得多:至少在glibc中,它将返回EINTR   并丢失所有已读取的数据(以防中间传送)。这是   不太可能发生,但我认为这可能是某些原因   一些守护程序中存在复杂的漏洞。

否,至少不是在我的测试中。丢失数据的是您的测试程序。当fgets()返回NULL表示错误时,并不表示它没有将任何数据传输到缓冲区,并且如果我修改了程序以在EINTR之后打印缓冲区表示我确实看到尝试1的数据已经在那里传输了。但是程序会忽略这些数据。

现在,其他程序可能会犯与您相同的错误,从而丢失数据,但这不是因为fgets()的实现存在缺陷。

  

FreeBSD 8 libc:第一次尝试返回部分读取的数据并设置ferror()和errno

我倾向于认为 this 行为存在缺陷-如果函数在到达行/文件末尾之前返回,则应该通过提供NULL返回值来表示错误。它可以但不必须将读取到该点的部分或全部数据传输到用户提供的缓冲区。 (但是,如果它不传输数据,那么它们应该仍然可供读取。)我也感到惊讶的是,该函数完全设置了文件的错误标志。我倾向于认为这是错误的,但目前我不准备为此提出观点。