Design owned temporary output and durable publication for encrypted attachments

This commit is contained in:
evgeny committed 2026-10-24 14:34:00 +02:00
1 parent 8cc97d2ffe
commit 2e3ab00ce6
2 files changed
+128 -20

No files matched your search

+123 -18
View File
@@ -29,25 +29,130 @@ source/destination. Разные строки могут обозначать о
- /tmp/media-encrypted-alias-lvVS77/ — сохранённые входы и результаты опыта.
Проектные .c/.h для воспроизведения не менялись.
Рекомендуемое исправление
Архитектурно уточнённый фикс — проект, код ещё не изменён
1. У обеих функций явно закрепить контракт: source остаётся неизменным;
destination — отдельный временный файл вызывающего, который допустимо удалить при ошибке.
Файлы/каталоги не должны подменяться во время вызова; это тот же контракт стабильности,
что у существующих file_io-примитивов.
2. До открытия назначения с усечением проверить идентичность через общий file_io_paths.
Результат1 (один файл) здесь означает ошибку. Отдельно проверять успешное открытие
источника до создания назначения. Использовать общие UTF-8/regular-file примитивы,
не дублировать POSIX/Windows сравнение идентичности в encrypted_file.
3. Ранняя ошибка не должна изменять source, удалять его alias или менять существующее
назначение. Сообщение об отказе должно назвать операцию/пути и причину до усечения.
4. Сохранить нынешнее владение: chat_attachment/dm_media публикуют результат после work.
Внутри encrypted_file не нужен второй независимый слой публикации или новый async owner.
5. Добавить регрессии write/read: совпадающая строка, /./, hardlink, конечная ссылка;
при отказе проверять неизменность файлов и отсутствие удаления alias. Проверить
отсутствующий источник и размеры0/1/32768/32769, обычные roundtrip и отмену pipeline.
6. Прогнать профильные тесты, ASAN/UBSan и полный набор; затем продолжить аудит
остальных контрактов обоих каталогов. Формат контейнера и nonce менять не требуется.
Одной проверки file_io_paths перед fopen(wb) недостаточно для полного исправления
контракта владения. Сейчас encrypted_file удаляет destination при ошибке, а
ak_file_done повторно удаляет заранее вычисленный .part даже при раннем отказе.
Нужно убрать открытие произвольного назначения с усечением и очистку по одному имени.
Граница модулей
- lib/file_io: UTF-8/regular-file проверки, идентичность, эксклюзивный temp,
flush/sync/close, atomic rename, владение temp и защита от orphan cleanup.
- encrypted_file: создание/проверка контейнера в собственном temp; без публикации
конечного имени, SQLite и состояния async-задачи.
- chat_attachment: задача владеет temp, выбирает конечное имя и выполняет одну
публикацию; completion меняет состояние БД/UI и освобождает только свои ресурсы.
Подготовка, sync и rename остаются в worker, как сейчас; новые файловые I/O в uasync не добавляются.
Предлагаемый контракт write/read
1. Функция получает source, конечный target_path и указатель на обнулённый file_io_temp.
target_path задаёт место будущего результата; он не открывается с усечением.
До начала работы file_io_paths(source,target_path) обязан вернуть 0.
Совпадение строки, /./, hardlink и конечная ссылка назначения отклоняются без изменений.
2. Источник открывается общим file_io_open_read. Только после успешного открытия
создаётся уникальный temp рядом с target_path через file_io_temp_open.
На POSIX он имеет права 0600; создание эксклюзивное, владелец сразу учтён в общем реестре.
3. Контейнер пишется/проверяется через уже открытый FILE этого temp. Нет второго .part,
второго открытия выхода по пути или дополнительного слоя публикации внутри codec.
4. Полностью проверяются данные, закрытие входа и durable завершение выхода.
При ошибке удаляется только созданный этой операцией temp; существующие source/target сохраняются.
При успехе возвращается закрытый, синхронизированный temp, который ещё не опубликован.
Результат info/type у read присваивается только после полного успешного завершения подготовки.
5. Владелец file_io_temp размещается прямо в ak_file и имеет стабильный адрес до done;
копировать его структуру нельзя: реестр хранит адрес владельца.
Функции не принимают уже активный output и не уничтожают его при отказе аргументов.
Единая политика durability
В file_io_temp_open добавить flags, в file_io_temp хранить выбранный режим на весь
срок операции. FILE_IO_DURABLE включает обе части: finish и publish.
Обычные пользователи передают 0 и сохраняют свою семантику; отдельные флаги в каждом
finish/publish не нужны. Это предотвращает случайное сохранение файла без сохранения имени.
Durable finish: fflush -> fsync/_commit того же открытого fd -> fclose. Ошибка любой
фазы сохраняет первый код ошибки; close выполняется и при отказе sync. Открывать путь
заново ради fsync не нужно. После отказа temp остаётся у владельца до discard.
Publish повторно проверяет идентичность source/target и принимает только закрытый temp.
На POSIX: rename и fsync каталога назначения; соседний temp находится в том же каталоге.
На Windows: общий wide API и MOVEFILE_WRITE_THROUGH. Родительский каталог вычисляется
без обрезания: имя без '/' означает '.', '/file' означает '/'.
Политика sync принадлежит file_io, а не формату зашифрованного контейнера.
Точка фиксации — успешный rename. До неё ошибка сохраняет старый target.
После rename ошибка fsync каталога означает: результат уже установлен, долговечность
имени не подтверждена. Нельзя обещать откат, удалять новый файл вслепую или восстанавливать
старое содержимое. В этот момент temp уже отсоединён от реестра и его имя в owner пусто.
Этот переход должен быть явно описан в file_io.h и проверен тестом.
Длительный fsync выполняется после снятия блокировки реестра временных файлов.
Владение задачи и отмена
ak_file хранит file_io_temp вместо temporary[...]. Worker после успешной подготовки
один раз вызывает file_io_temp_publish с тем же target и источником.
До publish поле temporary непустое; после успешного rename оно пустое даже при
ошибке directory sync. По этому переходу задача фиксирует факт своей публикации.
Completion всегда может вызвать discard: после rename он ничего не удаляет.
Cleanup при отмене/удалении канала учитывает отдельно действительно созданные/
заменённые артефакты. Вычисленный путь не является доказательством владения файлом.
При отказе до создания выхода нельзя удалять существующий target или его alias.
Отдельных async owners, refcount и фоновых потоков не требуется. Ключ по-прежнему сохраняется
до шифрования; ready получателя/публикация сообщения подтверждаются только после
успешного worker и проверки актуальности канала в completion. Авторская строка ключа
сохраняется до worker по прежним правилам. Общей транзакции ФС/SQLite нет: после rename
путь остаётся известен attachment_keys для последующей очистки/повторной операции.
Кеш и восстановление после crash
Активный temp защищён существующим file_io-реестром, в том числе после finish.
В chat_msg убрать специальное сохранение вычисленного cipher_path.part; конечный
cipher_path по-прежнему защищается attachment_keys, временный файл — своим owner.
После crash реестра нет: такие temp допустимо удалить.
Нельзя ограничиться нынешним orphan walk дерева media: plaintext-temp находится
в decrypted, а ak_recover_preparations знает только фиксированный .part.
В init/очистке после завершения workers нужно удалять оставшиеся временные файлы
строгого собственного формата <UUID>.uenc.tmp.<32hex> / <UUID>.file.tmp.<32hex>
в управляемых каталогах media/decrypted. Использовать file_io_remove_orphan,
чтобы сохранить активные temp другого instance того же процесса. Конечные файлы,
ключи и данные иных форматов такой проход не удаляет; ошибка обхода логируется.
Область изменений
Основные файлы: lib/file_io.c/h, media_async/encrypted_file.c/h,
chat/chat_attachment.c, chat/chat_msg.c, test_encrypted_file/test_media_primitives
и сетевые проверки зашифрованных вложений. Остальные вызовы file_io_temp_open
в media_async/media_delivery/video переводятся на явные flags=0.
DM не вызывает encrypted_file_write/read: он использует media_crypto streams
и общие sync/publish helper-функции из encrypted_file.h. При правке общего слоя
необходимо сохранить их durable контракт; перенос общих операций в file_io должен
быть прямым, без совместимых обёрток и без изменения DM state machine.
Формат контейнера, nonce, протокол ключей и схема БД для этого фикса не меняются.
Регрессии и критерии готовности
- Обе операции: одинаковая строка, /./, hardlink, symlink, отсутствующий/необычный source;
source и прежний target не меняются, alias не удаляется, чужой temp не освобождается.
- Обычные файлы размером 0/1/32767/32768/32769 байт, большой файл, UTF-8 пути;
успешная подготовка не меняет target до publish. Read не публикует info/type при ошибке.
- Внедрение ошибок open/fdopen/read/write/seek/flush/fsync/input-close/output-close/
rename/directory-sync/RNG; отсутствие утечек fd/памяти/регистраций temp.
- Подмена target на hardlink между подготовкой и publish: повторная проверка
отклоняет публикацию, исходник сохраняется, удаляется только собственный temp.
- Отказ directory sync после rename: новый файл цел, temp-owner отсоединён,
ошибка не представляется как откат; повторная операция корректна.
- Orphan cleanup во время work, после finish, после publish; crash/restart
в обоих деревьях, чужие имена сохраняются. Отмена, stop и удаление канала
до work, после подготовки и после rename: один done, нет публикации сообщения/ready.
- Профильные 20 тестов, ASAN/UBSan/LSan и полный набор. Изменение file_io требует
также проверки его прежних пользователей: copy, video и media blocks/download.
Это архитектурный проект. Уточнённый фикс ещё не реализован и не тестировался;
успешные 20 тестов ниже относятся к текущей реализации до исправления.
Что проверено и что осталось
+5 -2
View File
@@ -8,8 +8,11 @@
write/read отвергают только одинаковые строки; /./ и hardlink обходят проверку одного файла.
write возвращает0, заменяя исходные30 байт контейнером пустого файла; read возвращает0,
заменяя входной ciphertext182 байта plaintext30 байт. Проверены реальные функции, inode и SHA256.
До fopen(wb) проверять идентичность общими file_io-примитивами; один файл — ошибка без изменения/удаления.
Сохранить destination как временный файл вызывающего, публикацию оставить chat_attachment/dm_media.
Архитектурный фикс: один file_io_temp в ak_file; codec готовит закрытый проверенный temp,
worker публикует его одним rename. Target не открывается с усечением; alias проверяется до work и перед publish.
Durability задаётся на весь срок temp; ошибка directory sync после rename не считается откатом.
Cleanup удаляет только собственные артефакты; crash recovery учитывает temp в media и decrypted.
DM не вызывает codec: сохранить его контракт общих sync/publish helpers при изменениях file_io.
Детали, проверка архитектуры исправления и незавершённый аудит: [media_async_audit_findings.txt](media_async_audit_findings.txt).
Воспроизведение: /tmp/media-encrypted-alias-repro.c, /tmp/media-encrypted-alias-repro.log.
Linux-сборка и20/20 профильных тестов PASS; штатный test_encrypted_file не проверяет alias.