fgets (), сигналы (EINTR) и целостность входных данных - PullRequest
5 голосов
/ 02 июня 2019

fgets() предназначался для чтения некоторой строки до тех пор, пока не произойдет EOF или \n. Например, это очень удобно для чтения текстовых конфигурационных файлов, но есть некоторые проблемы.

Во-первых, он может вернуть EINTR в случае доставки сигнала, поэтому его следует обернуть проверкой цикла.

Вторая проблема гораздо хуже: по крайней мере, в glibc он вернет EINTR и потеряет все уже прочитанные данные, если они доставлены посередине. Это вряд ли произойдет, но я думаю, что это может быть причиной некоторых сложных уязвимостей в некоторых демонах.

Установка флага SA_RESTART для сигналов, похоже, помогает избежать этой проблемы, но я не уверен, что он охватывает ВСЕ возможные случаи на всех платформах. Это 1013 *

Если нет, есть ли способ вообще избежать этой проблемы?

Если нет, похоже, что 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: buffer содержит эти частично прочитанные данные, но НЕ NULL-TERM, поэтому единственный способ получить его длину - очистить весь буфер перед вызовом fgets (), который выглядит не так, как предполагалось

FreeBSD 8 libc: по-прежнему возвращает правильно прочитанные данные с нулевым символом в конце

Ответы [ 2 ]

3 голосов
/ 02 июня 2019

stdio действительно нецелесообразно использовать с обработчиками прерывания сигнала.

Согласно ISO C 11 7.21.7.2 Функция fgets , параграф 3:

Функция fgets возвращает s в случае успеха.Если обнаружен конец файла и в массив не были прочитаны символы, содержимое массива остается неизменным и возвращается нулевой указатель.Если во время операции возникает ошибка чтения, содержимое массива неопределенно и возвращается нулевой указатель.

EINTR - ошибка чтения, поэтому содержимое массива после такого возврата неопределенно.

Теоретически поведение может быть указано для fgets таким образом, что вы могли бы значительно восстановиться после ошибки в середине операции, настроив буфер соответствующим образом перед вызовом, так каквы знаете, что fgets не записывает '\n' за исключением последнего символа перед нулевым завершением (аналогично методам использования fgets со встроенными NUL).Однако этот способ не указан, и не было бы аналогичного способа обработки других функций stdio, таких как scanf, которым некуда сохранять состояние для их возобновления после EINTR.

Действительно, сигналы - это простодействительно отсталый способ ведения дел, а прерывание сигналов - еще более отсталый инструмент, полный условий гонки и других неприятных и неумолимых угловых случаев.Если вы хотите делать такие вещи безопасным и современным способом, вам, вероятно, понадобится поток, который направляет stdin через канал или сокет, и закройте пишущий конец канала или сокета в обработчике сигналов, чтобы основнойчасть вашей программы, читающая из нее, получает EOF.

2 голосов
/ 02 июня 2019

Во-первых, он может вернуть EINTR в случае доставки сигнала, поэтому его следует обернуть проверкой цикла для этого.

Конечно, вы имеете в виду, что fgets() вернетсяNULL и установите errno на EINTR.Да, это возможно, и не только для fgets(), или даже для функций stdio вообще - такое поведение может проявляться в широком спектре функций из области ввода-вывода и других.Большинство функций POSIX, которые могут блокировать события, внешние по отношению к программе, могут не работать с EINTR и различными поведениями, связанными с функциями.Это характерно для среды программирования и работы.

Вторая проблема гораздо хуже: по крайней мере, в glibc он вернет EINTR и потеряет все уже прочитанные данные, если они будут доставлены посередине.Это вряд ли произойдет, но я думаю, что это может быть причиной некоторых сложных уязвимостей в некоторых демонах.

Нет, по крайней мере, не в моих тестах. ваша тестовая программа теряет данные.Когда fgets() возвращает NULL, чтобы сообщить об ошибке, это не означает, что она не передала какие-либо данные в буфер, и если я изменяю вашу программу для печати буфера после сигнализации EINTR, тогда я действительно вижу, чтоданные с попытки 1 были перенесены туда.Но программа игнорирует эти данные.

Теперь возможно, что другие программы совершают ту же ошибку, что и ваша, и поэтому теряют данные, но это не из-за недостатка в реализации fgets().

FreeBSD 8 libc: первая попытка возвращает частично прочитанные данные и устанавливает ferror () и errno

Я склонен думать, что это поведениеНедостаток - если функция возвращается до достижения конца строки / файла, она должна сообщить об ошибке, предоставив возвращаемое значение NULL.Он может, но не обязан, передавать некоторые или все данные, считанные в этот момент, в предоставленный пользователем буфер.(Но если он не передает данные, тогда они должны оставаться доступными для чтения.) Я также нахожу удивительным, что функция вообще устанавливает флаг ошибки файла.Я склонен считать это ошибочным, но я не готов сейчас приводить аргументы для этого.

...