惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
В С неопределённое поведение повсюду
Дмитрий Брайт · 2026-05-24 · via Все публикации подряд на Хабре

В С неопределённое поведение повсюду

Средний

10 мин

28K

Если бы Кардинал Ришелье был программистом, он бы сказал: «Дайте мне шесть строк кода, написанных рукой самого профессионального C-программиста в мире, и я найду в них лазейку для вызова неопределённого поведения.

Никто не может написать безошибочный код на С или C++. И я говоря об этом как человек, который пишет на этих языках почти каждый день около 30 лет. Я слушаю подкасты по C++. Я смотрю выступления про C++ на конференциях. Мне нравится читать и писать на этом языке.

C++ послужил нам сполна, но на дворе 2026 год, и современная рабочая среда явно отличается от среды 1985 (C++) или 1972 (С).

И я далеко не первый, кто об этом заговорил. Помню ещё с десять лет назад читал статью какого-то известного человека, в которой он утверждал, что использование C++ вполне обоснованно можно подвести под нарушение закона Сарбейнза-Оксли (SOX). И хотя с остальной его критикой я не был согласен (как и с тем, что он путал «its» и «it’s»), конкретно с этим пунктом я никогда не спорил.

Мало того, со временем я всё больше убеждался в его истинности. На деле в С для возникновения неопределённого поведения (undefined behaviour, UB) есть гораздо больше возможных причин, чем вы могли предполагать.

Все знают, что двойное освобождение памяти, её использование после освобождения, выход за границы объекта (например, массива) и чтение неинициализированной памяти — это UB. Как ни крути, но в контексте работы с памятью C и C++ безопасными не назовёшь. Тем не менее даже эти ошибки продолжают совершаться повсеместно раз за разом.

А ведь есть и другие, более тонкие и нелогичные.

Дело не в оптимизациях

Похоже, некоторые считают, что достаточно компилировать код без включения оптимизаций, и тогда UB не страшно. Они верят в какую-то изначальную враждебность компилятора, который только и думает — «Ага! Неопределённое поведение! Я могу делать всё, что захочу!» — и рассчитывают, что отключение оптимизаций его остановит.

Это не так.

UB не означает, что компилятор может воспользоваться вашей оплошностью. Оно означает, что компилятор может принять ваш код за корректный. Это говорит о том, что заложенное в код намерение, которое может быть столь очевидным для человека, невозможно выразить между этапами компиляции или модулями.

UB означает, что компилятору при генерации кода даже не нужно реализовывать некоторые особые случаи, потому что они попросту «не могут произойти».

Компилятор, да и само аппаратное обеспечение, играют в глухой телефон, пытаясь распознать ваши неопределённые намерения. В итоге вы, конечно, можете получить желаемый результат, но без каких-либо гарантий сейчас или в будущем.

UB повсюду

Я не стану пытаться перечислить все варианты неопределённого поведения, а просто аргументированно покажу, что оно повсюду. И если никому не под силу сделать всё идеально, то как вообще можно винить в этом программистов? Мой главный посыл в том, что ВЕСЬ нетривиальный код на С и C++ содержит в себе UB.

Обращение к объекту с неправильным выравниванием

Возьмём для примера такой код:

int foo(const int* p) {
   return *p;
}

Если эту функцию вызвать с неправильно выровненным указателем (вероятно, с адресом, кратным sizeof(int), но кто знает), то это UB (пункт 6.3.2.3 стандарта C23).

На Linux Alpha это иногда приводило к вызову аппаратного исключения, которое ядро обрабатывало программно, эмулируя нужное вам поведение. В других случаях ваша программа могла просто упасть по сигналу SIGBUS.

На архитектуре SPARC это гарантированно приводило к SIGBUS.

Естественно, на x86/amd64 (далее просто x86) такой код проблем не вызовет. Да что тут говорить, это даже наверняка будет операцией атомарного чтения. Архитектура x86 известна своей крайней снисходительностью к ошибкам синхронизации кэша.

