openlang

Одна и та же проверка переполнения, разные ветки ошибки на -O0 и -O2

Враждебный pack с count=65536 и rec_size=32768. На -O0 получаю "pack too large". На -O2 вместо этого получаю "out of memory". Исходник один и тот же, и я правда не понимаю, как так. -O2 неправильно компилирует, или malloc делает что-то странное?

int total = count * rec_size;
if (total < 0) {
    LOG("pack too large");
    fclose(f);
    return -1;
}
out->data = malloc(total);
Переведено с английскогоПоказать оригинал

Разговор продолжается 6

Следуя доводам
  1. Ссылка на ответ

    65536 * 32768 — это 2^31, что не помещается в знаковый int. Это неопределённое поведение, так что ни одна из веток не "правильная". На -O0 оно случайно заворачивается в INT_MIN, и ваша проверка срабатывает. На -O2 компилятор может исходить из того, что переполнения нет, и выкинуть проверку < 0. malloc тут ни при чём. Проверяйте до умножения, например count > INT_MAX / rec_size, или используйте size_t.

    Переведено с английскогоПоказать оригинал
  2. Ссылка на ответ

    Ха, классика. Причину уже назвал e.lund. Добавлю часть, которая объясняет ваше сообщение "out of memory".

    Если компилятор выбрасывает проверку < 0, в total всё равно лежит завёрнутое значение, INT_MIN. Затем malloc(total) преобразует его в size_t. На 64-битной машине это становится 0xFFFFFFFF80000000, то есть около 16 эксабайт. malloc отказывает, и срабатывает ваша OOM-ветка. Так что malloc не странный. Просто у него просят что-то абсурдное. (Кстати, поэтому у тех, кто твердит "просто проверяйте на NULL", тут возникает ложное чувство безопасности.)

    Мой вопрос про объявления. Какие типы у count и rec_size? Если они читаются из файла как uint32_t, то умножение происходит в беззнаковой арифметике и законно заворачивается. Тогда присваивание в int total — implementation-defined, а не UB. Это сильно меняет картину.

    Пробовали -fsanitize=undefined на -O2? Он должен прямо пометить умножение. И приводит ли -fwrapv оба билда снова к одинаковому поведению?

    Переведено с английскогоПоказать оригинал
  3. Ссылка на ответ

    Проверка < 0 выполняется уже после того, как переполнение произошло, так что я бы не пытался её спасать. Проверяйте значения из заголовка до умножения и держите результат в size_t.

    size_t total;
    if (rec_size == 0 ||
        __builtin_mul_overflow((size_t)count, (size_t)rec_size, &total) ||
        total > max_pack_bytes) {
        LOG("pack too large");
        fclose(f);
        return -1;
    }

    С size_t 2^31 — вполне допустимый размер, так что ваш враждебный pack теперь попросит у malloc 2 GiB. Одной проверки переполнения для этого недостаточно. Я бы ограничил max_pack_bytes размером файла или бюджетом вашего загрузчика, чтобы заголовок не мог заявлять больше, чем есть в файле.

    Билтин описан здесь: https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html. Он есть и в GCC, и в clang. Если собираете MSVC, используйте вариант count > SIZE_MAX / rec_size.

    Переведено с английскогоПоказать оригинал
  4. Ссылка на ответ

    Это не ошибка компилятора. Знаковое переполнение — UB, поэтому компилятор вправе удалить проверку < 0. Посмотрите asm через gcc -O2 -S, и вы увидите, что сравнения там вообще нет.

    Остаётся 32-битный imul. Результат 0x80000000. Затем movslq расширяет его со знаком для аргумента malloc, и получается 0xFFFFFFFF80000000. malloc на этом падает, и вы попадаете в OOM-ветку.

    На -O0 сравнение всё ещё генерируется и видит знаковый бит, поэтому получаете "pack too large".

    Одной починки проверки недостаточно. Даже с size_t 2^31 — это всего 256 MB, так что malloc спокойно вам это выдаст. Враждебный заголовок может просто запросить меньше, чем граница переполнения. Сравнивайте с реальной длиной файла до выделения памяти. И в итоге ещё ограничьте count.

    Переведено с английскогоПоказать оригинал
  5. Ссылка на ответ

    Хорошо подмечено про movslq. Именно на этом шаге INT_MIN превращается в тот запрос на 16 эксабайт, о котором я говорил выше.

    Одна придирка. 2^31 байт — это 2 GiB, а не 256 MB. drawer6 был прав. Это только усиливает ваш аргумент. В Linux с overcommit по умолчанию malloc на 2 GiB обычно проходит мгновенно, потому что страницы ещё не тронуты. Так что "исправленная" версия с size_t не падает чисто. Она возвращает указатель, а проблемы начинаются позже, когда чтение или memset подгружают страницы. (Спросите, откуда я знаю. Однажды обработчик загрузок у клиента делал ровно это, и OOM killer прибил не тот процесс.)

    Так что да, сначала сравнивайте с реальной длиной файла. Размер даёт fstat. Вычтите байты заголовка, которые уже прочитаны, и проверьте count * rec_size против оставшегося, в форме, безопасной от переполнения.

    Кроме того, если кто-то собирает это под 32 бита, size_t там 32-битный, и умножение может законно завернуться в беззнаковой арифметике. Проверка переполнения там всё равно нужна. Одна проверка длины файла вас не спасёт, если завёрнутое произведение случайно окажется маленьким.

    Переведено с английскогоПоказать оригинал
  6. Ссылка на ответ

    int total = count * rec_size;

    У меня сомнение: почему размер в байтах вообще int? Размеры должны быть size_t с момента чтения заголовка. И count с rec_size ведь приходят прямо из файла, нет? Тогда проверка после умножения запоздала by design. Сначала прогоните UBSan на -O2.

    Переведено с английскогоПоказать оригинал

Другие стороны того же вопроса

  1. -race сообщает DATA RACE, хотя ack — это приём из небуферизованного канала. Виноват time.After?

    Может ли быть, что race detector здесь ошибается, или я неправильно понимаю, что мне даёт ack? Это pushgate, небольшой сервис…

    Ответы 4Последняя активность go
  2. Несовпадение HMAC только на одном вебхуке (с акцентами в имени)

    Написал небольшой чекер на Node (чистый JS, без сборки): он переигрывает сохранённые доставки вебхуков и проверяет HMAC-SHA25…

    Ответы 7Последняя активность typescriptjavascript
  3. memset перед free проходит с флагами по умолчанию, падает с OPT=-O2

    Пишу небольшой хранитель учётных данных на C (WSL, Neovim, ничего особенного). vault_release() должна затирать секрет, а пото…

    Ответы 6Последняя активность c