fgets(), signals (EINTR) and input data integrity

fir*_*irk 5 c posix signals stdio libc

fgets() was intended for reading some string until EOF or \n occurred. It is very handy for reading text config files, for example, but there are some problems.

First, it may return EINTR in case of signal delivery, so it should be wrapped with loop checking for that.

Second problem is much worse: at least in glibc, it will return EINTR and loss all already read data in case it delivered in middle. This is very unlikely to happen, but I think this may be source of some complicated vulnerabilities in some daemons.

Setting SA_RESTART flag on signals seems to help avoiding this problem but I'm not sure it covers ALL possible cases on all platforms. Is it?

If no, is there a way to avoid the problem at all?

If no, it seems that fgets() is not usable for reading files in daemons because it may lead to random data loss.

Example code for tests:

#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;
}
Run Code Online (Sandbox Code Playgroud)

This code can be used to test signal delivery in middle of "attempt 1" and ensure that its partially read data become completely lost after that.

How to test:

  1. run program with strace
  2. enter some line (do not press Enter), press Ctrl+D, see read() syscall completed with some data
  3. send SIGINT
  4. see fread() returned NULL, "attempt 2" and enter some data and press Enter
  5. it will print second entered data but will not print first anywhere

FreeBSD 11 libc: same behaviour

FreeBSD 8 libc: first attempt returns partially read data and sets ferror() and errno

EDIT: according with @John Bollinger recommendations I've added dumping of the buffer after NULL return. Results:

glibc and FreeBSD 11 libc: buffer contains that partially read data but NOT NULL-TERM so the only way to get its length is to clear entire buffer before calling fgets() which looks not like intended use

FreeBSD 8 libc: still returns properly null-terminated partially-read data

R..*_*R.. 5

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

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

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

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

从理论上讲,该行为可能被指定fgets以一种方式,你能够真正从错误中中间操作的情况下调用之前适当地建立缓冲区恢复,因为你知道,fgets不写'\n'除空终止前的最后一个字符(类似于使用fgets嵌入式 NUL 的技术)。但是,它没有以这种方式指定,并且没有类似的方法来处理其他 stdio 函数,例如scanf,这些函数在 之后无处存储状态以恢复它们EINTR

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


Joh*_*ger 1

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

当然,你的意思是fgets()会返回NULL设置errnoEINTR。是的,这是一种可能性,不仅对于fgets(),甚至对于一般的 stdio 函数来说——来自 I/O 领域和其他领域的各种函数都可能表现出这种行为。大多数可能阻止程序外部事件的 POSIX 函数可能会失败,EINTR并出现各种特定于函数的相关行为。这是编程和操作环境的一个特征。

第二个问题更糟糕:至少在 glibc 中,EINTR 如果它在中间传递,它将返回并丢失所有已读取的数据。这种情况不太可能发生,但我认为这可能是某些守护程序中一些复杂漏洞的根源。

不,至少在我的测试中不是。丢失数据的是您的测试程序。fgets()返回NULL信号错误时,这并不意味着它没有将任何数据传输到缓冲区,如果我修改您的程序以在发出信号后打印缓冲区,EINTR那么我确实看到尝试 1 中的数据已传输到那里。但程序会忽略该数据。

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

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

我倾向于认为这种行为是有缺陷的——如果函数在到达行/文件末尾之前返回,那么它应该通过提供NULL返回值来发出错误信号。它可以(但没有义务)将读取到该点的部分或全部数据传输到用户提供的缓冲区。(但如果它不传输数据,那么它们应该仍然可供读取。)我还发现令人惊讶的是该函数设置了文件的错误标志。我倾向于认为这是错误的,但我目前不准备对此提出论据。