https://github.com/xoreaxeaxeax/schrodingers-toctou
TL;DR Когда мы скомпилируем что-то эдакое:
unsigned int g(unsigned short *p) {
short t = *p; //copy *p into a local for safekeeping
return (unsigned short)t - t;
}мы в ассемблере (arm gcc 14.2.0, -O2) можем вполне себе увидеть:
g:
ldrh r2, [r0] # load *p, once
ldrsh r0, [r0] # load *p, twice
subs r0, r2, r0
bx lr
одно чтение стало два!
Собственно, размножение почкованием доступно потому, что абстрактная машина С считает, что память между двумя чтениями не изменится. Invented load называется.
Да вот незадача.
if (sh->len <= 20) // CHECK reads shared->len
// ** attacker modifies shared->len **
memcpy(out, sh->data, sh->len); // USE reads it again: buffer overflow
Неатомарненько выходит. Там аж PoC в репозитории есть.
При том подвержена куча компиляторов на куче таргетов. Как на -О2, так и на -О3.
Сложно винить сами компиляторы - в них длиннющий пайплайн с кучей стадий, вот и неудивительно, что такое на выходе. Но в то же время они и виноваты, потому что написали-то нормально.
Можно изъёбываться с барьерами и volatile, но это всё не убивает проблему на корню. Volatile, например, относится к lvalue, тот же memcpy квалификатор не сохраняет. Тогда оверфлова просто закапывается чуть ниже. А барьеры нередко стоят не там, где надо.
Даже деды хуесосят комитет С, по этому и не только по этому поводу. В половине случаев, правда, дед вполне себе конкретный - начинается на Л, кончается на инус Торвальдс.
Ёжик плакал и кололся, но на С писать не переставал.