【问题标题】:fgets(), signals (EINTR) and input data integrityfgets()、信号 (EINTR) 和输入数据完整性
【发布时间】:2019-06-02 11:18:18
【问题描述】:

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() syscall completed with some data
  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:仍然返回正确的以 null 结尾的部分读取数据

【问题讨论】:

  • 您可以在循环中使用 getc() 而不是 fgets()
  • 是的,我知道还有其他方法可以做到这一点,但问题是关于 fgets,因为与其他任务相比,它是非常方便的功能。
  • @firk 我认为这是 stdio 的主要问题之一。我有自己的缓冲 IO 实现的原因之一。
  • 我通常不会对此发表评论,但由于提供的代码包含所有其他需要的 #include 指令,我会发现它未能包含 stdio.h
  • 那是格式错误

标签: c signals posix stdio libc


【解决方案1】:

stdio 确实不能合理使用中断信号处理程序。

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

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

EINTR是读取错误,所以这样返回后数组内容是不确定的。

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

真的,信号只是一种非常倒退的做事方式,而中断信号是一种更加倒退的工具,充满了竞争条件和其他令人不快和无法修复的极端情况。如果你想以安全和现代的方式做这种事情,你可能需要有一个线程通过管道或套接字转发标准输入,并在信号处理程序中关闭管道或套接字的写入端,以便主从中读取的程序的一部分会得到 EOF。

【讨论】:

    【解决方案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 返回值来表示错误。它可以但没有义务将读取到该点的部分或全部数据传输到用户提供的缓冲区。 (但如果它不传输数据,那么它们应该仍然可供读取。)我还发现该函数完全设置文件的错误标志令人惊讶。我倾向于认为这是错误的,但我目前不准备为此提出论据。

    【讨论】:

    • 未指定发生错误时输出缓冲区的内容。
    • 没错,@R..,但 OP 的抱怨是 特定实现 在信号中断的情况下会丢失数据,我正在展示该断言是错误的,或者至少 OP 的测试不能可靠地证明这种行为。我认为在这些情况下没有数据丢失是实施质量问题。
    • 实现丢失了数据,因为没有可访问的合同。事实上(一些?)读取的字节存在于正式内容不确定的缓冲区中,没有向调用者指示实际读取的字节数与最初在数组中留下的字节数,并没有使它们“没有丢失”。
    • 我的问题不是关于具体的实现。只是有一些不能不具体的例子。
    猜你喜欢
    • 1970-01-01
    • 2014-11-09
    • 2019-03-18
    • 1970-01-01
    • 1970-01-01
    • 2020-01-29
    • 1970-01-01
    • 1970-01-01
    • 2013-11-23
    相关资源
    最近更新 更多