Итак, здесь мы имеем три случая:

  • помощь со стороны ядра (для некоторых инструкций чтения на Alpha);

  • сбой (другие инструкции чтения на Alpha и SPARC);

  • всё в порядке (x86).

А что с ARM, RISC-V и другими архитектурами? Что насчёт будущих? В какой-нибудь будущей архитектуре могут даже появиться специальные регистры для указателей на int, которые вообще не заполняют младшие биты, потому что таких указателей существовать не может.

И даже если такой код будет работать, компилятор может однажды переключиться с использования одной инструкции на другую, и внезапно ядро больше не поможет.

И всё потому, что компилятор не обязан генерировать инструкции ассемблера, работающие с не выровненными указателями. Ведь это UB.

Или вот другой вариант:

void set_it(std::atomic<int>* p) {
        p->store(123);
}
int get_it(std::atomic<int>* p) {
        return p->load();
}

Будет ли эта операция атомарной, если объект невыровнен? Му*— вопрос поставлен неверно. Это UB. (Хотя, да, на практике это вполне может обернуться проблемой с атомарностью).

*Прим. пер.: Иероглиф 無 (му) часто используется в дзэн-буддизме и обозначает отсутствие, небытие или пустоту. В текущем контексте он используется автором как образный способ указать на бессмысленность вопроса.

Если вам этих подтверждений мало, попробуйте представить, что произойдёт, если объект, который вы рассчитывали прочитать атомарно, окажется на стыке двух страниц. Только не задумывайтесь слишком сильно, а то вам может показаться, что «это нормально». Но это не так. Это неопределённое поведение.

По правде говоря, оно возникло даже раньше.

Не нужно винить функцию foo() выше. Проблему вызвало не разыменовывание указателя — для её появления было достаточно простого создания указателя.

Вот пример:

bool parse_packet(const uint8_t* bytes) {
        const int* magic_intp = (const int*)bytes;   // UB!
        int magic_raw = foo(magic_intp);  // Возможен сбой на SPARC.
        int magic = ntohl(magic_raw); // Здесь всё в порядке.
        […]
}

Проблема в приведении, а не в foo().

Компилятор вполне может придумать для младших битов int* конкретное назначение, например, использовать их для сборки мусора или хранения тегов безопасности.

Применение isxdigit() к char

bool bar(char ch) {
        return isxdigit(ch);
}

isxdigit() — это простая функция, которая получает символ и возвращает 1, если это шестнадцатеричная цифра: 0–9 или a–f. Она также может принимать значение EOF. Так, хорошо. А какое у EOF значение? Из раздела 7.4p1 стандарта C23 мы знаем, что это int, и можем сделать вывод, что его нельзя представить в виде unsigned char.

Поэтому isxdigit() получает int, а не char. Но любое значение типа char помещается в int, так что здесь проблем быть не должно. Приведение из char в int корректно, и если верить разделу 6.3.1.3, то всё в порядке, так?

Нет. Если bar() вызвать со значением вне диапазона 0–127, а в вашей архитектуре char является знаковым (что согласно параграфу 20 раздела 6.2.5 стандарта C23 определяется реализацией), то целое число окажется отрицательным.

Ниже я привёл вполне рабочую реализацию isxdigit(), которая вызовет чтение чёрт знает какого участка памяти. Это может быть даже память, связанная с устройствами ввода-вывода, что уже чревато проблемами почище простого получения случайного значения или сбоя. Такое поведение может, например, привести к запуску двигателя. Конечно, в десктопном приложении это менее вероятно, чем во встраиваемых системах, но иногда сетевые драйверы выносят в пространство пользователя (ради производительности), так что даже оно не гарантирует защиту.

int isxdigit(int c) {
        if (c == EOF) {
                return false;
        }
        return some_array[c];
}

Приведение float к int

int milliseconds(float seconds) {
        int tmp = (int)(seconds * 1000.0); /* НЕПРАВИЛЬНО */
        return tmp + 1; /* тоже НЕПРАВИЛЬНО (знаковое переполнение — это UB) */
}

