
Обнаружение создаваемых компилятором загрузок из памяти, которые превращают безопасный код на C в TOCTOU-уязвимости. Включает автоматизированный аудит исходного кода, бинарный анализ на основе Unicorn и прогоны по компиляторам/архитектурам/флагам в 100+ проектах.
«...определение „вменяемого компилятора“ становится всё более расплывчатым.»
Бинарный файл, который вы запускаете, — это не программа, которую вы написали. Оптимизатор компилятора переписывает
ваш исходный код так, что вы этого никогда не видите — и некоторые из этих изменений могут
незаметно и легально превращать, казалось бы, безопасный код в
уязвимые бинарные файлы. Та же самая строка может быть
безопасной в одном компиляторе и эксплуатируемой в другом,
при этом ничто в исходном коде не говорит вам, какой именно: уязвимость в
суперпозиции, схлопывающаяся только при сборке. Schrödinger's TOCTOU исследует
изобретённые компилятором загрузки и их широкие
последствия для уязвимостей типа «время проверки — время использования» (TOCTOU) — обнаруженных
в проектах с открытым исходным кодом:
ядрах,
гипервизорах,
анклавах,
прошивках,
и библиотеках.
Куда ни посмотри, казалось бы, безопасный код остаётся . Но это лишь выборка, а не граница; те
же ошибки весьма вероятны и в .
«Начните с чего-нибудь простого.»
Сколько раз эта функция загружает *p?```c
unsigned int g(unsigned short *p)
{
short t = p; / copy *p into a local for safekeeping */
return (unsigned short)t - t;
}
Подсказка: ответ — 1 — исходный код загружает `*p` один раз в `t`.
Вставьте его в [Compiler Explorer](https://godbolt.org/z/c5K9P4dPd) (`arm gcc 14.2.0`,
`-O2`) и посчитайте загрузки из `r0`, который содержит `p`:```asm
g:
ldrh r2, [r0] # load *p, once
ldrsh r0, [r0] # load *p, twice
subs r0, r2, r0
bx lr
Одно чтение в исходном коде, два — в бинарнике. Второе — изобретённое чтение — чтение, которое создал компилятор, хотя вы его не писали. Оно допустимо в рамках абстрактной машины C, которая предполагает, что память не может измениться между двумя чтениями. Но когда эта память доступна атакующему для записи, предположение становится эксплойтом: изобретённое чтение может оказаться после проверки безопасности, незаметно открывая окно от проверки до использования (TOCTOU), которое программист считал закрытым. Больше не гарантировано, что значение, которое вы проверили, и значение, которое вы используете, совпадают — даже если вы никогда не писали код, который перечитывает его.
Задача доказывает, что изобретённое чтение существует; посмотрим, как это превращается в повреждение памяти.
В уязвимости TOCTOU программа проверяет, что значение безопасно, затем использует значение. Однако окно для эксплуатации существует, если атакующий может изменить значение в крошечном промежутке времени между этими двумя чтениями – безвредное значение проходит проверку, а опасное — именно то, которое используется:```c if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow
Классическое исправление — **сначала сделать снимок**: скопировать любые данные, с которыми атакующий может возиться, в локальную переменную, недоступную атакующему, и затем не доверять ничему, кроме этой локальной переменной. Как только `len` оказывается в локальной переменной, она замораживается — атакующий, участвующий в гонке за разделяемую память, больше не может до неё добраться, — поэтому проверка и копирование гарантированно видят одно и то же значение. Именно так код в `receive` ниже устраняет проблему TOCTOU: он делает снимок сообщения, проверяет снимок и публикует проверенную копию в `slot`, чтобы потребитель мог её переслать:```c
#include <string.h>
struct message {
int len; /* payload length */
char data[20]; /* payload */
};
struct message slot; /* the most recently validated message */
char out[20]; /* fixed 20-byte destination */
void receive(struct message *shared) {
struct message local = *shared; /* 1. snapshot untrusted input */
if (local.len <= 20) /* 2. validate the snapshot */
slot = local; /* 3. publish the validated copy */
}
void forward(void) { /* the time of use, later */
memcpy(out, slot.data, slot.len); /* slot.len was checked <= 20 ... right? */
}
Исходно, по коду, это корректно. len читается ровно один раз — в снапшот —
поэтому значение, прошедшее проверку <= 20, — это значение, опубликованное в slot.
Окно TOCTOU закрыто, и код безопасен.
Но это не так. При компиляции x86-64 gcc с -O2 функция receive читает его из
исходной разделяемой памяти дважды: один
раз как скаляр для проверки условия и ещё раз как часть массового
копирования, результат которого публикуется в slot:```nasm
receive:
cmp DWORD PTR [rdi], 20 ; READ #1: the CHECK reads shared->len directly
movdqu xmm0, XMMWORD PTR [rdi] ; READ #2: the bulk copy re-reads it (len is byte 0)
mov rax, QWORD PTR [rdi+16] ; (the bulk copy's tail: struct bytes 16-23)
jg .L1 ; len > 20? skip the publish
mov QWORD PTR slot[rip+16], rax ; (publish that tail)
movaps XMMWORD PTR slot[rip], xmm0 ; and publish the TOCTOU-vulnerable snapshot
.L1:
ret
forward:
movsx rdx, DWORD PTR slot[rip] ; copy size = slot.len, the unchecked READ #2 value
mov esi, OFFSET FLAT:slot+4 ; src = slot.data
mov edi, OFFSET FLAT:out ; dst = out[20]
jmp memcpy ; copies slot.len bytes into out[20]
Проверка выполняется на ЧТЕНИИ №1; значение, которое попадает в `slot.len`, — это ЧТЕНИЕ №2. Атакующий, который изменяет `len` между ними, передаёт проверке `<= 20` безопасное значение, тогда как переразмеренное публикуется в `slot`, — и `forward` затем копирует это количество байтов в `out[20]`, порождая то самое переполнение, которое снимок должен был предотвратить и которое вернул оптимизатор.
Это превращено в полноценный proof-of-concept в [`poc/example.c`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/poc/example.c), где код использует канонический подход с защитой от TOCTOU: недоверенная структура `message` попадает в снимок `local`, чтобы её нельзя было изменить, `local.len` снимка проверяется на соответствие ёмкости буфера, и в `slot` публикуется только проверенная копия; позже потребитель копирует `slot.len` байтов полезной нагрузки в фиксированный буфер. Одновременно атакующий ведёт гонку на `shared->len`. Неожиданная изобретённая загрузка со стороны компилятора перечитывает `shared->len` для массовой публикации, поэтому `slot.len` несёт переразмеренное значение атакующего, хотя проверка была пройдена, — возвращая TOCTOU, от которого программист пытался защититься, и создавая, казалось бы, невозможное переполнение буфера — из ниоткуда.
## Причина
> *К тому моменту, как C добирается до машинного кода, он уже перекроен понижением во фронтенде, IR-оптимизациями, распределением регистров и бэкенд-генерацией кода — глубоким многоступенчатым конвейером, принимающим решения, которые вам не видны. Нет ни одной стадии, которую можно было бы обвинить. Изобретённая загрузка — это эмерджентное свойство всего конвейера, а не баг какой-то его части.*
На данный момент: компиляторы *могут* порождать изобретённые загрузки — и именно тот самый идиоматический приём, призванный предотвратить баг, — снимок, проверка, использование, — и возвращает его. Следующий шаг (чтобы понять, действительно ли мы уязвимы) — охарактеризовать, *когда* это происходит. Оказывается, это трудно.
В [`cat-states/`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states) мы ищем proof-of-concept'ы, показывающие, что это реально и что это повсюду:
| Механизм | Тулчейны | Целевые архитектуры |
|---|---|---|
| [**Рематериализация**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#rematerialization-class-1) | GCC, Clang, ICX, ICC, MSVC | x86-64, i386, m68k, VAX, MSP430 |
| [**Перезагрузка с несовпадением ширины**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#width-mismatch-reload-class-2) | GCC | ARM, MIPS, MIPS64, RV64, s390x |
| [**Перекрытие массовой и скалярной загрузок**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#bulk-vs-scalar-overlap-class-3) | GCC, Clang, ICX, MSVC | x86-64, ARM, AArch64, AVR, Xtensa, SPARC, PPC64, s390x, MIPS64, RV64, m68k, MSP430, VAX, HPPA |
| [**Межклассовая перезагрузка**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#cross-class-reload-class-4) | GCC | x86-64, s390x |
| [**Сворачивание mem-операции в CISC**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#cisc-alu-mem-op-fold-class-7) | GCC, Clang | m68k, MSP430, s390x, VAX, 6502 |
| [**Перезагрузка с порядком байтов**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#byte-order-divergent-reload-class-8) | GCC | s390x |
Каждый PoC выше закрепляет одну-единственную точку, где загрузка *может* появиться; [`alpha-lab/`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab) наносит на карту пространство вокруг неё, чтобы найти, где пролегают границы, — трёхступенчатый конвейер, управляемый из одного `.c`-файла. [Прогонщик матрицы](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab/matrix_runner.py) прогоняет матрицу компилятор × архитектура × флаги на [Compiler Explorer](https://godbolt.org); [детектор загрузок](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab/detect.py) запускает каждый полученный бинарник под [Unicorn](https://www.unicorn-engine.org/) и ловит любой байт, прочитанный дважды; а [минимизатор флагов](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab/flag_search.py) дельта-отлаживает каждое срабатывание до минимального набора флагов, который превращает безопасную сборку в TOCTOU с двойным чтением.
**Результат**: нельзя винить ни один отдельный компилятор, флаг или проход — двойное чтение возникает из сложного взаимодействия множества слоёв компилятора, каждый из которых принимает локально корректные решения. Эффект нелинеен: небольшие изменения в исходнике, флагах или целевой архитектуре могут [каскадно приводить к разным результатам](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-imagemagick-7.1.2-25.md#candidate-1--readsunimage-sun_infolength-alloc-vs-copy). Единственный надёжный способ узнать, уязвима ли конкретная строка, — [**скомпилировать её и посмотреть.**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab/README.md#same-source-different-outcome)
**Кот жив — и его нет.** Пока вы не соберёте проект, место вызова, которое снимает, проверяет и использует локальную копию, *не является* ни безопасным, ни уязвимым — оно является и тем и другим, а компилятор, его версия, целевая архитектура и флаги решают, чем именно. Сборка — это измерение, и она схлопывает суперпозицию в ту или иную сторону. Это **шрёдингеровский TOCTOU**: проверка значения, которое программист считал замороженным, но которое стандарт C тихо позволяет компилятору перечитать из контролируемой атакующим памяти. Ящик остаётся закрытым, пока кто-то где-то не выберет тулчейн и не откроет его.
## Эффект
> *Паттерн встречается почти повсюду — вплетённый в самый тщательно проверяемый код в мире через простой идиоматический C.*
Проблема **практически неразрешима**. Один и тот же фрагмент кода может быть уязвимым или неуязвимым в зависимости от точной комбинации компилятор × версия × архитектура × флаги — а таких комбинаций больше, чем атомов в наблюдаемой вселенной. Ограничить её хотя бы для одной кодовой базы — почти безнадёжный поиск; сделать это по всей экосистеме — куда хуже.
Даже решение вопроса, безопасно ли *одно-единственное* место вызова, не поддаётся инспекции: такой возможный барьер, как `copy_from_user` из ядра, [исключает баг](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/README.md#analysis) лишь после того, как ~шесть слоёв инлайнинга, макросов и развилок `CONFIG`/CPU-фич упираются в непрозрачный `asm`, — а *та же самая* строка исходника в других конфигурациях вообще не является барьером. Чтение места вызова не даёт нам ничего.
Единственный путь вперёд — автоматизация. Эвристический анализ был запущен по заметным open-source целям — гипервизорам, рантаймам TEE/enclave, прошивкам, подсистемам ядра, библиотекам протоколов — и обнаружил **300+ шрёдингеровских TOCTOU** в **100+ критически важных для безопасности проектах**: местах, где стандарт C *разрешает* компилятору перечитать записываемую атакующим память между проверкой и её использованием. Автоматизированный анализ определяет границы доверия, ищет шрёдингеровский паттерн и оценивает вероятность/влияние/риск.
Результаты показывают, что, казалось бы, безобидные изобретённые компилятором загрузки легко каскадно приводят к разрушительным последствиям.
Компилятор изобретает не столько *загрузку*, сколько *возможность*, которую эта загрузка передаёт атакующему:
---
- **изобретённый компилятором побег из VM** — [QEMU](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-qemu-v11.0.1.md#candidate-1--ahci-prdtl-highest-impact), [Xen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-xen-ptwalk-RELEASE-4.21.1.md#candidate-1--guest_walk_tables-pte-walk), [bhyve](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-bhyve-release-15.0.0.md#candidate-1--ahci-prdt-byte-count-write-path-oob-write), [KVM](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-kvm-host.md#candidate-1--svm-nested-vmcb12-save-area-cache-flagship), [ACRN](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-acrn-v3.3.md#candidate-1--nested-ept-shadow-walk)
- **изобретённый компилятором root** — [siw](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline), [VMBus](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-hyperv-vmbus.md#candidate-2--msgtype-dispatch-index), [systemd](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-systemd-v260.md#candidate-1--sd_journal_enumerate_fields-sz-field-payload-size-alloc-vs-copy), [af-packet](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-af-packet.md#candidate-1--tp_len-tx-packet-length), [snd-pcm](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-snd-pcm-v7.0.md#candidate-1--snd_pcm_indirect_playback_transfer-appl_ptr-snapshot-used-for-diff-and-stored-baseline), [seL4](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-sel4-15.0.0.md#candidate-1-flagship--untyped-retype-object-window)
- **изобретённое компилятором закрепление на платформе** — [edk2](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-edk2-edk2-stable202605.md#candidate-1--smmlockboxrestore), [coreboot](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-coreboot-26.03.md#candidate-1--smmstore_rawread_region-bufsize--com-buffer-mapping-overflow), [U-Boot](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-u-boot-v2026.04.md#candidate-1--virtqueue_get_buf-used-ring-id-primary), [OpenSBI](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-opensbi-v1.8.1.md#candidate-1--dbtr-update-trigger-index-primary)
- **изобретённый компилятором взлом анклава** — [SGX](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-intel-sgx-sdk-sgx_2.29.md#candidate-1--generated-ecall-ininout-copy-in-headline-structural), [Keystone](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-keystone-master-88c49ee.md#candidate-1--edge_call_get_ptr_from_offset--edge_call_ret_ptr-host-written-return-offsetsize), [OpenEnclave](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-openenclave-v0.19.15.md#candidate-1--sgx-ecall-context-ocall-buffer), [OP-TEE](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-optee-os-4.10.0.md#candidate-1--register_shm-raw-tmem-reads)
---
Каждое из этих явлений по отдельности может быть катастрофическим, но тревожит именно широта охвата: та же форма обнаруживается повсюду, куда смотрит анализ, — в коде, который не имеет ничего общего, кроме идиомы:
| Цель | Место | Влияние |
|---|---|---|
| **QEMU** | [`ahci_populate_sglist`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-qemu-v11.0.1.md#candidate-1--ahci-prdtl-highest-impact) | длина гостевого PRDT AHCI защёлкивается один раз → **OOB чтение / управляемый атакующим DMA хоста** |
| **Linux / RDMA** | [`siw_rqe_get`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline) | `num_sge` в software-RDMA повторно используется → **OOB запись в ядре** |
| **edk2 / UEFI** | [`SmmLockBoxRestore`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-edk2-edk2-stable202605.md#candidate-1--smmlockboxrestore) | длина SMM-буфера повторно используется → **OOB запись в SMRAM** (ring -2) |
| **TPM 2.0** | [`CryptParameterDecryption`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-ms-tpm-20-ref-v1.83r1.md#candidate-1--cryptparameterdecryption-in-place-decrypt-length) | длина расшифровки на месте повторно используется → **OOB запись в корне доверия TPM** |
| **seL4** | [`decodeUntypedInvocation`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-sel4-15.0.0.md#candidate-1-flagship--untyped-retype-object-window) | окно объекта retype повторно используется → **компрометация ядра** |
| **Xen** | [`guest_walk_tables`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-xen-ptwalk-RELEASE-4.21.1.md#86-per-candidate-finding) | гостевой PTE повторно используется при обходе → **повышение привилегий** |
| **SGX** | [мост edger8r ECALL](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-intel-sgx-sdk-sgx_2.29.md#candidate-1--generated-ecall-ininout-copy-in-headline-structural) | длина `[in]`/`[in,out]` повторно используется для `malloc`/`memcpy_s` → **переполнение кучи анклава** (каждый ECALL) |
| **ARM TF-A** | [`spmc_ffa_fill_desc`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-tf-a-v2.15.0.md#candidate-1--spmc-ffa_mem_sharelend-send-path-primary-could--yes) | поле дескриптора FF-A повторно используется для задания размера `memcpy` → **переполнение кучи в безопасном мониторе EL3** |
| **Linux / Hyper-V** | [Hyper-V VMBus `__vmbus_on_msg_dpc`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-hyperv-vmbus.md#candidate-2--msgtype-dispatch-index) | `msgtype` хоста повторно используется для индексации таблицы обработчиков → **неконтролируемый косвенный вызов** в гостевом ядре |
| **U-Boot** | [`virtqueue_get_buf`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-u-boot-v2026.04.md#candidate-1--virtqueue_get_buf-used-ring-id-primary) | `id` used-ring virtio повторно используется как индекс массива → **OOB чтение/запись в куче** в загрузчике |
| **glibc** | [`_dl_check_map_versions`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-glibc-glibc-2.42.md#candidate-1--_dl_check_map_versions-verneed-version-index-write) | индекс версии VERNEED динамического загрузчика повторно используется как индекс записи → **OOB запись в `ld.so`** при отображении специально созданной разделяемой библиотеки |
| **systemd** | [`sd_journal_enumerate_fields`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-systemd-v260.md#candidate-1--sd_journal_enumerate_fields-sz-field-payload-size-alloc-vs-copy) | размер поля журнала повторно используется между alloc/copy → **OOB запись в куче** в `journalctl`/`coredumpctl` (часто от root) |
| **git** | [`read_table_of_contents`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-git-v2.54.0.md#candidate-1--read_table_of_contents-chunk-offset-to-start-pointer) | смещение чанка хранилища объектов повторно используется как база/размер чанка → **OOB чтение** при разборе специально созданного `.idx` / multi-pack-index / commit-graph (общий репозиторий / бэкенд форджа) |
| **SQLite** | [`btreeComputeFreeSpace`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-sqlite-version-3.53.2.md#candidate-1--btreecomputefreespace-freeblock-offset-pc-→-data-index) | смещение freeblock B-дерева повторно используется как индекс страницы → **OOB чтение `mmap`'нутой страницы базы данных** |
| **FreeType** | [`ft_var_readpackedpoints`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-freetype-VER-2-14-3.md#candidate-1--ft_var_readpackedpoints-gvar-packed-point-count-n) | упакованное количество точек вариативного шрифта повторно используется → **OOB запись в куче** при рендеринге специально созданного шрифта (повсеместно: Android / Chrome / десктоп) |
| **libtiff** | [`NeXTDecode`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-libtiff-v4.7.1.md#candidate-1--nextdecode-literalspan-off--n-controlled-oob-write) | смещение/длина спана NeXT-RLE повторно используются → **OOB запись в куче** при декодировании специально созданного TIFF (режим чтения по умолчанию с `mmap`) |
| **binutils / ld** | [`sframe_decode`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-binutils-binutils-2_46_1.md#candidate-1--sframe_decode-sfh_num_fdes-fde-table-alloc-vs-fill) | количество FDE в SFrame повторно используется как размер аллокации **и** граница заполнения → **OOB запись в куче в линкере** при обработке специально созданного объектного файла |
| **ClamAV** | [`autoit` EA05 `csize`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-clamav-clamav-1.5.2.md#candidate-1--autoit-ea05-csize-alloc-vs-fill-heap-oob-write) | `csize` AutoIt повторно используется как размер аллокации **и** длина копирования → **OOB запись в куче** в сканере |
| **YARA** | [`pe_parse_exports`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-yara-v4.5.7.md#candidate-1--pe_parse_exports-number_of_exports-loop-bound) | количество экспортов PE повторно используется как граница цикла → **OOB чтение** при сканировании специально созданного сэмпла |
| **WAMR** | [`_vprintf_wa`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-wamr-WAMR-2.4.4.md#candidate-1--_vprintf_wa-s-handler-s_offset-string-address-rematerialization) | смещение гостевого `%s` перечитано за пределами арены песочницы → **OOB чтение, утекающее память хоста в wasm-гостя** |
| **ImageMagick** | [`ReadSUNImage`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-imagemagick-7.1.2-25.md#candidate-1--readsunimage-sun_infolength-alloc-vs-copy) | длина SUN-raster повторно используется как размер аллокации **и** длина копирования → **OOB запись в куче → RCE** при декодировании специально созданного изображения (LTO-сборки) |
| **FreeBSD** | [`virtqueue_dequeue`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-freebsd-drivers-release-15.0.0.md#candidate-1--virtqueue_dequeue-used-ring-desc_idx) | записанный хостом `id` used-ring virtio повторно используется как неограниченный индекс массива → **двойное освобождение дескриптора / UAF** в ядре |
Уязвимо всё. И не уязвимо ничего. В каждой ситуации исходник делает всё правильно: снимает недоверенный ввод, проверяет копию, использует копию. Но в каждом случае стандарт C тихо позволяет компилятору опционально *отменить* этот процесс и создать TOCTOU из ниоткуда. Является ли конкретное место эксплуатируемым — *не свойство исходника*: это решают компилятор, его версия, архитектура и флаги, и схлопывается это в ту или иную сторону только при сборке. До этого момента каждое из них является и тем и другим — уязвимостью, находящейся в суперпозиции и неотличимой на уровне исходного кода от действительно корректного кода. Каждое из них — шрёдингеровский TOCTOU, и таблица выше показывает, как они выглядят в масштабе.
Тревожит не то, что именно эти проекты небезупречны, — а то, что паттерн обнаруживается почти везде, куда смотрит анализ, вплетённый в [самый тщательно проверяемый код в мире](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-sel4-15.0.0.md#82-executive-summary) через не более чем идиоматический C. 100+ репозиториев — это **выборка, а не граница**: та же скрытая уязвимость почти наверняка дотягивается до вашей собственной кодовой базы.
Полный аудит и анализ влияния — в [observer-effect/](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect) и его [REPORT.md](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/REPORT.md).
## Решения
> *Их нет.*
Но вот кое-что, что мы всё же можем попробовать.
Рефлекторное решение — попытаться закрепить загрузку: [`volatile`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch), [`READ_ONCE`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch), [атомарная операция](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-2--atomic--acquire-load), [`barrier()` с клоббером `"memory"`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier). Они корректны с точки зрения спецификации и переживают `-O3`, LTO и инлайнинг; там, где *[обнаружено](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-kvm-host-sev-snp.md#candidate-1--snp_begin_psc-idx_end-loop-bound)* голое чтение, они — [правильный патч](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/README.md#confirmed-in-the-wild). К сожалению, они залатывают рану, но не причину:
- **`volatile` тихо вымывается.** Он [квалифицирует *доступ через lvalue*](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#the-volatile-cat-state), а не объект, указатель или область памяти. Чтение `volatile T *p` через обычное lvalue [даёт нулевую защиту](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/volatile_lvalue_launder.c), а квалификатор отбрасывается **без какой-либо диагностики**, когда он [проходит через `const void *` у `memcpy`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/volatile_memcpy_overlap.c), — [не существует `memcpy`, сохраняющего `volatile`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#false-friends--look-like-barriers-but-are-not). Барьер, который вы написали, испаряется на вызове, которого вы не писали.
- **`READ_ONCE` не масштабируется.** «Используйте `READ_ONCE`» на самом деле означает: аннотируйте *каждый* доступный атакующему доступ к [*каждому* полю](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline), всегда, и [размещайте барьер между чтением и *всеми* его использованиями](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier). [Пропустите одно](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-v7.0-io_uring.md#candidate-1--nvme_uring_cmd_io-nsid) — и дисциплина теряет силу. Это [нельзя обеспечить в масштабе](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch), и оно тихо регрессирует.
- **Корректность `barrier()` живёт на фреймах дальше от строки исходника.** Решение о том, несёт ли вообще один `copy_from_user(&local, uptr, n)` клоббер `"memory"`, означает [трассировку пяти инлайновых слоёв и одного out-of-line вызова](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/README.md#analysis) из обобщённого C в архитектурно-специфичный asm, с разбором целой горсти развилок `CONFIG`/CPU-фич/`__builtin`. И даже когда он найден, клоббер [не называет ни одного чтения](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier): шаг не туда — и он [ничего не закрепляет](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier); шаг в другую сторону — и он [*вынуждает* ту самую перезагрузку, которую должен остановить](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#principles--shared-facts-the-cards-lean-on).
Но что ещё важнее: исходник вообще никогда не запрашивает перезагрузку. Это более глубокая проблема. Программист написал `local.len` и *имел в виду* `local.len`: одно значение, читаемое один раз. Если мы говорим `x`, мы имеем в виду `x`, а не «`x`, но `y`, если компилятору больше нравится так». Перезагрузка изобретается под абстрактной машиной, поэтому код, которому нужна аннотация, [выглядит идентично коду, которому она не нужна](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/alpha-lab/README.md#same-source-different-outcome), — [на месте нет никакого сигнала, что требуется барьер](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md#proposed-levers-do-not-exist-in-usable-form-today). Нельзя вспомнить, что нужно защитить чтение, которое вы никогда не писали.
Полный каталог средств защиты — с их [сильными сторонами](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-linux-binder-v7.0.md#executive-summary) и [неудачами](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/audits/audit-libspdm-3.8.2.md#durability-assessment) — находится в [отчёте о барьерах](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/BARRIERS.md).
## Откройте ящик
> *Паттерн TOCTOU-из-ниоткуда повсюду. Проверьте, есть ли он в вашем коде.*
Проверьте свой код с помощью [`observer-effect/AUDIT-PROMPT.md`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/observer-effect/AUDIT-PROMPT.md): он найдёт границы доверия, поищет шрёдингеровский паттерн, отсеет варианты на основе согласованных со спецификацией барьеров и оценит вероятность/влияние/риск. Передайте его своему предпочитаемому кодинг-агенту с исходником в контексте и наведите его на подсистему:```sh
cd ~/your-project # the codebase you want audited
claude -p "$(cat path/to/observer-effect/AUDIT-PROMPT.md)
Audit drivers/net/ for invented-load TOCTOUs."
Этот инструмент ни от чего в этом репозитории не зависит — скопируйте один файл и работайте.
Schrödinger's TOCTOU разбирает один конкретный случай некой случайной
оптимизации, допускаемой 500-страничной C-спецификацией. Но это лишь верхушка
айсберга: впереди огромное поле для исследований. Этот репозиторий будет
продолжать тыкать, захватывать и каталогизировать неожиданные способы, которыми ваш любимый
компилятор подставляет вас — молча, легально и на каждом уровне оптимизации.
«...если бы gcc так делал, значительная часть ядра полетела бы в тартарары».
— Paul E. McKenney, LKML, 2009-04-16 · lore
«Люди любят говорить о "безопасном C", но разработчики компиляторов десятилетиями активно пытались сделать C более небезопасным. Комитет по стандартам C был в этом соучастником».
— Линус Торвальдс, 2025-02-21 · lore
«Я бы предпочёл переключатель компилятора, который велит компилятору не делать подобных идиотских вещей, вместо того чтобы помечать каждую вторую операцию чтения/записи в ядре как volatile».
— Peter Zijlstra, 2015-06-17 · lore
«Спецификация — это просто туалетная бумага. Единственное, что имеет значение, — это то, что делает реальное железо».
— Линус Торвальдс, 2006-12-04 · lore
«При такой аргументации нам нужно облепить половину ядра _ONCE()… Может, уже пора наконец топнуть ногой и сказать разработчикам компиляторов и членам комитета по стандартам: прекратите это безумие?
— Thomas Gleixner, 2019-08-16 · lore
«Компиляторы, которые "оптимизируют" так, что трогают поля, не затрагиваемые исходным кодом, — это просто по своей сути багованое дерьмо. Мне совершенно не интересно потакать их безумию... Утверждение, что их нужно помечать volatile, — симптом больного разработчика компиляторов».
— Линус Торвальдс, 2014-12-04 · lore
«Безумие? Возможно. Но есть парни из компиляторной сферы, которые на этом клянутся».
— Paul E. McKenney, LKML, 2008-02-04 · lore
«Хорошо, если они протестировали все пути выполнения кода, но они неизменно тестировались с компилятором, который не старается специально генерировать "легальный, но идиотский" код. Поэтому тестирование, как правило, не выявит случаи, когда компилятору, возможно, было разрешено сделать что-то иное. ... Люди, занимающиеся компиляторами, которые этого не понимают, — не люди-компиляторщики. Они — академики, предающиеся умственной мастурбации».
— Линус Торвальдс, LKML, 2007-01-04 · lore
«Конечно, меня беспокоят не тупые компиляторы, а скорее умные...»
— Paul E. McKenney, LKML, 2013-10-09 · lore
«... у нас были разработчики компиляторов, которые говорят: "если вы читаете спецификации, это нормально". Нет, это не нормально. Потому что реальность важнее любого выкручивания из чтения спецификаций».
— Линус Торвальдс, LKML, 2019-08-16 · lore
«... определение "вменяемого компилятора" становится всё более размытым».
— Paul E. McKenney, LKML, 2013-09-24 · lore
Schrödinger's TOCTOU — исследовательский проект Кристофера Домаса (@xoreaxeaxeax)
