﻿# Коробка багов \(взрывается\): кроссплатформенное коварство

В сентябре мы рассматривали релиз 86Box v5\.0, приуроченный к тридцати годам со дня выхода в розничную продажу Windows 95, и пообещали показать ещё кое\-что\. О чём мы сознательно умолчали, и почему оставили находку для отдельной статьи?

![1304_fseek_86Box_v5_ru/image1.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image1.png)

## Освежим память

86Box — это "аппаратно\-точный" эмулятор IBM PC и совместимых с его архитектурой компьютеров\. В [предыдущей статье](https://pvs-studio.ru/ru/blog/posts/cpp/1289/) мы рассмотрели потенциальные баги, опираясь в том числе на техническую документацию  эмулируемых компонентов, и упомянули, что проект был собран в релизной конфигурации с отладочной информацией \(`RelWithDebInfo`\)\. Теперь она нам пригодится, потому что у нас взорвался виртуальный танталовый конденсатор в процессе изучения срабатываний PVS\-Studio\.

## Сегодня в программе

Сначала пропустим в студию главного героя сегодняшнего выпуска — срабатывание анализатора PVS\-Studio:

[V575](https://pvs-studio.ru/ru/docs/warnings/v575/) The null pointer is passed into 'fseek' function\. Inspect the first argument\. [vid\_ati\_eeprom\.c 61](https://github.com/86Box/86Box/blob/v5.0/src/video/vid_ati_eeprom.c#L61)

```cpp
void
ati_eeprom_load_mach8(ati_eeprom_t *eeprom, char *fn, int mca)
{
    FILE *fp;
    ....
    fp   = nvr_fopen(eeprom->fn, "rb");
    size = 128;
    if (!fp) {
        if (mca) {
            (void) fseek(fp, 2L, SEEK_SET);             // <=
            memset(eeprom->data + 2, 0xff, size - 2);
            fp = nvr_fopen(eeprom->fn, "wb");
            fwrite(eeprom->data, 1, size, fp);
    ....
}
```

Нам надо загрузить данные, сохранённые в NVRAM видеоадаптера, и храним мы их в файле в двоичном виде\. Если файла нет, то надо создать в нём "начальные" данные\. Вот как раз сценарий, когда файла нет\. Мы смещаем указатель на файл, а он нулевой, и получаем разыменование нулевого указателя `fp` как из палаты мер и весов\.

Взглянем подробнее на `fseek`\. [Стандарт C11](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#page=354) не определяет требования к первому параметру функции и, соответственно, не гарантирует проверку на `NULL`\. Это значит, что его обработка остаётся на совести разработчиков стандартной библиотеки\. В студию приглашаются:

* GNU glibc;
* BSD libc из FreeBSD 14\.3;
* Microsoft Universal CRT из Windows SDK 10\.0\.26100;
* musl v1\.2\.5\.

Последние две реализации стандартной библиотеки языка C здесь в качестве гостей: 86Box не рассчитан на использование с ними или их совместимость не проверялась\. В [инструкции по сборке](https://86box.readthedocs.io/en/v5.0/dev/buildguide.html) альтернативные реализации тоже не упомянуты\. Поэтому начнём с ожидаемых к использованию стандартных библиотек и попросим их повторить те же самые действия над нулевым указателем на файл\.

## Подаём напряжение

Достаём с воображаемого стеллажа IBM PS/2 model 55SX и "вставляем" в него 2D\-ускоритель IBM 8514/A в исполнении ATI\.

![1304_fseek_86Box_v5_ru/image2.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image2.png)

Первым испытуемым станет собранный с помощью MinGW экземпляр для Windows\. Убеждаемся в отсутствии файла NVRAM перед стартом: для этого нужно проверить папку `%userprofile%\86Box VMs\<имя виртуальной машины>\nvr` на наличие файла `ati8514_mca.nvr`\. Если есть, то удаляем\.

![1304_fseek_86Box_v5_ru/image3.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image3.png)

Включаем наш агрегат, и\.\.\.

![1304_fseek_86Box_v5_ru/image4.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image4.png)

Ничего не взорвалось\! Всё хорошо, файл NVRAM записан, компьютер работает и smoke\-тест на glibc окончен\. Дефект не обнаружен\.

![1304_fseek_86Box_v5_ru/image5.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image5.png)

Переходим к FreeBSD\. Стандартную библиотеку языка C в этой операционной системе реализует libc\. В принципе, это справедливо для всех операционных систем семейства BSD\.

Конфигурацию используем ту же самую\. Отсутствие файла NVRAM `ati8514_mca.nvr` проверяем по пути `~/.local/share/86Box/Virtual Machines/<имя виртуальной машины>/nvr`\. Три, два, один, включаем\.\.\.

![1304_fseek_86Box_v5_ru/image6.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image6.png)

Ну, лучше эту ситуацию опишет только произошедшее давным\-давно у [Макса Крюкова](https://www.youtube.com/watch?v=YTQ2ZYPunLg&t=20288s) :\)

![1304_fseek_86Box_v5_ru/image7.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image7.png)

Открываем зажмуренные после взрыва глаза и смотрим в консоль: у нас подтверждён ненормальный выход\!

```cpp
void VMManagerSystem::launchMainProcess() Full Command:
"/root/86Box/build_freebsd/src/86Box"
("--vmpath", "/root/.local/share/86Box/Virtual Machines/somevm",
 "--vmname",
 "somevm")
Connection received on 86Box.socket.5876c5
Connection disconnected
Abnormal program termination while launching main process:
exit code 11, exit status QProcess::CrashExit
```

Рядом с исполняемым файлом эмулятора появился дамп ядра\. Приглашаем в студию LLDB:

```cpp
root@freebsd:~/86Box/build_freebsd/src # lldb 86Box -c 86Box.core
(lldb) target create "86Box" --core "86Box.core"
Core file '/root/86Box/build_freebsd/src/86Box.core' (x86_64) was loaded.
(lldb) bt
* thread #1, name = '86Box', stop reason = signal SIGSEGV
  * frame #0: 0x0000000832f880bf
              libc.so.7`_flockfile(fp=0x0000000000000000)
              at _flock_stub.c:65:20
    frame #1: 0x0000000832f8b675
              libc.so.7`fseek(fp=0x0000000000000000, offset=2, whence=0)
              at fseek.c:62:2
    frame #2: 0x00000000018cd964
              86Box`ati_eeprom_load_mach8(eeprom=...., fn=<unavailable>, mca=1)
              at vid_ati_eeprom.c:61:20
```

Нулевой указатель `fp` устраивает светошумовое представление: не получается заблокировать файл, потому что нет действительного файлового дескриптора\. К сожалению, LLDB очень не хотел работать в реальном времени и падал то с тихим `lost connection`, то с грохотом и спецэффектами\. Поэтому хождения по коду, как в Windows, я показать не смогу\.

<details>
   <summary>Эти два случая вполне могут быть багами графической оболочки LLDB\\\.\\\.\\\.</summary>

![1304_fseek_86Box_v5_ru/image8.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image8.png)



![1304_fseek_86Box_v5_ru/image9.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image9.png)


</details>
## Изучаем схему включения

Как так вышло, что в одной библиотеке функция работает штатно, а в другой — нет? Взглянем теперь на устройство `fseek` в [glibc](https://github.com/bminor/glibc) и BSD [libc](https://github.com/freebsd/freebsd-src/tree/master/lib/libc)\. Придётся походить по макросам\.

glibc:

1\. `fp` передаётся в макрос `CHECK_FILE` в [`fseek.c`](https://github.com/bminor/glibc/blob/master/libio/fseek.c#L34)\.

```cpp
int
fseek (FILE *fp, long int offset, int whence)
{
  int result;
  CHECK_FILE (fp, -1);               // <=
  _IO_acquire_lock (fp);
  result = _IO_fseek (fp, offset, whence);
  _IO_release_lock (fp);
  return result;
}
```

2\. Внутри макроса `CHECK_FILE` из [`libioP.h`](https://github.com/bminor/glibc/blob/master/libio/libioP.h#L983) с `FILE` происходит\.\.\. великое ничего?

```cpp
#ifdef IO_DEBUG
# define CHECK_FILE(FILE, RET) do {        \
    if ((FILE) == NULL            \
  || ((FILE)->_flags & _IO_MAGIC_MASK) != _IO_MAGIC)  \
      {                \
  __set_errno (EINVAL);          \
  return RET;            \
      }                \
  } while (0)
#else
# define CHECK_FILE(FILE, RET) do { } while (0)
#endif
```

Не совсем\. Как минимум у нас выставляется ответ `-1` в MinGW версии glibc\. А вот что происходит в glibc из Devuan 6 "Excalibur"\.\.\.

![1304_fseek_86Box_v5_ru/image10.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image10.png)

Чисто случайно в списке рассылки [bugs\-devel](https://bugs-devel.debian.org/cgi-bin/bugreport.cgi?bug=1002601) у Debian нашлось тематическое обсуждение похожей проблемы\. Приходим к заключению, что поведение функции ещё зависит от того, как glibc был скомпилирован\. Неожиданно и неприятно\.

Ещё больше настораживает то, как обошлись с возвращаемым значением функции: её попросили замолчать, приведя значение к `void`\.

```cpp
(void) fseek(fp, 2L, SEEK_SET);
```

glibc стерпела такое обращение к себе на MinGW, но на GNU/Linux проявила силу и ударила без предупреждения\. И никто не докажет её неправоту, ведь, напомню, поведение функции `fseek` с нулевым указателем на файл не определено стандартом\!

BSD libc:

1\. `fp` передаётся в макрос `FLOCKFILE_CANCELSAFE` в [`fseek.c`](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/stdio/fseek.c#L59)\.

```cpp
int
fseek(FILE *fp, long offset, int whence)
{
  int ret;
  int serrno = errno;

  /* make sure stdio is set up */
  if (!__sdidinit)
    __sinit();

  FLOCKFILE_CANCELSAFE(fp);   // <=
  ret = _fseeko(fp, (off_t)offset, whence, 1);
  FUNLOCKFILE_CANCELSAFE();
  if (ret == 0)
    errno = serrno;
  return (ret);
}
```

2\. `fp` передаётся в макрос `_FLOCKFILE` в `[local.h](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/stdio/local.h#L149)`\.

```cpp
#define  FLOCKFILE_CANCELSAFE(fp)          \
  {                \
    struct _pthread_cleanup_info __cleanup_info__;    \
    if (__isthreaded) {          \
      _FLOCKFILE(fp);          \ // <=
      ___pthread_cleanup_push_imp(      \
          __stdio_cancel_cleanup, (fp),     \
          &__cleanup_info__);        \
    } else {            \
      ___pthread_cleanup_push_imp(      \
          __stdio_cancel_cleanup, NULL,     \
          &__cleanup_info__);        \
    }              \
    {
#define  FUNLOCKFILE_CANCELSAFE()          \
      (void)0;          \
    }              \
    ___pthread_cleanup_pop_imp(1);        \
  }
```

3\. Макрос [разворачивается](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/include/libc_private.h#L88) в вызов функции `_flockfile` в [`_flock_stub.c`](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/stdio/_flock_stub.c#L65)\.

```cpp
#ifdef  _FLOCK_DEBUG
#define _FLOCKFILE(x)  _flockfile_debug(x, __FILE__, __LINE__)
#else
#define _FLOCKFILE(x)  _flockfile(x)
#endif
```

И на третьем шагу у нас тоже пробой по указателю\.

```cpp
void
_flockfile(FILE *fp)
{
    pthread_t curthread = _pthread_self();

    if (fp->_fl_owner == curthread)           // <=
        fp->_fl_count++;
    else {
      ....
    }
}
```

Что же делать? Не поверите, но не пытаться шерстить по нулевому указателю\. `fp` всё равно дальше по коду переиспользуется для открытия файла на запись\. Нужно просто _удалить_ строку\.

```cpp
void
ati_eeprom_load_mach8(ati_eeprom_t *eeprom, char *fn, int mca)
{
    FILE *fp;
    ....
    fp   = nvr_fopen(eeprom->fn, "rb");
    size = 128;
    if (!fp) {
        if (mca) {
            memset(eeprom->data + 2, 0xff, size - 2);
            fp = nvr_fopen(eeprom->fn, "wb");
            fwrite(eeprom->data, 1, size, fp);
    ....
}
```

Убираем воображаемый паяльник обратно в подставку, собираем "тестовый стенд" ещё раз и три, два, один, включаем\.\.\.

Есть картинка\!

![1304_fseek_86Box_v5_ru/image12.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image12.png)

Никаких больше аварийных выключений\. Мы пришли к ожидаемому для PS/2 model 55SX состоянию с необходимостью настроить BIOS\.

Примечательно, что в соседней функции [`ati_eeprom_load_mach8_vga`](https://github.com/86Box/86Box/blob/v5.0/src/video/vid_ati_eeprom.c#L88) именно так и сделано, и файл сразу переоткрывается на запись\.

![1304_fseek_86Box_v5_ru/image13.png](https://import.viva64.com/docx/blog/1304_fseek_86Box_v5_ru/image13.png)

## А что у других стандартных библиотек?

Паяльник убран, флюс отмыт\. Теперь можно и про оставшиеся два аналога стандартной библиотеки языка C поговорить\.

Продолжим нашу программу демонстрацией поведения универсальной версией от Microsoft — Microsoft Universal C Runtime\. Смотрим в Windows SDK 10\.0\.26100:

```cpp
static int __cdecl common_fseek(
    __crt_stdio_stream const stream,
    __int64            const offset,
    int                const whence,
    __crt_cached_ptd_host&   ptd
    ) throw()
{
    _UCRT_VALIDATE_RETURN(ptd, stream.valid(), EINVAL, -1);
    ....
}

extern "C" int __cdecl fseek(
    FILE* const public_stream,
    long  const offset,
    int   const whence
    )
{
  __crt_cached_ptd_host ptd;
  return common_fseek(__crt_stdio_stream(public_stream), offset, whence, ptd);
}
```

Макрос `_UCRT_VALIDATE_RETURN` отбивает любую попытку подать в функцию недействительный файловый дескриптор, и в конфигурации Release приложение упадёт с исключением:

> Unhandled exception at 0x00007FFAB796CBA8 \(ucrtbase\.dll\) in example\.exe: An invalid parameter was passed to a function that considers invalid parameters fatal\.

Таким образом, у нас уже есть три варианта исхода с кодом без проверки: "компиляционно\-зависимое" у glibc, шок от непроверенных данных у BSD libc и праведное возмущение у UCRT\.

Что там у musl?

1\. Функция `fseek` переходит в функцию `__fseeko` в [`fseek.c`](https://github.com/kraj/musl/blob/v1.2.5/src/stdio/fseek.c#L37), оттуда в макрос `FLOCK` для блокировки файла\.

```cpp
int __fseeko(FILE *f, off_t off, int whence)
{
  int result;
  FLOCK(f);                             // <=
  result = __fseeko_unlocked(f, off, whence);
  FUNLOCK(f);
  return result;
}

int fseek(FILE *f, long off, int whence)
{
  return __fseeko(f, off, whence);
}
```

2\. Аргумент `f` в макросе `FLOCK` в [`stdio_impl.h`](https://github.com/kraj/musl/blob/v1.2.5/src/internal/stdio_impl.h#L10), что является указателем на файловый дескриптор, разыменовывается без валидации\.

```cpp
#define FLOCK(f) int __need_unlock = ((f)->lock>=0 ? __lockfile((f)) : 0)
```

Повторяется сценарий BSD libc\! Там все сговорились что ли? Да нет же\. В очередной раз напомню: если что\-то не определено стандартом, то полагаться на реализацию вредно для здоровья программы\. Другого не дано\.

## Акт выполненных работ \(итоги\)

Итак, проблема идентифицирована и устранена, работоспособность графического адаптера в проблемном окружении восстановлена\. Нахождение таких проблем средствами статического анализа — весомый аргумент в их пользу, в том числе в сценарии [регулярного использования](https://pvs-studio.ru/ru/blog/posts/1293/)\. Вообще, сборку под FreeBSD после выхода 86Box v5\.0 чинили несколько раз, и в версии v5\.1 уже сходу можно получить рабочую программу\.

На этом наш "ремонт" закончен\. Благодарю за уделённое время, увидимся в новой статье\!