Когда конечное значение реального типа с плавающей запятой преобразуется в целочисленный тип […] Если целую часть значения нельзя представить целочисленным типом, поведение не определено.

— 6.3.1.4

Кроме того, ввиду отсутствия явных правил в стандарте, это UB также возникнет, если float окажется бесконечным или NaN.

Так как же сравнивать float с INT_MAX? Нужно ли приводить float к int? Нет, это вызовет то самое UB, которого мы хотим избежать. Тогда приводить INT_MAX к float? А откуда вы знаете, что его получится выразить точно? Что, если при приведении INT_MAX к float значение округлится так, что перестанет влезать в int, и тогда ваше сравнение потеряет всякий смысл?

Может, сработает следующий вариант? Так вы упустите некоторые реально большие значения, но вдруг это не страшно?

int milliseconds(float seconds) {
        const float ftmp = seconds * 1000.0f;
        if (!isfinite(ftmp)) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        if ((float)(INT_MIN + 1000) > ftmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        if ((float)(INT_MAX - 1000) < ftmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        // Теперь преобразование безопасно.
        const int tmp = (int)ftmp;
        if (INT_MAX == tmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        // Теперь можно безопасно прибавить единицу.
        return tmp + 1;
}

А ведь я всего лишь хотел превратить float в int. :-(

Уверен, существует много программ, код которых принимает значения в секундах и преобразует их в целочисленные миллисекунды просто через умножение и приведение.

Хранение объектов по нулевому адресу

Большинство программистов с этим не столкнутся, но я сомневаюсь, что существует отвечающий стандартам С способ поместить объект по нулевому адресу. Тем не менее такое вполне может потребоваться при написании ядра ОС и встраиваемых программ.

Согласно разделу 6.3.2.3, целочисленная константа нуль (которая преобразуется в указатель) и nullptr являются «константой нулевого указателя» (я буду называть её просто NULL). При этом стандарт C не указывает, что конкретный указатель NULL должен ссылаться именно на физический нулевой адрес, поскольку стандарт описывает абстрактную машину C, а не реальное аппаратное обеспечение».

Гарантируется лишь то, что при сравнении NULL с нулём вы получите равенство. Но вы и знать не знаете, как это происходит внутренне — возможно, этот нуль преобразуется в нативный NULL для данной платформы, которым вполне может оказаться 0xffff.

В стандарте также явно говорится, что разыменовывание нулевого указателя, независимо от его значения, ведёт к неопределённому поведению. Такой пример приводится в разделе 3.4.3.

Это также значит, что вы не можете рассчитывать на создание нулевого указателя с помощью memset(&ptr, 0, sizeof(ptr));. Нельзя инициализировать структуры таким образом и думать, что указатели в их полях окажутся нулевыми. Причём это уже касается большинства программистов.

К слову, в некоторых старых машинах использовались ненулевые указатели NULL.

Но предположим, что у вас современная машина, где NULL — это указатель на нулевой адрес, и у вас там реально хранится объект.

Опять же, в разделе 6.3.2.3 говорится, что NULL не равен «любому объекту или функции». Значит, это UB:

void (*func_ptr)() = NULL;
func_ptr();

Стандарт С говорит «там нет функции». Откуда вам знать — возможно, у компилятора в этом случае даже нет внутреннего способа выразить ваше намерение. Вы можете возразить: «Но он же просто сгенерирует инструкцию вызова по битовому шаблону из всех нулей? Других адекватных вариантов нет».

А что вообще значит «из всех нулей»? На 16-битной x86 это будет 0000:0000? Или CS:0000?

Переменные аргументы и типы (например, printf с %ld вместо %lld)

Здесь получаем UB:

execl("/bin/sh", "sh", "-c", "date", NULL);     /* НЕПРАВИЛЬНО */
execl("/bin/sh", "sh", "-c", "date", 0);     /* НЕПРАВИЛЬНО */

А вот здесь уже нет:

execl("/bin/sh", "sh", "-c", "date", (char*)NULL);

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

И здесь тоже UB:

uint64_t blah = 123;
printf("%ld\n", blah);  /* НЕПРАВИЛЬНО */

Должно быть так:

uint64_t blah = 123;
printf("%"PRIu64"\n", blah);

Так как же тогда выводить uid_t? Как вариант, вы можете привести его к uintmax_t и вывести с помощью PRIuMAX. Но уверены ли вы, что uid_t беззнаковый? Думаю, в худшем случае вы получите на выходе бессмысленное число вместо -1.

Деление на нуль — это UB

Уверен, вы и так это знали. Но учитывали ли вы сопутствующие аспекты безопасности? Нередко бывает, что знаменатель поступает из непроверенных источников.

И здесь ещё много других нюансов. Стандарт C23 содержит 283 вхождения слова «undefined», и это без учёта тех случаев, которые не задокументированы.

Бонус: реализация без UB

Никто не способен просчитывать правила целочисленного расширения при беглом просмотре кода. Никто!

Статья и так уже затянулась, поэтому ограничусь парой примеров:

unsigned char a = 0xff;
unsigned char b = 1;
unsigned char zero = 0;
bool overflowed = (a + b) == zero;
// overflowed примет значение false (0), а не true (1).

unsigned char a = 0x80;
uint64_t b = a << 24;     // Бонусное UB(?)
// Теперь b равна 18446744071562067968 (ffffffff80000000), а не 2147483648 (0x80000000).
// Даже при том, что все наши переменные беззнаковые.

LLM здесь справляются лучше нас

Покажите LLM ЛЮБОЙ код на C, попросите найти в нём UB, и она справится. Причём на сегодня окажется права почти в каждом случае.

Честно говоря, мне стало немного не по себе, когда ИИ корректно нашёл неопределённое поведение в моём коде, и тогда я решил таким же образом прощупать зрелую и скрупулёзно написанную OpenBSD. Я выбрал первый инструмент, какой пришёл мне на ум — find — и он выдал целую кучу предупреждений.

Я отправил мейнтейнерам патч для исправления выхода за границы объекта при записи (а также логической ошибки, не связанной с UB). Я не стал слать им патчи для всех случаев UB, которые встречались на каждом шагу, отчасти, потому что проект OpenBSD в прошлом неохотно реагировал на баг-репорты. К тому же, у меня было ощущение, что «на практике всё наверняка не так плохо», и если уж разработчики OpenBSD решат искоренить из кодовой базы всё UB, то это должен быть масштабный системный проект, а не кто-то вроде меня, выступающий посредником между LLM и мейнтейнерами.

Так что же нам делать?

Мы не можем просто взять и выбросить кодовые базы на С и C++. Но и оставлять их в таком глубоко уязвимом состоянии тоже не вариант.

Нам нужен способ исправления существующего UB в масштабах всей индустрии. Причём такой, чтобы при этом не наплодить ИИ-слопа и не завалить ревьюеров работой.

Хотя эта идея тоже не нова и озарением не является.

Тем не менее в 2026 году написание кода на С или С++ без анализа на UB с помощью LLM уже следует расценивать как нарушение SOX, да и просто как банальную безответственность. Если уж разработчики OpenBSD более 30 лет не могут отыскать эти проблемы, то каковы шансы у всех остальных?

Такой подход может не масштабироваться на крупные кодовые базы, но конкретно в своих проектах я прошу LLM найти UB, а при необходимости объяснить и исправить. После этого я внимательно изучаю результат, пока не убеждаюсь, что баг реально присутствовал и был исправлен.

Проблема такой методики в том, что для подтверждения найденных ошибок необходим опытный специалист. Но такие люди обычно заняты другими делами. Подобная перепроверка — это рутинная работа «санитара», но в то же время слишком тонкая, чтобы доверить её джуниорам, которым обычно такую рутину поручают.