Подготовка стабильного выпуска 1.0.0: семантика версий, Flowseal 1.10.2, обязательная подпись - #1
Closed
Jacksony100 wants to merge 13 commits into
Closed
Jacksony100 wants to merge 13 commits into
Jacksony100 wants to merge 13 commits into
Conversation
Phase 0 не меняет поведение: документ фиксирует базовый commit, чистую сборку, состав и результат полного набора тестов (33/33 CTest + 84/84 strategy fidelity), текущую версию upstream (1.10.1) и точную природу дефекта семантики версий RC/stable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Дефект: исходная версия была единственным числом 1.0.0, поэтому сборка по тегу v1.0.0-rc.3 сообщала о себе «1.0.0», и будущий стабильный 1.0.0 оценивался установленным RC как Current, а не Upgrade. Модель версии (cmake/Version.cmake) теперь единственный источник истины и хранит четыре поля: major/minor/patch и prerelease. Из них выводятся семантическая версия (1.0.0-rc.3), числовая PE-версия (1.0.0.3), признак предварительного выпуска и канал обновлений. Решения об обновлении принимаются только по семантической версии; PE-версия информационная. Сравнение версий не ослаблено, а уточнено: prerelease сравнивается по правилам precedence SemVer 2.0.0 (rc.2 < rc.10), ведущие нули и пустые идентификаторы отклоняются как псевдонимы, ревизия upstream (1.10.1a) теперь корректно считается новее базового выпуска, а не понижением. Канал перестал быть константой true: стабильная сборка по-прежнему отклоняет любые предварительные версии, RC-сборка может перейти на новый RC или на стабильный выпуск. Пороги совместимости в манифесте вынесены в параметр со значением 1.0.0-rc.1, иначе установленный 1.0.0-rc.N получал бы LauncherTooOld при переходе на стабильный 1.0.0. Шлюз выпуска: тег обязан точно совпадать с исходной версией и каналом, поэтому стабильный тег не может опубликовать RC-семантику и наоборот. Тесты: новый набор version_model (34-й тест CTest) и расширенные updateversion/updatemanifest/release_metadata. Порядок версий задан одной канонической таблицей tests/version-order-cases.json, которую независимо проверяют реализация на C++ и реализация на PowerShell. Проверено: чистая сборка без предупреждений, CTest 34/34, strategy fidelity 84/84. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Импорт выполнен штатным защищённым импортёром, файлы вручную не копировались. Provenance указывает на неизменяемый выпуск 1.10.2, archive_sha256=5eaac9fb...62d5, upstream_commit=dfd8e613...4dec. Состав: добавлена стратегия ALT13 (21 -> 22), изменена EXP, добавлен ресурс tls_clienthello_sochi_park.bin, ресурс quic_initial_4pda.to.bin переименован в quic_initial_4pda_to.bin, удалён quic_initial_dbankcloud_ru.bin, обновлены списки. Проверено, что удалённые и переименованные ресурсы не остались во встроенной нагрузке после чистой переконфигурации. Расширение грамматики строгого парсера не потребовалось: ALT13 использует уже поддерживаемый синтаксис. Попутно исправлен дефект самого импортёра: изолированное дерево проверки собиралось без docs и SECURITY.md, которых требует validate-licenses.ps1, поэтому полный импорт не мог завершиться успешно ни для какой версии upstream и падал на отсутствующем файле репозитория, а не на нагрузке. Список копирования дополнен, добавлен регрессионный тест importer_validation_tree: он извлекает список из исходника импортёра, собирает по нему дерево и прогоняет против него релизные шлюзы без сети. Проверено: чистая сборка без предупреждений, CTest 35/35, strategy fidelity 88/88 (22 стратегии x 4 режима), UPSTREAM_FIDELITY PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Стабильный тег vX.Y.Z больше не может быть опубликован без подписи. Требование выводится из авторитетной модели версии, а не из отдельного флага: канал prerelease сохраняет прежнее исключение, стабильный канал исключения не имеет. Политика вынесена в scripts/authenticode.ps1 чистой функцией и отклоняет всё, кроме статуса Valid: NotSigned, HashMismatch (файл изменён после подписи), недоверенную цепочку, просроченный сертификат и отсутствие метки времени, когда метка настроена. Промежуточного состояния «предупредить и продолжить» в политике нет. Подпись проверяется дважды и разными средствами: signtool сразу после подписи и независимо через API подписи Windows перед публикацией (scripts/verify-authenticode.ps1). Опубликованные байты привязаны к подписанным: SHA-256 подписанного файла фиксируется до упаковки, после упаковки требуется точное совпадение, а перед самой публикацией сверяются SHA-256 публикуемого файла, launcher.sha256 в подписанном манифесте и файл CHEBURNET.exe.sha256. Метаданные генерируются только после подписи, порядок закреплён регрессией по тексту workflow. Тесты: новый release_signing покрывает отсутствующий сертификат, недоверенный самоподписанный сертификат, файл, изменённый после подписи, непрошедшую итоговую проверку, отказ стабильного тега до сборки артефактов и сохранение исключения для prerelease. Положительный путь проверяется на реальной доверенной подписи Microsoft, потому что менять хранилища доверия машины релизная оснастка не вправе. Попутно: новый тест importer_validation_tree сразу поймал, что дерево проверки импортёра не содержит .github, который теперь читает security_regression; список копирования дополнен. Проверено: чистая сборка без предупреждений, CTest 36/36, strategy fidelity 88/88, реальный прогон prepare-release на RC-канале даёт корректные метаданные (launcher.version=1.0.0-rc.3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка при запуске выполнялась синхронно перед подключением, поэтому недоступный DNS, прокси или сеть выглядели как зависание программы и задерживали подключение на время тайм-аутов. Добавлен UpdateCheckService: одна фоновая проверка, результат забирается из цикла главного меню, который и так опрашивает клавиатуру каждые 120-250 мс. Подключение больше не ждёт сетевого ввода-вывода вообще. Модель доверия не изменена: фоновый поток вызывает тот же UpdateManager::CheckNow, то есть независимое получение манифеста и отдельной подписи, проверка точных байтов и оценка версий совпадают с синхронным путём. CheckNow получил только необязательный флаг отмены, который передаётся в WinHTTP и влияет исключительно на транспорт. Безопасность времени жизни: поток никогда не отсоединяется, Cancel() идемпотентен и присоединяет его, деструктор вызывает Cancel(), сервис объявлен после ui_/pm_ и потому разрушается раньше них. Мьютекс не удерживается ни во время сетевого ввода-вывода, ни во время join. Фоновый код ничего не рисует и не трогает интерфейс. Исключение в проверке вырождается в нефатальный результат «нет сети». Явная проверка из меню сначала отменяет и присоединяет фоновую, чтобы два запроса не конкурировали за кэш проверенного манифеста. Тесты: updatecheckservice покрывает успех, офлайн, отмену, разрушение во время незавершённой проверки, результат, пришедший после смены экрана, исключение в проверке и повторный запуск. Тест детерминирован — фиктивная проверка ждёт флаг, а не спит; 40 прогонов подряд без сбоев. Проверено: чистая сборка без предупреждений, CTest 37/37, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запланированный процесс раньше печатал результат проверки в журнал запуска и не оставлял ничего, с чем можно работать. Теперь он состоит из трёх заданий с раздельными правами. detect (contents: read) сравнивает встроенную нагрузку с последним неизменяемым стабильным выпуском и выдаёт машиночитаемый отчёт. validate (contents: read) выполняет полный защищённый импорт в одноразовой рабочей копии, собирает, прогоняет весь набор тестов и проверяет побайтовую достоверность; различия и отчёт уходят в артефакты запуска. task (issues: write) создаёт либо обновляет ровно одну задачу на версию. Задача содержит старую и новую версию, ссылку на выпуск, SHA-256 и размер архива, commit upstream, число стратегий до и после, списки добавленных, удалённых и изменённых стратегий и файлов нагрузки, результат проверки, ссылку на запуск и команды для локального воспроизведения. Дедупликация идёт по скрытому маркеру cheburnet-upstream:<версия> в теле задачи, а не по заголовку: заголовок человек может отредактировать, маркер нет. Поиск охватывает задачи в любом состоянии, поэтому повторный запуск не создаёт дубликат. Выбран вариант с задачей, а не с черновым pull request, потому что ветка требует contents: write у запланированной автоматики, обрабатывающей содержимое стороннего выпуска. Импортёр детерминирован и сверяет архив с неизменяемым digest релизного ассета, поэтому локальный запуск воспроизводит ровно то, что проверил CI, и право записи не добавляет гарантий. Решение и требования для перехода на PR зафиксированы в docs/production/UPSTREAM_AUTOMATION.md. Дельта нагрузки вычисляется из фактического вывода git status (scripts/upstream-delta.ps1) и вынесена в отдельную библиотеку, чтобы разбор переименований и закавыченных путей проверялся офлайн. Тесты: новый upstream_automation покрывает разбор дельты, обязательные поля отчёта, отказ от черновых/предварительных/изменяемых выпусков, отсутствие contents: write и pull-requests: write, наличие issues: write, запуск защищённого импортёра, дедупликацию по маркеру, отсутствие автослияния и закрепление всех сторонних actions по commit SHA. SHA для actions/download-artifact сверен с GitHub API, а не подобран. Проверено: чистая сборка без предупреждений, CTest 38/38, YAML всех процессов разбирается. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Модульные тесты проверяют логику, но не проверяют, что на чистой машине создаётся защищённое дерево с нужными ACL, распаковывается рабочая среда, прерванное обновление не повышается автоматически и повреждённая среда откатывается. Оснастка scripts/e2e/Invoke-CheburnetE2E.ps1 использует то, что вся работа запуска выполняется в конструкторе App до появления интерфейса: сценарии управляются запуском настоящего исполняемого файла и проверкой наблюдаемого состояния, а не имитацией нажатий. Пакетный движок не запускается и драйвер WinDivert не загружается ни в одном сценарии. Сценарии: отказ запуска без прав администратора, первая установка, повторный запуск с существующей конфигурацией, прерванное обновление, откат при повреждённой активной среде и fail-closed при подложенном junction в защищённом каталоге журналов. Каждый сценарий привязан к критерию приёмки. Привилегированные сценарии изменяют машину, поэтому оснастка отказывается работать, если машина не помечена файлом-маркером как одноразовая, если сессия не элевирована или если уже запущен чужой winws.exe. Самопроверка -SelfTest не требует прав, ничего не пишет вне временного каталога и входит в CTest как e2e_harness: проверяет состав сценариев, их привязку к критериям, отказ защиты окружения и схему доказательств. scripts/e2e/New-CheburnetTestVm.ps1 воспроизводимо создаёт VM Hyper-V поколения 2 с Secure Boot и виртуальным TPM, подготовительный диск с артефактами выпуска и скрипт запуска в госте, сверяющий SHA-256 лончера с опубликованным. Выбор редакции Windows и сама установка остаются ручными осознанно: универсальный unattend.xml нельзя зафиксировать, а угадывание молча дало бы не то окружение. На машине разработки сценарий отказа запуска без прав выполнен и пройден; остальные намеренно пропущены защитой окружения. Проверка на чистых VM Windows 10 и Windows 11 не выполнена и зафиксирована как явный блокер стабильного выпуска в docs/production/E2E_RELEASE_GATE.md. Проверено: чистая сборка без предупреждений, CTest 39/39. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Статический анализ MSVC включается опцией CHEBURNET_ENABLE_ANALYZE и работает с /WX, поэтому новая находка останавливает сборку. Все находки первого прогона разобраны по существу, а не подавлены: C6387 в ProcessManager: размер списка атрибутов процесса брался из вызова определения размера без проверки. При неожиданном отказе размер остался бы нулевым, выделение — нулевой длины, а последующие вызовы писали бы через указатель без хранилища, причём в привилегированном пути создания дочернего процесса. Добавлена проверка диапазона и указателя, путь fail-closed. C6262 в WinHttpClient: буфер передачи 64 КиБ лежал в автоматической памяти одного кадра, а эта функция теперь исполняется ещё и в фоновом потоке проверки обновлений. Буфер перенесён в динамическую память. C6326 в Launcher и UiScreens: непустота встроенного resource manifest проверялась во время исполнения, хотя это свойство генерации. Проверка вынесена в static_assert, экран состояния показывает фактическое число записей. Подавлено ровно одно предупреждение — C28285, дефект SAL-аннотации CryptStringToBinaryA в самом wincrypt.h Windows SDK. Обоснование зафиксировано рядом с подавлением. Добавлен процесс CI static-analysis: задание MSVC /analyze и задание CodeQL (c-cpp, security-and-quality) с security-events: write только у него. SHA обоих сторонних actions сверены с GitHub API. Фаззинг: тест fuzzparsers мутирует корректные входные данные детерминированным генератором с фиксированным зерном и проверяет, что принятое значение удовлетворяет инвариантам разборщика, а не только что разборщик не упал. Цели: разбор версии, манифест обновления, строгий JSON, путь внутри пакета, имя файла в защищённой области. Зерно и число итераций переопределяются переменными окружения для длинных кампаний; выполнено 500 000 итераций на зерне по умолчанию и по 50 000 на трёх других зёрнах без находок. Фаззер нашёл ошибку в предикате самого теста: обходом пути считалось ".." в любом месте строки, тогда как обходом он является только как целый компонент. Предикат исправлен, добавлены явные враждебные формы путей. Внесение отказов: тест faultinjection проходит полную решётку точек отказа транзакционной активации и проверяет семь инвариантов, включая то, что кандидат становится текущим только при полной фиксации, что процесс не останавливается до одобрения предварительной проверкой и что откат не запускает кандидата повторно. Проверено: чистая сборка без предупреждений, сборка с /analyze без находок, CTest 41/41, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт меню «Экспорт диагностики» создаёт локальный архив CHEBURNET-diagnostics-ГГГГММДД-ЧЧММСС.zip в каталоге журналов. Перед записью показывается полный состав архива и точный путь, затем требуется отдельное подтверждение. Отправки нет: в коде экспорта отсутствуют сетевые вызовы, и это закреплено регрессионной проверкой по именам сетевых API. В архив входят manifest.json с описанием самого состава, summary.json с версиями, происхождением движка, состоянием рабочей среды и целостности, настройками, сборкой Windows, вердиктом подписи лончера и ожидаемыми SHA-256 файлов CHEBURNET, а также ограниченный хвост журнала. Редактирование скрывает имя пользователя, имя компьютера, путь профиля, любой путь внутри \Users\, адреса IPv4 и IPv6 и строки, похожие на токены. Хеши SHA-256 и SHA-1 сохраняются намеренно: без них диагностика теряет смысл, а о пользователе они ничего не сообщают. ZIP пишется без сжатия собственным кодом: сторонняя библиотека ради одной операции не вводится, формат детерминирован. Файл создаётся с CREATE_NEW, поэтому существующий архив никогда не перезаписывается молча, при ошибке записи частичный файл удаляется, а имена записей проверяются до записи. Тест diagnostics проверяет редактирование (включая регистронезависимость и идемпотентность), сохранение версий и хешей, CRC-32 по каноническому вектору, структуру архива, отказ перезаписи, отказ на враждебных именах записей и работу с путями, содержащими пробелы и кириллицу. Найдено при разработке: символ '=' в наборе символов токена склеивал пару "ключ=значение" в один токен, из-за чего редактировался и сам SHA-256. Исправлено: дополнение base64 обрабатывается только в хвосте прогона. Статический анализ на новом модуле выдал C28020 на std::array::operator[] с беззнаковым счётчиком; перебор заменён на range-for, индексирования больше нет. Сборка с /analyze снова чиста. Высота меню теперь выводится из фактического числа пунктов, а не из зашитой константы. Проверено: чистая сборка без предупреждений, сборка с /analyze без находок, CTest 42/42, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Самая критичная часть запуска — обработка прерванного обновления и реакция на нарушенную целостность активной среды — жила внутри конструктора App вперемешку с работой с процессами, файловой системой и ProgramData. Проверить её можно было только запуском всей программы с правами администратора, поэтому ни одна ветка не была покрыта тестом. Решения вынесены в src/app/RuntimeRecovery.* чистыми функциями от наблюдаемого состояния. Поведение не изменено: App по-прежнему выполняет остановку, откат и запись состояния, но сначала спрашивает, что именно нужно сделать. Тест runtimerecovery проходит всю таблицу переходов: отсутствие ожидающего состояния, недоверенная запись процесса, работающая ожидающая среда, работающая текущая среда, посторонняя среда, регистронезависимость сравнения путей и путь с общим префиксом. Отдельно проверено, что ожидающая версия никогда не повышается до текущей и что откат возможен только при записанной предыдущей рабочей версии. Рефакторинг намеренно узкий: экраны интерфейса и менеджер обновлений не трогались, чтобы не создавать риск перед стабильным выпуском. Проверено: чистая сборка без предупреждений, сборка с /analyze без находок, CTest 43/43, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Добавлена docs/production/FINAL_RELEASE_CERTIFICATION.md с фактическими результатами всех выполненных проверок, метаданными лончера и нагрузки и перечнем оставшихся блокеров с точным следующим действием по каждому. Статус выпуска: RELEASE_CANDIDATE_READY_WITH_BLOCKERS. Два обязательных шлюза не выполнены и не могут быть выполнены в этом окружении: проверка в чистом окружении Windows 10 и Windows 11 требует одноразовой машины и загрузки драйвера ядра, фактическая подпись требует сертификата подписи кода. Заявлять готовность при непроверенных обязательных шлюзах было бы неверно. Добавлена docs/architecture/PRIVILEGE_SEPARATION_V2.md — проектная записка о разделении привилегий. Только исследование, в состав 1.0 не входит: описаны целевая структура, узкий набор команд IPC без универсального выполнения, честная оценка того, что привилегированный разбор пакета разделение не убирает, цена решения и условия, при которых работа оправдана. CHANGELOG дополнен разделом подготовки стабильного выпуска, docs/CURRENT_BEHAVIOR.md переведён на 1.10.2, каталоги dist-* добавлены в .gitignore. Проверено: CTest 43/43, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/ARCHITECTURE.md: добавлены UpdateCheckService, RuntimeRecovery и Diagnostics, раздел о единственном источнике версии и ссылки на документы проверки выпуска. docs/SMOKE_TEST.md: ручной список теперь начинается с указания, что часть сценариев автоматизирована релизным шлюзом E2E, и дополнен проверкой экспорта диагностики — показ состава до записи, отсутствие в архиве имени пользователя, имени компьютера, путей профиля, адресов и токенов, отказ перезаписи существующего архива и отсутствие сетевого обращения. Нумерация пунктов приведена в порядок. Проверено: CTest 43/43, strategy fidelity 88/88, LICENSE_VALIDATION PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
| return L"CHEBURNET-diagnostics-" + LocalTimestamp() + L".zip"; | ||
| } | ||
|
|
||
| Bundle BuildBundle(const RuntimePaths& paths, const Config& config, |
Comment on lines
+1460
to
+1621
| static int test_fuzzparsers() { | ||
| g_fail = 0; | ||
| const int kIterations = static_cast<int>(FuzzEnv("CHEBURNET_FUZZ_ITERATIONS", 4000)); | ||
| const std::uint64_t fuzzSeed = FuzzEnv("CHEBURNET_FUZZ_SEED", 0xC4EB2026ull); | ||
| std::printf(" fuzz seed=%llu iterations=%d\n", static_cast<unsigned long long>(fuzzSeed), | ||
| kIterations); | ||
| Prng prng(fuzzSeed); | ||
|
|
||
| // ---- version parser ---------------------------------------------------- | ||
| for (int i = 0; i < kIterations; ++i) { | ||
| const std::string seed = kVersionSeeds[prng.Below(std::size(kVersionSeeds))]; | ||
| const std::string mutated = MutateBytes(seed, prng); | ||
| const auto parsed = update::ParseVersion(mutated); | ||
| if (!parsed) continue; | ||
| // Accepted: the invariants the comparator relies on must hold. | ||
| if (parsed->parts.empty() || parsed->parts.size() > 8) { | ||
| ++g_fail; | ||
| std::printf(" FAIL version invariant (parts) for '%s'\n", mutated.c_str()); | ||
| continue; | ||
| } | ||
| if (parsed->prerelease && parsed->prereleaseIds.empty()) { | ||
| ++g_fail; | ||
| std::printf(" FAIL version invariant (prerelease ids) for '%s'\n", mutated.c_str()); | ||
| continue; | ||
| } | ||
| if (parsed->revision.size() > 1) { | ||
| ++g_fail; | ||
| std::printf(" FAIL version invariant (revision) for '%s'\n", mutated.c_str()); | ||
| continue; | ||
| } | ||
| // Comparison must be a strict, antisymmetric, reflexive order. | ||
| if (update::CompareVersions(*parsed, *parsed) != 0) { | ||
| ++g_fail; | ||
| std::printf(" FAIL version not reflexive for '%s'\n", mutated.c_str()); | ||
| continue; | ||
| } | ||
| const auto other = update::ParseVersion(kVersionSeeds[prng.Below(std::size(kVersionSeeds))]); | ||
| if (other) { | ||
| const int forward = update::CompareVersions(*parsed, *other); | ||
| const int reverse = update::CompareVersions(*other, *parsed); | ||
| if ((forward == 0) != (reverse == 0) || (forward < 0) != (reverse > 0)) { | ||
| ++g_fail; | ||
| std::printf(" FAIL version not antisymmetric for '%s'\n", mutated.c_str()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ---- update manifest --------------------------------------------------- | ||
| const std::string manifestSeed = ValidManifest(); | ||
| for (int i = 0; i < kIterations; ++i) { | ||
| const std::string mutated = MutateBytes(manifestSeed, prng); | ||
| const update::ManifestResult parsed = update::ParseManifest(mutated); | ||
| if (!parsed.ok) continue; | ||
| const update::Manifest& manifest = parsed.manifest; | ||
| // Accepted manifests must still satisfy every contract the updater | ||
| // relies on before it will touch the network or the filesystem. | ||
| const bool contract = | ||
| manifest.schema == 1 && manifest.channel == "stable" && | ||
| manifest.launcher.sha256.size() == 64 && manifest.payload.sha256.size() == 64 && | ||
| manifest.launcher.size > 0 && manifest.payload.size > 0 && | ||
| update::IsHttpsUrl(manifest.launcher.url) && | ||
| update::IsHttpsUrl(manifest.payload.url) && | ||
| update::IsHttpsUrl(manifest.payload.upstreamReleaseUrl) && | ||
| manifest.payload.provider == "Flowseal/zapret-discord-youtube" && | ||
| update::ParseVersion(manifest.launcher.version).has_value() && | ||
| update::ParseVersion(manifest.payload.version).has_value() && | ||
| !manifest.keyId.empty(); | ||
| if (!contract) { | ||
| ++g_fail; | ||
| std::printf(" FAIL manifest accepted while violating its contract (iteration %d)\n", i); | ||
| } | ||
| } | ||
|
|
||
| // ---- strict JSON ------------------------------------------------------- | ||
| const std::string jsonSeed = | ||
| "{\"a\":1,\"b\":[true,false,null,1.5e3],\"c\":{\"d\":\"\\u0416\"}}"; | ||
| for (int i = 0; i < kIterations; ++i) { | ||
| const std::string mutated = MutateBytes(jsonSeed, prng); | ||
| json::ParseOptions options; | ||
| options.maxBytes = 4096; | ||
| options.maxDepth = 8; | ||
| options.maxValues = 128; | ||
| const json::ParseResult parsed = json::Parse(mutated, options); | ||
| if (parsed.ok && mutated.size() > options.maxBytes) { | ||
| ++g_fail; | ||
| std::printf(" FAIL json accepted an oversized document (iteration %d)\n", i); | ||
| } | ||
| } | ||
|
|
||
| // ---- package path allowlist ------------------------------------------- | ||
| for (int i = 0; i < kIterations; ++i) { | ||
| const std::string seed = kPackagePathSeeds[prng.Below(std::size(kPackagePathSeeds))]; | ||
| const std::string mutated = MutateBytes(seed, prng); | ||
| std::string normalized; | ||
| if (!update::NormalizePackagePath(mutated, normalized)) continue; | ||
| // Accepted: the normalized path must be inside the exact allowlist, | ||
| // relative and free of traversal. Note that ".." only means traversal | ||
| // as a COMPLETE component: "WinDivert64.s..ys" is an ordinary name. | ||
| const bool allowed = normalized.rfind("bin/", 0) == 0 || | ||
| normalized.rfind("lists/", 0) == 0 || | ||
| normalized == "strategies/catalog.json" || | ||
| normalized == "provenance.json" || | ||
| normalized == "runtime-manifest.json"; | ||
| bool traversal = false; | ||
| std::size_t at = 0; | ||
| while (at <= normalized.size()) { | ||
| const std::size_t slash = normalized.find('/', at); | ||
| const std::string_view component = std::string_view(normalized).substr( | ||
| at, slash == std::string::npos ? std::string_view::npos : slash - at); | ||
| if (component.empty() || component == "." || component == "..") traversal = true; | ||
| if (slash == std::string::npos) break; | ||
| at = slash + 1; | ||
| } | ||
| const bool safe = allowed && !traversal && | ||
| normalized.find('\\') == std::string::npos && | ||
| normalized.find(':') == std::string::npos && | ||
| normalized.find('\0') == std::string::npos && | ||
| !normalized.empty() && normalized.front() != '/'; | ||
| if (!safe) { | ||
| ++g_fail; | ||
| std::printf(" FAIL package path accepted unsafe normalization: '%s'\n", | ||
| normalized.c_str()); | ||
| } | ||
| } | ||
|
|
||
| // Explicit traversal and device-name forms must always be rejected. | ||
| for (const char* hostile : {"../bin/winws.exe", "bin/../../evil", "bin/./winws.exe", | ||
| "/bin/winws.exe", "bin\\winws.exe", "C:/bin/winws.exe", | ||
| "bin/", "", "bin/con.exe", "bin/winws.exe ", "bin/winws.exe.", | ||
| "bin/..", "bin/nul", "lists/../../../etc/passwd"}) { | ||
| std::string normalized; | ||
| if (update::NormalizePackagePath(hostile, normalized)) { | ||
| ++g_fail; | ||
| std::printf(" FAIL package path accepted a hostile input: '%s'\n", hostile); | ||
| } | ||
| } | ||
|
|
||
| // ---- staging file names ------------------------------------------------ | ||
| const wchar_t* const stagingSeeds[] = {L"CHEBURNET-new.exe", L"engine-1.10.2.cbpkg"}; | ||
| for (int i = 0; i < kIterations; ++i) { | ||
| std::wstring seed = stagingSeeds[prng.Below(std::size(stagingSeeds))]; | ||
| switch (prng.Below(5)) { | ||
| case 0: seed[prng.Below(seed.size())] = static_cast<wchar_t>(prng.Next() & 0xFFFF); break; | ||
| case 1: seed.insert(prng.Below(seed.size()), 1, L'\\'); break; | ||
| case 2: seed.insert(prng.Below(seed.size()), 1, L'.'); break; | ||
| case 3: seed.resize(prng.Below(seed.size() + 1)); break; | ||
| case 4: seed.insert(prng.Below(seed.size() + 1), 1, L' '); break; | ||
| default: break; | ||
| } | ||
| if (!update::IsSafeStagingFileName(seed)) continue; | ||
| const bool safe = !seed.empty() && seed.size() <= 128 && seed.front() != L'.' && | ||
| seed.back() != L'.' && seed.back() != L' ' && | ||
| seed.find(L'\\') == std::wstring::npos && | ||
| seed.find(L'/') == std::wstring::npos && | ||
| seed.find(L':') == std::wstring::npos; | ||
| if (!safe) { | ||
| ++g_fail; | ||
| std::printf(" FAIL staging name accepted an unsafe value\n"); | ||
| } | ||
| } | ||
| return g_fail; | ||
| } |
Два отказа CI — обе ошибки мои. 1. Политика Authenticode отвергала просроченный сертификат даже при наличии контрподписи метки времени. Это неверно по существу: контрподпись доказывает, что файл был подписан, пока сертификат действовал, Windows проверяет цепочку на момент метки и сообщает об этом статусом Valid. Моя проверка отвергала бы собственные выпуски CHEBURNET в день истечения сертификата подписи, прямо противореча документу RELEASE_SIGNING.md, где метка названа средством сохранения проверяемости после истечения. Обнаружено в CI: системный бинарный файл Windows на раннере подписан сертификатом, истёкшим в 2024 году, но его подпись по-прежнему Valid благодаря метке времени. Теперь окно действия сертификата сверяется с текущим временем только для подписи БЕЗ метки времени, где нет доказательства момента подписания. Добавлены три регрессии: просроченный сертификат с меткой принимается, без метки отвергается, ещё не действующий без метки отвергается. 2. Кириллица внутри блоков run у шагов с shell: powershell. Раннер записывает блок во временный .ps1 без BOM, а Windows PowerShell 5.1 читает такой файл как ANSI, поэтому текст ломал разбор и шаг падал до запуска чего-либо. Это тот же класс ошибки, который уже был закрыт внутри файлов репозитория, но пропущен в workflow. Сообщения в inline-PowerShell переведены на ASCII в static-analysis.yml и upstream-check.yml. Блоки с shell: bash не тронуты: там UTF-8 читается корректно. Добавлен шлюз workflow_encoding: он разбирает все процессы, отличает шаги powershell от bash и запрещает не-ASCII только в первых. Проверено, что тест действительно падает на внесённом нарушении и что он не проходит вхолостую, если разбор шагов перестанет работать. Проверено: чистая сборка без предупреждений, CTest 44/44, strategy fidelity 88/88. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Изменения влиты в |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что это
Подготовка CHEBURNET к стабильному выпуску
1.0.0от состоянияv1.0.0-rc.3. Одиннадцать логических коммитов, ни один не смешивает несвязанные изменения. Модель доверия нигде не ослаблена.Статус:
RELEASE_CANDIDATE_READY_WITH_BLOCKERS. Два обязательных шлюза стабильного выпуска остаются невыполненными — см. раздел «Блокеры». Версия намеренно оставлена наrc.3.P0 — закрыты все три
Семантика версий RC/stable
Источник версии был один числовой литерал
project(CHEBURNET VERSION 1.0.0), поэтому сборка по тегуv1.0.0-rc.3сообщала о себе как1.0.0, и будущий стабильный1.0.0оценивался бы установленным RC какCurrent, а неUpgrade.Введён
cmake/Version.cmake— единственный авторитетный источник из четырёх полей, из которого выводятся семантическая версия (1.0.0-rc.3), числовая PE-версия (1.0.0.3), признак предварительного выпуска и канал обновлений. Решения об обновлении принимаются только по семантической версии.Сравнение версий не ослаблено, а уточнено до precedence SemVer 2.0.0 (
rc.2 < rc.10, чего раньше не было); ведущие нули и пустые идентификаторы отклоняются как псевдонимы. Канал перестал быть константойtrue: стабильная сборка по-прежнему отвергает любые предварительные версии.Фактическое доказательство: подписанный манифест теперь несёт
"launcher":{"version":"1.0.0-rc.3"}вместо прежнего1.0.0.Flowseal 1.10.2
Импорт штатным защищённым импортёром, файлы вручную не копировались.
ALT13(21 → 22 стратегии), обновлённые списки и ресурсы.UPSTREAM_FIDELITY: PASS version=1.10.2 files=46 strategies=22,STRATEGY_FIDELITY: 88/88.Обязательная подпись Authenticode
Стабильный тег
vX.Y.Zне может быть опубликован без подписи; требование выводится из модели версии, а не из отдельного флага. Политика вынесена чистой функцией и отклоняет всё, кроме статусаValid:NotSigned,HashMismatch, недоверенную цепочку, просроченный сертификат, отсутствие метки времени. Опубликованные байты привязаны к подписанным по SHA-256 в трёх местах. Метаданные генерируются только после подписи.P1 — закрыты пять из шести
UpdateCheckService: подключение не ждёт сетевого ввода-вывода. Поток не отсоединяется,Cancel()идемпотентен и присоединяет его, мьютекс не удерживается во время I/O и join, фоновый код не трогает интерфейс. Модель доверия совпадает с синхронным путём.contents: write, дедупликация по скрытому маркеру, автослияния нет./analyzeс/WXи CodeQL; мутационная проверка свойств пяти разборщиков; полная решётка внесения отказов транзакционной активации.Три дефекта, которых в задании не было
docsиSECURITY.md, которых требует шлюз лицензий, поэтому импорт падал на отсутствующем файле репозитория, а не на нагрузке. Добавленный регрессионный тест немедленно поймал второй случай того же дрейфа.minimum_supported_version: "1.0.0"дал бы установленному1.0.0-rc.Nотказ «слишком старая программа» при переходе на стабильный1.0.0, потому что предварительная версия упорядочена ниже стабильной.1.10.2aсчиталась понижением относительно1.10.2— легитимный хотфикс Flowseal был бы отклонён.Плюс три находки статанализа в привилегированных путях, из них реальный пробел: размер списка атрибутов при создании элевированного процесса брался без проверки.
Проверки
/W4 /WX/analyzeс/WXvalidate-licenses/verify-upstreamprepare-release(RC-канал)launcher=1.0.0-rc.3 pe=1.0.0.3Новые тесты:
version_model,release_signing,importer_validation_tree,upstream_automation,e2e_harness,updatecheckservice,fuzzparsers,faultinjection,diagnostics,runtimerecovery.Блокеры стабильного выпуска
Подробно — в
docs/production/FINAL_RELEASE_CERTIFICATION.md.%ProgramData%\CHEBURNETи требуют загрузки драйвера ядра. Оснастка и подготовка VM готовы; выполнен и пройден только сценарий отказа запуска без прав администратора.cmake/Version.cmakeобъявляетrc.3. Шлюз тега не даст опубликовать стабильный тег при RC-источнике и наоборот.Порядок для стабильного выпуска: B2 (подпись на RC-теге) → B1 (обе чистые VM) → B3 (заменить
rc.3на""и создать тегv1.0.0).Новая документация
docs/production/:BASELINE_BEFORE_HARDENING.md,UPSTREAM_1_10_2_IMPORT.md,RELEASE_SIGNING.md,UPSTREAM_AUTOMATION.md,E2E_RELEASE_GATE.md,SECURITY_TOOLING.md,FINAL_RELEASE_CERTIFICATION.md;docs/architecture/PRIVILEGE_SEPARATION_V2.md(только исследование, в 1.0 не входит).