Изложение https://qinsb.blogspot.ru/2018/03/ub-will-delete-your-null-checks.html
Однажды один человек закоммитил один коммит в clang и компилятор перестал проходить один из тестов. После тщательного анализа выяснилось, что в коде была возможна ситуация UB, но оптимизатор её проигнорировал и соптимизировал код так, как будто эта ситуация невозможна.
Вот код, который моделирует поведение кода из коммита (Код покоцан движком блога, вырезаны аргументы шаблонов. Я не буду пытаться их восстановить, т.к. аргумент-тип у llvm::SmallVector может быть понят из контекста, где надо, а аргумент-число — не особо важен):
struct Foo {
llvm::SmallVector Vals;
};
struct Bar {
Foo *getLastFoo() { return Foos.empty() ? nullptr : Foos.back(); }
llvm::SmallVector Foos;
void *Pad;
void doUB();
};
void __attribute__((noinline)) Bar::doUB() {
if (getLastFoo()->Vals.empty())
puts("Vals empty");
else
puts("Vals non-empty");
}
int main() {
Bar b;
b.doUB();
}
В SmallVector::back
стоит assert на не-пустоту вектора (!empty()
). Т.к. код этих методов инлайнится (в getLastFoo
) и компилятор видит, что проверка на пустоту в assert-е находится в false-ветке тернарного оператора, в условии которого та же самая проверка уже сделана, то в assert подставляется константа true и, в общем, можно считать, что assert выкинут.
Далее. Компилятор видит, что результат вызова getLastFoo
в Bar::doUB()
всегда разыменовывается, поэтому он не может быть нулевым указателем. Следовательно, условие тернарного оператора в getLastFoo
не может быть истинным и при инлайнинге оператор безусловно заменяется на свою false-ветку (Foos.back()
(в котором уже нет проверки на empty, т.к. assert был выкинут ранее)).
SmallVector
устроен так, что в пустом векторе back()
указывает внутрь самого SmallVector, и, короче говоря, код интерпретирует кусок памяти объекта Bar b
как объект типа struct Foo
и выполняет проверку Vals.empty()
с непредсказуемым результатом.
Автор заканчивает тем, что радуется новому юз-кейсу для опции -fno-delete-null-checks
, но по-прежнему не считает, что эту опцию стоит использовать для решения проблем ядра linux.
(видимо, имеется в виду типичный говнокод ядра
struct foo* p = bar->p;
if (!bar) {
...
}
Перемещено tailgunner из development