Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
schrodingers-toctou — Обнаружение создаваемых компилятором загрузок из памяти, которые превращают безопасный код на C в TOCTOU-уязвимости. Включает автоматизированный аудит исходного кода, бинарный анализ на основе Unicorn и прогоны по компиляторам/архитектурам/флагам в 100+ проектах. | Kitploit
Инструменты/GitHubGitHub/xoreaxeaxeax/schrodingers-toctou
Динамический анализ (песочница)Статический анализ кода (SAST)Анализ уязвимостейЭксплуатацияАнализ Бинарных ФайловОбучение и Образование
GitHubxoreaxeaxeax/schrodingers-toctou

schrodingers-toctou

Обнаружение создаваемых компилятором загрузок из памяти, которые превращают безопасный код на C в TOCTOU-уязвимости. Включает автоматизированный аудит исходного кода, бинарный анализ на основе Unicorn и прогоны по компиляторам/архитектурам/флагам в 100+ проектах.

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Репозиторий
944181 месяц назадЕщё не проверено
Поделиться

Schrödinger's TOCTOU

«...определение „вменяемого компилятора“ становится всё более расплывчатым.»

Бинарный файл, который вы запускаете, — это не программа, которую вы написали. Оптимизатор компилятора переписывает ваш исходный код так, что вы этого никогда не видите — и некоторые из этих изменений могут незаметно и легально превращать, казалось бы, безопасный код в уязвимые бинарные файлы. Та же самая строка может быть безопасной в одном компиляторе и эксплуатируемой в другом, при этом ничто в исходном коде не говорит вам, какой именно: уязвимость в суперпозиции, схлопывающаяся только при сборке. 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; }

root@kitploit:~
Подсказка: ответ — 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

root@kitploit:~
Классическое исправление — **сначала сделать снимок**: скопировать любые данные, с которыми атакующий может возиться, в локальную переменную, недоступную атакующему, и затем не доверять ничему, кроме этой локальной переменной. Как только `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]

root@kitploit:~
Проверка выполняется на ЧТЕНИИ №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

Ссылки

  • Whitepaper: (скоро)
  • Слайды: (скоро)
  • Презентация: (скоро)

Автор

Schrödinger's TOCTOU — исследовательский проект Кристофера Домаса (@xoreaxeaxeax)


Эксперимент


Скачать инструмент