diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index ce5ae89f..7c656879 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -1,6 +1,7 @@ self-hosted-runner: labels: - labpics-ci-gvisor-v1 + - labcolors-ephemeral paths: .github/workflows/mutation.yml: # actionlint 1.7.12 отстаёт от текущей GitHub-схемы concurrency.queue; diff --git a/.github/workflows/arb.yml b/.github/workflows/arb.yml new file mode 100644 index 00000000..318e8169 --- /dev/null +++ b/.github/workflows/arb.yml @@ -0,0 +1,305 @@ +name: Arb evaluator build and runtime + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - .github/workflows/arb.yml + - crates/labcolors-core/contracts/contextual-region-formula-v1.lcir + - proof/region/v1/** +permissions: + contents: read + +concurrency: + group: arb-evaluator-build-runtime-${{ github.run_id }} + cancel-in-progress: false + +jobs: + diagnostic-build-runtime: + name: two offline builds and runtime tests (no artifact) + # Docker is root-equivalent, so this label is provisioned only on a fresh + # one-job VM whose runner group is bound to this exact workflow revision. + runs-on: [self-hosted, Linux, X64, labcolors-ephemeral] + timeout-minutes: 360 + env: + PYTHONDONTWRITEBYTECODE: "1" + PYTHONHASHSEED: "0" + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: complete fast Arb contract with exact skip manifest + shell: bash + run: | + set -euo pipefail + python3 proof/region/v1/arb/tests/gate.py + PYTHONOPTIMIZE=2 python3 proof/region/v1/arb/tests/gate.py + + - name: complete fast MPFI source contract with exact inventory + shell: bash + run: | + set -euo pipefail + python3 proof/region/v1/mpfi/tests/gate.py + PYTHONOPTIMIZE=2 python3 proof/region/v1/mpfi/tests/gate.py + + - name: bind run-local native paths after the fast gate + shell: bash + run: | + set -euo pipefail + scope="/sys/fs/cgroup/labcolors-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + { + echo "LABCOLORS_CGROUP_SCOPE_V1=$scope" + echo "LABCOLORS_EXECUTOR_CGROUP_V1=$scope/proof" + } >> "$GITHUB_ENV" + + - name: acquire and hash-check exact source archives + shell: bash + run: | + set -euo pipefail + source_dir="$RUNNER_TEMP/arb-source-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + install -d -m 0700 "$source_dir" + echo "LABCOLORS_ARB_SOURCE_DIR=$source_dir" >> "$GITHUB_ENV" + export PYTHONPATH="$GITHUB_WORKSPACE/proof/region/v1" + python3 - <<'PY' > "$source_dir/lock.tsv" + import provenance + + for source in provenance.arb_source_lock_v1().sources: + print( + source.role.name, + source.archive_url, + source.archive_sha256.hex(), + source.archive_length, + sep="\t", + ) + PY + count=0 + while IFS=$'\t' read -r role url digest length; do + archive="$source_dir/${role}.archive" + curl --fail --location --silent --show-error \ + --connect-timeout 30 --max-time 600 --retry 3 --retry-all-errors \ + "$url" --output "$archive" + test "$(stat --format=%s "$archive")" = "$length" + echo "$digest $archive" | sha256sum --check --strict + case "$role" in + GMP) echo "LABCOLORS_GMP_ARCHIVE=$archive" >> "$GITHUB_ENV" ;; + MPFR) echo "LABCOLORS_MPFR_ARCHIVE=$archive" >> "$GITHUB_ENV" ;; + FLINT_ARB) echo "LABCOLORS_FLINT_ARCHIVE=$archive" >> "$GITHUB_ENV" ;; + *) exit 64 ;; + esac + count=$((count + 1)) + done < "$source_dir/lock.tsv" + test "$count" -eq 3 + + - name: acquire and hash-check exact MPFI source closure + shell: bash + run: | + set -euo pipefail + source_dir="${LABCOLORS_ARB_SOURCE_DIR:?}" + export PYTHONPATH="$GITHUB_WORKSPACE/proof/region/v1" + python3 - <<'PY' > "$source_dir/mpfi-lock.tsv" + import provenance + + for source in provenance.mpfi_source_lock_v1().sources: + print( + source.role.name, + source.archive_url, + source.archive_sha256.hex(), + source.archive_length, + sep="\t", + ) + PY + count=0 + while IFS=$'\t' read -r role url digest length; do + case "$role" in + GMP) + archive="${LABCOLORS_GMP_ARCHIVE:?}" + ;; + MPFR) + archive="${LABCOLORS_MPFR_ARCHIVE:?}" + ;; + MPFI) + archive="$source_dir/MPFI.archive" + curl --fail --location --silent --show-error \ + --connect-timeout 30 --max-time 600 --retry 3 --retry-all-errors \ + "$url" --output "$archive" + echo "LABCOLORS_MPFI_ARCHIVE=$archive" >> "$GITHUB_ENV" + ;; + *) exit 64 ;; + esac + test "$(stat --format=%s "$archive")" = "$length" + echo "$digest $archive" | sha256sum --check --strict + count=$((count + 1)) + done < "$source_dir/mpfi-lock.tsv" + test "$count" -eq 3 + + - name: acquire the exact pinned OCI manifest + shell: bash + run: | + set -euo pipefail + export PYTHONPATH="$GITHUB_WORKSPACE/proof/region/v1" + docker_path="$(realpath "$(command -v docker)")" + test -f "$docker_path" + test ! -L "$docker_path" + image="$(python3 - <<'PY' + from arb import pipeline + print(pipeline.OCI_IMAGE_REFERENCE_V1) + PY + )" + "$docker_path" image inspect "$image" >/dev/null 2>&1 || + /usr/bin/timeout --signal=TERM --kill-after=30s 15m \ + "$docker_path" pull "$image" + echo "LABCOLORS_ARB_PIPELINE_DOCKER=$docker_path" >> "$GITHUB_ENV" + + - name: acquire the exact pinned MPFI OCI manifest + shell: bash + run: | + set -euo pipefail + export PYTHONPATH="$GITHUB_WORKSPACE/proof/region/v1" + docker_path="${LABCOLORS_ARB_PIPELINE_DOCKER:?}" + image="$(python3 - <<'PY' + from mpfi import build + print(build.MPFI_BUILD_IMAGE_REFERENCE_V1) + PY + )" + "$docker_path" image inspect "$image" >/dev/null 2>&1 || + /usr/bin/timeout --signal=TERM --kill-after=30s 15m \ + "$docker_path" pull "$image" + echo "LABCOLORS_MPFI_DOCKER=$docker_path" >> "$GITHUB_ENV" + + - name: require the exact diagnostic Docker boundary + shell: bash + run: | + set -euo pipefail + export PYTHONPATH="$GITHUB_WORKSPACE/proof/region/v1" + export LABCOLORS_ARB_PIPELINE_DOCKER + python3 - <<'PY' + import os + import sys + from pathlib import Path + + from arb import pipeline + from build import transport as build_transport + + docker = build_transport.NativeDockerBuildBackendV1( + Path(os.environ["LABCOLORS_ARB_PIPELINE_DOCKER"]), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ).probe() + print(repr(docker)) + if type(docker) is not build_transport.DockerSupportedV1: + sys.exit(78) + PY + + - name: delegate one disposable cgroup subtree + shell: bash + run: | + set -euo pipefail + apparmor_userns=/proc/sys/kernel/apparmor_restrict_unprivileged_userns + if [[ -f "$apparmor_userns" ]]; then + original_userns="$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns)" + case "$original_userns" in + 0|1) ;; + *) exit 78 ;; + esac + echo "LABCOLORS_APPARMOR_USERNS_V1=$original_userns" >> "$GITHUB_ENV" + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + test "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns)" = 0 + else + # The restriction sysctl exists only on kernels carrying the + # AppArmor userns mediation; its absence is proof that there is + # nothing to lift, and the cleanup restore stays a no-op. + echo "LABCOLORS_APPARMOR_USERNS_V1=" >> "$GITHUB_ENV" + fi + scope="$LABCOLORS_CGROUP_SCOPE_V1" + sudo mkdir "$scope" + sudo chown "$(id -u):$(id -g)" \ + "$scope" \ + "$scope/cgroup.procs" \ + "$scope/cgroup.threads" \ + "$scope/cgroup.subtree_control" + printf '+memory +pids' > "$scope/cgroup.subtree_control" + mkdir "$scope/tasks" "$scope/proof" + printf '+memory +pids' > "$scope/proof/cgroup.subtree_control" + printf '2' > "$scope/proof/pids.max" + mkdir "$scope/proof/observer" + grep --fixed-strings --quiet 'memory' "$scope/proof/cgroup.subtree_control" + grep --fixed-strings --quiet 'pids' "$scope/proof/cgroup.subtree_control" + test "$(cat "$scope/proof/pids.max")" = 2 + + - name: one source-bound BUILD to RUN receipt and evaluator runtime + shell: bash + run: | + set -euo pipefail + # The kernel admits a cgroup migration only with write access to the + # common ancestor of the source and destination groups; the runner + # cgroup is root-owned, so root admits this step into the owned + # subtree first and the controller's observer placement then stays a + # proven self-migration between delegated groups. + echo "$$" | sudo tee \ + "$LABCOLORS_CGROUP_SCOPE_V1/tasks/cgroup.procs" >/dev/null + exec python3 proof/region/v1/arb/tests/native_gate.py receipt + + - name: one source-bound MPFI BUILD to RUN receipt and evaluator runtime + shell: bash + run: | + set -euo pipefail + # Identical admission contract as the Arb receipt lane above. + echo "$$" | sudo tee \ + "$LABCOLORS_CGROUP_SCOPE_V1/tasks/cgroup.procs" >/dev/null + exec python3 proof/region/v1/mpfi/tests/native_gate.py receipt + + - name: native containment under an atomic two-task subtree + shell: bash + run: | + set -euo pipefail + echo "$$" | sudo tee \ + "$LABCOLORS_EXECUTOR_CGROUP_V1/observer/cgroup.procs" >/dev/null + exec python3 proof/region/v1/arb/tests/native_gate.py executor + + # No upload step: the static binary is an ephemeral observation until a + # linker/member inventory plus notices/source/relink distribution gate exists. + + - name: remove disposable inputs and cgroup + if: always() + shell: bash + run: | + set -uo pipefail + status=0 + record_failure() { + local code="$?" + if (( status == 0 )); then + status="$code" + fi + } + if [[ -n "${LABCOLORS_ARB_SOURCE_DIR:-}" ]]; then + rm -rf -- "$LABCOLORS_ARB_SOURCE_DIR" || record_failure + fi + if [[ -n "${LABCOLORS_CGROUP_SCOPE_V1:-}" && \ + -d "$LABCOLORS_CGROUP_SCOPE_V1" ]]; then + if [[ -f "$LABCOLORS_CGROUP_SCOPE_V1/cgroup.kill" ]]; then + echo 1 | sudo tee "$LABCOLORS_CGROUP_SCOPE_V1/cgroup.kill" \ + >/dev/null || record_failure + fi + if [[ -f "$LABCOLORS_CGROUP_SCOPE_V1/cgroup.events" ]]; then + for _ in {1..100}; do + grep --fixed-strings --quiet 'populated 0' \ + "$LABCOLORS_CGROUP_SCOPE_V1/cgroup.events" && break + sleep 0.01 + done + grep --fixed-strings --quiet 'populated 0' \ + "$LABCOLORS_CGROUP_SCOPE_V1/cgroup.events" || record_failure + fi + for child in proof/observer proof tasks; do + if [[ -d "$LABCOLORS_CGROUP_SCOPE_V1/$child" ]]; then + sudo rmdir "$LABCOLORS_CGROUP_SCOPE_V1/$child" || record_failure + fi + done + sudo rmdir "$LABCOLORS_CGROUP_SCOPE_V1" || record_failure + fi + if [[ -n "${LABCOLORS_APPARMOR_USERNS_V1:-}" ]]; then + sudo sysctl -w \ + "kernel.apparmor_restrict_unprivileged_userns=$LABCOLORS_APPARMOR_USERNS_V1" \ + >/dev/null || record_failure + fi + exit "$status" diff --git a/README.md b/README.md index 45d640a2..11529287 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,12 @@ anchors ## Runtime и фон -### `effectiveBackground` +### Наблюдение фона (внутренняя граница) -Текущая функция — вспомогательная эталонная оценка для поддерживаемой цепочки CSS-цветов, а не доказательство того, что пользователь видит именно этот пиксель. +Вспомогательные функции с областью видимости пакета разбирают поддерживаемую +цепочку CSS-цветов для runtime-контроллеров. Это не функция публичного +корневого API и не доказательство того, что пользователь видит именно этот +пиксель. Критические ограничения: @@ -247,7 +250,9 @@ anchors - отсутствие непрозрачной базы, предел обхода и ошибка style API должны рассматриваться как неизвестный контекст, а не как белый или чёрный fallback; - эмитированные движком значения предпочтительно передавать байтами, а не повторно декодировать из CSS-строки. -Текущий helper совместимости ещё не реализует весь строгий типизированный контракт наблюдения. Его bare hex нельзя использовать как сертификат браузера или дисплея. +Результат внутренней границы наблюдения нельзя использовать как сертификат +браузера или дисплея. Неподдерживаемый или неизвестный контекст остаётся +типизированным `Unknown`, а не превращается в правдоподобный цвет. ### `watchTheme` diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 2ae67b34..a992badd 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -193,13 +193,15 @@ display-measured evidence без соответствующего измерен после устойчивого относительного снижения запускает новый resolve и переход к его результату. -`effectiveBackground` — reference estimate поддерживаемой цепочки CSS-цветов, -не наблюдение пикселя. Image, gradient, video, filter, blend mode и +Внутренняя граница наблюдения использует вспомогательные функции с областью +видимости пакета для разбора поддерживаемой цепочки CSS-цветов; это опорная +оценка, а не наблюдение пикселя. Image, gradient, video, filter, blend mode и `backdrop-filter` требуют явных образцов или другого источника фактов. Строгий -occurrence-контракт не вправе заменять неизвестный фон белым или чёрным. Текущий -legacy helper всё ещё использует белую базу по умолчанию и пропускает -неподдерживаемые CSS-слои, поэтому его результат — только compatibility estimate, -не evidence; для обязательной проверки caller передаёт явный фон или samples. +контракт наблюдения не вправе заменять неизвестный фон белым или чёрным: если +непрозрачная база не доказана и вызывающая сторона не объявила `canvas`, +наблюдение имеет типизированный исход `Unknown`. Для обязательной проверки +вызывающая сторона передаёт явный фон или образцы; неподдерживаемые слои и +эффекты не отбрасываются молча. ## Доказательный статус diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 6ad29cd0..9ff5ae3d 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -27,12 +27,12 @@ ] }, "measurement": { - "source": "github-actions-run-30790916413", + "source": "github-actions-run-30864842276", "platform": "linux-x64", - "rawBytes": 376554 + "rawBytes": 376907 }, "policy": { - "maxRawBytes": 376554, + "maxRawBytes": 376907, "basis": "generic-exact-point-representation", "gzip": "diagnostic-only" } diff --git a/packages/colors/test/public-api-cleanup.test.mjs b/packages/colors/test/public-api-cleanup.test.mjs index 3738abaa..02d37fe6 100644 --- a/packages/colors/test/public-api-cleanup.test.mjs +++ b/packages/colors/test/public-api-cleanup.test.mjs @@ -3,8 +3,6 @@ import { existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { test } from "node:test"; -import * as publicRoot from "../index.js"; - const ROOT = resolve(import.meta.dirname, "../../.."); const read = (...parts) => readFileSync(join(ROOT, ...parts), "utf8"); @@ -54,7 +52,8 @@ test("effective-background math stays internal to the browser shell", () => { ); }); -test("public initialisation cannot leak raw WASM exports", () => { +test("public initialisation cannot leak raw WASM exports", async () => { + const publicRoot = await import("../index.js"); const result = publicRoot.initSync({ module: new WebAssembly.Module( readFileSync(new URL("../pkg/labcolors_bg.wasm", import.meta.url)), @@ -97,6 +96,31 @@ test("runtime documentation names the declared canvas instead of the removed fal assert.match(docs, /^\s*canvas\??\s*:/mu); }); +test("repository docs preserve the Point-or-Unknown observation contract", () => { + const rootReadme = read("README.md"); + const whitepaper = read("docs", "whitepaper.md"); + + assert.doesNotMatch(rootReadme, /\beffectiveBackground\b/u); + assert.doesNotMatch( + whitepaper, + /legacy helper.*(?:белую базу|white base)/isu, + ); + for (const documentation of [rootReadme, whitepaper]) { + assert.doesNotMatch( + documentation, + /package-private helpers|observation boundary/u, + ); + } + assert.doesNotMatch( + whitepaper, + /reference estimate|occurrence-\s*контракт|\bcaller\b/u, + ); + assert.match(rootReadme, /границы наблюдения/u); + assert.match(whitepaper, /граница наблюдения/u); + assert.match(rootReadme, /типизированным `Unknown`/u); + assert.match(whitepaper, /типизированный исход `Unknown`/u); +}); + test("the unshipped JavaScript FNV mirror stays deleted", () => { for (const path of [ ["packages", "colors", "fnv1a.js"], diff --git a/proof/region/v1/PROTOCOL.md b/proof/region/v1/PROTOCOL.md index c180abb7..abbe53a5 100644 --- a/proof/region/v1/PROTOCOL.md +++ b/proof/region/v1/PROTOCOL.md @@ -5,17 +5,22 @@ ## Граница -Протокол переносит immutable job и заявленные результаты будущих Arb/MPFI -processes. `region_proof_protocol.py` определяет только structural codecs и -admission функций сравнения. Текущий `controller.py` безопасно читает и -повторно проверяет пять frozen protocol fixtures; он ещё не строит и не -запускает evaluator, не разрешает comparator manifest и не создаёт provenance -receipt. Ни один текущий модуль не вычисляет цвет или interval enclosure и не -создаёт semantic proof type. - -`V5b2c-0` определяет protocol/admission, но сам не является математическим -proof. В c0 нет `DualProofReceiptV1`: structural agreement кодируется -только как `DualComparisonCandidateV1`. C0 не создаёт полный family image, +Протокол переносит immutable job и structural claims результатов evaluator +processes. `region_proof_protocol.py` определяет только codecs и admission +функций сравнения, а `controller.py` повторно проверяет committed frozen +protocol fixtures и не является evaluator runner. `arb/evaluator` вычисляет +Arb-enclosures и выпускает связанные transcript bytes; +`SourceBoundArbControllerV1` заново собирает evaluator, запускает его и создаёт +только provenance receipt. Ни один из этих путей не выполняет независимый +semantic replay и не создаёт mathematical proof type. MPFI source lock, +archive admission и sealed source input (не evaluator replay) уже представлены. +`mpfi/evaluator` содержит отдельный source-owned M1.5 build/run path, а +`mpfi/receipt.py` — controller-only source-bound BUILD→RUN receipt boundary. +Semantic verifier для MPFI в текущем release всё ещё отсутствует. + +Structural protocol/admission сам не является математическим proof. +`DualComparisonCandidateV1` кодирует только structural agreement и не создаёт +`DualProofReceiptV1`, полный family image, `SemanticFamilyReleaseIdV2`, `FamilyArtifactReceiptIdV2` или `FamilyImageCertificateV2`. @@ -24,17 +29,22 @@ evidence. В тесте протокола такое значение явно хранится или не публикуется как proof artifact. Протокол не входит в Cargo workspace, Core, WASM, FFI, bindings или packages. -Раздельные evaluator implementations и их допустимый общий dependency overlap -появятся в следующих срезах; c1a ещё не подтверждает их происхождение или -diversity. - -## Wire и identity - -Все целые беззнаковые и записаны big-endian как `u8`, `u32be` или `u64be`. -`digest` — ровно 32 ненулевых bytes SHA-256. `blob` равен -`u64be(length) || bytes`. Enum занимает один `u8` и принимает только -перечисленные значения. Padding, alignment, reserved fields и trailing bytes -отсутствуют. +`SourceBoundEvaluatorReceiptV1` и `MpfiSourceBoundEvaluatorReceiptV1` +подтверждают причинную цепь только в пределах своих controller-owned +provenance boundaries. MPFI receipt не заявляет cross-path dependency overlap, +diversity или semantic correctness; structural coordinates и локальная сборка +этого не восполняют. + +## Бинарный формат и идентичность + +Для wire-artifact-ов из `region_proof_protocol.py` все целые беззнаковые и +записаны big-endian как `u8`, `u32be` или `u64be`; `digest` — ровно 32 +ненулевых bytes SHA-256, а `blob` равен `u64be(length) || bytes`. +`SourceReleaseLockV1` и связанные provenance-artifact-ы имеют отдельный codec +в `provenance.py`: его `blob` равен `u32be(length) || bytes`; grammar также +содержит свои `u16` и 20-byte OpenPGP/SHA-1 coordinates. Enum занимает один +`u8` и принимает только перечисленные значения. Padding, alignment, reserved +fields и trailing bytes отсутствуют. До allocation и цикла по records parser проверяет арифметику длины без переполнения, остаток input, точный или минимальный wire-размер всех @@ -53,15 +63,18 @@ artifact, а повторный encode обязан вернуть byte-identica | `ReducedDomainManifestV1` | `LCDOM1\0\0` | `labcolors.proof-region.domain.v1` | | `ProofPolicyV1` | `LCPOL1\0\0` | `labcolors.proof-region.policy.v1` | | `ProofJobV1` | `LCJOB1\0\0` | `labcolors.proof-region.job.v1` | -| `ComparatorManifestV1` | `LCMAN1\0\0` | `labcolors.proof-region.comparator-manifest.v1` | +| `ComparatorManifestV2` | `LCMAN2\0\0` | `labcolors.proof-region.comparator-manifest.v2` | | `DecisionTranscriptV1` | `LCTRN1\0\0` | `labcolors.proof-region.transcript.v1` | | `RunClaimV1` | `LCRUN1\0\0` | `labcolors.proof-region.run-claim.v1` | | `EvaluatorProvenanceClaimV1` | `LCPRV1\0\0` | `labcolors.proof-region.evaluator-provenance-claim.v1` | | `DualComparisonClaimV1` | `LCCMP1\0\0` | `labcolors.proof-region.dual-comparison.v1` | +Версия принадлежит отдельному artifact type. Composite V1 wire связывает +identity независимо версионированного comparator manifest как opaque digest. + ## `ContextualRegionDefinitionV1` -Definition не получает protocol magic. Это точный V5b2b canonical preimage: +Definition не получает protocol magic. Это точный canonical preimage: последовательность полей `u64be(field_length) || field_bytes`. Grammar содержит `22 + 4 × knot_count` полей; fixture-specific count не является grammar. Поле 21 содержит `knot_count: u64be`; count ненулевой, но не имеет ad-hoc cap. @@ -69,8 +82,8 @@ Definition не получает protocol magic. Это точный V5b2b canon `knot_count × 4 × (8-byte length + 8-byte payload)`. Так wire bytes, а не произвольный protocol limit, ограничивают count до цикла по records. -Admission строго парсит typed V5b2b definition, проверяет его domain-инварианты, -повторно кодирует и требует byte-identical preimage. Заявленный +Admission строго парсит typed contextual definition, проверяет его +domain-инварианты, повторно кодирует и требует byte-identical preimage. Заявленный `FamilyDefinitionDigestV2` равен `SHA256(definition_preimage)`. Formula digest внутри definition должен совпасть с приложенным immutable strict @@ -140,30 +153,310 @@ Wire после `LCJOB1\0\0`, по порядку: Admission проверяет definition и formula identities, canonical re-encode и identity каждого вложенного artifact. Formula release обязан совпасть с полем -definition. Job задаёт единственный канонический input contract будущих -вычислителей; только controlled-executor slice сможет доказать отсутствие -ambient inputs. Альтернативный JSON/TOML definition запрещён протоколом. - -## `ComparatorManifestV1` - -Wire после `LCMAN1\0\0` содержит comparator kind `u8` +definition. Job задаёт единственный канонический input contract evaluator-ов. +Соответствующий source-bound controller связывает его с наблюдёнными BUILD/RUN +внутри объявленной ниже границы доверия; receipt не заявляет отсутствие ambient +inputs за пределами этой границы. Альтернативный JSON/TOML definition запрещён +протоколом. + +### Профили исполнения V1 + +Грамматика формата передачи сама не превращается в неограниченный +распределитель ресурсов. Прямые исполняемые файлы Arb и MPFI принимают разные +версионированные профили — `LC-ARB-RUNTIME-V1` и `LC-MPFI-RUNTIME-V1` — с +одинаковыми эксплуатационными параметрами: входной job не более 16 MiB, не +более 4096 bits на precision rung, не более 32 rung-ов, не более 1024 +contextual knots и не более 16 MiB aggregate transcript output. Равенство +параметров обеспечивает симметричный допуск одного proof job; identity +профилей остаются разными. Это эксплуатационный допуск, а не математический +предел definition/domain. Лимиты job, precision, rung-ов и knots отклоняются +до соответствующего выделения ресурсов, переполнение transcript имеет +отдельный typed `output_limit`. + +В коде `ArbRuntimeProfileV1` и `MpfiRuntimeProfileV1` владеют своими exact +tuple. `ArbRuntimeBindingV1` и `MpfiRuntimeBindingV1` связывают профиль с одним +immutable `ExecutionLimitsV1`: `max_stdin_bytes` обязан равняться +`max_job_bytes`, а `max_stdout_bytes` — `max_output_bytes`; остальные executor +limits входят в ту же binding identity явно. Поэтому контроллер не может +заменить память, время или stderr-лимит и сохранить прежнюю source-bound +BUILD/RUN identity. Ни executor, ни общий protocol leaf не импортируют +конкретный evaluator: оба контракта принадлежат конкретному lane, а прямые +binaries не являются самостоятельным публичным API вычислителя. + +## Фиксация источников и наблюдения целостности + +`SourceReleaseLockV1` фиксирует bytes и структурный состав архива. Поле +`.integrity` содержит один `SourceIntegrityPolicyV1`; это точная граница +доступного evidence, а не безусловное заявление о publisher origin. Поле +`legal_files` — только точный project-pinned набор находящихся в архиве legal +files; оно не заявляет полноту legal-набора или compliance распространяемого +бинарника. Несовпадение этого набора имеет отдельную причину +`legal_files_mismatch`. + +Для GMP и MPFR locked detached signature, key packets и исторический +`VALIDSIG` связываются только в +`HistoricalPathRecheckedSignatureDiagnosticV1`. Digest и version запущенного +`gpgv` остаются диагностикой. Запуск принадлежит переданному клиентом +`DiagnosticProcessRunnerV1`: Core ограничивает и парсит возвращённые bytes, но +не выдаёт runner за sandbox, containment или provenance authority. Встроенного +`Popen` fallback нет. Этот тип не устанавливает текущего publisher, +текущий статус или отзыв ключа, происхождение полученных bytes и exact sealed +execution verifier. Такой diagnostic сам по себе не создаёт и не заменяет +`SourceBoundEvaluatorReceiptV1` и не усиливает его до publisher claim. + +Для FLINT `GitContentRelationPolicyV1` фиксирует commit, tree, исключённые +paths и отдельные `project_pinned_release_only_files`. `run_git_tree` принимает +такой же client-owned diagnostic runner, после чего Core независимо +пересчитывает commit, commit-to-tree edge, recursive tree и каждый +blob. Поэтому admission создаёт один `RecomputedGitContentRelationV1`: paths +архива должны быть точным дизъюнктным объединением общих Git files и +project-pinned release-only files, а исключённые paths обязаны отсутствовать. +Git executable/version, repository URL и tag являются диагностикой или +координатами поиска и не входят в authority этой relation. Relation доказывает +совпадение content graph, но не publisher или канал получения архива. + +Для MPFI 1.5.4 не подтверждены detached signature, опубликованный upstream +checksum или равенство release-архива Git tag/tree. Поэтому +`ProjectPinnedArchiveDigestPolicyV1` намеренно не содержит внешнего payload: +Lab Colors фиксирует exact HTTPS URL, длину и SHA-256 полученных archive bytes, +но не приписывает этот digest издателю и не заявляет publisher authentication. +`MpfiSourceLockV1` использует те же единичные GMP/MPFR source declarations, что +и Arb, однако имеет отдельную aggregate identity и отдельный typed admission. + +## Диагностическая граница исполнения + +`proof/region/v1/executor.py` — общий для enclosure engines leaf без импорта +Arb/MPFI, formula или comparator semantics. Он же единолично кодирует +`execution-invocation.v1` и `execution-platform.v1`; engine pipeline не может +вводить параллельную identity того же процесса. Sandbox release +`labcolors.proof-region.executor.linux-x86_64.v1` намеренно не сохраняет +старую Arb-domain identity: это hard cut, а не compatibility alias. + +`ControlledExecutorV1` — единственный владелец one-shot capability: новый, +неуспешный, перекрывающийся probe или замена backend отзывают ранее выданный +объект до RUN. Capability выпускается контроллером для одного probe-поколения +и одного process id; fork не дублирует право запуска. Backend сообщает только +наблюдённые свойства хоста, получает guard текущего probe и не может продлить +жизнь capability повторно используемым report-объектом. + +`ExecutionRequestV1`, его limits и `SupportedV1` являются структурно +неизменяемыми значениями. Публичные execution identity functions воспроизводят +admission из точных координат и возвращают +`bytes | ExecutionIdentityRejectedV1`: foreign или даже намеренно forged +malformed value становится versioned typed rejection, а не новой identity и не +exception-channel. `ControlledExecutorV1` отвергает такой request до probe и +backend run как `ObserverFailureV1(REQUEST_NOT_ADMITTED)`. + +`executor.canonical_cgroup_parent_v1` разбирает объявленный parent без +разрешения пути через файловую систему. +`executor.enter_observer_cgroup_v1` — единственная versioned межмодульная +операция размещения: engine controller передаёт этот +абсолютный канонический parent, а executor помещает текущий controller в +`parent/observer` и пробрасывает отказ для typed mapping вызывающего. Engine не +копирует этот descriptor protocol. Executor открывает каждый сегмент cgroup +descriptor-relative с `O_PATH|O_NOFOLLOW`: metadata-проверка допускает каталог, +доступный только для поиска, но символическая ссылка не может незаметно связать +controller с другой cgroup. Размещение остаётся self-migration: kernel +допускает перенос задачи только при праве записи в `cgroup.procs` общего +предка исходной и целевой cgroup, поэтому caller обязан уже находиться внутри +делегированного job-owned subtree; вход в этот subtree — отдельная root +admission операция workflow, а не часть этого placement protocol. + +Linux backend допускается лишь в отдельном helper process. Helper находится в +прямом дочернем cgroup объявленного parent, а весь parent subtree имеет +`pids.max = 2` и перед probe содержит ровно observer. Эти два task slots имеют +не эвристический смысл: один занимает observer, второй — либо новый thread, +либо единственный controlled child. Kernel pids controller атомарно разрешает +только один из вариантов; поэтому check→fork race не маскируется повторным +опросом `/proc`. Execution child дополнительно получает собственный +`pids.max = 1`, memory limit и `cgroup.kill`; фактические limits читаются назад +до запуска. Отсутствие этой структуры возвращает typed unsupported/setup +outcome. Этот runtime остаётся diagnostic observation и не создаёт receipt. +Самостоятельный `ControlledExecutorV1` по-прежнему остаётся только такой +observation. Право на Arb receipt получает не executor, а отдельный one-shot +`SourceBoundArbControllerV1`, который владеет всей цепью BUILD → RUN и не +принимает backend, capability либо diagnostic observation от вызывающего. + +## Общая граница BUILD + +Source replay имеет две намеренно разные стадии. Сначала provenance канонически +перепарсивает source lock, bounded-decompresses и сканирует archive, чтобы +сверить lock, manifest, tree и compressed bytes. Эта metadata replay не создаёт +отдельные file-byte buffers. Затем +`provenance.replay_materialize_admitted_source_v1` из одного такого replay +создаёт token-closed снимок lock, archive и exact relative regular files. +Aggregate Arb/MPFI admission владеет только свежими replayed archives; runtime +получает все три file-byte materializations только через один +`replay_admitted_source_closure_v1`. Общий leaf не вводит USTAR namespace, +recipe или engine semantics. Lane выбирает layout и связывает собственный +aggregate source capability. `proof/region/v1/build/input.py` принимает уже +нормализованные lane entries, кодирует один канонический USTAR и владеет точными input bytes. +`SealedInputV1` структурно неизменяем, связывает целостность байтов с opaque +digest вызывающей стороны и не утверждает recipe либо engine semantics. +Ограничения ресурсов +передаёт lane: общий encoder не вводит собственный fixture-specific cap. + +`mpfi/input.py` строит `SealedInputV1` только из одного owned replay snapshot +пары `MpfiSourceLockV1` и `AdmittedMpfiSourcesV1`. Тот же снимок даёт exact +regular files, aggregate identity и versioned MPFI-only namespace +`sources//` и связывает свежую aggregate source capability с +exact USTAR bytes. Роль, а не archive root, разделяет три source trees: lock +не требует уникальности root. Целостность `SealedInputV1` сама по себе не +доказывает принадлежность MPFI closure; это отдельно перепроверяет MPFI +source-input binding. Вызывающая сторона передаёт канонический +`CanonicalInputLimitsV1`: lane сверяет declared exact file count и payload +closure до повторной materialization archive bytes, а общий encoder сверяет все +final USTAR bounds после materialization. Limits — operational boundary, не +координата MPFI source-input binding и не build policy. Для неверного public +capability boundary возвращается `MpfiSourceInputErrorV1`; failure exact archive +replay остаётся `ProvenanceErrorV1`, а limits/USTAR rejection — `InputErrorV1`. +Эта ступень не вводит recipe, Docker policy, BUILD/RUN authority, executable, +comparator, receipt или semantic verifier. + +`mpfi/build.py` — следующая отдельная BUILD-граница: она повторно принимает +только pinned workspace files, exact generated formula bytes и MPFI source +snapshot, затем строит один USTAR bundle. `MPFI_BUILD_TRANSPORT_POLICY_V1` +фиксирует отдельный linux/amd64 Clang-19 image manifest, bootstrap и bounded +tmpfs; sealed input identity связывает их с source/build bytes. Это ещё не +receipt: до source-bound controller и реального disposable BUILD→RUN здесь +нет provenance claim. + +`proof/region/v1/build/transport.py` владеет immutable Docker policy, +одноразовым probe→build lease, bounded stdin/stdout observation, cleanup и +двумя свежими попытками. Доказательные координаты разделены по причинам: + +1. transport policy identity связывает все поля точной policy; +2. native command contract identity связывает один типизированный grammar для + probe, build и cleanup и один immutable child-launch context + (environment, cwd, umask, stdio topology, FD и session behavior); фактический argv и + Popen kwargs строятся только этими значениями; +3. daemon observation identity связывает только два raw probe stdout; +4. Docker capability identity связывает policy, command contract и exact CLI + path, daemon observation и наблюдённые host uid/gid. + +`docker_command_coordinate_v1` допускает exact absolute Docker-safe argv path +без разрешения пути через файловую систему. Перед native Linux probe adapter +descriptor-relative +открывает каждый его сегмент с `O_PATH|O_NOFOLLOW`: metadata-проверка не требует +права чтения от CLI, имеющего только право исполнения, или родительского +каталога, доступного только для поиска, но символическая ссылка всё равно не может +изменить фактическую координату. Adapter +принимает только текущий regular CLI file. Это проверка pathname, а не заявление +о неизменном file object между probe и BUILD: native host, его Docker CLI и +daemon остаются явной незапечатанной границей доверия. + +`BuildSessionV1` и каждый `DockerBuildRequestV1` сохраняют только ту же +capability, те же input bytes и output cap. Request не содержит host path, +CID file или имя контейнера: native adapter сам создаёт свежий приватный CID +path. Native cleanup поддерживается только в fresh one-job VM workflow Arb: +другой субъект с тем же effective UID либо Docker-daemon authority там не +сосуществует. Права `0700` закрывают лишь cross-UID pathname access и не +аутентифицируют same-UID writer. В этой объявленной operational boundary для +cleanup допускается только полный ID, который Docker записал в CID path; перед +`rm --force ` adapter сверяет, что `docker container inspect` вернул тот же +ID. Имя контейнера и fallback-координата в cleanup не участвуют. Вне этой +границы CID path не является доказательством ownership. Чужая либо не +полученная текущим probe capability отвергается до process spawn; ambient +path/user повторно не считываются. Разрешение принадлежит создавшему process: +fork и конкурентное повторное использование отвергаются до блокировки. После +возврата Popen handle `BaseException` до повторного выброса исходного +interruption запускает детерминированные попытки остановить и reap CLI, закрыть +streams и очистить допущенный container. Во время самого Popen construction +handle может ещё отсутствовать: тогда возможна только best-effort попытка CID +cleanup, без ложного заявления о reap CLI. `TwoBuildObservationV1` хранит обе успешные попытки и только +классифицирует их байты как identical или different, не называя пару +универсальным доказательством воспроизводимости. При отказе после создания +валидной session сохраняется весь уже завершённый causal prefix. Context-free +contract violation, обнаруженный до создания session (например, невалидная +session или сбой `TemporaryDirectory`), может вернуть `BuildRejectedV1` без +`session` и `completed_processes`. +Transport не знает formula, ELF, comparator или +source provenance: engine lane отдельно перепроверяет свой engine-owned input binding перед +каждым process и передаёт output admission. MPFI sealed source input сам по себе +не является MPFI build policy; `mpfi/build.sh` объявляет source-owned recipe, +а `mpfi/receipt.py` связывает его с BUILD/RUN observation. Production admission +всё ещё требует exact native BUILD→RUN gate на том же source head. Recipe не +заимствует Arb semantics. + +## Воспроизведение Arb, связанное с источником + +`PipelineRequestV1` до операции отдельно перепроверяет и владеет metadata-only +source closure: это ранняя integrity boundary для public input, не shared cache +операции. Затем `SourceBoundArbControllerV1` получает один detached operation +snapshot: канонический source lock, owned replayed archives и единственные для +этой операции file-byte materializations, заново допущенные копии build files, +job и limits. Он передаёт этот же private snapshot в `ControlledPipelineV1`; +самостоятельный BUILD создаёт snapshot сам до probe/spawn. Внутренний transport +recheck сверяет только owned snapshot, а public verifier независимо строит +новый snapshot из request, сохранённого внутри evidence, а не из исходного +объекта вызывающего. До replay он фиксирует structural projection всех +evidence coordinates и сверяет каждый используемый protocol identity cache с +независимо восстановленным canonical wire. Он принимает результат только если +та же projection на входе, после source replay и после edge replay совпадает. +Projection сверяет retained bytes, manifests и protocol wire, но не открывает +вторую source materialization; она доказывает стабильность значения в пределах +одного вызова, а не неизменность объекта после возврата. Один +immutable bundle object дважды передаётся через bounded stdin; каждый свежий контейнер до +распаковки сверяет exact length и SHA-256, распаковывает только в private +bounded tmpfs, а executable возвращает через stdout. Semantic host bind mounts, +host output path и повторное открытие результата отсутствуют. Эта граница +доказывает точный controller-observed byte stream, а не непрерывность inode +между host и Docker daemon. Raw daemon observation входит в capability, но сам +daemon остаётся явно доверенным input объявленной границы. + +Успешный replay хранится одним token-closed +`ContentResolvedEvaluatorReplayV1`, который повторно выводит три причинные +identity без зеркальных промежуточных dataclass: + +1. source identity связывает lock, три admitted archive closures, build inputs + и formula support. Job сюда не входит: одинаковый evaluator build не меняет + source identity от конкретного RUN; +2. build identity связывает source identity, versioned Docker capability, + pipeline policy, trust boundary, один sealed bundle object, два exact + transfer и два byte-identical executable stdout. Comparator verifier + заново выводит все десять preimage bytes и canonical manifest из того же + operation snapshot и retained BUILD observation, затем сверяет все поля и + identity с build observation. Это не независимая реализация или semantic + replay, а exact re-derivation тех же причинных координат; +3. run identity впервые связывает canonical job с тем же retained executable + bytes object, exact argv/env/cwd/stdin/limits, единственной допустимой + `linux-x86_64` sandbox platform, typed child exit, stdout, canonical + transcript и `RunClaimV1`. + +Только соответствующий one-shot controller может собрать этот корень, создать +raw `EvaluatorProvenanceClaimV1` и privately sealed +`SourceBoundEvaluatorReceiptV1` либо `MpfiSourceBoundEvaluatorReceiptV1`. +Отдельного pipeline RUN-authority и public promotion пути нет. + +Ни raw claim, ни digest/content resolver, ни diagnostic BUILD/RUN object, ни +public constructor не создают receipt. Receipt доказывает только наблюдённую +причинность source → build → exact executable → run → stdout/transcript в +объявленной границе доверия. Он допустим для canonical transcript с +`BoundaryUnproven` или `ResourceLimitReached`. Semantic correctness, Arb/MPFI +diversity, mathematical proof и `DualProofReceiptV1` этим типом не представлены. +Host/Docker, instruction-level inputs, publisher origin и distribution +compliance не усиливаются и не называются SLSA/in-toto attestation. + +## `ComparatorManifestV2` + +Wire после `LCMAN2\0\0` содержит comparator kind `u8` (`1 = Arb`, `2 = MPFI`), затем десять digest coordinates в фиксированном порядке: 1. engine release; 2. upstream source; -3. arithmetic closure; +3. arithmetic input set; 4. wrapper source; 5. evaluator source; 6. build identity, включая compiler, target и exact flags; 7. operation allowlist; -8. test receipt; -9. license closure; +8. test observation; +9. legal file set; 10. exclusions. -Результат wire parse — только raw `ComparatorManifestV1`: его ненулевые +Результат wire parse — только raw `ComparatorManifestV2`: его ненулевые coordinates являются заявленными content addresses, а не доказанным -source binding. `ContentResolvedComparatorManifestV1` создаётся только +source binding. `ContentResolvedComparatorManifestV2` создаётся только после того, как переданный вызывающим `resolve_content_address` для каждой из десяти coordinates вернул exact `bytes` или `Iterable[bytes]`. Сам protocol повторяет SHA-256 по этим bytes/chunks и сравнивает результат с coordinate. Boolean, @@ -181,8 +474,9 @@ V1 не доказывает independence. Как anti-vacuum declared-diversity `ComparatorKindV1.MPFI` и попарно различные `engine_release`, `upstream_source`, `wrapper_source`, `evaluator_source` и `RunClaimV1.binary_identity`. Это лишь различие заявленных coordinates: оно не -доказывает разное происхождение или реализацию. Допустимый общий GMP/MPFR -overlap и обязательные distinct edges появятся в typed replay evidence. +доказывает разное происхождение или реализацию. Arb receipt уже связывает свой +dependency graph; cross-path GMP/MPFR overlap и обязательные distinct edges не +считаются установленными без отдельного MPFI receipt. ## `DecisionTranscriptV1` @@ -255,15 +549,16 @@ Witness ordinals строго возрастают, уникальны и при а число таких records равно exact-equality count. Missing/extra/foreign witness не может быть исправлен самим битом `Inside`. -`trace_digest` и `enclosure_digest` в c0 — только ненулевые content -coordinates, а не доказательство replay или enclosure. Будущий semantic -verifier receipt обязан разрешить и replay эти records, проверить exact -equality/enclosure math и связать результат с job, comparator, run и transcript. -Controller этого не делает. Любая недоказанная transcendental equality +`trace_digest` и `enclosure_digest` — только ненулевые content +coordinates, а не доказательство replay или enclosure. Semantic verification +требует разрешить и replay эти records, проверить exact equality/enclosure math +и связать результат с job, comparator, run и transcript; такого semantic +receipt в текущем release нет. Ни Arb-, ни MPFI-controller этого не делает. Любая +недоказанная transcendental equality остаётся `BoundaryUnproven`: epsilon или midpoint не превращают её в `Inside`. -`DecisionTranscriptV1` в c0 остаётся structural claim. Нулевые unresolved +`DecisionTranscriptV1` остаётся structural claim. Нулевые unresolved counters не превращают его в semantic resolved/proven type. ## `RunClaimV1` @@ -280,27 +575,30 @@ Wire после `LCRUN1\0\0` содержит шесть digest coordinates: Wire parse и `for_transcript` создают только structural run claim из заявленных coordinates. `for_transcript` проверяет лишь bindings job/comparator/transcript; binary, invocation и platform получает от вызывающего и не наблюдает. Причинную -цепь сможет установить только будущий controlled rebuild/replay. +цепь устанавливает только `SourceBoundArbControllerV1`; raw claim сам этого +права не имеет. ## `EvaluatorProvenanceClaimV1` Wire после `LCPRV1\0\0` содержит три unresolved digest declarations: -1. provenance policy identity — versioned правила и trust boundary будущего replay; +1. provenance policy identity — versioned правила и trust boundary replay; 2. `RunClaimV1` identity — subject, к которому относится заявление; -3. replay evidence identity — predicate с будущей source/build/run цепью. +3. replay evidence identity — unresolved coordinate source/build/run predicate. Этот тип аналогичен структурному statement, а не attestation о выполненном -build. `parse` проверяет только canonical wire и ненулевые coordinates. В c1a -нет `SourceBoundEvaluatorReceiptV1`, public admission, resolver или флага -успешного replay. Sealed receipt появится только из реально наблюдаемого -rebuild/run и будет иметь отдельную domain-separated identity. +build. `parse` проверяет только canonical wire и ненулевые coordinates. Сам raw +тип не имеет public admission, resolver или флага успешного replay. Sealed +`SourceBoundEvaluatorReceiptV1` или `MpfiSourceBoundEvaluatorReceiptV1` создаёт +только соответствующий controller после фактически наблюдённого typed replay +DAG. Receipt identity равна identity связанного claim и не дублирует subject +отдельным digest; parse raw claim этого права не даёт. Назначение трёх внутренних coordinates только вдохновлено разделением ролей в [in-toto Statement V1.2.0](https://github.com/in-toto/attestation/blob/v1.2.0/spec/v1/statement.md) и definition/run model в [SLSA Build Provenance V1.2](https://slsa.dev/spec/v1.2/build-provenance). -Wire остаётся внутренним бинарным протоколом Lab Colors; c1a не заявляет +Wire остаётся внутренним бинарным протоколом Lab Colors и не заявляет in-toto Statement/ResourceDescriptor/predicate schema, envelope/signature, SLSA level или соответствие SLSA builder contract. @@ -331,7 +629,7 @@ raw claim. Он никогда не возвращает admitted candidate. Н refined type. Candidate строится в canonical order Arb → MPFI из двух -`ContentResolvedComparatorManifestV1`, согласованных `RunClaimV1` и +`ContentResolvedComparatorManifestV2`, согласованных `RunClaimV1` и structurally admitted transcripts. Все bindings ведут к одному job, definition, domain и policy; `domain_point_count` равен count связанного manifest. Unresolved counters равны нулю, decision payloads совпадают побайтно, @@ -343,13 +641,14 @@ failure и не создают candidate. Успешный candidate фикси структурное согласие над exact bound domain manifest; он не является proof receipt и не доказывает correctness ни одного evaluator. -Математический proof требует будущих semantic verifier receipts для обоих -evaluator paths и независимой проверки их bindings/replay. Family mint +`DualProofReceiptV1` требует semantic verification receipts для обоих evaluator +paths и независимой проверки их bindings/replay; этих admitted типов текущий +release не содержит. Family mint дополнительно разрешает `domain_identity` и допускает отдельно exact full manifest: единственный range `[0, 2^24)` и point count `2^24`. Совпадение только point count или reduced-domain candidate этот gate не проходят. -## Ошибки admission +## Ошибки допуска `ProtocolReasonV1` — закрытая сумма: diff --git a/proof/region/v1/arb/__init__.py b/proof/region/v1/arb/__init__.py new file mode 100644 index 00000000..be37528c --- /dev/null +++ b/proof/region/v1/arb/__init__.py @@ -0,0 +1 @@ +"""Arb source-bound proof lane.""" diff --git a/proof/region/v1/arb/build-inner.sh b/proof/region/v1/arb/build-inner.sh new file mode 100644 index 00000000..1d44ef7b --- /dev/null +++ b/proof/region/v1/arb/build-inner.sh @@ -0,0 +1,162 @@ +#!/bin/sh +# Internal Arb recipe. The public build.sh entrypoint always starts this file +# through its sealed environment; no caller-controlled variable selects a +# pre-sanitized execution path. +set -eu + +if [ "$#" -ne 0 ]; then + printf '%s\n' 'arb build takes no arguments' >&2 + exit 64 +fi + +umask 022 + +readonly inputs=/build/snapshot/inputs +readonly workspace=/build/snapshot/workspace +readonly build=/build/work + +require_regular() { + if [ ! -f "$1" ] || [ -L "$1" ]; then + printf 'missing regular build input: %s\n' "$1" >&2 + exit 66 + fi +} + +require_directory() { + if [ ! -d "$1" ] || [ -L "$1" ]; then + printf 'missing normalized source directory: %s\n' "$1" >&2 + exit 66 + fi +} + +require_empty_directory() { + if [ ! -d "$1" ] || [ -L "$1" ]; then + printf 'missing build directory: %s\n' "$1" >&2 + exit 66 + fi + if [ -n "$(find "$1" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + printf 'build directory is not empty: %s\n' "$1" >&2 + exit 65 + fi +} + +require_directory "$inputs/gmp-6.3.0" +require_directory "$inputs/mpfr-4.2.2" +require_directory "$inputs/flint-3.6.0" +require_regular "$inputs/formula.generated.c" +printf '%s %s\n' \ + '9958f20c8ca598625db0593a45f8f8bc79e4b2f22b53263b6c32d78a5e1d2693' \ + "$inputs/formula.generated.c" \ + | /usr/bin/sha256sum --check --strict - +for source in main.c wire.c hash.c interval.c region.c; do + require_regular "$workspace/proof/region/v1/arb/evaluator/$source" +done +require_regular "$workspace/proof/region/v1/arb/evaluator/formula.h" +for header in wire.h hash.h interval.h region.h; do + require_regular "$workspace/proof/region/v1/arb/evaluator/$header" +done +require_empty_directory "$build" + +/usr/bin/mkdir "$build/prefix" "$build/gmp" "$build/mpfr" "$build/flint" "$build/tmp" + +# GCC 15 changed its implicit dialect to GNU C23, where GMP 6.3.0's locked +# no-prototype configure probes have different semantics. GNU C17 is the last +# default those probes targeted; changing it requires a source/toolchain slice +# and a fresh live build, not reliance on a compiler's moving default. +readonly common_cflags='-O2 -g0 -fno-ident -fno-fast-math -ffp-contract=off -fno-lto -std=gnu17 -march=x86-64 -mtune=generic -ffile-prefix-map=/build=. -fdebug-prefix-map=/build=.' +readonly common_ldflags='-Wl,--build-id=none -fno-lto' +readonly prefix="$build/prefix" + +cd "$build/gmp" +ABI=64 CC=/usr/local/bin/gcc CFLAGS="$common_cflags" LDFLAGS="$common_ldflags" \ + "$inputs/gmp-6.3.0/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --disable-shared \ + --enable-static \ + --disable-assembly \ + --disable-cxx +/usr/bin/make -j1 +/usr/bin/make check -j1 +/usr/bin/make install + +cd "$build/mpfr" +CC=/usr/local/bin/gcc CFLAGS="$common_cflags" LDFLAGS="$common_ldflags" \ + "$inputs/mpfr-4.2.2/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --with-gmp="$prefix" \ + --disable-shared \ + --enable-static \ + --enable-formally-proven-code +/usr/bin/make -j1 +/usr/bin/make check -j1 +/usr/bin/make install + +cd "$build/flint" +CC=/usr/local/bin/gcc CFLAGS="$common_cflags" LDFLAGS="$common_ldflags" \ + "$inputs/flint-3.6.0/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --with-gmp="$prefix" \ + --with-mpfr="$prefix" \ + --disable-shared \ + --enable-static \ + --disable-assembly \ + --disable-lto \ + --enable-assert +/usr/bin/make -j1 +/usr/bin/make check -j1 +/usr/bin/make install + +cd "$workspace/proof/region/v1/arb/evaluator" +# The pinned GMP/MPFR/FLINT headers enter as one system include directory: +# assert-enabled FLINT 3.6.0 degrades FLINT_UNUSED(x) to a bare parameter, +# which -Wextra diagnoses inside flint_rand_clear no matter which C dialect +# is selected. System-header status scopes diagnostics to the evaluator's +# own sources, where -Wall -Wextra -Werror -pedantic stay fully strict. +/usr/local/bin/gcc \ + -O2 -g0 -fno-ident -fno-fast-math -ffp-contract=off -fno-lto \ + -march=x86-64 -mtune=generic \ + -ffile-prefix-map=/build=. -fdebug-prefix-map=/build=. \ + -std=gnu17 -Wall -Wextra -Werror -pedantic \ + -I. -isystem "$prefix/include" \ + main.c wire.c hash.c interval.c region.c "$inputs/formula.generated.c" \ + -static -Wl,--build-id=none -fno-lto \ + "$prefix/lib/libflint.a" "$prefix/lib/libmpfr.a" "$prefix/lib/libgmp.a" \ + -lm -lpthread \ + -o "$build/arb-evaluator-v1" + +if ! /usr/bin/readelf -l "$build/arb-evaluator-v1" > "$build/program-headers"; then + printf '%s\n' 'cannot inspect evaluator program headers' >&2 + exit 70 +fi +if /usr/bin/grep -q INTERP "$build/program-headers"; then + printf '%s\n' 'evaluator unexpectedly contains PT_INTERP' >&2 + exit 70 +else + grep_status=$? + if [ "$grep_status" -ne 1 ]; then + printf '%s\n' 'cannot search evaluator program headers' >&2 + exit 70 + fi +fi +if ! /usr/bin/readelf -d "$build/arb-evaluator-v1" > "$build/dynamic-section"; then + printf '%s\n' 'cannot inspect evaluator dynamic section' >&2 + exit 70 +fi +if /usr/bin/grep -q NEEDED "$build/dynamic-section"; then + printf '%s\n' 'evaluator unexpectedly contains DT_NEEDED' >&2 + exit 70 +else + grep_status=$? + if [ "$grep_status" -ne 1 ]; then + printf '%s\n' 'cannot search evaluator dynamic section' >&2 + exit 70 + fi +fi + +/usr/bin/sha256sum "$build/arb-evaluator-v1" diff --git a/proof/region/v1/arb/build.sh b/proof/region/v1/arb/build.sh new file mode 100755 index 00000000..d52dfc4d --- /dev/null +++ b/proof/region/v1/arb/build.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Public Arb build entrypoint. It always creates the sealed environment before +# invoking the recipe; no caller-controlled sentinel can select the inner path. +set -eu + +if [ "$#" -ne 0 ]; then + printf '%s\n' 'arb build takes no arguments' >&2 + exit 64 +fi + +script_dir=$(/usr/bin/dirname -- "$0") +inner="$script_dir/build-inner.sh" +if [ ! -f "$inner" ] || [ -L "$inner" ]; then + printf '%s\n' 'missing regular Arb inner build recipe' >&2 + exit 66 +fi + +exec /usr/bin/env -i \ + PATH=/usr/local/bin:/usr/bin:/bin \ + LC_ALL=C \ + LANG=C \ + TZ=UTC \ + HOME=/nonexistent \ + TMPDIR=/build/work/tmp \ + SOURCE_DATE_EPOCH=0 \ + ZERO_AR_DATE=1 \ + ARFLAGS=crD \ + /bin/sh "$inner" diff --git a/proof/region/v1/arb/evaluator/formula.h b/proof/region/v1/arb/evaluator/formula.h new file mode 100644 index 00000000..79ccb359 --- /dev/null +++ b/proof/region/v1/arb/evaluator/formula.h @@ -0,0 +1,20 @@ +#ifndef LABCOLOR_ARB_FORMULA_H +#define LABCOLOR_ARB_FORMULA_H + +#include + +#include "interval.h" + +/* Point evaluation owns three output coordinates, so its caller supplies an + array of exactly three initialized Arb elements rather than one arb_t. */ +lc_status lc_formula_point( + arb_ptr output, + const uint8_t rgb[3], + arb_srcptr context, + uint8_t surround, + slong precision +); +lc_status lc_formula_segment(arb_t output, arb_srcptr input, slong precision); +lc_status lc_formula_singleton(arb_t output, arb_srcptr input, slong precision); + +#endif diff --git a/proof/region/v1/arb/evaluator/formula.py b/proof/region/v1/arb/evaluator/formula.py new file mode 100644 index 00000000..90936b7e --- /dev/null +++ b/proof/region/v1/arb/evaluator/formula.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +"""Generate the Arb V1 evaluator from the immutable exact-real SSA.""" + +from __future__ import annotations + +import hashlib +import sys +from dataclasses import dataclass +from pathlib import Path + + +SOURCE_SHA256 = "a6f77ac462f226453b1c27bbd8637b62780b9a640c317a6f50028dacd1de8540" +RELEASE_DOMAIN = b"labcolors.nominal-exact-real-lift.ascii-ssa.v1\0" +RELEASE_SHA256 = "2c626d8ee60eeb62ae4db53660d61bbc25e0efd4e557f0dc1e77565c130b6e52" + +TYPE_DECLARATIONS = ( + "type u8 unsigned_integer_0_255", + "type real mathematical_real", + "type bool exact_boolean", + "type surround_profile closed_enum", +) + +OPERATOR_DECLARATIONS = ( + "operator lookup 2 real table_u8_exact_dyadic_at_ordinal", + "operator eq 2 bool exact_same_type_equality", + "operator select 3 same bool_true_second_else_third", + "operator add 2 real exact_x_plus_y", + "operator sub 2 real exact_x_minus_y", + "operator mul 2 real exact_x_times_y", + "operator div 2 real domain_y_ne_zero_x_div_y_else_domain_unproven", + "operator min 2 real exact_lesser_real", + "operator max 2 real exact_greater_real", + "operator root3 1 real domain_x_ge_zero_unique_y_ge_zero_y_cubed_eq_x_else_domain_unproven", + "operator sqrt 1 real domain_x_ge_zero_unique_y_ge_zero_y_squared_eq_x_else_domain_unproven", + "operator exp 1 real analytic_natural_exponential", + "operator log 1 real domain_x_gt_zero_analytic_natural_logarithm_else_domain_unproven", + "operator sin 1 real analytic_sine_radians", + "operator cos 1 real analytic_cosine_radians", + "operator abs 1 real exact_absolute_value", + "operator sign 1 real negative_minus_one_zero_zero_positive_one", + "operator pow_pos 2 real domain_x_gt_zero_exp_y_mul_log_x_else_domain_unproven", + "operator pow_nn 2 real if_x_eq_zero_and_y_gt_zero_zero_else_pow_pos", + "operator ratio0 2 real if_x_eq_zero_and_y_eq_zero_zero_else_domain_y_gt_zero_x_div_y", +) + +DRIVER_RULES = ( + "rule tone_domain closed_first_last", + "rule out_of_tone_domain outside", + "rule one_knot_tone exact_equality_required", + "rule one_knot_predicate singleton_f_le_zero", + "rule multi_knot_predicate piecewise_linear_segment_f_le_zero", + "rule boundary inclusive", +) + +PROGRAM_INTERFACES = { + "point": ( + (("r8", "u8"), ("g8", "u8"), ("b8", "u8"), + ("adapting_luminance", "real"), ("background_ratio", "real"), + ("surround", "surround_profile")), + 226, + ("jp", "ap", "bp"), + ), + "segment": ( + tuple( + (name, "real") + for name in ( + "segment_t", "segment_a", "segment_b", "segment_t0", + "segment_t1", "segment_c0a", "segment_c0b", "segment_c1a", + "segment_c1b", "segment_rho0", "segment_rho1", "segment_g00", + "segment_g01", "segment_g11", + ) + ), + 27, + ("segment_f",), + ), + "singleton": ( + tuple( + (name, "real") + for name in ( + "singleton_a", "singleton_b", "singleton_ca", "singleton_cb", + "singleton_rho", "singleton_g00", "singleton_g01", "singleton_g11", + ) + ), + 12, + ("singleton_f",), + ), +} + + +class FormulaError(ValueError): + pass + + +@dataclass(frozen=True) +class Node: + name: str + result: str + operator: str + arguments: tuple[str, ...] + + +@dataclass(frozen=True) +class Program: + name: str + inputs: tuple[tuple[str, str], ...] + nodes: tuple[Node, ...] + outputs: tuple[str, ...] + + +@dataclass(frozen=True) +class Formula: + decode: tuple[int, ...] + literals: tuple[tuple[str, int], ...] + enums: tuple[tuple[str, int], ...] + programs: tuple[Program, ...] + + +class Lines: + def __init__(self, values: list[str]): + self.values = values + self.cursor = 0 + + def next(self) -> str: + if self.cursor >= len(self.values): + raise FormulaError(f"unexpected end at line {self.cursor + 1}") + value = self.values[self.cursor] + self.cursor += 1 + return value + + def expect(self, expected: str) -> None: + actual = self.next() + if actual != expected: + raise FormulaError( + f"line {self.cursor}: expected {expected!r}, got {actual!r}" + ) + + +def identifier(value: str) -> bool: + return bool(value) and value[0].islower() and all( + byte.islower() or byte.isdigit() or byte == "_" for byte in value + ) + + +def fields(line: str, count: int) -> tuple[str, ...]: + result = tuple(line.split(" ")) + if len(result) != count: + raise FormulaError(f"record arity {len(result)} != {count}") + return result + + +def finite_bits(token: str) -> int: + if len(token) != 16 or any(byte not in "0123456789abcdef" for byte in token): + raise FormulaError("noncanonical binary64 payload") + bits = int(token, 16) + if bits & 0x7FF0_0000_0000_0000 == 0x7FF0_0000_0000_0000: + raise FormulaError("nonfinite binary64 payload") + if bits == 0x8000_0000_0000_0000: + raise FormulaError("negative zero") + return bits + + +def parse_program(lines: Lines, name: str, globals_: dict[str, str]) -> Program: + expected_inputs, expected_nodes, expected_outputs = PROGRAM_INTERFACES[name] + lines.expect(f"{name}_inputs {len(expected_inputs)}") + symbols = dict(globals_) + inputs: list[tuple[str, str]] = [] + for expected in expected_inputs: + record = fields(lines.next(), 3) + if record != ("input", *expected): + raise FormulaError(f"foreign {name} input") + if record[1] in symbols: + raise FormulaError("shadowed input") + symbols[record[1]] = record[2] + inputs.append((record[1], record[2])) + + lines.expect(f"{name}_nodes {expected_nodes}") + nodes: list[Node] = [] + for _ in range(expected_nodes): + record = tuple(lines.next().split(" ")) + if len(record) < 5 or record[0] != "node" or not identifier(record[1]): + raise FormulaError("invalid node") + node = Node(record[1], record[2], record[3], record[4:]) + validate_node(node, symbols) + if node.name in symbols: + raise FormulaError("shadowed node") + symbols[node.name] = node.result + nodes.append(node) + + if name == "point": + lines.expect("point_checkpoints 39") + checkpoint_names: set[str] = set() + node_names = {node.name for node in nodes} + for _ in range(39): + record = fields(lines.next(), 3) + if ( + record[0] != "checkpoint" + or record[1] in checkpoint_names + or record[2] not in node_names + or symbols[record[2]] != "real" + ): + raise FormulaError("invalid checkpoint") + checkpoint_names.add(record[1]) + + lines.expect(f"{name}_outputs {len(expected_outputs)}") + outputs: list[str] = [] + for expected in expected_outputs: + record = fields(lines.next(), 3) + if record != ("output", expected, "real") or symbols.get(expected) != "real": + raise FormulaError("foreign output") + outputs.append(expected) + return Program(name, tuple(inputs), tuple(nodes), tuple(outputs)) + + +def validate_node(node: Node, symbols: dict[str, str]) -> None: + try: + types = tuple(symbols[value] for value in node.arguments) + except KeyError as error: + raise FormulaError(f"unknown or forward reference {error.args[0]}") from None + unary = {"root3", "sqrt", "exp", "log", "sin", "cos", "abs", "sign"} + binary = {"add", "sub", "mul", "div", "min", "max", "pow_pos", "pow_nn", "ratio0"} + if node.operator == "lookup": + valid = node.result == "real" and types == ("decode_table", "u8") + elif node.operator == "eq": + valid = node.result == "bool" and len(types) == 2 and types[0] == types[1] != "decode_table" + elif node.operator == "select": + valid = len(types) == 3 and types[0] == "bool" and types[1] == types[2] == node.result + elif node.operator in unary: + valid = node.result == "real" and types == ("real",) + elif node.operator in binary: + valid = node.result == "real" and types == ("real", "real") + else: + valid = False + if not valid: + raise FormulaError(f"operator/type mismatch for {node.name}") + + +def parse(source: bytes) -> Formula: + if hashlib.sha256(source).hexdigest() != SOURCE_SHA256: + raise FormulaError("formula source is not the registered V1 content") + release = hashlib.sha256( + RELEASE_DOMAIN + len(source).to_bytes(8, "big") + source + ).hexdigest() + if release != RELEASE_SHA256: + raise FormulaError("formula release mismatch") + if not source.isascii() or not source.endswith(b"\n") or source.endswith(b"\n\n"): + raise FormulaError("formula is not canonical ASCII with one final LF") + text = source.decode("ascii")[:-1] + values = text.split("\n") + for index, line in enumerate(values, 1): + if ( + not line + or line.startswith(" ") + or line.endswith(" ") + or " " in line + or "\t" in line + or "\r" in line + or "#" in line + ): + raise FormulaError(f"line {index} is not canonical") + + lines = Lines(values) + lines.expect("labcolors_exact_real_ssa 1") + lines.expect("arithmetic exact_real_v1") + lines.expect(f"types {len(TYPE_DECLARATIONS)}") + for declaration in TYPE_DECLARATIONS: + lines.expect(declaration) + lines.expect(f"operators {len(OPERATOR_DECLARATIONS)}") + for declaration in OPERATOR_DECLARATIONS: + lines.expect(declaration) + + lines.expect("decode_table decode_srgb8 256") + decode: list[int] = [] + for ordinal in range(256): + record = fields(lines.next(), 3) + if record[:2] != ("decode", f"{ordinal:02x}"): + raise FormulaError("decode order drift") + decode.append(finite_bits(record[2])) + + lines.expect("literals 56") + literals: list[tuple[str, int]] = [] + literal_names: set[str] = set() + literal_values: set[int] = set() + for _ in range(56): + record = fields(lines.next(), 3) + bits = finite_bits(record[2]) + if ( + record[0] != "literal" + or not identifier(record[1]) + or record[1] in literal_names + or bits in literal_values + ): + raise FormulaError("invalid literal") + literal_names.add(record[1]) + literal_values.add(bits) + literals.append((record[1], bits)) + + lines.expect("enum_type surround_profile 3") + enums: list[tuple[str, int]] = [] + for name, tag in (("surround_average", 1), ("surround_dim", 2), ("surround_dark", 3)): + record = fields(lines.next(), 4) + if record != ("enum", "surround_profile", name, f"{tag:02x}"): + raise FormulaError("foreign surround enum") + enums.append((name, tag)) + + globals_: dict[str, str] = {"decode_srgb8": "decode_table"} + globals_.update((name, "real") for name, _ in literals) + globals_.update((name, "surround_profile") for name, _ in enums) + programs = tuple(parse_program(lines, name, globals_) for name in PROGRAM_INTERFACES) + lines.expect(f"driver {len(DRIVER_RULES)}") + for rule in DRIVER_RULES: + lines.expect(rule) + lines.expect("end") + if lines.cursor != len(lines.values): + raise FormulaError("trailing records") + return Formula(tuple(decode), tuple(literals), tuple(enums), programs) + + +def real_expression(name: str, real: dict[str, int]) -> str: + return f"real + {real[name]}" + + +def emit_program(formula: Formula, program: Program) -> list[str]: + real: dict[str, int] = {} + surround: dict[str, str] = {name: str(tag) for name, tag in formula.enums} + boolean: dict[str, str] = {} + lines: list[str] = [] + + for name, _ in formula.literals: + real[name] = len(real) + for name, kind in program.inputs: + if kind == "real": + real[name] = len(real) + elif kind == "surround_profile": + surround[name] = "surround" + + for node in program.nodes: + if node.result == "real": + real[node.name] = len(real) + elif node.result == "bool": + boolean[node.name] = f"condition_{len(boolean)}" + + signature = { + "point": "lc_status lc_formula_point(arb_ptr output, const uint8_t rgb[3], arb_srcptr context, uint8_t surround, slong precision)", + "segment": "lc_status lc_formula_segment(arb_t output, arb_srcptr input, slong precision)", + "singleton": "lc_status lc_formula_singleton(arb_t output, arb_srcptr input, slong precision)", + }[program.name] + lines.extend((signature, "{", " lc_status status = LC_OK;", f" arb_struct real[{len(real)}];")) + for index in range(len(real)): + lines.append(f" arb_init(real + {index});") + for name, bits in formula.literals: + lines.append( + f" status = lc_set_dyadic_bits(real + {real[name]}, UINT64_C(0x{bits:016x}));" + ) + lines.append(" if (status != LC_OK) goto cleanup;") + + real_cursor = 0 + u8_cursor = 0 + u8_values: dict[str, str] = {} + for name, kind in program.inputs: + if kind == "real": + lines.append(f" arb_set(real + {real[name]}, {'context' if program.name == 'point' else 'input'} + {real_cursor});") + real_cursor += 1 + elif kind == "u8": + u8_values[name] = f"rgb[{u8_cursor}]" + u8_cursor += 1 + + adapter = { + "add": "lc_add", "sub": "lc_sub", "mul": "lc_mul", "div": "lc_div", + "min": "lc_min", "max": "lc_max", "root3": "lc_root3", "sqrt": "lc_sqrt", + "exp": "lc_exp", "log": "lc_log", "sin": "lc_sin", "cos": "lc_cos", + "abs": "lc_abs", "sign": "lc_sign", "pow_pos": "lc_pow_pos", + "pow_nn": "lc_pow_nn", "ratio0": "lc_ratio0", + } + for node in program.nodes: + target = real_expression(node.name, real) if node.result == "real" else "" + if node.operator == "lookup": + lines.append( + f" status = lc_set_dyadic_bits({target}, LC_DECODE_BITS[(size_t){u8_values[node.arguments[1]]}]);" + ) + lines.append(" if (status != LC_OK) goto cleanup;") + elif node.operator == "eq": + left = surround[node.arguments[0]] + right = surround[node.arguments[1]] + lines.append(f" int {boolean[node.name]} = ({left} == {right});") + elif node.operator == "select": + condition = boolean[node.arguments[0]] + left = real_expression(node.arguments[1], real) + right = real_expression(node.arguments[2], real) + lines.append(f" arb_set({target}, {condition} ? {left} : {right});") + else: + arguments = ", ".join(real_expression(name, real) for name in node.arguments) + lines.append( + f" status = {adapter[node.operator]}({target}, {arguments}, precision);" + ) + lines.append(" if (status != LC_OK) goto cleanup;") + + for index, name in enumerate(program.outputs): + destination = f"output + {index}" if len(program.outputs) > 1 else "output" + lines.append(f" arb_set({destination}, {real_expression(name, real)});") + lines.append("cleanup:") + for index in range(len(real) - 1, -1, -1): + lines.append(f" arb_clear(real + {index});") + lines.extend((" return status;", "}", "")) + return lines + + +def emit(formula: Formula) -> bytes: + output = [ + "/* Generated from the registered exact-real SSA; do not edit. */", + "#include ", + "#include ", + "#include \"formula.h\"", + "", + "static const uint64_t LC_DECODE_BITS[256] = {", + ] + for index in range(0, 256, 4): + values = ", ".join( + f"UINT64_C(0x{value:016x})" for value in formula.decode[index : index + 4] + ) + output.append(f" {values},") + output.extend(("};", "")) + for program in formula.programs: + output.extend(emit_program(formula, program)) + return ("\n".join(output) + "\n").encode("ascii") + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: formula.py FORMULA", file=sys.stderr) + return 2 + try: + source = Path(argv[1]).read_bytes() + generated = emit(parse(source)) + except (OSError, FormulaError) as error: + print(f"formula rejected: {error}", file=sys.stderr) + return 1 + sys.stdout.buffer.write(generated) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/proof/region/v1/arb/evaluator/hash.c b/proof/region/v1/arb/evaluator/hash.c new file mode 100644 index 00000000..20e6543f --- /dev/null +++ b/proof/region/v1/arb/evaluator/hash.c @@ -0,0 +1,173 @@ +#include "hash.h" + +#include + +static const uint32_t round_constants[64] = { + UINT32_C(0x428a2f98), UINT32_C(0x71374491), UINT32_C(0xb5c0fbcf), UINT32_C(0xe9b5dba5), + UINT32_C(0x3956c25b), UINT32_C(0x59f111f1), UINT32_C(0x923f82a4), UINT32_C(0xab1c5ed5), + UINT32_C(0xd807aa98), UINT32_C(0x12835b01), UINT32_C(0x243185be), UINT32_C(0x550c7dc3), + UINT32_C(0x72be5d74), UINT32_C(0x80deb1fe), UINT32_C(0x9bdc06a7), UINT32_C(0xc19bf174), + UINT32_C(0xe49b69c1), UINT32_C(0xefbe4786), UINT32_C(0x0fc19dc6), UINT32_C(0x240ca1cc), + UINT32_C(0x2de92c6f), UINT32_C(0x4a7484aa), UINT32_C(0x5cb0a9dc), UINT32_C(0x76f988da), + UINT32_C(0x983e5152), UINT32_C(0xa831c66d), UINT32_C(0xb00327c8), UINT32_C(0xbf597fc7), + UINT32_C(0xc6e00bf3), UINT32_C(0xd5a79147), UINT32_C(0x06ca6351), UINT32_C(0x14292967), + UINT32_C(0x27b70a85), UINT32_C(0x2e1b2138), UINT32_C(0x4d2c6dfc), UINT32_C(0x53380d13), + UINT32_C(0x650a7354), UINT32_C(0x766a0abb), UINT32_C(0x81c2c92e), UINT32_C(0x92722c85), + UINT32_C(0xa2bfe8a1), UINT32_C(0xa81a664b), UINT32_C(0xc24b8b70), UINT32_C(0xc76c51a3), + UINT32_C(0xd192e819), UINT32_C(0xd6990624), UINT32_C(0xf40e3585), UINT32_C(0x106aa070), + UINT32_C(0x19a4c116), UINT32_C(0x1e376c08), UINT32_C(0x2748774c), UINT32_C(0x34b0bcb5), + UINT32_C(0x391c0cb3), UINT32_C(0x4ed8aa4a), UINT32_C(0x5b9cca4f), UINT32_C(0x682e6ff3), + UINT32_C(0x748f82ee), UINT32_C(0x78a5636f), UINT32_C(0x84c87814), UINT32_C(0x8cc70208), + UINT32_C(0x90befffa), UINT32_C(0xa4506ceb), UINT32_C(0xbef9a3f7), UINT32_C(0xc67178f2), +}; + +static uint32_t +rotate_right(uint32_t value, unsigned distance) +{ + return (value >> distance) | (value << (32U - distance)); +} + +static uint32_t +read_u32_be(const uint8_t *bytes) +{ + return ((uint32_t) bytes[0] << 24) + | ((uint32_t) bytes[1] << 16) + | ((uint32_t) bytes[2] << 8) + | (uint32_t) bytes[3]; +} + +static void +write_u32_be(uint8_t *bytes, uint32_t value) +{ + bytes[0] = (uint8_t) (value >> 24); + bytes[1] = (uint8_t) (value >> 16); + bytes[2] = (uint8_t) (value >> 8); + bytes[3] = (uint8_t) value; +} + +static void +compress(lc_sha256_context *context, const uint8_t block[64]) +{ + uint32_t words[64]; + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; + uint32_t e; + uint32_t f; + uint32_t g; + uint32_t h; + + for (size_t index = 0; index < 16; ++index) { + words[index] = read_u32_be(block + index * 4); + } + for (size_t index = 16; index < 64; ++index) { + uint32_t s0 = rotate_right(words[index - 15], 7) + ^ rotate_right(words[index - 15], 18) + ^ (words[index - 15] >> 3); + uint32_t s1 = rotate_right(words[index - 2], 17) + ^ rotate_right(words[index - 2], 19) + ^ (words[index - 2] >> 10); + words[index] = words[index - 16] + s0 + words[index - 7] + s1; + } + + a = context->state[0]; + b = context->state[1]; + c = context->state[2]; + d = context->state[3]; + e = context->state[4]; + f = context->state[5]; + g = context->state[6]; + h = context->state[7]; + for (size_t index = 0; index < 64; ++index) { + uint32_t sum1 = rotate_right(e, 6) ^ rotate_right(e, 11) ^ rotate_right(e, 25); + uint32_t choose = (e & f) ^ ((~e) & g); + uint32_t temporary1 = h + sum1 + choose + round_constants[index] + words[index]; + uint32_t sum0 = rotate_right(a, 2) ^ rotate_right(a, 13) ^ rotate_right(a, 22); + uint32_t majority = (a & b) ^ (a & c) ^ (b & c); + uint32_t temporary2 = sum0 + majority; + + h = g; + g = f; + f = e; + e = d + temporary1; + d = c; + c = b; + b = a; + a = temporary1 + temporary2; + } + context->state[0] += a; + context->state[1] += b; + context->state[2] += c; + context->state[3] += d; + context->state[4] += e; + context->state[5] += f; + context->state[6] += g; + context->state[7] += h; +} + +void +lc_sha256_init(lc_sha256_context *context) +{ + context->state[0] = UINT32_C(0x6a09e667); + context->state[1] = UINT32_C(0xbb67ae85); + context->state[2] = UINT32_C(0x3c6ef372); + context->state[3] = UINT32_C(0xa54ff53a); + context->state[4] = UINT32_C(0x510e527f); + context->state[5] = UINT32_C(0x9b05688c); + context->state[6] = UINT32_C(0x1f83d9ab); + context->state[7] = UINT32_C(0x5be0cd19); + context->bit_length = 0; + context->block_length = 0; +} + +void +lc_sha256_update(lc_sha256_context *context, const uint8_t *bytes, size_t length) +{ + while (length != 0) { + size_t available = sizeof(context->block) - context->block_length; + size_t copied = length < available ? length : available; + + memcpy(context->block + context->block_length, bytes, copied); + context->block_length += copied; + bytes += copied; + length -= copied; + if (context->block_length == sizeof(context->block)) { + compress(context, context->block); + context->bit_length += UINT64_C(512); + context->block_length = 0; + } + } +} + +void +lc_sha256_finish(lc_sha256_context *context, uint8_t digest[32]) +{ + uint64_t total_bits = context->bit_length + (uint64_t) context->block_length * 8U; + + context->block[context->block_length++] = UINT8_C(0x80); + if (context->block_length > 56) { + memset(context->block + context->block_length, 0, 64 - context->block_length); + compress(context, context->block); + context->block_length = 0; + } + memset(context->block + context->block_length, 0, 56 - context->block_length); + for (size_t index = 0; index < 8; ++index) { + context->block[63 - index] = (uint8_t) (total_bits >> (index * 8)); + } + compress(context, context->block); + for (size_t index = 0; index < 8; ++index) { + write_u32_be(digest + index * 4, context->state[index]); + } + memset(context, 0, sizeof(*context)); +} + +void +lc_sha256(const uint8_t *bytes, size_t length, uint8_t digest[32]) +{ + lc_sha256_context context; + + lc_sha256_init(&context); + lc_sha256_update(&context, bytes, length); + lc_sha256_finish(&context, digest); +} diff --git a/proof/region/v1/arb/evaluator/hash.h b/proof/region/v1/arb/evaluator/hash.h new file mode 100644 index 00000000..5fa1ab7e --- /dev/null +++ b/proof/region/v1/arb/evaluator/hash.h @@ -0,0 +1,19 @@ +#ifndef LABCOLOR_ARB_HASH_H +#define LABCOLOR_ARB_HASH_H + +#include +#include + +typedef struct { + uint32_t state[8]; + uint64_t bit_length; + uint8_t block[64]; + size_t block_length; +} lc_sha256_context; + +void lc_sha256_init(lc_sha256_context *context); +void lc_sha256_update(lc_sha256_context *context, const uint8_t *bytes, size_t length); +void lc_sha256_finish(lc_sha256_context *context, uint8_t digest[32]); +void lc_sha256(const uint8_t *bytes, size_t length, uint8_t digest[32]); + +#endif diff --git a/proof/region/v1/arb/evaluator/interval.c b/proof/region/v1/arb/evaluator/interval.c new file mode 100644 index 00000000..a7f9a1e3 --- /dev/null +++ b/proof/region/v1/arb/evaluator/interval.c @@ -0,0 +1,198 @@ +#include "interval.h" + +#include + +lc_status +lc_set_dyadic_bits(arb_t output, uint64_t bits) +{ + uint64_t exponent_bits = (bits >> 52) & UINT64_C(0x7ff); + uint64_t significand = bits & UINT64_C(0x000fffffffffffff); + slong exponent; + fmpz_t integer; + fmpz_t power; + + if (exponent_bits == UINT64_C(0x7ff) || bits == UINT64_C(0x8000000000000000)) { + return LC_INVALID_DYADIC; + } + if (exponent_bits == 0) { + exponent = -1074; + } else { + significand |= UINT64_C(0x0010000000000000); + exponent = (slong) exponent_bits - 1075; + } + + fmpz_init(integer); + fmpz_init(power); + fmpz_set_ui(integer, significand); + if ((bits >> 63) != 0 && significand != 0) { + fmpz_neg(integer, integer); + } + fmpz_set_si(power, exponent); + arb_set_fmpz_2exp(output, integer, power); + fmpz_clear(power); + fmpz_clear(integer); + return LC_OK; +} + +void +lc_interval_get_dyadic_bounds( + fmpz_t lower, + fmpz_t upper, + fmpz_t exponent, + arb_srcptr value +) +{ + arb_get_interval_fmpz_2exp(lower, upper, exponent, value); +} + +lc_status +lc_add(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + arb_add(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_sub(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + arb_sub(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_mul(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + arb_mul(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_div(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + if (arb_contains_zero(right)) { + return LC_DOMAIN_UNPROVEN; + } + arb_div(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_min(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + arb_min(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_max(arb_t output, arb_srcptr left, arb_srcptr right, slong precision) +{ + arb_max(output, left, right, precision); + return LC_OK; +} + +lc_status +lc_root3(arb_t output, arb_srcptr input, slong precision) +{ + if (!arb_is_nonnegative(input)) { + return LC_DOMAIN_UNPROVEN; + } + arb_root_ui(output, input, 3, precision); + return LC_OK; +} + +lc_status +lc_sqrt(arb_t output, arb_srcptr input, slong precision) +{ + if (!arb_is_nonnegative(input)) { + return LC_DOMAIN_UNPROVEN; + } + arb_sqrt(output, input, precision); + return LC_OK; +} + +lc_status +lc_exp(arb_t output, arb_srcptr input, slong precision) +{ + arb_exp(output, input, precision); + return LC_OK; +} + +lc_status +lc_log(arb_t output, arb_srcptr input, slong precision) +{ + if (!arb_is_positive(input)) { + return LC_DOMAIN_UNPROVEN; + } + arb_log(output, input, precision); + return LC_OK; +} + +lc_status +lc_sin(arb_t output, arb_srcptr input, slong precision) +{ + arb_sin(output, input, precision); + return LC_OK; +} + +lc_status +lc_cos(arb_t output, arb_srcptr input, slong precision) +{ + arb_cos(output, input, precision); + return LC_OK; +} + +lc_status +lc_abs(arb_t output, arb_srcptr input, slong precision) +{ + (void) precision; + arb_abs(output, input); + return LC_OK; +} + +lc_status +lc_sign(arb_t output, arb_srcptr input, slong precision) +{ + (void) precision; + arb_sgn(output, input); + return LC_OK; +} + +lc_status +lc_pow_pos(arb_t output, arb_srcptr base, arb_srcptr exponent, slong precision) +{ + arb_t logarithm; + + if (!arb_is_positive(base)) { + return LC_DOMAIN_UNPROVEN; + } + arb_init(logarithm); + arb_log(logarithm, base, precision); + arb_mul(logarithm, logarithm, exponent, precision); + arb_exp(output, logarithm, precision); + arb_clear(logarithm); + return LC_OK; +} + +lc_status +lc_pow_nn(arb_t output, arb_srcptr base, arb_srcptr exponent, slong precision) +{ + if (arb_is_zero(base) && arb_is_positive(exponent)) { + arb_zero(output); + return LC_OK; + } + return lc_pow_pos(output, base, exponent, precision); +} + +lc_status +lc_ratio0(arb_t output, arb_srcptr numerator, arb_srcptr denominator, slong precision) +{ + if (arb_is_zero(numerator) && arb_is_zero(denominator)) { + arb_zero(output); + return LC_OK; + } + if (!arb_is_positive(denominator)) { + return LC_DOMAIN_UNPROVEN; + } + arb_div(output, numerator, denominator, precision); + return LC_OK; +} diff --git a/proof/region/v1/arb/evaluator/interval.h b/proof/region/v1/arb/evaluator/interval.h new file mode 100644 index 00000000..88c936e1 --- /dev/null +++ b/proof/region/v1/arb/evaluator/interval.h @@ -0,0 +1,41 @@ +#ifndef LABCOLOR_ARB_INTERVAL_H +#define LABCOLOR_ARB_INTERVAL_H + +#include + +#include +#include + +typedef enum { + LC_OK = 0, + LC_DOMAIN_UNPROVEN = 1, + LC_INVALID_DYADIC = 2 +} lc_status; + +lc_status lc_set_dyadic_bits(arb_t output, uint64_t bits); +void lc_interval_get_dyadic_bounds( + fmpz_t lower, + fmpz_t upper, + fmpz_t exponent, + arb_srcptr value +); + +lc_status lc_add(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_sub(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_mul(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_div(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_min(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_max(arb_t output, arb_srcptr left, arb_srcptr right, slong precision); +lc_status lc_root3(arb_t output, arb_srcptr input, slong precision); +lc_status lc_sqrt(arb_t output, arb_srcptr input, slong precision); +lc_status lc_exp(arb_t output, arb_srcptr input, slong precision); +lc_status lc_log(arb_t output, arb_srcptr input, slong precision); +lc_status lc_sin(arb_t output, arb_srcptr input, slong precision); +lc_status lc_cos(arb_t output, arb_srcptr input, slong precision); +lc_status lc_abs(arb_t output, arb_srcptr input, slong precision); +lc_status lc_sign(arb_t output, arb_srcptr input, slong precision); +lc_status lc_pow_pos(arb_t output, arb_srcptr base, arb_srcptr exponent, slong precision); +lc_status lc_pow_nn(arb_t output, arb_srcptr base, arb_srcptr exponent, slong precision); +lc_status lc_ratio0(arb_t output, arb_srcptr numerator, arb_srcptr denominator, slong precision); + +#endif diff --git a/proof/region/v1/arb/evaluator/main.c b/proof/region/v1/arb/evaluator/main.c new file mode 100644 index 00000000..ac33f687 --- /dev/null +++ b/proof/region/v1/arb/evaluator/main.c @@ -0,0 +1,613 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hash.h" +#include "wire.h" + +typedef struct { + uint8_t *bytes; + size_t length; + size_t capacity; + size_t maximum; + bool limit_exceeded; + bool allocation_failed; +} byte_buffer; + +typedef enum { + LC_ARB_READ_OK = 0, + LC_ARB_READ_EMPTY = 1, + LC_ARB_READ_TOO_LARGE = 2, + LC_ARB_READ_IO_FAILED = 3, + LC_ARB_READ_ALLOCATION_FAILED = 4 +} lc_arb_read_status; + +typedef enum { + LC_ARB_EVALUATION_OK = 0, + LC_ARB_EVALUATION_RESOURCE_LIMIT = 1, + LC_ARB_EVALUATION_FAILED = 2 +} lc_arb_evaluation_status; + +static const uint8_t transcript_magic[8] = "LCTRN1\0"; +static const uint8_t accounting_domain[] = "labcolors.arb-evaluation-accounting.v1\0"; +static const uint8_t exact_trace_domain[] = + "labcolors.proof-region.exact-zero-signal-trace.v1\0"; +static const uint8_t boundary_enclosure_domain[] = + "labcolors.arb-boundary-enclosure.v1\0"; + +static void +buffer_clear(byte_buffer *buffer) +{ + free(buffer->bytes); + memset(buffer, 0, sizeof(*buffer)); +} + +static bool +buffer_reserve(byte_buffer *buffer, size_t additional) +{ + size_t required; + size_t capacity; + uint8_t *replacement; + + if (additional > SIZE_MAX - buffer->length) { + return false; + } + required = buffer->length + additional; + if (buffer->maximum != 0 && required > buffer->maximum) { + buffer->limit_exceeded = true; + return false; + } + if (required <= buffer->capacity) { + return required == 0 || buffer->bytes != NULL; + } + capacity = buffer->capacity == 0 ? 4096 : buffer->capacity; + while (capacity < required) { + if (capacity > SIZE_MAX / 2) { + capacity = required; + break; + } + capacity *= 2; + } + replacement = realloc(buffer->bytes, capacity); + if (replacement == NULL) { + buffer->allocation_failed = true; + return false; + } + buffer->bytes = replacement; + buffer->capacity = capacity; + return true; +} + +static bool +buffer_append(byte_buffer *buffer, const uint8_t *bytes, size_t length) +{ + if (length == 0) { + return true; + } + if (!buffer_reserve(buffer, length)) { + return false; + } + memcpy(buffer->bytes + buffer->length, bytes, length); + buffer->length += length; + return true; +} + +static bool +buffer_u64(byte_buffer *buffer, uint64_t value) +{ + uint8_t bytes[8]; + + lc_write_u64_be(bytes, value); + return buffer_append(buffer, bytes, sizeof(bytes)); +} + +static lc_arb_read_status +read_stdin(byte_buffer *input) +{ + uint8_t chunk[16384]; + + for (;;) { + ssize_t count = read(STDIN_FILENO, chunk, sizeof(chunk)); + + if (count < 0) { + if (errno == EINTR) { + continue; + } + return LC_ARB_READ_IO_FAILED; + } + if (count == 0) { + return input->length == 0 ? LC_ARB_READ_EMPTY : LC_ARB_READ_OK; + } + if (input->maximum != 0 + && (size_t) count > input->maximum - input->length) { + return LC_ARB_READ_TOO_LARGE; + } + if (!buffer_append(input, chunk, (size_t) count)) { + return input->allocation_failed + ? LC_ARB_READ_ALLOCATION_FAILED + : LC_ARB_READ_IO_FAILED; + } + } +} + +static bool +digest_is_nonzero(const uint8_t digest[32]) +{ + uint8_t aggregate = 0; + + for (size_t index = 0; index < 32; ++index) { + aggregate |= digest[index]; + } + return aggregate != 0; +} + +static bool +parse_manifest_identity(const char *text, uint8_t identity[32]) +{ + uint8_t aggregate = 0; + + if (strlen(text) != 64) { + return false; + } + for (size_t index = 0; index < 32; ++index) { + uint8_t value = 0; + + for (size_t nibble = 0; nibble < 2; ++nibble) { + unsigned char character = (unsigned char) text[index * 2 + nibble]; + + value <<= 4; + if (character >= '0' && character <= '9') { + value |= (uint8_t) (character - '0'); + } else if (character >= 'a' && character <= 'f') { + value |= (uint8_t) (character - 'a' + 10); + } else { + return false; + } + } + identity[index] = value; + aggregate |= value; + } + return aggregate != 0; +} + +static bool +exact_trace_digest( + const lc_job *job, + uint32_t ordinal, + const lc_region_result *result, + uint8_t digest[32] +) +{ + lc_sha256_context context; + uint8_t encoded_ordinal[4]; + uint8_t encoded_branch[8]; + + /* + * Precision and enclosure belong to an engine run, not to the exact + * signal. Job, ordinal and the first exact branch select one replayable + * mathematical trace identically for Arb and an independent comparator. + */ + lc_write_u32_be(encoded_ordinal, ordinal); + lc_write_u64_be(encoded_branch, result->exact_branch); + lc_sha256_init(&context); + lc_sha256_update(&context, exact_trace_domain, sizeof(exact_trace_domain) - 1); + lc_sha256_update(&context, job->job_identity, 32); + lc_sha256_update(&context, encoded_ordinal, sizeof(encoded_ordinal)); + lc_sha256_update(&context, encoded_branch, sizeof(encoded_branch)); + lc_sha256_finish(&context, digest); + return digest_is_nonzero(digest); +} + +static bool +boundary_enclosure_digest( + const lc_job *job, + uint32_t ordinal, + uint32_t precision, + const lc_region_result *result, + uint8_t digest[32] +) +{ + lc_sha256_context context; + uint8_t encoded[9]; + fmpz_t lower; + fmpz_t upper; + fmpz_t exponent; + char *lower_text = NULL; + char *upper_text = NULL; + char *exponent_text = NULL; + bool success = false; + + lc_write_u32_be(encoded, ordinal); + lc_write_u32_be(encoded + 4, precision); + encoded[8] = (uint8_t) result->formula_status; + lc_sha256_init(&context); + lc_sha256_update( + &context, + boundary_enclosure_domain, + sizeof(boundary_enclosure_domain) - 1 + ); + lc_sha256_update(&context, job->job_identity, 32); + lc_sha256_update(&context, encoded, sizeof(encoded)); + encoded[0] = result->has_enclosure ? 1 : 0; + lc_sha256_update(&context, encoded, 1); + if (result->has_enclosure) { + uint8_t length[8]; + + fmpz_init(lower); + fmpz_init(upper); + fmpz_init(exponent); + lc_interval_get_dyadic_bounds(lower, upper, exponent, &result->enclosure); + lower_text = fmpz_get_str(NULL, 16, lower); + upper_text = fmpz_get_str(NULL, 16, upper); + exponent_text = fmpz_get_str(NULL, 16, exponent); + if (lower_text == NULL || upper_text == NULL || exponent_text == NULL) { + goto cleanup; + } + const char *values[3] = {lower_text, upper_text, exponent_text}; + for (size_t index = 0; index < 3; ++index) { + size_t text_length = strlen(values[index]); + + lc_write_u64_be(length, (uint64_t) text_length); + lc_sha256_update(&context, length, sizeof(length)); + lc_sha256_update(&context, (const uint8_t *) values[index], text_length); + } + } + lc_sha256_finish(&context, digest); + success = digest_is_nonzero(digest); + +cleanup: + if (result->has_enclosure) { + flint_free(exponent_text); + flint_free(upper_text); + flint_free(lower_text); + fmpz_clear(exponent); + fmpz_clear(upper); + fmpz_clear(lower); + } + return success; +} + +static void +account_point( + lc_sha256_context *accounting, + uint32_t ordinal, + uint32_t precision, + uint64_t consumed, + lc_region_outcome outcome +) +{ + uint8_t record[17]; + + lc_write_u32_be(record, ordinal); + lc_write_u32_be(record + 4, precision); + lc_write_u64_be(record + 8, consumed); + record[16] = (uint8_t) outcome; + lc_sha256_update(accounting, record, sizeof(record)); +} + +static bool +append_digest_witness( + byte_buffer *output, + uint8_t kind, + uint32_t ordinal, + const uint8_t digest[32] +) +{ + uint8_t record[37]; + + record[0] = kind; + lc_write_u32_be(record + 1, ordinal); + memcpy(record + 5, digest, 32); + return buffer_append(output, record, sizeof(record)); +} + +static bool +append_resource_witness( + byte_buffer *output, + uint32_t ordinal, + uint8_t scope, + uint64_t grant +) +{ + uint8_t record[22]; + + record[0] = 3; + lc_write_u32_be(record + 1, ordinal); + record[5] = scope; + lc_write_u64_be(record + 6, grant); + lc_write_u64_be(record + 14, grant); + return buffer_append(output, record, sizeof(record)); +} + +static uint64_t +lesser_u64(uint64_t left, uint64_t right) +{ + return left < right ? left : right; +} + +static lc_arb_evaluation_status +evaluate( + const lc_job *job, + const uint8_t comparator_identity[32], + byte_buffer *output +) +{ + lc_domain_iterator iterator; + lc_region_result result; + lc_sha256_context accounting; + uint64_t counters[4] = {0, 0, 0, 0}; + uint64_t equality_count = 0; + uint64_t witness_count = 0; + uint64_t global_remaining = job->policy.global_pregrant; + uint8_t accounting_digest[32]; + size_t decision_length; + size_t decision_offset; + size_t counters_offset; + size_t equality_count_offset; + size_t accounting_offset; + size_t witness_count_offset; + lc_arb_evaluation_status status = LC_ARB_EVALUATION_FAILED; + static const uint8_t zero_digest[32] = {0}; + + if (job->domain.point_count == 0 + || job->policy.precision_count == 0 + || job->domain.point_count > SIZE_MAX - 3) { + return LC_ARB_EVALUATION_FAILED; + } + decision_length = ((size_t) job->domain.point_count + 3) / 4; + if (decision_length == 0 + || !buffer_append(output, transcript_magic, sizeof(transcript_magic)) + || !buffer_append(output, job->job_identity, 32) + || !buffer_append(output, job->domain.identity, 32) + || !buffer_append(output, comparator_identity, 32) + || !buffer_u64(output, job->domain.point_count) + || !buffer_u64(output, decision_length)) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + decision_offset = output->length; + if (!buffer_reserve(output, decision_length) || output->bytes == NULL) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + memset(output->bytes + decision_offset, 0, decision_length); + output->length += decision_length; + counters_offset = output->length; + for (size_t index = 0; index < 4; ++index) { + if (!buffer_u64(output, 0)) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + } + equality_count_offset = output->length; + if (!buffer_u64(output, 0)) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + accounting_offset = output->length; + if (!buffer_append(output, zero_digest, sizeof(zero_digest))) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + witness_count_offset = output->length; + if (!buffer_u64(output, 0)) { + return output->limit_exceeded + ? LC_ARB_EVALUATION_RESOURCE_LIMIT + : LC_ARB_EVALUATION_FAILED; + } + lc_sha256_init(&accounting); + lc_sha256_update(&accounting, accounting_domain, sizeof(accounting_domain) - 1); + lc_sha256_update(&accounting, job->job_identity, 32); + lc_sha256_update(&accounting, job->domain.identity, 32); + lc_sha256_update(&accounting, job->policy.identity, 32); + lc_sha256_update(&accounting, comparator_identity, 32); + lc_region_result_init(&result); + lc_domain_iterator_init(&iterator, &job->domain); + for (uint64_t point_index = 0; point_index < job->domain.point_count; ++point_index) { + uint64_t point_grant = lesser_u64( + job->policy.per_point_work, + global_remaining + ); + uint64_t point_remaining = point_grant; + uint64_t point_consumed = 0; + uint8_t resource_scope = job->policy.per_point_work <= global_remaining + ? 1 + : 2; + uint32_t ordinal; + uint32_t final_precision = job->policy.precision_ladder[0]; + uint8_t rgb[3]; + + /* A point owns its ordinal-prefix pregrant even when it uses none. */ + global_remaining -= point_grant; + if (!lc_domain_iterator_next(&iterator, &ordinal)) { + goto cleanup; + } + lc_ordinal_to_rgb(ordinal, rgb); + for (size_t rung = 0; rung < job->policy.precision_count; ++rung) { + uint64_t grant = point_remaining; + + final_precision = job->policy.precision_ladder[rung]; + lc_region_evaluate_rgb( + &result, + rgb, + job->context, + job->surround, + &job->region, + (slong) final_precision, + grant + ); + if (result.consumed_branches > grant + || result.consumed_branches > point_remaining) { + goto cleanup; + } + point_remaining -= result.consumed_branches; + point_consumed += result.consumed_branches; + if (result.outcome != LC_REGION_BOUNDARY_UNPROVEN) { + break; + } + } + if ((unsigned) result.outcome > LC_REGION_RESOURCE_LIMIT_REACHED) { + goto cleanup; + } + output->bytes[decision_offset + point_index / 4] |= (uint8_t) result.outcome + << (6U - 2U * (unsigned) (point_index % 4)); + ++counters[result.outcome]; + account_point(&accounting, ordinal, final_precision, point_consumed, result.outcome); + if (result.outcome == LC_REGION_INSIDE && result.exact_boundary) { + uint8_t digest[32]; + + if (!exact_trace_digest(job, ordinal, &result, digest) + || !append_digest_witness(output, 1, ordinal, digest)) { + goto cleanup; + } + ++equality_count; + ++witness_count; + } else if (result.outcome == LC_REGION_BOUNDARY_UNPROVEN) { + uint8_t digest[32]; + + if (!boundary_enclosure_digest( + job, + ordinal, + final_precision, + &result, + digest + ) + || !append_digest_witness(output, 2, ordinal, digest)) { + goto cleanup; + } + ++witness_count; + } else if (result.outcome == LC_REGION_RESOURCE_LIMIT_REACHED) { + if (point_consumed != point_grant + || !append_resource_witness( + output, + ordinal, + resource_scope, + point_grant + )) { + goto cleanup; + } + ++witness_count; + } + } + lc_sha256_finish(&accounting, accounting_digest); + if (!digest_is_nonzero(accounting_digest)) { + goto cleanup; + } + for (size_t index = 0; index < 4; ++index) { + lc_write_u64_be(output->bytes + counters_offset + index * 8, counters[index]); + } + lc_write_u64_be(output->bytes + equality_count_offset, equality_count); + memcpy(output->bytes + accounting_offset, accounting_digest, 32); + lc_write_u64_be(output->bytes + witness_count_offset, witness_count); + status = LC_ARB_EVALUATION_OK; + +cleanup: + if (status != LC_ARB_EVALUATION_OK && output->limit_exceeded) { + status = LC_ARB_EVALUATION_RESOURCE_LIMIT; + } + lc_region_result_clear(&result); + return status; +} + +int +main(int argc, char **argv) +{ + byte_buffer input = {0}; + byte_buffer output = {0}; + lc_job job; + lc_wire_error error; + uint8_t comparator_identity[32]; + int status = LC_ARB_EXIT_INTERNAL_V1; + lc_arb_read_status read_status; + + input.maximum = (size_t) LC_ARB_MAX_JOB_BYTES_V1; + output.maximum = (size_t) LC_ARB_MAX_OUTPUT_BYTES_V1; + + /* The evaluator owns its versioned exit contract. A closed consumer must + surface as EPIPE/IO instead of escaping that contract as SIGPIPE. */ + if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) { + fputs("signal setup failed\n", stderr); + return LC_ARB_EXIT_INTERNAL_V1; + } + if (argc != 5 + || strcmp(argv[1], "--manifest-identity") != 0 + || !parse_manifest_identity(argv[2], comparator_identity) + || strcmp(argv[3], "--job") != 0 + || strcmp(argv[4], "/dev/stdin") != 0) { + fputs( + "usage: arb-evaluator --manifest-identity HEX64 --job /dev/stdin\n", + stderr + ); + return LC_ARB_EXIT_USAGE_V1; + } + read_status = read_stdin(&input); + if (read_status != LC_ARB_READ_OK) { + const char *reason; + + if (read_status == LC_ARB_READ_TOO_LARGE) { + reason = "input_limit"; + status = LC_ARB_EXIT_INPUT_LIMIT_V1; + } else if (read_status == LC_ARB_READ_EMPTY) { + reason = "empty_input"; + status = LC_ARB_EXIT_INPUT_REJECTED_V1; + } else if (read_status == LC_ARB_READ_ALLOCATION_FAILED) { + reason = "internal"; + status = LC_ARB_EXIT_INTERNAL_V1; + } else { + reason = "io"; + status = LC_ARB_EXIT_IO_V1; + } + + fprintf(stderr, "job read failed: %s\n", reason); + goto cleanup_input; + } + if (!lc_parse_job(&job, input.bytes, input.length, &error)) { + fprintf(stderr, "job rejected: %s\n", lc_wire_error_name(error)); + if (error == LC_WIRE_RESOURCE_LIMIT) { + status = LC_ARB_EXIT_RESOURCE_LIMIT_V1; + } else if (error == LC_WIRE_ALLOCATION_FAILED) { + status = LC_ARB_EXIT_INTERNAL_V1; + } else { + status = LC_ARB_EXIT_INPUT_REJECTED_V1; + } + goto cleanup_input; + } + lc_arb_evaluation_status evaluation = + evaluate(&job, comparator_identity, &output); + if (evaluation != LC_ARB_EVALUATION_OK) { + fprintf( + stderr, + "evaluation failed: %s\n", + evaluation == LC_ARB_EVALUATION_RESOURCE_LIMIT + ? "output_limit" + : "internal" + ); + status = evaluation == LC_ARB_EVALUATION_RESOURCE_LIMIT + ? LC_ARB_EXIT_OUTPUT_LIMIT_V1 + : LC_ARB_EXIT_INTERNAL_V1; + goto cleanup_job; + } + if (!lc_write_all(STDOUT_FILENO, output.bytes, output.length)) { + fputs("result write failed\n", stderr); + status = LC_ARB_EXIT_IO_V1; + goto cleanup_job; + } + status = 0; + +cleanup_job: + buffer_clear(&output); + lc_job_clear(&job); +cleanup_input: + buffer_clear(&input); + return status; +} diff --git a/proof/region/v1/arb/evaluator/region.c b/proof/region/v1/arb/evaluator/region.c new file mode 100644 index 00000000..ada1334c --- /dev/null +++ b/proof/region/v1/arb/evaluator/region.c @@ -0,0 +1,287 @@ +#include "region.h" + +#include + +#include "formula.h" + +static void +reset_result(lc_region_result *result) +{ + result->outcome = LC_REGION_BOUNDARY_UNPROVEN; + result->formula_status = LC_OK; + result->exact_boundary = false; + result->has_enclosure = false; + result->exact_branch = 0; + result->consumed_branches = 0; + arb_zero(&result->enclosure); +} + +static void +record_enclosure(lc_region_result *result, arb_srcptr value, slong precision) +{ + if (result->has_enclosure) { + arb_union(&result->enclosure, &result->enclosure, value, precision); + } else { + arb_set(&result->enclosure, value); + result->has_enclosure = true; + } +} + +bool +lc_region_init(lc_region *region, size_t knot_count) +{ + region->knots = NULL; + region->knot_count = 0; + arb_init(®ion->metric_aa); + arb_init(®ion->metric_ab); + arb_init(®ion->metric_bb); + if (knot_count == 0 || knot_count > SIZE_MAX / sizeof(*region->knots)) { + lc_region_clear(region); + return false; + } + region->knots = calloc(knot_count, sizeof(*region->knots)); + if (region->knots == NULL) { + lc_region_clear(region); + return false; + } + region->knot_count = knot_count; + for (size_t index = 0; index < knot_count; ++index) { + arb_init(®ion->knots[index].tone); + arb_init(®ion->knots[index].center_a); + arb_init(®ion->knots[index].center_b); + arb_init(®ion->knots[index].radius_squared); + } + return true; +} + +void +lc_region_clear(lc_region *region) +{ + if (region->knots != NULL) { + for (size_t index = 0; index < region->knot_count; ++index) { + arb_clear(®ion->knots[index].radius_squared); + arb_clear(®ion->knots[index].center_b); + arb_clear(®ion->knots[index].center_a); + arb_clear(®ion->knots[index].tone); + } + free(region->knots); + } + arb_clear(®ion->metric_bb); + arb_clear(®ion->metric_ab); + arb_clear(®ion->metric_aa); + region->knots = NULL; + region->knot_count = 0; +} + +void +lc_region_result_init(lc_region_result *result) +{ + arb_init(&result->enclosure); + reset_result(result); +} + +void +lc_region_result_clear(lc_region_result *result) +{ + arb_clear(&result->enclosure); +} + +static void +evaluate_singleton( + lc_region_result *result, + arb_srcptr point, + const lc_region *region, + slong precision, + uint64_t branch_grant +) +{ + arb_struct input[8]; + arb_t predicate; + + if (!arb_equal(point, ®ion->knots[0].tone)) { + result->outcome = arb_overlaps(point, ®ion->knots[0].tone) + ? LC_REGION_BOUNDARY_UNPROVEN + : LC_REGION_OUTSIDE; + return; + } + if (branch_grant == 0) { + result->outcome = LC_REGION_RESOURCE_LIMIT_REACHED; + return; + } + for (size_t index = 0; index < 8; ++index) { + arb_init(input + index); + } + arb_set(input + 0, point + 1); + arb_set(input + 1, point + 2); + arb_set(input + 2, ®ion->knots[0].center_a); + arb_set(input + 3, ®ion->knots[0].center_b); + arb_set(input + 4, ®ion->knots[0].radius_squared); + arb_set(input + 5, ®ion->metric_aa); + arb_set(input + 6, ®ion->metric_ab); + arb_set(input + 7, ®ion->metric_bb); + arb_init(predicate); + result->formula_status = lc_formula_singleton(predicate, input, precision); + result->consumed_branches = 1; + if (result->formula_status == LC_OK) { + record_enclosure(result, predicate, precision); + if (arb_is_nonpositive(predicate)) { + result->outcome = LC_REGION_INSIDE; + result->exact_boundary = arb_is_zero(predicate); + result->exact_branch = 0; + } else if (arb_is_positive(predicate)) { + result->outcome = LC_REGION_OUTSIDE; + } + } + arb_clear(predicate); + for (size_t index = 8; index-- != 0;) { + arb_clear(input + index); + } +} + +void +lc_region_decide( + lc_region_result *result, + arb_srcptr point, + const lc_region *region, + slong precision, + uint64_t branch_grant +) +{ + bool any_segment = false; + bool all_inside = true; + bool all_outside = true; + bool exact_zero = false; + bool outside_possible; + uint64_t exact_branch = 0; + arb_t segment_domain; + arb_t intersection; + + reset_result(result); + /* FLINT's two-bit minimum applies to the public decision entry point too; + otherwise a singleton can bypass the policy before any segment exists. */ + if (precision < 2) { + result->formula_status = LC_DOMAIN_UNPROVEN; + return; + } + if (region->knot_count == 1) { + evaluate_singleton(result, point, region, precision, branch_grant); + return; + } + if (region->knot_count < 2) { + result->formula_status = LC_DOMAIN_UNPROVEN; + return; + } + if (arb_lt(point, ®ion->knots[0].tone) + || arb_gt(point, ®ion->knots[region->knot_count - 1].tone)) { + result->outcome = LC_REGION_OUTSIDE; + return; + } + outside_possible = !arb_ge(point, ®ion->knots[0].tone) + || !arb_le(point, ®ion->knots[region->knot_count - 1].tone); + arb_init(segment_domain); + arb_init(intersection); + for (size_t index = 0; index + 1 < region->knot_count; ++index) { + const lc_region_knot *left = region->knots + index; + const lc_region_knot *right = region->knots + index + 1; + arb_struct input[14]; + arb_t predicate; + + arb_union(segment_domain, &left->tone, &right->tone, precision); + if (!arb_intersection(intersection, point, segment_domain, precision)) { + continue; + } + any_segment = true; + if (result->consumed_branches == branch_grant) { + result->outcome = LC_REGION_RESOURCE_LIMIT_REACHED; + goto cleanup; + } + for (size_t input_index = 0; input_index < 14; ++input_index) { + arb_init(input + input_index); + } + arb_set(input + 0, intersection); + arb_set(input + 1, point + 1); + arb_set(input + 2, point + 2); + arb_set(input + 3, &left->tone); + arb_set(input + 4, &right->tone); + arb_set(input + 5, &left->center_a); + arb_set(input + 6, &left->center_b); + arb_set(input + 7, &right->center_a); + arb_set(input + 8, &right->center_b); + arb_set(input + 9, &left->radius_squared); + arb_set(input + 10, &right->radius_squared); + arb_set(input + 11, ®ion->metric_aa); + arb_set(input + 12, ®ion->metric_ab); + arb_set(input + 13, ®ion->metric_bb); + arb_init(predicate); + result->formula_status = lc_formula_segment(predicate, input, precision); + ++result->consumed_branches; + if (result->formula_status == LC_OK) { + bool inside = arb_is_nonpositive(predicate); + bool outside = arb_is_positive(predicate); + bool branch_exact = arb_is_zero(predicate); + + record_enclosure(result, predicate, precision); + all_inside = all_inside && inside; + all_outside = all_outside && outside; + /* Strict segment order makes the first exact branch canonical. */ + if (branch_exact && !exact_zero) { + exact_branch = (uint64_t) index; + } + exact_zero = exact_zero || branch_exact; + } else { + all_inside = false; + all_outside = false; + } + arb_clear(predicate); + for (size_t input_index = 14; input_index-- != 0;) { + arb_clear(input + input_index); + } + } + if (!any_segment) { + result->outcome = LC_REGION_BOUNDARY_UNPROVEN; + } else if (all_outside) { + result->outcome = LC_REGION_OUTSIDE; + } else if (all_inside && !outside_possible) { + result->outcome = LC_REGION_INSIDE; + result->exact_boundary = exact_zero; + result->exact_branch = exact_branch; + } else { + result->outcome = LC_REGION_BOUNDARY_UNPROVEN; + } + +cleanup: + arb_clear(intersection); + arb_clear(segment_domain); +} + +void +lc_region_evaluate_rgb( + lc_region_result *result, + const uint8_t rgb[3], + arb_srcptr context, + uint8_t surround, + const lc_region *region, + slong precision, + uint64_t branch_grant +) +{ + arb_struct point[3]; + + reset_result(result); + /* FLINT defines two bits as its minimum working precision. Lower policy + rungs remain unresolved and must never enter Arb arithmetic. */ + if (precision < 2) { + result->formula_status = LC_DOMAIN_UNPROVEN; + return; + } + for (size_t index = 0; index < 3; ++index) { + arb_init(point + index); + } + result->formula_status = lc_formula_point(point, rgb, context, surround, precision); + if (result->formula_status == LC_OK) { + lc_region_decide(result, point, region, precision, branch_grant); + } + for (size_t index = 3; index-- != 0;) { + arb_clear(point + index); + } +} diff --git a/proof/region/v1/arb/evaluator/region.h b/proof/region/v1/arb/evaluator/region.h new file mode 100644 index 00000000..7a2252ed --- /dev/null +++ b/proof/region/v1/arb/evaluator/region.h @@ -0,0 +1,63 @@ +#ifndef LABCOLOR_ARB_REGION_H +#define LABCOLOR_ARB_REGION_H + +#include +#include +#include + +#include "interval.h" + +typedef enum { + LC_REGION_INSIDE = 0, + LC_REGION_OUTSIDE = 1, + LC_REGION_BOUNDARY_UNPROVEN = 2, + LC_REGION_RESOURCE_LIMIT_REACHED = 3 +} lc_region_outcome; + +typedef struct { + arb_struct tone; + arb_struct center_a; + arb_struct center_b; + arb_struct radius_squared; +} lc_region_knot; + +typedef struct { + arb_struct metric_aa; + arb_struct metric_ab; + arb_struct metric_bb; + lc_region_knot *knots; + size_t knot_count; +} lc_region; + +typedef struct { + lc_region_outcome outcome; + lc_status formula_status; + bool exact_boundary; + bool has_enclosure; + uint64_t exact_branch; + uint64_t consumed_branches; + arb_struct enclosure; +} lc_region_result; + +bool lc_region_init(lc_region *region, size_t knot_count); +void lc_region_clear(lc_region *region); +void lc_region_result_init(lc_region_result *result); +void lc_region_result_clear(lc_region_result *result); +void lc_region_decide( + lc_region_result *result, + arb_srcptr point, + const lc_region *region, + slong precision, + uint64_t branch_grant +); +void lc_region_evaluate_rgb( + lc_region_result *result, + const uint8_t rgb[3], + arb_srcptr context, + uint8_t surround, + const lc_region *region, + slong precision, + uint64_t branch_grant +); + +#endif diff --git a/proof/region/v1/arb/evaluator/wire.c b/proof/region/v1/arb/evaluator/wire.c new file mode 100644 index 00000000..f7c0358e --- /dev/null +++ b/proof/region/v1/arb/evaluator/wire.c @@ -0,0 +1,634 @@ +#include "wire.h" + +#include +#include +#include +#include + +#include "hash.h" + +typedef struct { + const uint8_t *bytes; + size_t length; + size_t offset; + lc_wire_error *error; +} reader; + +static const uint8_t job_magic[8] = {'L', 'C', 'J', 'O', 'B', '1', 0, 0}; +static const uint8_t domain_magic[8] = {'L', 'C', 'D', 'O', 'M', '1', 0, 0}; +static const uint8_t policy_magic[8] = {'L', 'C', 'P', 'O', 'L', '1', 0, 0}; +static const uint8_t definition_domain[] = "labcolors.contextual-region-family-provider.v1\0"; +static const uint8_t formula_domain[] = "labcolors.nominal-exact-real-lift.ascii-ssa.v1\0"; +static const uint8_t domain_identity_label[] = "labcolors.proof-region.domain.v1\0"; +static const uint8_t policy_identity_label[] = "labcolors.proof-region.policy.v1\0"; +static const uint8_t job_identity_label[] = "labcolors.proof-region.job.v1\0"; +/* The registered V1 SSA has this exact wire length; changing either is a new + formula release, never a permissive parser adjustment. */ +static const size_t formula_spec_bytes_v1 = 24434; +static const uint8_t formula_release_v1[32] = { + 0x2c, 0x62, 0x6d, 0x8e, 0xe6, 0x0e, 0xeb, 0x62, + 0xae, 0x4d, 0xb5, 0x36, 0x60, 0xd6, 0x1b, 0xbc, + 0x25, 0xe0, 0xef, 0xd4, 0xe5, 0x57, 0xf0, 0xdc, + 0x1e, 0x77, 0x56, 0x5c, 0x13, 0x0b, 0x6e, 0x52, +}; + +static bool +reject(reader *input, lc_wire_error error) +{ + if (*input->error == LC_WIRE_OK) { + *input->error = error; + } + return false; +} + +static size_t +remaining(const reader *input) +{ + return input->length - input->offset; +} + +static bool +take(reader *input, size_t length, lc_slice *slice) +{ + if (length > remaining(input)) { + return reject(input, LC_WIRE_TRUNCATED); + } + slice->bytes = input->bytes + input->offset; + slice->length = length; + input->offset += length; + return true; +} + +static bool +expect(reader *input, const uint8_t *bytes, size_t length, lc_wire_error error) +{ + lc_slice actual; + + return take(input, length, &actual) + && (memcmp(actual.bytes, bytes, length) == 0 || reject(input, error)); +} + +static bool +read_u8(reader *input, uint8_t *value) +{ + lc_slice bytes; + + if (!take(input, 1, &bytes)) { + return false; + } + *value = bytes.bytes[0]; + return true; +} + +static bool +read_u32(reader *input, uint32_t *value) +{ + lc_slice bytes; + + if (!take(input, 4, &bytes)) { + return false; + } + *value = ((uint32_t) bytes.bytes[0] << 24) + | ((uint32_t) bytes.bytes[1] << 16) + | ((uint32_t) bytes.bytes[2] << 8) + | (uint32_t) bytes.bytes[3]; + return true; +} + +static bool +read_u64(reader *input, uint64_t *value) +{ + lc_slice bytes; + uint64_t result = 0; + + if (!take(input, 8, &bytes)) { + return false; + } + for (size_t index = 0; index < 8; ++index) { + result = (result << 8) | bytes.bytes[index]; + } + *value = result; + return true; +} + +static bool +read_blob(reader *input, size_t exact_length, lc_slice *value) +{ + uint64_t declared; + + if (!read_u64(input, &declared)) { + return false; + } + if (declared > SIZE_MAX || (exact_length != SIZE_MAX && declared != exact_length)) { + return reject(input, LC_WIRE_LENGTH_OUT_OF_BOUNDS); + } + if ((size_t) declared > remaining(input)) { + return reject(input, LC_WIRE_LENGTH_OUT_OF_BOUNDS); + } + return take(input, (size_t) declared, value); +} + +static bool +finish(reader *input) +{ + return remaining(input) == 0 || reject(input, LC_WIRE_TRAILING_BYTES); +} + +void +lc_write_u32_be(uint8_t output[4], uint32_t value) +{ + output[0] = (uint8_t) (value >> 24); + output[1] = (uint8_t) (value >> 16); + output[2] = (uint8_t) (value >> 8); + output[3] = (uint8_t) value; +} + +void +lc_write_u64_be(uint8_t output[8], uint64_t value) +{ + for (size_t index = 0; index < 8; ++index) { + output[7 - index] = (uint8_t) (value >> (index * 8)); + } +} + +static void +content_identity( + const uint8_t *label, + size_t label_length, + const uint8_t *bytes, + size_t length, + uint8_t digest[32] +) +{ + lc_sha256_context context; + uint8_t encoded_length[8]; + + lc_write_u64_be(encoded_length, (uint64_t) length); + lc_sha256_init(&context); + lc_sha256_update(&context, label, label_length); + lc_sha256_update(&context, encoded_length, sizeof(encoded_length)); + lc_sha256_update(&context, bytes, length); + lc_sha256_finish(&context, digest); +} + +static bool +exact_bits(lc_slice field, arb_t output) +{ + uint64_t bits = 0; + + if (field.length != 8) { + return false; + } + for (size_t index = 0; index < 8; ++index) { + bits = (bits << 8) | field.bytes[index]; + } + return lc_set_dyadic_bits(output, bits) == LC_OK; +} + +static bool +is_one_byte(lc_slice field, uint8_t value) +{ + return field.length == 1 && field.bytes[0] == value; +} + +static bool +parse_definition(lc_job *job, lc_slice encoded, reader *outer) +{ + static const size_t lengths[22] = { + sizeof(definition_domain) - 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 4, + 8, 8, 1, 1, 1, 32, 1, 8, 8, 8, 8, + }; + reader input = {encoded.bytes, encoded.length, 0, outer->error}; + lc_slice fields[22]; + uint64_t knot_count; + arb_t determinant; + arb_t product; + arb_t one; + + for (size_t index = 0; index < 22; ++index) { + if (!read_blob(&input, lengths[index], fields + index)) { + return false; + } + } + knot_count = 0; + for (size_t index = 0; index < 8; ++index) { + knot_count = (knot_count << 8) | fields[21].bytes[index]; + } + if (knot_count == 0) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + if (knot_count > LC_ARB_MAX_KNOTS_V1) { + return reject(&input, LC_WIRE_RESOURCE_LIMIT); + } + if (knot_count > SIZE_MAX / 64 + || remaining(&input) != (size_t) knot_count * 64) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + if (memcmp(fields[0].bytes, definition_domain, sizeof(definition_domain) - 1) != 0) { + return reject(&input, LC_WIRE_UNKNOWN_RELEASE); + } + for (size_t index = 1; index <= 17; ++index) { + bool fixed_one = index == 1 || index == 2 || index == 3 || index == 4 + || index == 5 || index == 6 || index == 8 || index == 9 + || index == 14 || index == 15 || index == 17; + if (fixed_one && !is_one_byte(fields[index], 1)) { + return reject(&input, LC_WIRE_UNKNOWN_RELEASE); + } + } + if (memcmp(fields[7].bytes, "\x01\x01\x01\x01", 4) != 0 + || memcmp(fields[10].bytes, "\x01\x01\x01\x01", 4) != 0 + || fields[13].bytes[0] < 1 || fields[13].bytes[0] > 3 + || memcmp(fields[16].bytes, formula_release_v1, 32) != 0) { + return reject(&input, LC_WIRE_UNKNOWN_RELEASE); + } + + arb_init(job->context + 0); + arb_init(job->context + 1); + job->context_ready = true; + if (!exact_bits(fields[11], job->context + 0) + || !exact_bits(fields[12], job->context + 1) + || !arb_is_positive(job->context + 0) + || !arb_is_positive(job->context + 1)) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + arb_init(one); + arb_one(one); + if (!arb_le(job->context + 1, one)) { + arb_clear(one); + return reject(&input, LC_WIRE_NONCANONICAL); + } + arb_clear(one); + job->surround = fields[13].bytes[0]; + memcpy(job->formula_release, fields[16].bytes, 32); + + if (!lc_region_init(&job->region, (size_t) knot_count)) { + return reject(&input, LC_WIRE_ALLOCATION_FAILED); + } + job->region_ready = true; + if (!exact_bits(fields[18], &job->region.metric_aa) + || !exact_bits(fields[19], &job->region.metric_ab) + || !exact_bits(fields[20], &job->region.metric_bb) + || !arb_is_positive(&job->region.metric_aa)) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + arb_init(determinant); + arb_init(product); + /* Binary64 coordinates are exact dyadics. Exact precision keeps SPD + admission independent of their exponent span. */ + arb_mul( + determinant, + &job->region.metric_aa, + &job->region.metric_bb, + ARF_PREC_EXACT + ); + arb_mul(product, &job->region.metric_ab, &job->region.metric_ab, ARF_PREC_EXACT); + arb_sub(determinant, determinant, product, ARF_PREC_EXACT); + if (!arb_is_exact(determinant) || !arb_is_positive(determinant)) { + arb_clear(product); + arb_clear(determinant); + return reject(&input, LC_WIRE_NONCANONICAL); + } + arb_clear(product); + arb_clear(determinant); + + for (size_t index = 0; index < (size_t) knot_count; ++index) { + lc_slice knot[4]; + lc_region_knot *target = job->region.knots + index; + + for (size_t coordinate = 0; coordinate < 4; ++coordinate) { + if (!read_blob(&input, 8, knot + coordinate)) { + return false; + } + } + if (!exact_bits(knot[0], &target->tone) + || !exact_bits(knot[1], &target->center_a) + || !exact_bits(knot[2], &target->center_b) + || !exact_bits(knot[3], &target->radius_squared) + || !arb_is_nonnegative(&target->radius_squared) + || (index != 0 && !arb_lt(&job->region.knots[index - 1].tone, &target->tone))) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + } + return finish(&input); +} + +static bool +parse_domain(lc_domain *domain, lc_slice encoded, const uint8_t expected[32], reader *outer) +{ + reader input = {encoded.bytes, encoded.length, 0, outer->error}; + uint8_t release; + uint64_t range_count; + uint64_t maximum; + uint64_t total = 0; + + if (!expect(&input, domain_magic, sizeof(domain_magic), LC_WIRE_BAD_MAGIC) + || !read_u8(&input, &release) || release != 1 + || !read_u64(&input, &domain->point_count) + || domain->point_count == 0 || domain->point_count > UINT64_C(0x1000000) + || !read_u64(&input, &range_count)) { + return *input.error != LC_WIRE_OK + ? false + : reject(&input, LC_WIRE_NONCANONICAL); + } + maximum = domain->point_count; + if (UINT64_C(0x1000001) - domain->point_count < maximum) { + maximum = UINT64_C(0x1000001) - domain->point_count; + } + if (range_count == 0 || range_count > maximum || range_count > SIZE_MAX / sizeof(*domain->ranges) + || range_count > remaining(&input) / 8 || (size_t) range_count * 8 != remaining(&input)) { + return reject(&input, LC_WIRE_LENGTH_OUT_OF_BOUNDS); + } + domain->ranges = calloc((size_t) range_count, sizeof(*domain->ranges)); + if (domain->ranges == NULL) { + return reject(&input, LC_WIRE_ALLOCATION_FAILED); + } + domain->range_count = (size_t) range_count; + for (size_t index = 0; index < domain->range_count; ++index) { + lc_ordinal_range *range = domain->ranges + index; + + if (!read_u32(&input, &range->start) || !read_u32(&input, &range->end)) { + return false; + } + if (range->start >= range->end || range->end > UINT32_C(0x1000000) + || (index != 0 && range->start <= domain->ranges[index - 1].end)) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + total += (uint64_t) range->end - range->start; + } + if (total != domain->point_count || !finish(&input)) { + return *input.error != LC_WIRE_OK + ? false + : reject(&input, LC_WIRE_NONCANONICAL); + } + content_identity( + domain_identity_label, + sizeof(domain_identity_label) - 1, + encoded.bytes, + encoded.length, + domain->identity + ); + return memcmp(domain->identity, expected, 32) == 0 + || reject(&input, LC_WIRE_DIGEST_MISMATCH); +} + +static bool +parse_policy(lc_arb_policy *policy, lc_slice encoded, const uint8_t expected[32], reader *outer) +{ + reader input = {encoded.bytes, encoded.length, 0, outer->error}; + uint8_t equality_release; + uint8_t comparator_count; + + if (!expect(&input, policy_magic, sizeof(policy_magic), LC_WIRE_BAD_MAGIC) + || !read_u8(&input, &equality_release) + || !read_u8(&input, &comparator_count)) { + return false; + } + if (equality_release != 1 || comparator_count != 2) { + return reject(&input, LC_WIRE_UNKNOWN_RELEASE); + } + for (uint8_t expected_kind = 1; expected_kind <= 2; ++expected_kind) { + uint8_t kind; + uint32_t rung_count; + uint32_t previous = 0; + size_t minimum_tail; + + if (!read_u8(&input, &kind) || !read_u32(&input, &rung_count)) { + return false; + } + minimum_tail = expected_kind == 1 ? 41 : 16; + if (kind != expected_kind || rung_count == 0 || remaining(&input) < minimum_tail + || rung_count > (remaining(&input) - minimum_tail) / 4) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + if (rung_count > LC_ARB_MAX_POLICY_RUNGS_V1) { + return reject(&input, LC_WIRE_RESOURCE_LIMIT); + } + if (expected_kind == 1) { + /* The rung count is already bounded by + LC_ARB_MAX_POLICY_RUNGS_V1 above, so the ladder allocation + cannot overflow size_t; a separate overflow guard would be + provably dead code. */ + policy->precision_ladder = calloc(rung_count, sizeof(*policy->precision_ladder)); + if (policy->precision_ladder == NULL) { + return reject(&input, LC_WIRE_ALLOCATION_FAILED); + } + policy->precision_count = rung_count; + } + for (size_t index = 0; index < rung_count; ++index) { + uint32_t precision; + + if (!read_u32(&input, &precision)) { + return false; + } + if (precision == 0 || (index != 0 && precision <= previous)) { + return reject(&input, LC_WIRE_NONCANONICAL); + } + if (precision > LC_ARB_MAX_PRECISION_BITS_V1) { + return reject(&input, LC_WIRE_RESOURCE_LIMIT); + } + if (expected_kind == 1) { + policy->precision_ladder[index] = precision; + } + previous = precision; + } + if (expected_kind == 1) { + if (!read_u64(&input, &policy->per_point_work) + || !read_u64(&input, &policy->global_pregrant)) { + return false; + } + } else { + uint64_t ignored; + + if (!read_u64(&input, &ignored) || !read_u64(&input, &ignored)) { + return false; + } + } + } + if (!finish(&input)) { + return false; + } + content_identity( + policy_identity_label, + sizeof(policy_identity_label) - 1, + encoded.bytes, + encoded.length, + policy->identity + ); + return memcmp(policy->identity, expected, 32) == 0 + || reject(&input, LC_WIRE_DIGEST_MISMATCH); +} + +static bool +formula_release(lc_slice formula, uint8_t digest[32]) +{ + lc_sha256_context context; + uint8_t length[8]; + + lc_write_u64_be(length, (uint64_t) formula.length); + lc_sha256_init(&context); + lc_sha256_update(&context, formula_domain, sizeof(formula_domain) - 1); + lc_sha256_update(&context, length, sizeof(length)); + lc_sha256_update(&context, formula.bytes, formula.length); + lc_sha256_finish(&context, digest); + return memcmp(digest, formula_release_v1, 32) == 0; +} + +bool +lc_parse_job( + lc_job *job, + const uint8_t *bytes, + size_t length, + lc_wire_error *error +) +{ + reader input; + lc_slice definition; + lc_slice formula; + lc_slice domain; + lc_slice policy; + lc_slice definition_digest; + lc_slice declared_formula_release; + lc_slice domain_identity; + lc_slice policy_identity; + uint8_t actual[32]; + + memset(job, 0, sizeof(*job)); + *error = LC_WIRE_OK; + if (length > (size_t) LC_ARB_MAX_JOB_BYTES_V1) { + *error = LC_WIRE_RESOURCE_LIMIT; + return false; + } + input = (reader) {bytes, length, 0, error}; + if (!expect(&input, job_magic, sizeof(job_magic), LC_WIRE_BAD_MAGIC) + || !take(&input, 32, &definition_digest) + || !read_blob(&input, SIZE_MAX, &definition) + || !take(&input, 32, &declared_formula_release) + || !read_blob(&input, formula_spec_bytes_v1, &formula) + || !take(&input, 32, &domain_identity) + || !read_blob(&input, SIZE_MAX, &domain) + || !take(&input, 32, &policy_identity) + || !read_blob(&input, SIZE_MAX, &policy) + || !finish(&input)) { + lc_job_clear(job); + return false; + } + lc_sha256(definition.bytes, definition.length, actual); + if (memcmp(actual, definition_digest.bytes, 32) != 0) { + *error = LC_WIRE_DIGEST_MISMATCH; + lc_job_clear(job); + return false; + } + if (!parse_definition(job, definition, &input) + || memcmp(declared_formula_release.bytes, job->formula_release, 32) != 0 + || !formula_release(formula, actual) + || memcmp(actual, declared_formula_release.bytes, 32) != 0 + || !parse_domain(&job->domain, domain, domain_identity.bytes, &input) + || !parse_policy(&job->policy, policy, policy_identity.bytes, &input)) { + if (*error == LC_WIRE_OK) { + *error = LC_WIRE_DIGEST_MISMATCH; + } + lc_job_clear(job); + return false; + } + content_identity( + job_identity_label, + sizeof(job_identity_label) - 1, + bytes, + length, + job->job_identity + ); + return true; +} + +void +lc_job_clear(lc_job *job) +{ + free(job->policy.precision_ladder); + free(job->domain.ranges); + if (job->region_ready) { + lc_region_clear(&job->region); + } + if (job->context_ready) { + arb_clear(job->context + 1); + arb_clear(job->context + 0); + } + memset(job, 0, sizeof(*job)); +} + +void +lc_domain_iterator_init(lc_domain_iterator *iterator, const lc_domain *domain) +{ + iterator->domain = domain; + iterator->range_index = 0; + iterator->ordinal = domain->ranges[0].start; + iterator->emitted = 0; +} + +bool +lc_domain_iterator_next(lc_domain_iterator *iterator, uint32_t *ordinal) +{ + if (iterator->emitted == iterator->domain->point_count) { + return false; + } + *ordinal = iterator->ordinal; + ++iterator->emitted; + ++iterator->ordinal; + if (iterator->ordinal == iterator->domain->ranges[iterator->range_index].end + && iterator->emitted != iterator->domain->point_count) { + /* Canonical parsing proves ordered disjoint ranges whose sizes sum to + point_count, so remaining output implies that a next range exists. */ + ++iterator->range_index; + iterator->ordinal = iterator->domain->ranges[iterator->range_index].start; + } + return true; +} + +void +lc_ordinal_to_rgb(uint32_t ordinal, uint8_t rgb[3]) +{ + rgb[0] = (uint8_t) (ordinal >> 16); + rgb[1] = (uint8_t) (ordinal >> 8); + rgb[2] = (uint8_t) ordinal; +} + +const char * +lc_wire_error_name(lc_wire_error error) +{ + static const char *const names[] = { + "ok", + "truncated", + "trailing_bytes", + "length_out_of_bounds", + "bad_magic", + "unknown_release", + "noncanonical", + "digest_mismatch", + "allocation_failed", + "resource_limit", + }; + + return (unsigned) error < sizeof(names) / sizeof(names[0]) + ? names[error] + : "unknown_wire_error"; +} + +bool +lc_write_all(int descriptor, const uint8_t *bytes, size_t length) +{ + while (length != 0) { + ssize_t written = write(descriptor, bytes, length); + + if (written < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (written == 0) { + return false; + } + bytes += (size_t) written; + length -= (size_t) written; + } + return true; +} diff --git a/proof/region/v1/arb/evaluator/wire.h b/proof/region/v1/arb/evaluator/wire.h new file mode 100644 index 00000000..2b1f0946 --- /dev/null +++ b/proof/region/v1/arb/evaluator/wire.h @@ -0,0 +1,106 @@ +#ifndef LABCOLOR_ARB_WIRE_H +#define LABCOLOR_ARB_WIRE_H + +#include +#include +#include + +#include "region.h" + +/* + * M2a's direct executable has an explicit operational admission profile. + * These bounds cap transport and transcript storage; they do not change the + * mathematical wire grammar or the set of contextual regions it describes. + * The controller and executor must bind the same profile before minting an + * observation. + */ +#define LC_ARB_MAX_JOB_BYTES_V1 \ + (UINT64_C(16) * UINT64_C(1024) * UINT64_C(1024)) +#define LC_ARB_MAX_OUTPUT_BYTES_V1 \ + (UINT64_C(16) * UINT64_C(1024) * UINT64_C(1024)) +#define LC_ARB_MAX_PRECISION_BITS_V1 UINT32_C(4096) +#define LC_ARB_MAX_POLICY_RUNGS_V1 UINT32_C(32) +#define LC_ARB_MAX_KNOTS_V1 UINT64_C(1024) + +#define LC_ARB_EXIT_USAGE_V1 64 +#define LC_ARB_EXIT_INPUT_REJECTED_V1 65 +#define LC_ARB_EXIT_INPUT_LIMIT_V1 66 +#define LC_ARB_EXIT_OUTPUT_LIMIT_V1 67 +#define LC_ARB_EXIT_RESOURCE_LIMIT_V1 68 +#define LC_ARB_EXIT_INTERNAL_V1 70 +#define LC_ARB_EXIT_IO_V1 74 + +typedef enum { + LC_WIRE_OK = 0, + LC_WIRE_TRUNCATED = 1, + LC_WIRE_TRAILING_BYTES = 2, + LC_WIRE_LENGTH_OUT_OF_BOUNDS = 3, + LC_WIRE_BAD_MAGIC = 4, + LC_WIRE_UNKNOWN_RELEASE = 5, + LC_WIRE_NONCANONICAL = 6, + LC_WIRE_DIGEST_MISMATCH = 7, + LC_WIRE_ALLOCATION_FAILED = 8, + LC_WIRE_RESOURCE_LIMIT = 9 +} lc_wire_error; + +typedef struct { + const uint8_t *bytes; + size_t length; +} lc_slice; + +typedef struct { + uint32_t start; + uint32_t end; +} lc_ordinal_range; + +typedef struct { + lc_ordinal_range *ranges; + size_t range_count; + uint64_t point_count; + uint8_t identity[32]; +} lc_domain; + +typedef struct { + uint32_t *precision_ladder; + size_t precision_count; + uint64_t per_point_work; + uint64_t global_pregrant; + uint8_t identity[32]; +} lc_arb_policy; + +typedef struct { + lc_region region; + arb_struct context[2]; + uint8_t surround; + lc_domain domain; + lc_arb_policy policy; + uint8_t formula_release[32]; + uint8_t job_identity[32]; + bool context_ready; + bool region_ready; +} lc_job; + +typedef struct { + const lc_domain *domain; + size_t range_index; + uint32_t ordinal; + uint64_t emitted; +} lc_domain_iterator; + +bool lc_parse_job( + lc_job *job, + const uint8_t *bytes, + size_t length, + lc_wire_error *error +); +void lc_job_clear(lc_job *job); +void lc_domain_iterator_init(lc_domain_iterator *iterator, const lc_domain *domain); +bool lc_domain_iterator_next(lc_domain_iterator *iterator, uint32_t *ordinal); +void lc_ordinal_to_rgb(uint32_t ordinal, uint8_t rgb[3]); +const char *lc_wire_error_name(lc_wire_error error); + +bool lc_write_all(int descriptor, const uint8_t *bytes, size_t length); +void lc_write_u32_be(uint8_t output[4], uint32_t value); +void lc_write_u64_be(uint8_t output[8], uint64_t value); + +#endif diff --git a/proof/region/v1/arb/keys/gmp.asc b/proof/region/v1/arb/keys/gmp.asc new file mode 100644 index 00000000..e93791c6 --- /dev/null +++ b/proof/region/v1/arb/keys/gmp.asc @@ -0,0 +1,36 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQFNBFDrIWMBCgCyyYoTAD/aL6Yl90eSJ1xuFpODTcwyRZsNSUZKSmKwnqXo9LgS +2B00yVZ2nO2OrSmWPiYikTciitv04bAqFaggSstx6hlni6n3h2PL0jXpf9EI6qOO +oKwi2IVtbBnJAhWpfRcAce6WEqvnav6KjuBM3lr8/5GzDV8tm6+X/G/paTnBqTB9 +pBxrH7smB+iRjDt/6ykWkbYLd6uBKzIkAp4HqAZb/aZMvxI28PeWGjZJQYq2nVPf +LroM6Ub/sNlXpv/bmHJusFQjUL368njhZD1+aVLCUfBCCDzvZc3EYt3wBkbmuCiA +xOb9ramHgiVkNENtzXR+sbQHtKRQv/jllY1qxROM2/rWmL+HohdxL5E0VPple2bg +U/zqX0Hg2byb8FbpzPJO5PnBD+1PME3Uirsly4N7XT80OvhXlYe4t+9X0QARAQAB +tCROaWVscyBNw7ZsbGVyIDxuaXNzZUBseXNhdG9yLmxpdS5zZT6JAZUEEwEIAD8C +GwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAFiEENDwv8PvuXsLtvvOZ81mf+CjG +cpgFAl4h6wsFCRacyygACgkQ81mf+CjGcpjoSwoAmooT2ZjT3zA/km9iJ9pDEZov +gOyVlTSZdohKWp5xtI8C59uZuxuHV9iJigyNWnIBVBr8FjL6Zx5paNQ19SllE1bY +xL4J0jw5j0BP0odT5jORkIsylcKHmR+eSqJiSMvHGsd821UTagYcJu6emat+Kcwn +DHkKPjbEoRmi46n5UzIEG+uHv0sGZUjWZshTCQZVBnJj4sDNJl+kCbYTpUs0f2AE +PjKH6pBk56vIKBP/bNWs2Q2s+VdA7/g5A1N0SkaPt3/+qNslu84qRdIFcqc54stm +R//Qa3C1EBxrrT2P3EzzpkHWxO72jaGlwuN6utX+7YuNe5Cy5ls/BSjugKMiRqBE +AYvFmnbKV2eJS1bqTSR+qTzLn+VS88yvdumAHNNOPsJyMmKPxJD08maMCsqOOys3 +TMl5J+Yz5bSPJQAZ7mu5AU0EUOshYwEKAMqU40j7kGpy7r37vZ+Ytk+LPMRSwhED +ZjTDZETv64nkSz39hOnk+dYA2k9PsZLwkmdzo0kl6HoaQyQYbCrk6nsIOyNb2lBn +S8Bb3ReOfKeINr1bRb6bn5f8s87OH6eKz1lx/Xs/3W2mssIuL5M45vfnG3f3qln5 +L4/C5XR0uIhh1VhXd7os0JXQuOESqnndNHBOstM09BWe3QM9hOH8qfXHp3nM5LQw +rhDJso3VYlTqdghBFfJYqSLGNuz76NyBX+O5yT3pV7RuW+foN+p+kbxjNuapEK58 +ujrzcu2UFRnRz7OesPWei6pfYRv8LKUbxDxlQdeKYIn6DpF8f2Q6a1Uf/bTy7+cO +h9Uv9DR28Bd9Tkxfj1ztdjLsHatOWT7ie415oczRpTZjXj5JDL6xHrPJ27t4Yt2q +PNXQJf96SCuNABEBAAGJAXwEGAEIACYCGwwWIQQ0PC/w++5ewu2+85nzWZ/4KMZy +mAUCXiHtDwUJFpzNLAAKCRDzWZ/4KMZymFJeCgCIHV4v0PhMU92bROWeZRUPsMIJ +kSi53NMq7ztneDCTbfksvxGSt3W8yERVj2bpGEYNumOMkopb/INxauW2otmn7/lq +N99toS9UWr26SLSGGw0OO4I/QJVsmPCDeLsdwDiOpuA4tvYrRuYfRvJ2P7839ktT +MZ54Cj1XJtds4LUqEPVW8eFGX8IcqrP1aiLDzYgufQLLmo+OTxhF9iQVBzRgc3PM +V3yVr/yXod4mQJGWU0vt1N0tff6dvQoQwUQswMo5UDz5BSwbSQsp/J7fKRmayQSW +8g05NxluhXDoiPh6r59XgCRgvv8uc0U3Bvu8PqN2dZxiAwQaNEL4WEqfZqzozjlK +aosC2vbrYplaC2IHPARcmDmxioKPJdFjDKdDOorXLTejndVsPK2NW6sB+bh3akNt +3lIXaMiLvAfNoFNnWg== +=FW3C +-----END PGP PUBLIC KEY BLOCK----- diff --git a/proof/region/v1/arb/keys/mpfr.asc b/proof/region/v1/arb/keys/mpfr.asc new file mode 100644 index 00000000..9681e510 --- /dev/null +++ b/proof/region/v1/arb/keys/mpfr.asc @@ -0,0 +1,21 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYweR+BYJKwYBBAHaRw8BAQdAo8zZnH90b32CtE+OOvk+OgdGxLDRDgm0PC/H +5lwgkm20JFZpbmNlbnQgTGVmZXZyZSA8dmluY2VudEB2aW5jMTcubmV0PoiTBBMW +CAA7AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEpTS+P4PiQdkYKArrWDHR +Gg1NsCoFAmMHlsMCGQEACgkQWDHRGg1NsCrGvQD/dN7dyWX1soay9vDjFAkyDX5O +acyJyRc7aiP555IBb8cBALsg/fSngQDyBeFyTb+jPK+N5gjNTdkGyMCnIlG9LqIE +tC1WaW5jZW50IExlZmV2cmUgPFZpbmNlbnQuTGVmZXZyZUBlbnMtbHlvbi5mcj6I +kAQTFggAOBYhBKU0vj+D4kHZGCgK61gx0RoNTbAqBQJjB5YyAhsDBQsJCAcCBhUK +CQgLAgQWAgMBAh4BAheAAAoJEFgx0RoNTbAqcwoA/RGKEwncAU9UtSVEDSNKGNv9 +Qj4cqBrEvweIWYO97iH0AP4tWPrKZtMiOi9lasyyPJAXqqYMgfsxVfYZr1I0taB+ +C7QqVmluY2VudCBMZWZldnJlIDxWaW5jZW50LkxlZmV2cmVAaW5yaWEuZnI+iJAE +ExYIADgWIQSlNL4/g+JB2RgoCutYMdEaDU2wKgUCYweWEAIbAwULCQgHAgYVCgkI +CwIEFgIDAQIeAQIXgAAKCRBYMdEaDU2wKtfBAP4xWrEvbuLr03iPr5yq46ld298r +WTo/L/XghLLcJHDyIQD8DLgv/4A9e8J+y+2VxU/tM9hEEE/OtFipHahlVlqMeQe4 +OARjB5H4EgorBgEEAZdVAQUBAQdAQA+SDNGmtq+LxAUvL1mWCUhicUWCIX8+d3bc +nN34+GkDAQgHiHgEGBYIACAWIQSlNL4/g+JB2RgoCutYMdEaDU2wKgUCYweR+AIb +DAAKCRBYMdEaDU2wKqGeAP4rKkunb9wTjtUyLiaJ6haNOEFnCVj4H06n3FL8f+Hz +tgD/aEyC0d0L3TEMXnGQhELJAYeoTKlUBvzfZ8dqenK0ZAw= +=3az7 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/proof/region/v1/arb/origin.py b/proof/region/v1/arb/origin.py new file mode 100644 index 00000000..e53a417d --- /dev/null +++ b/proof/region/v1/arb/origin.py @@ -0,0 +1,1187 @@ +#!/usr/bin/env python3 +"""Pure admission primitives for scoped source-integrity observations.""" + +from __future__ import annotations + +import base64 +import binascii +import hashlib +import os +import stat +import tempfile +from dataclasses import dataclass +from datetime import UTC, date, datetime +from enum import StrEnum +from functools import cmp_to_key +from pathlib import Path +from typing import NoReturn, Protocol + +import provenance + + +OPENPGP_V4_FINGERPRINT_BYTES = 20 +ARMOUR_BEGIN = b"-----BEGIN PGP PUBLIC KEY BLOCK-----" +ARMOUR_END = b"-----END PGP PUBLIC KEY BLOCK-----" +CRC24_INITIAL = 0xB704CE +CRC24_POLYNOMIAL = 0x1864CFB + + +class OriginReasonV1(StrEnum): + INVALID_ARMOUR = "invalid_armour" + ARMOUR_CRC_MISMATCH = "armour_crc_mismatch" + INVALID_FINGERPRINT = "invalid_fingerprint" + INVALID_STATUS = "invalid_status" + SIGNATURE_REJECTED = "signature_rejected" + COORDINATE_MISMATCH = "coordinate_mismatch" + VERIFIER_FAILED = "verifier_failed" + VERIFIER_UNAVAILABLE = "verifier_unavailable" + VERIFIER_OUTPUT_LIMIT = "verifier_output_limit" + VERIFIER_TIMEOUT = "verifier_timeout" + CONTENT_RELATION_MISMATCH = "content_relation_mismatch" + + +@dataclass(frozen=True) +class OriginErrorV1(ValueError): + reason: OriginReasonV1 + detail: str + + def __str__(self) -> str: + return f"{self.reason}: {self.detail}" + + +def _fail(reason: OriginReasonV1, detail: str) -> NoReturn: + raise OriginErrorV1(reason, detail) + + +def _crc24(payload: bytes) -> bytes: + value = CRC24_INITIAL + for byte in payload: + value ^= byte << 16 + for _ in range(8): + value <<= 1 + if value & 0x1000000: + value ^= CRC24_POLYNOMIAL + return (value & 0xFFFFFF).to_bytes(3, "big") + + +def decode_public_key_armour(armour: bytes) -> bytes: + """Decode the one canonical ASCII-armour shape stored by this proof lane.""" + + if type(armour) is not bytes or not armour.endswith(b"\n") or b"\r" in armour: + _fail(OriginReasonV1.INVALID_ARMOUR, "armour must be LF-terminated bytes") + try: + text = armour.decode("ascii") + except UnicodeDecodeError: + _fail(OriginReasonV1.INVALID_ARMOUR, "armour is not ASCII") + lines = text.split("\n") + if ( + len(lines) < 7 + or lines[0] != ARMOUR_BEGIN.decode("ascii") + or lines[1] != "" + or lines[-2] != ARMOUR_END.decode("ascii") + or lines[-1] != "" + ): + _fail(OriginReasonV1.INVALID_ARMOUR, "unexpected armour envelope") + body = lines[2:-3] + checksum = lines[-3] + if ( + not body + or any(len(line) != 64 for line in body[:-1]) + or not 1 <= len(body[-1]) <= 64 + or len(body[-1]) % 4 + or not checksum.startswith("=") + or len(checksum) != 5 + ): + _fail(OriginReasonV1.INVALID_ARMOUR, "noncanonical base64 body") + try: + packets = base64.b64decode("".join(body), validate=True) + expected_crc = base64.b64decode(checksum[1:], validate=True) + except (binascii.Error, ValueError): + _fail(OriginReasonV1.INVALID_ARMOUR, "invalid base64") + if not packets or len(expected_crc) != 3: + _fail(OriginReasonV1.INVALID_ARMOUR, "empty packets or invalid CRC") + if _crc24(packets) != expected_crc: + _fail(OriginReasonV1.ARMOUR_CRC_MISMATCH, "CRC-24 mismatch") + return packets + + +@dataclass(frozen=True) +class AcceptedHistoricalSignatureStatusV1: + signer_fingerprint: bytes + signature_unix_time: int + + def __post_init__(self) -> None: + if ( + type(self.signer_fingerprint) is not bytes + or len(self.signer_fingerprint) != OPENPGP_V4_FINGERPRINT_BYTES + or self.signer_fingerprint == bytes(OPENPGP_V4_FINGERPRINT_BYTES) + ): + raise TypeError("invalid signer fingerprint") + if type(self.signature_unix_time) is not int or self.signature_unix_time <= 0: + raise TypeError("invalid signature time") + + +_ALLOWED_STATUS_TAGS = frozenset( + ( + "NEWSIG", + "KEYEXPIRED", + "KEY_CONSIDERED", + "SIG_ID", + "EXPKEYSIG", + "GOODSIG", + "VALIDSIG", + ) +) +_REJECTED_STATUS_TAGS = frozenset( + ( + "BADSIG", + "ERRSIG", + "REVKEYSIG", + "KEYREVOKED", + "NO_PUBKEY", + "NODATA", + "FAILURE", + "ERROR", + ) +) + + +def _fingerprint(value: bytes) -> bytes: + if ( + type(value) is not bytes + or len(value) != OPENPGP_V4_FINGERPRINT_BYTES + or value == bytes(OPENPGP_V4_FINGERPRINT_BYTES) + ): + _fail(OriginReasonV1.INVALID_FINGERPRINT, "expected fingerprint length") + return value + + +def parse_gpgv_status( + status: bytes, expected_fingerprint: bytes +) -> AcceptedHistoricalSignatureStatusV1: + """Accept one historical machine-status shape; stderr has no authority.""" + + expected = _fingerprint(expected_fingerprint) + if ( + type(status) is not bytes + or not status.endswith(b"\n") + or b"\r" in status + or b"\0" in status + ): + _fail(OriginReasonV1.INVALID_STATUS, "status must be LF-terminated bytes") + lines = status[:-1].split(b"\n") + if not lines: + _fail(OriginReasonV1.INVALID_STATUS, "empty status") + + newsig_count = 0 + valid: list[tuple[bytes, int]] = [] + prefix = b"[GNUPG:] " + for line in lines: + if not line.startswith(prefix): + _fail(OriginReasonV1.INVALID_STATUS, "unframed output") + payload = line[len(prefix) :] + tag_bytes, separator, arguments = payload.partition(b" ") + try: + tag = tag_bytes.decode("ascii") + except UnicodeDecodeError: + _fail(OriginReasonV1.INVALID_STATUS, "non-ASCII tag") + if tag in _REJECTED_STATUS_TAGS: + _fail(OriginReasonV1.SIGNATURE_REJECTED, tag) + if tag not in _ALLOWED_STATUS_TAGS: + _fail(OriginReasonV1.INVALID_STATUS, f"unknown tag {tag}") + if tag == "NEWSIG": + newsig_count += 1 + continue + if not separator: + _fail(OriginReasonV1.INVALID_STATUS, f"missing arguments for {tag}") + if tag != "VALIDSIG": + continue + + fields = arguments.split(b" ") + if len(fields) != 10 or any(not item for item in fields): + _fail(OriginReasonV1.INVALID_STATUS, "invalid VALIDSIG fields") + try: + signer = bytes.fromhex(fields[0].decode("ascii")) + primary = bytes.fromhex(fields[9].decode("ascii")) + signature_time = int(fields[2], 10) + date_text = fields[1].decode("ascii") + parsed_date = date.fromisoformat(date_text) + except (UnicodeDecodeError, ValueError, OverflowError): + _fail(OriginReasonV1.INVALID_STATUS, "invalid VALIDSIG coordinate") + try: + timestamp_date = datetime.fromtimestamp(signature_time, UTC).date() + except (OverflowError, OSError, ValueError): + _fail(OriginReasonV1.INVALID_STATUS, "invalid VALIDSIG time range") + if ( + signer != expected + or primary != expected + or signature_time <= 0 + or parsed_date.isoformat() != date_text + or timestamp_date != parsed_date + ): + _fail(OriginReasonV1.SIGNATURE_REJECTED, "foreign signer or time") + valid.append((signer, signature_time)) + + if newsig_count != 1 or len(valid) != 1: + _fail(OriginReasonV1.SIGNATURE_REJECTED, "expected exactly one signature") + return AcceptedHistoricalSignatureStatusV1(valid[0][0], valid[0][1]) + + +def _digest(value: bytes, field: str) -> bytes: + if type(value) is not bytes or len(value) != 32 or value == bytes(32): + raise TypeError(f"invalid {field}") + return value + + +_GPGV_PROCESS_TOKEN = object() +_SIGNATURE_RELATION_TOKEN = object() + + +@dataclass(frozen=True, init=False) +class GpgvProcessObservationV1: + returncode: int + status: bytes + stderr: bytes + source_tree_identity: bytes + archive_sha256: bytes + signature_sha256: bytes + public_key_packets_sha256: bytes + executable_sha256: bytes + version_sha256: bytes + + def __init__( + self, + returncode: int, + status: bytes, + stderr: bytes, + source_tree_identity: bytes, + archive_sha256: bytes, + signature_sha256: bytes, + public_key_packets_sha256: bytes, + executable_sha256: bytes, + version_sha256: bytes, + *, + _token: object, + ) -> None: + if _token is not _GPGV_PROCESS_TOKEN: + raise TypeError("GpgvProcessObservationV1 is created only by run_gpgv") + if type(returncode) is not int or returncode < 0: + raise TypeError("invalid gpgv returncode") + if type(status) is not bytes or len(status) > 64 * 1024: + raise TypeError("invalid gpgv status") + if type(stderr) is not bytes or len(stderr) > 64 * 1024: + raise TypeError("invalid gpgv stderr") + _digest(source_tree_identity, "source tree identity") + _digest(archive_sha256, "source archive digest") + _digest(signature_sha256, "detached signature digest") + _digest(public_key_packets_sha256, "public key packets digest") + _digest(executable_sha256, "gpgv executable digest") + _digest(version_sha256, "gpgv version digest") + object.__setattr__(self, "returncode", returncode) + object.__setattr__(self, "status", status) + object.__setattr__(self, "stderr", stderr) + object.__setattr__(self, "source_tree_identity", source_tree_identity) + object.__setattr__(self, "archive_sha256", archive_sha256) + object.__setattr__(self, "signature_sha256", signature_sha256) + object.__setattr__( + self, + "public_key_packets_sha256", + public_key_packets_sha256, + ) + object.__setattr__(self, "executable_sha256", executable_sha256) + object.__setattr__(self, "version_sha256", version_sha256) + + +@dataclass(frozen=True, init=False) +class _SignatureRelationObservationV1: + archive_sha256: bytes + source_tree_identity: bytes + signature_sha256: bytes + public_key_packets_sha256: bytes + signer_fingerprint: bytes + signature_unix_time: int + verifier_executable_sha256: bytes + verifier_version_sha256: bytes + + def __init__( + self, + archive_sha256: bytes, + source_tree_identity: bytes, + signature_sha256: bytes, + public_key_packets_sha256: bytes, + signer_fingerprint: bytes, + signature_unix_time: int, + verifier_executable_sha256: bytes, + verifier_version_sha256: bytes, + *, + _token: object, + ) -> None: + if _token is not _SIGNATURE_RELATION_TOKEN: + raise TypeError("signature relation is created only by admission") + for field in ( + "archive_sha256", + "source_tree_identity", + "signature_sha256", + "public_key_packets_sha256", + "verifier_executable_sha256", + "verifier_version_sha256", + ): + _digest(locals()[field], field) + AcceptedHistoricalSignatureStatusV1( + signer_fingerprint, + signature_unix_time, + ) + for field in self.__dataclass_fields__: + object.__setattr__(self, field, locals()[field]) + + +class HistoricalPathRecheckedSignatureDiagnosticV1(_SignatureRelationObservationV1): + """Historical signature diagnostic; no current publisher trust is implied.""" + + +def admit_detached_signature_observation( + *, + expected: provenance.SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, + signature: bytes, + public_key_armour: bytes, + process: GpgvProcessObservationV1, +) -> HistoricalPathRecheckedSignatureDiagnosticV1: + """Replay one historical signature relation as a path-rechecked diagnostic. + + The result records what the invoked verifier reported for project-pinned + bytes. It does not establish current publisher identity, key status, or an + exact sealed verifier execution. + """ + + if type(expected) is not provenance.SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + if type(admitted) is not provenance.SafeSourceArchiveV1: + raise TypeError("admitted must be SafeSourceArchiveV1") + if type(expected.integrity) is not provenance.DetachedSignaturePolicyV1: + raise TypeError("source must declare a detached signature policy") + if type(signature) is not bytes: + raise TypeError("signature must be bytes") + if type(process) is not GpgvProcessObservationV1: + raise TypeError("process must be a sealed GpgvProcessObservationV1") + archive = admitted.archive_bytes + actual_archive_sha256 = hashlib.sha256(archive).digest() + actual_signature_sha256 = hashlib.sha256(signature).digest() + key_packets = decode_public_key_armour(public_key_armour) + actual_key_packets_sha256 = hashlib.sha256(key_packets).digest() + if ( + admitted.source_lock_identity != expected.identity + or admitted.archive_sha256 != expected.archive_sha256 + or actual_archive_sha256 != expected.archive_sha256 + or len(signature) != expected.integrity.signature_length + or actual_signature_sha256 != expected.integrity.signature_sha256 + or actual_key_packets_sha256 + != expected.integrity.public_key_packets_sha256 + or process.source_tree_identity != admitted.tree_identity + or process.archive_sha256 != actual_archive_sha256 + or process.signature_sha256 != actual_signature_sha256 + or process.public_key_packets_sha256 != actual_key_packets_sha256 + ): + _fail(OriginReasonV1.COORDINATE_MISMATCH, "source, signature, key, or replay") + if process.returncode != 0: + _fail(OriginReasonV1.VERIFIER_FAILED, f"gpgv exit {process.returncode}") + signature_observation = parse_gpgv_status( + process.status, + expected.integrity.signer_fingerprint, + ) + return HistoricalPathRecheckedSignatureDiagnosticV1( + actual_archive_sha256, + admitted.tree_identity, + actual_signature_sha256, + actual_key_packets_sha256, + signature_observation.signer_fingerprint, + signature_observation.signature_unix_time, + process.executable_sha256, + process.version_sha256, + _token=_SIGNATURE_RELATION_TOKEN, + ) + + +def _read_regular_file_descriptor(descriptor: int) -> bytes: + metadata = os.fstat(descriptor) + if not stat.S_ISREG(metadata.st_mode) or metadata.st_size <= 0: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "gpgv is not a regular file") + chunks: list[bytes] = [] + offset = 0 + while offset < metadata.st_size: + chunk = os.pread(descriptor, min(64 * 1024, metadata.st_size - offset), offset) + if not chunk: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "short gpgv read") + chunks.append(chunk) + offset += len(chunk) + return b"".join(chunks) + +@dataclass(frozen=True) +class DiagnosticProcessRequestV1: + """Client-owned diagnostic execution request with no authority semantics.""" + + argv: tuple[str, ...] + stdin: bytes | None + cwd: Path + environment: dict[str, str] + pass_fds: tuple[int, ...] + timeout_seconds: int | float + stdout_limit: int + stderr_limit: int + + def __post_init__(self) -> None: + if ( + type(self.argv) is not tuple + or not self.argv + or any(type(item) is not str or not item for item in self.argv) + or (self.stdin is not None and type(self.stdin) is not bytes) + or not isinstance(self.cwd, Path) + or type(self.environment) is not dict + or any( + type(key) is not str or type(value) is not str + for key, value in self.environment.items() + ) + or type(self.pass_fds) is not tuple + or any(type(fd) is not int or fd < 0 for fd in self.pass_fds) + or type(self.timeout_seconds) not in (int, float) + or self.timeout_seconds <= 0 + or type(self.stdout_limit) is not int + or self.stdout_limit < 0 + or type(self.stderr_limit) is not int + or self.stderr_limit < 0 + ): + raise TypeError("invalid diagnostic process request") + + +@dataclass(frozen=True) +class DiagnosticProcessObservationV1: + """Untrusted bytes returned by client-owned diagnostic execution.""" + + returncode: int + stdout: bytes + stderr: bytes + + def __post_init__(self) -> None: + if ( + type(self.returncode) is not int + or not -(1 << 31) <= self.returncode < 1 << 31 + or type(self.stdout) is not bytes + or type(self.stderr) is not bytes + ): + raise TypeError("invalid diagnostic process observation") + + +class DiagnosticProcessRunnerV1(Protocol): + """Client-owned resource runner; this interface grants no sandbox claim.""" + + def run( + self, + request: DiagnosticProcessRequestV1, + ) -> DiagnosticProcessObservationV1: ... + + +def _observe_diagnostic_process_v1( + runner: DiagnosticProcessRunnerV1, + request: DiagnosticProcessRequestV1, +) -> DiagnosticProcessObservationV1: + try: + observed = runner.run(request) + except Exception: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "diagnostic runner failed") + if type(observed) is not DiagnosticProcessObservationV1: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "foreign diagnostic observation") + if ( + len(observed.stdout) > request.stdout_limit + or len(observed.stderr) > request.stderr_limit + ): + _fail(OriginReasonV1.VERIFIER_OUTPUT_LIMIT, "diagnostic output exceeded policy") + return observed + + +def _run_diagnostic_v1( + runner: DiagnosticProcessRunnerV1, + argv: tuple[str, ...], + *, + stdin: bytes | None, + cwd: Path, + environment: dict[str, str], + pass_fds: tuple[int, ...], + timeout_seconds: int | float, + stdout_limit: int, + stderr_limit: int, +) -> DiagnosticProcessObservationV1: + return _observe_diagnostic_process_v1( + runner, + DiagnosticProcessRequestV1( + argv, + stdin, + cwd, + environment, + pass_fds, + timeout_seconds, + stdout_limit, + stderr_limit, + ), + ) + + +def run_gpgv( + source: provenance.SafeSourceArchiveV1, + signature: bytes, + public_key_armour: bytes, + *, + executable: Path, + runner: DiagnosticProcessRunnerV1, +) -> GpgvProcessObservationV1: + """Request a client-owned gpgv diagnostic; never mint execution authority.""" + + if type(source) is not provenance.SafeSourceArchiveV1: + raise TypeError("source must be SafeSourceArchiveV1") + if any(type(value) is not bytes for value in (signature, public_key_armour)): + raise TypeError("gpgv signature and key must be bytes") + archive = source.archive_bytes + key_packets = decode_public_key_armour(public_key_armour) + signature_sha256 = hashlib.sha256(signature).digest() + public_key_packets_sha256 = hashlib.sha256(key_packets).digest() + try: + resolved = executable.resolve(strict=True) + descriptor = os.open( + resolved, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + except (OSError, RuntimeError): + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "cannot open gpgv") + try: + executable_bytes = _read_regular_file_descriptor(descriptor) + executable_sha256 = hashlib.sha256(executable_bytes).digest() + descriptor_exec_supported = Path("/proc/self/fd").is_dir() + descriptor_path = ( + f"/proc/self/fd/{descriptor}" + if descriptor_exec_supported + else str(resolved) + ) + inherited_descriptors = (descriptor,) if descriptor_exec_supported else () + with tempfile.TemporaryDirectory(prefix="labcolors-gpgv-") as temporary: + root = Path(temporary) + keyring = root / "keyring.gpg" + detached = root / "signature.bin" + keyring.write_bytes(key_packets) + detached.write_bytes(signature) + os.chmod(keyring, 0o400) + os.chmod(detached, 0o400) + environment = { + "HOME": "/nonexistent", + "LANG": "C", + "LC_ALL": "C", + "TZ": "UTC", + } + version = _run_diagnostic_v1( + runner, + (descriptor_path, "--version"), + stdin=None, + cwd=root, + environment=environment, + pass_fds=inherited_descriptors, + timeout_seconds=10, + stdout_limit=64 * 1024, + stderr_limit=64 * 1024, + ) + verified = _run_diagnostic_v1( + runner, + ( + descriptor_path, + "--homedir", + str(root), + "--keyring", + str(keyring), + "--status-fd", + "1", + str(detached), + "-", + ), + stdin=archive, + cwd=root, + environment=environment, + pass_fds=inherited_descriptors, + timeout_seconds=60, + stdout_limit=64 * 1024, + stderr_limit=64 * 1024, + ) + if version.returncode != 0 or not version.stdout or version.stderr: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "gpgv version failed") + if verified.returncode < 0: + _fail( + OriginReasonV1.VERIFIER_FAILED, + f"gpgv terminated by signal {-verified.returncode}", + ) + if not descriptor_exec_supported: + try: + if hashlib.sha256(resolved.read_bytes()).digest() != executable_sha256: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "gpgv changed during replay") + except OSError: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "cannot re-read gpgv") + return GpgvProcessObservationV1( + verified.returncode, + verified.stdout, + verified.stderr, + source.tree_identity, + source.archive_sha256, + signature_sha256, + public_key_packets_sha256, + executable_sha256, + hashlib.sha256(version.stdout).digest(), + _token=_GPGV_PROCESS_TOKEN, + ) + finally: + os.close(descriptor) + + +def _sha1(value: bytes, field: str) -> bytes: + if type(value) is not bytes or len(value) != 20 or value == bytes(20): + raise TypeError(f"invalid {field}") + return value + + +def _source_path(value: str) -> bytes: + if type(value) is not str or not value or value.startswith("/") or "\\" in value: + raise TypeError("invalid source path") + try: + encoded = value.encode("ascii") + except UnicodeEncodeError: + raise TypeError("source path must be ASCII") from None + if ( + len(encoded) > 4096 + or any(byte < 0x20 or byte == 0x7F for byte in encoded) + or any(part in ("", ".", "..") for part in value.split("/")) + ): + raise TypeError("invalid source path") + return encoded + + +@dataclass(frozen=True) +class FileCoordinateV1: + path: str + mode: int + length: int + sha256: bytes + + def __post_init__(self) -> None: + _source_path(self.path) + if type(self.mode) is not int or self.mode not in (0o644, 0o700, 0o755): + raise TypeError("invalid source mode") + if type(self.length) is not int or self.length < 0 or self.length >= 1 << 64: + raise TypeError("invalid source length") + _digest(self.sha256, "source file digest") + + +def _canonical_files(value: tuple[FileCoordinateV1, ...], field: str) -> None: + if type(value) is not tuple or any(type(item) is not FileCoordinateV1 for item in value): + raise TypeError(f"invalid {field}") + paths = tuple(item.path for item in value) + if paths != tuple(sorted(set(paths))): + raise TypeError(f"noncanonical {field}") + + +def _file_set_digest(files: tuple[FileCoordinateV1, ...], label: bytes) -> bytes: + hasher = hashlib.sha256(label) + hasher.update(len(files).to_bytes(8, "big")) + for item in files: + path = item.path.encode("ascii") + hasher.update(len(path).to_bytes(4, "big")) + hasher.update(path) + hasher.update(item.mode.to_bytes(4, "big")) + hasher.update(item.length.to_bytes(8, "big")) + hasher.update(item.sha256) + return hasher.digest() + + +_GIT_PROCESS_TOKEN = object() +_GIT_RELATION_TOKEN = object() + + +@dataclass(frozen=True, init=False) +class GitTreeProcessObservationV1: + commit: bytes + tree: bytes + commit_object_sha256: bytes + files: tuple[FileCoordinateV1, ...] + executable_sha256: bytes + version_sha256: bytes + + def __init__( + self, + commit: bytes, + tree: bytes, + commit_object_sha256: bytes, + files: tuple[FileCoordinateV1, ...], + executable_sha256: bytes, + version_sha256: bytes, + *, + _token: object, + ) -> None: + if _token is not _GIT_PROCESS_TOKEN: + raise TypeError("GitTreeProcessObservationV1 is created only by run_git_tree") + _sha1(commit, "Git commit") + _sha1(tree, "Git tree") + _digest(commit_object_sha256, "Git commit object digest") + _canonical_files(files, "Git files") + if not files or any(item.mode == 0o700 for item in files): + raise TypeError("invalid Git tree") + _digest(executable_sha256, "Git executable digest") + _digest(version_sha256, "Git version digest") + object.__setattr__(self, "commit", commit) + object.__setattr__(self, "tree", tree) + object.__setattr__(self, "commit_object_sha256", commit_object_sha256) + object.__setattr__(self, "files", files) + object.__setattr__(self, "executable_sha256", executable_sha256) + object.__setattr__(self, "version_sha256", version_sha256) + + +@dataclass(frozen=True, init=False) +class RecomputedGitContentRelationV1: + archive_sha256: bytes + source_tree_identity: bytes + commit: bytes + tree: bytes + commit_object_sha256: bytes + git_files_identity: bytes + archive_files_identity: bytes + common_file_count: int + omitted_file_count: int + project_pinned_release_only_file_count: int + + def __init__( + self, + archive_sha256: bytes, + source_tree_identity: bytes, + commit: bytes, + tree: bytes, + commit_object_sha256: bytes, + git_files_identity: bytes, + archive_files_identity: bytes, + common_file_count: int, + omitted_file_count: int, + project_pinned_release_only_file_count: int, + *, + _token: object, + ) -> None: + if _token is not _GIT_RELATION_TOKEN: + raise TypeError("Git relation is created only by admission") + _sha1(commit, "Git commit") + _sha1(tree, "Git tree") + for field in ( + "archive_sha256", + "source_tree_identity", + "git_files_identity", + "archive_files_identity", + "commit_object_sha256", + ): + _digest(locals()[field], field) + for field in ( + "common_file_count", + "omitted_file_count", + "project_pinned_release_only_file_count", + ): + value = locals()[field] + if type(value) is not int or value <= 0: + raise TypeError(f"invalid {field}") + for field in self.__dataclass_fields__: + object.__setattr__(self, field, locals()[field]) + + +def admit_git_content_relation_observation( + *, + expected: provenance.SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, + process: GitTreeProcessObservationV1, +) -> RecomputedGitContentRelationV1: + """Relate archive bytes to a project-pinned, independently replayed graph. + + Git supplies bytes and diagnostics only. The admitted relation derives + from locally recomputed commit, tree, and blob identities, so executable + metadata is intentionally absent from its identity and authority surface. + """ + + if type(expected) is not provenance.SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + if type(admitted) is not provenance.SafeSourceArchiveV1: + raise TypeError("admitted must be SafeSourceArchiveV1") + if type(expected.integrity) is not provenance.GitContentRelationPolicyV1: + raise TypeError("source must declare a Git content relation policy") + if type(process) is not GitTreeProcessObservationV1: + raise TypeError("process must be a sealed GitTreeProcessObservationV1") + if ( + admitted.source_lock_identity != expected.identity + or admitted.archive_sha256 != expected.archive_sha256 + or process.commit != expected.integrity.commit + or process.tree != expected.integrity.tree + ): + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "source, commit, or tree") + expected_common_file_count = expected.integrity.common_file_count + omitted_paths = expected.integrity.omitted_paths + project_pinned_release_only_files = tuple( + FileCoordinateV1(item.path, item.mode, item.length, item.sha256) + for item in expected.integrity.project_pinned_release_only_files + ) + archive_files = tuple( + FileCoordinateV1(item.path, item.mode, item.length, item.sha256) + for item in admitted.files + ) + _canonical_files( + project_pinned_release_only_files, + "project-pinned release-only files", + ) + _canonical_files(archive_files, "archive files") + if not project_pinned_release_only_files or not archive_files: + raise TypeError("empty content relation") + if type(omitted_paths) is not tuple or not omitted_paths: + raise TypeError("empty omitted paths") + for path in omitted_paths: + _source_path(path) + if omitted_paths != tuple(sorted(set(omitted_paths))): + raise TypeError("noncanonical omitted paths") + + git_by_path = {item.path: item for item in process.files} + archive_by_path = {item.path: item for item in archive_files} + release_only_by_path = { + item.path: item for item in project_pinned_release_only_files + } + omitted = set(omitted_paths) + release_only = set(release_only_by_path) + git_paths = set(git_by_path) + archive_paths = set(archive_by_path) + common_paths = git_paths - omitted + if ( + len(common_paths) != expected_common_file_count + or not omitted <= git_paths + or omitted & archive_paths + or release_only & git_paths + or not release_only <= archive_paths + or archive_paths != common_paths | release_only + ): + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "path partition") + if any(archive_by_path[path] != git_by_path[path] for path in common_paths): + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "common file content") + if any( + archive_by_path[path] != release_only_by_path[path] + for path in release_only + ): + _fail( + OriginReasonV1.CONTENT_RELATION_MISMATCH, + "project-pinned release-only file content", + ) + return RecomputedGitContentRelationV1( + admitted.archive_sha256, + admitted.tree_identity, + process.commit, + process.tree, + process.commit_object_sha256, + _file_set_digest(process.files, b"labcolors.git-tree-files.v1\0"), + _file_set_digest(archive_files, b"labcolors.release-archive-files.v1\0"), + len(common_paths), + len(omitted), + len(release_only), + _token=_GIT_RELATION_TOKEN, + ) + + +def _git_object_id(value: bytes) -> bytes: + if len(value) != 40: + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git object id") + try: + decoded = bytes.fromhex(value.decode("ascii")) + except (UnicodeDecodeError, ValueError): + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git object id") + if decoded == bytes(20): + _fail(OriginReasonV1.INVALID_STATUS, "zero Git object id") + return decoded + + +def _parse_git_listing(raw: bytes) -> tuple[tuple[bytes, str, int], ...]: + if type(raw) is not bytes or not raw or not raw.endswith(b"\0"): + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git listing") + records: list[tuple[bytes, str, int]] = [] + previous: str | None = None + for encoded in raw[:-1].split(b"\0"): + metadata, separator, path_raw = encoded.partition(b"\t") + fields = metadata.split(b" ") + if not separator or len(fields) != 3 or fields[1] != b"blob": + _fail(OriginReasonV1.INVALID_STATUS, "non-blob Git entry") + if fields[0] == b"100644": + mode = 0o644 + elif fields[0] == b"100755": + mode = 0o755 + else: + _fail(OriginReasonV1.INVALID_STATUS, "unsupported Git mode") + try: + path = path_raw.decode("ascii") + except UnicodeDecodeError: + _fail(OriginReasonV1.INVALID_STATUS, "non-ASCII Git path") + try: + _source_path(path) + except TypeError: + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git path") + _git_object_id(fields[2]) + if previous is not None and previous >= path: + _fail(OriginReasonV1.INVALID_STATUS, "noncanonical Git path order") + previous = path + records.append((fields[2], path, mode)) + if not records: + _fail(OriginReasonV1.INVALID_STATUS, "empty Git tree") + return tuple(records) + + +def _recompute_git_tree_identity( + listing: tuple[tuple[bytes, str, int], ...] +) -> bytes: + """Rebuild recursive Git tree objects without trusting `git ls-tree` IDs.""" + + if type(listing) is not tuple or not listing: + _fail(OriginReasonV1.INVALID_STATUS, "empty Git listing") + root: dict[bytes, object] = {} + for object_id_raw, path, mode in listing: + object_id = _git_object_id(object_id_raw) + components = path.encode("ascii").split(b"/") + node = root + for component in components[:-1]: + existing = node.get(component) + if existing is None: + child: dict[bytes, object] = {} + node[component] = child + node = child + elif type(existing) is dict: + node = existing + else: + _fail(OriginReasonV1.INVALID_STATUS, "Git file/directory collision") + leaf = components[-1] + if leaf in node: + _fail(OriginReasonV1.INVALID_STATUS, "duplicate Git tree entry") + node[leaf] = (mode, object_id) + + def compare_entries( + left: tuple[bytes, bool, bytes, bytes], + right: tuple[bytes, bool, bytes, bytes], + ) -> int: + left_name, left_tree, _left_mode, _left_id = left + right_name, right_tree, _right_mode, _right_id = right + common = min(len(left_name), len(right_name)) + if left_name[:common] != right_name[:common]: + return -1 if left_name[:common] < right_name[:common] else 1 + left_next = left_name[common] if common < len(left_name) else (47 if left_tree else 0) + right_next = right_name[common] if common < len(right_name) else (47 if right_tree else 0) + return left_next - right_next + + # Git permits paths deeper than Python's recursion limit. Explicit + # post-order traversal keeps the accepted path grammar independent of the + # host interpreter stack while preserving Git's byte ordering exactly. + digests: dict[int, bytes] = {} + stack: list[tuple[dict[bytes, object], bool]] = [(root, False)] + while stack: + node, visited = stack.pop() + if not visited: + stack.append((node, True)) + for child in node.values(): + if type(child) is dict: + stack.append((child, False)) + continue + + entries: list[tuple[bytes, bool, bytes, bytes]] = [] + for name, child in node.items(): + if type(child) is dict: + entries.append((name, True, b"40000", digests[id(child)])) + else: + mode, object_id = child # type: ignore[misc] + encoded_mode = b"100644" if mode == 0o644 else b"100755" + entries.append((name, False, encoded_mode, object_id)) + entries.sort(key=cmp_to_key(compare_entries)) + body = b"".join( + mode + b" " + name + b"\0" + object_id + for name, _is_tree, mode, object_id in entries + ) + digests[id(node)] = hashlib.sha1( + b"tree " + str(len(body)).encode("ascii") + b"\0" + body + ).digest() + + return digests[id(root)] + + +def _admit_git_commit_object(body: bytes, commit: bytes, tree: bytes) -> bytes: + if type(body) is not bytes or not body: + _fail(OriginReasonV1.INVALID_STATUS, "empty Git commit object") + expected_commit = _sha1(commit, "Git commit") + expected_tree = _sha1(tree, "Git tree") + header = b"commit " + str(len(body)).encode("ascii") + b"\0" + if hashlib.sha1(header + body).digest() != expected_commit: + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "Git commit identity") + first_line, separator, _remaining = body.partition(b"\n") + if not separator or first_line != b"tree " + expected_tree.hex().encode("ascii"): + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "commit to tree edge") + return hashlib.sha256(body).digest() + + +def _parse_git_batch( + raw: bytes, listing: tuple[tuple[bytes, str, int], ...] +) -> tuple[FileCoordinateV1, ...]: + if type(raw) is not bytes: + raise TypeError("Git batch output must be bytes") + offset = 0 + files: list[FileCoordinateV1] = [] + for object_id_raw, path, mode in listing: + header_end = raw.find(b"\n", offset) + if header_end < 0: + _fail(OriginReasonV1.INVALID_STATUS, "truncated Git batch header") + header = raw[offset:header_end].split(b" ") + if len(header) != 3 or header[0] != object_id_raw or header[1] != b"blob": + _fail(OriginReasonV1.INVALID_STATUS, "foreign Git batch object") + try: + length = int(header[2], 10) + except ValueError: + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git blob length") + if ( + length < 0 + or length >= 1 << 64 + or not header[2].isdigit() + or header[2] != str(length).encode("ascii") + ): + _fail(OriginReasonV1.INVALID_STATUS, "invalid Git blob length") + body_start = header_end + 1 + body_end = body_start + length + if body_end >= len(raw) or raw[body_end : body_end + 1] != b"\n": + _fail(OriginReasonV1.INVALID_STATUS, "truncated Git blob") + body = raw[body_start:body_end] + object_id = _git_object_id(object_id_raw) + object_header = b"blob " + str(length).encode("ascii") + b"\0" + if hashlib.sha1(object_header + body).digest() != object_id: + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "Git blob identity") + files.append(FileCoordinateV1(path, mode, length, hashlib.sha256(body).digest())) + offset = body_end + 1 + if offset != len(raw): + _fail(OriginReasonV1.INVALID_STATUS, "trailing Git batch bytes") + return tuple(files) + + +def run_git_tree( + repository: Path, + expected_commit: bytes, + expected_tree: bytes, + *, + executable: Path, + runner: DiagnosticProcessRunnerV1, +) -> GitTreeProcessObservationV1: + """Parse a client-owned Git diagnostic and recompute every content edge.""" + + commit = _sha1(expected_commit, "expected Git commit") + tree = _sha1(expected_tree, "expected Git tree") + try: + root = repository.resolve(strict=True) + except (OSError, RuntimeError): + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "Git repository unavailable") + if not root.is_dir(): + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "Git repository is not a directory") + try: + resolved = executable.resolve(strict=True) + descriptor = os.open( + resolved, + os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), + ) + except (OSError, RuntimeError): + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "cannot open Git") + try: + executable_bytes = _read_regular_file_descriptor(descriptor) + executable_sha256 = hashlib.sha256(executable_bytes).digest() + descriptor_exec_supported = Path("/proc/self/fd").is_dir() + executable_path = ( + f"/proc/self/fd/{descriptor}" + if descriptor_exec_supported + else str(resolved) + ) + inherited_descriptors = (descriptor,) if descriptor_exec_supported else () + environment = { + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_NO_LAZY_FETCH": "1", + "GIT_OPTIONAL_LOCKS": "0", + "GIT_PAGER": "cat", + "HOME": "/nonexistent", + "LANG": "C", + "LC_ALL": "C", + "PATH": "/usr/bin:/bin", + "TZ": "UTC", + } + + def invoke( + arguments: tuple[str, ...], + *, + stdin: bytes | None = None, + timeout: int = 60, + stdout_limit: int = 64 * 1024, + ) -> bytes: + process = _run_diagnostic_v1( + runner, + (executable_path, "-C", str(root), *arguments), + stdin=stdin, + cwd=root, + environment=environment, + pass_fds=inherited_descriptors, + timeout_seconds=timeout, + stdout_limit=stdout_limit, + stderr_limit=64 * 1024, + ) + if process.returncode != 0 or process.stderr: + _fail(OriginReasonV1.VERIFIER_FAILED, "Git command rejected") + return process.stdout + + version_process = _run_diagnostic_v1( + runner, + (executable_path, "--version"), + stdin=None, + cwd=root, + environment=environment, + pass_fds=inherited_descriptors, + timeout_seconds=10, + stdout_limit=64 * 1024, + stderr_limit=64 * 1024, + ) + if version_process.returncode != 0 or not version_process.stdout or version_process.stderr: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "Git version failed") + + commit_object = invoke( + ("cat-file", "commit", commit.hex()), + stdout_limit=1024 * 1024, + ) + commit_object_sha256 = _admit_git_commit_object(commit_object, commit, tree) + listing = _parse_git_listing( + invoke( + ("ls-tree", "-r", "-z", "--full-tree", tree.hex()), + stdout_limit=64 * 1024 * 1024, + ) + ) + if _recompute_git_tree_identity(listing) != tree: + _fail(OriginReasonV1.CONTENT_RELATION_MISMATCH, "Git tree identity") + query = b"".join(object_id + b"\n" for object_id, _path, _mode in listing) + if len(query) > 1024 * 1024: + _fail(OriginReasonV1.INVALID_STATUS, "oversized Git query") + batch = invoke( + ("cat-file", "--batch"), + stdin=query, + timeout=180, + stdout_limit=128 * 1024 * 1024, + ) + files = _parse_git_batch(batch, listing) + if not descriptor_exec_supported: + try: + if hashlib.sha256(resolved.read_bytes()).digest() != executable_sha256: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "Git changed during replay") + except OSError: + _fail(OriginReasonV1.VERIFIER_UNAVAILABLE, "cannot re-read Git") + return GitTreeProcessObservationV1( + commit, + tree, + commit_object_sha256, + files, + executable_sha256, + hashlib.sha256(version_process.stdout).digest(), + _token=_GIT_PROCESS_TOKEN, + ) + finally: + os.close(descriptor) diff --git a/proof/region/v1/arb/pipeline.py b/proof/region/v1/arb/pipeline.py new file mode 100644 index 00000000..e1016e04 --- /dev/null +++ b/proof/region/v1/arb/pipeline.py @@ -0,0 +1,2038 @@ +#!/usr/bin/env python3 +"""Controlled offline BUILD observations for the Arb evaluator. + +The unsealed Linux x64 host and its Docker daemon are explicitly inside this +V1 trust boundary. Provider identity and host freshness are not observable +here. This module emits neither SLSA nor source-bound receipts: it observes two +fresh-container builds and owns their exact output bytes. The one-shot +source-bound controller owns RUN and receipt sealing in ``receipt.py``. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass, fields +from enum import StrEnum +from functools import cached_property +from typing import NoReturn, TypeAlias + +from arb import runtime as arb_runtime +from build import input as build_input +from build import transport as build_transport + +import executor +import provenance +import region_proof_protocol as protocol + + +OCI_IMAGE_MANIFEST_SHA256_V1 = ( + "c74b2d34b775e6a1b14b13b1d41dc7233f62a18f7a6a4e139e0cf59eeab2e070" +) +OCI_IMAGE_REFERENCE_V1 = f"gcc@sha256:{OCI_IMAGE_MANIFEST_SHA256_V1}" +OCI_PLATFORM_V1 = "linux/amd64" +EVALUATOR_OUTPUT_NAME_V1 = "arb-evaluator-v1" +GENERATED_FORMULA_PATH_V1 = "generated/formula.generated.c" +FORMULA_SPEC_PATH_V1 = "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir" +FORMULA_GENERATOR_PATH_V1 = "proof/region/v1/arb/evaluator/formula.py" +BUILD_RECIPE_PATH_V1 = "proof/region/v1/arb/build.sh" +INNER_BUILD_RECIPE_PATH_V1 = "proof/region/v1/arb/build-inner.sh" + +FORMULA_SPEC_SHA256_V1 = "a6f77ac462f226453b1c27bbd8637b62780b9a640c317a6f50028dacd1de8540" +GENERATED_FORMULA_SHA256_V1 = "9958f20c8ca598625db0593a45f8f8bc79e4b2f22b53263b6c32d78a5e1d2693" + +# This is a drift gate, not documentation copied from memory. Admission below +# hashes every exact input and rejects a local source edit until this manifest +# is deliberately updated together with its binding tests. +_PINNED_BUILD_SOURCE_SHA256_V1 = { + FORMULA_SPEC_PATH_V1: FORMULA_SPEC_SHA256_V1, + GENERATED_FORMULA_PATH_V1: GENERATED_FORMULA_SHA256_V1, + BUILD_RECIPE_PATH_V1: "09addfaa10952d3e71baf8a9709fb6b875745dcacea06ce45fd84e382a78173e", + INNER_BUILD_RECIPE_PATH_V1: "8f09fc4089acf5155b4395e8ac61ff0d95c3832c82e3fc270203a78c6a98766b", + FORMULA_GENERATOR_PATH_V1: "16629cc3a2ef745ae244ae4762f8946a6546972886f96beeb9ee4920b043040c", + "proof/region/v1/arb/evaluator/formula.h": "46fd5ad1b68b728efcd990a71d1dcc273b75e3391d8c06ef2fd0ac6a4d7dfdbd", + "proof/region/v1/arb/evaluator/hash.c": "c28e6281208f09ca15fa74aea0091f27726ed68efc3480c34a7db33b8ca3567e", + "proof/region/v1/arb/evaluator/hash.h": "a62c07f2eca9294b4c1c802e2a9e6cff6ad9f8fd696a74b54a21489d56fab6c4", + "proof/region/v1/arb/evaluator/interval.c": "93f206258b83fc0f373ae865787ebf266c9d011f2578567ed913a7cb6c0ed899", + "proof/region/v1/arb/evaluator/interval.h": "f9d7416059d4b09979c22e6823a747f252c576558c750fe3e2ff92509894c7b3", + "proof/region/v1/arb/evaluator/main.c": "d50767b2a79fe12f21cd5c76a4a6cd29edc0953ea9c2b4862510a2d19db9cc95", + "proof/region/v1/arb/evaluator/region.c": "0026d501077911eae58933487a4cac0a83003cd70d1dbf0966890c29bfff8f99", + "proof/region/v1/arb/evaluator/region.h": "95da5117bb162c707b441242637d5e0e1bbeef2532ac1f10248f2b93ab16dcc8", + "proof/region/v1/arb/evaluator/wire.c": "97c8c793670d1a45378ecc0b1491ba5b8b440bd35001daf8be8003a1a4f52e2a", + "proof/region/v1/arb/evaluator/wire.h": "6899452d11cbc390557233e5fceef62e340050fce80c66a8da84c1d0f42fb456", +} + +REQUIRED_BUILD_SOURCE_MODES_V1 = tuple( + (path, 0o755 if path == BUILD_RECIPE_PATH_V1 else 0o644) + for path in sorted(_PINNED_BUILD_SOURCE_SHA256_V1) +) + +# The generic transport owns universal observer ceilings. This lane binds to +# those coordinates rather than recreating a coincident copy of the policy. +BUILD_STDOUT_LIMIT_V1 = build_transport.BUILD_STDOUT_LIMIT_V1 +BUILD_STDERR_LIMIT_V1 = build_transport.BUILD_STDERR_LIMIT_V1 +BUILD_TIMEOUT_NS_V1 = build_transport.BUILD_TIMEOUT_NS_V1 +DOCKER_PROBE_OUTPUT_LIMIT_V1 = build_transport.DOCKER_PROBE_OUTPUT_LIMIT_V1 +DOCKER_PROBE_TIMEOUT_NS_V1 = build_transport.DOCKER_PROBE_TIMEOUT_NS_V1 +MAX_BUILD_SOURCE_FILE_BYTES_V1 = 16 * 1024 * 1024 +MAX_BUILD_SOURCE_TOTAL_BYTES_V1 = 32 * 1024 * 1024 + +# FLINT's exact locked qsieve path uses /tmp directly rather than TMPDIR. This +# independent operational cap is part of the build policy; overflow rejects. +BUILD_TMP_LIMIT_BYTES_V1 = 512 * 1024 * 1024 +_BUILD_TMPFS_SPEC_V1 = ( + f"/tmp:rw,noexec,nosuid,nodev,size={BUILD_TMP_LIMIT_BYTES_V1},mode=1777" +) + +# This is a versioned resource policy, not a mathematical constant. Four GiB +# is the first shipping cap for one serial GMP/MPFR/FLINT build plus their +# upstream test artifacts. The no-skip native gate is the authority for +# lowering it; exhaustion rejects the build instead of falling back to a host +# directory or an unbounded Docker volume. +BUILD_STATE_LIMIT_BYTES_V1 = 4 * 1024 * 1024 * 1024 +_BUILD_STATE_TMPFS_SPEC_V1 = ( + f"/build:rw,exec,nosuid,nodev,size={BUILD_STATE_LIMIT_BYTES_V1},mode=0777" +) + +_BUILD_BOOTSTRAP_V1 = r"""set -eu +exec 3>&1 +exec 1>&2 +umask 077 +readonly bundle=/build/input.bundle +readonly snapshot=/build/snapshot +/usr/bin/cat > "$bundle" +actual_length=$(/usr/bin/wc -c < "$bundle") +if [ "$actual_length" != "$1" ]; then + printf '%s\n' 'build input bundle length mismatch' >&2 + exit 65 +fi +printf '%s %s\n' "$2" "$bundle" | /usr/bin/sha256sum --check --strict - +/usr/bin/mkdir "$snapshot" /build/work +umask 022 +/usr/bin/tar --extract --file "$bundle" --directory "$snapshot" --no-same-owner +/usr/bin/rm "$bundle" +umask 077 +/bin/sh "$snapshot/workspace/proof/region/v1/arb/build.sh" +/usr/bin/cat /build/work/arb-evaluator-v1 >&3 +""" + +_BUILD_SOURCES_ID_LABEL_V1 = b"labcolors.proof-region.arb-build-sources.v1\0" +_BUILD_INPUT_ID_LABEL_V1 = b"labcolors.proof-region.arb-compiler-inputs.v1\0" +_FORMULA_SUPPORT_ID_LABEL_V1 = b"labcolors.proof-region.arb-formula-support.v1\0" +_FLINT_COMMIT_CONTENT_ID_LABEL_V1 = ( + b"labcolors.proof-region.flint-commit-content.v1\0" +) +_FLINT_RELEASE_ONLY_ID_LABEL_V1 = ( + b"labcolors.proof-region.flint-project-pinned-release-only.v1\0" +) +_PIPELINE_POLICY_ID_LABEL_V2 = b"labcolors.proof-region.arb-pipeline-policy.v2\0" +_BUILD_INPUT_BUNDLE_ID_LABEL_V2 = ( + b"labcolors.proof-region.arb-build-input-bundle.v2\0" +) +_BUILD_SOURCES_TOKEN = object() +_COMPARATOR_TOKEN = object() +_BUILD_OBSERVATION_TOKEN = object() + + +def _blob(value: bytes) -> bytes: + return len(value).to_bytes(8, "big") + value + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join(_blob(chunk) for chunk in chunks) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def _valid_digest(value: object) -> bool: + return type(value) is bytes and len(value) == 32 and value != bytes(32) + + +class BuildSourceReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + INVALID_PATH = "invalid_path" + INVALID_MODE = "invalid_mode" + INVALID_CONTENT = "invalid_content" + NONCANONICAL_SET = "noncanonical_set" + CONTENT_DRIFT = "content_drift" + + +@dataclass(frozen=True) +class BuildSourceAdmissionErrorV1(ValueError): + reason: BuildSourceReasonV1 + path: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.path}" + + +def _source_fail(reason: BuildSourceReasonV1, path: str) -> NoReturn: + raise BuildSourceAdmissionErrorV1(reason, path) + + +def _logical_path(value: object) -> str: + if type(value) is not str or not value or value.startswith("/") or "\\" in value: + _source_fail(BuildSourceReasonV1.INVALID_PATH, str(value)) + try: + encoded = value.encode("ascii") + except UnicodeEncodeError: + _source_fail(BuildSourceReasonV1.INVALID_PATH, value) + if ( + len(encoded) > 4096 + or any(byte < 0x20 or byte == 0x7F for byte in encoded) + or any(part in ("", ".", "..") for part in value.split("/")) + ): + _source_fail(BuildSourceReasonV1.INVALID_PATH, value) + return value + + +@dataclass(frozen=True) +class BuildSourceFileV1: + path: str + mode: int + contents: bytes + + def __post_init__(self) -> None: + _logical_path(self.path) + if type(self.mode) is not int or self.mode not in (0o644, 0o755): + _source_fail(BuildSourceReasonV1.INVALID_MODE, self.path) + if ( + type(self.contents) is not bytes + or not self.contents + or len(self.contents) > MAX_BUILD_SOURCE_FILE_BYTES_V1 + ): + _source_fail(BuildSourceReasonV1.INVALID_CONTENT, self.path) + + +@dataclass(frozen=True, init=False) +class AdmittedBuildSourcesV1: + """Owned exact local build-support closure. + + ``build_input_identity`` covers the recipe, generated C and evaluator files + named by that recipe. ``formula_support_identity`` separately covers the + formula spec, generator and generated C. The latter is support/replay + material; it does not claim that build.sh executed the generator. + """ + + files: tuple[BuildSourceFileV1, ...] + identity: bytes + + def __init__( + self, + files_value: tuple[BuildSourceFileV1, ...], + identity: bytes, + *, + _token: object, + ) -> None: + if _token is not _BUILD_SOURCES_TOKEN: + raise TypeError("AdmittedBuildSourcesV1 is created only by source admission") + if type(files_value) is not tuple or any( + type(item) is not BuildSourceFileV1 for item in files_value + ): + raise TypeError("invalid build source files") + if not _valid_digest(identity): + raise TypeError("invalid build source identity") + object.__setattr__(self, "files", files_value) + object.__setattr__(self, "identity", identity) + + def contents(self, path: str) -> bytes: + for item in self.files: + if item.path == path: + return item.contents + raise KeyError(path) + + @property + def formula_spec(self) -> bytes: + return self.contents(FORMULA_SPEC_PATH_V1) + + @property + def generated_formula(self) -> bytes: + return self.contents(GENERATED_FORMULA_PATH_V1) + + @cached_property + def build_input_identity(self) -> bytes: + direct = tuple( + item + for item in self.files + if item.path not in (FORMULA_SPEC_PATH_V1, FORMULA_GENERATOR_PATH_V1) + ) + return _source_subset_identity(_BUILD_INPUT_ID_LABEL_V1, direct) + + @cached_property + def formula_support_identity(self) -> bytes: + support_paths = frozenset( + ( + FORMULA_SPEC_PATH_V1, + FORMULA_GENERATOR_PATH_V1, + GENERATED_FORMULA_PATH_V1, + ) + ) + support = tuple(item for item in self.files if item.path in support_paths) + return _source_subset_identity(_FORMULA_SUPPORT_ID_LABEL_V1, support) + + +def build_source_manifest_bytes_v1(sources: AdmittedBuildSourcesV1) -> bytes: + """Replay and encode the canonical retained build-source manifest.""" + + if type(sources) is not AdmittedBuildSourcesV1: + raise TypeError("sources must be AdmittedBuildSourcesV1") + replayed = admit_build_sources_v1(sources.files) + retained_identity = sources.identity + retained_build_input_identity = sources.build_input_identity + retained_formula_support_identity = sources.formula_support_identity + if ( + not _valid_digest(retained_identity) + or not _valid_digest(retained_build_input_identity) + or not _valid_digest(retained_formula_support_identity) + or retained_identity != replayed.identity + or retained_build_input_identity != replayed.build_input_identity + or retained_formula_support_identity != replayed.formula_support_identity + ): + raise TypeError("retained build-source coordinates changed") + chunks: list[bytes] = [len(replayed.files).to_bytes(4, "big")] + for item in replayed.files: + chunks.extend( + ( + item.path.encode("ascii"), + item.mode.to_bytes(4, "big"), + len(item.contents).to_bytes(8, "big"), + hashlib.sha256(item.contents).digest(), + ) + ) + return b"".join(_blob(chunk) for chunk in chunks) + + +def _source_subset_identity( + label: bytes, + files_value: tuple[BuildSourceFileV1, ...], +) -> bytes: + chunks: list[bytes] = [len(files_value).to_bytes(4, "big")] + for item in files_value: + chunks.extend( + ( + item.path.encode("ascii"), + item.mode.to_bytes(4, "big"), + hashlib.sha256(item.contents).digest(), + len(item.contents).to_bytes(8, "big"), + ) + ) + return _identity(label, tuple(chunks)) + + +def _build_sources_identity(files_value: tuple[BuildSourceFileV1, ...]) -> bytes: + return _source_subset_identity(_BUILD_SOURCES_ID_LABEL_V1, files_value) + + +def _snapshot_build_source_files_v1( + files_value: tuple[BuildSourceFileV1, ...], +) -> tuple[BuildSourceFileV1, ...]: + """Copies exact primitives before admission derives any retained identity.""" + + if type(files_value) is not tuple or any( + type(item) is not BuildSourceFileV1 for item in files_value + ): + _source_fail(BuildSourceReasonV1.WRONG_TYPE, "files") + try: + return tuple( + BuildSourceFileV1(item.path, item.mode, item.contents) + for item in files_value + ) + except BuildSourceAdmissionErrorV1: + raise + except Exception: + _source_fail(BuildSourceReasonV1.WRONG_TYPE, "files") + + +def admit_build_sources_v1( + files_value: tuple[BuildSourceFileV1, ...], +) -> AdmittedBuildSourcesV1: + files_value = _snapshot_build_source_files_v1(files_value) + actual = tuple((item.path, item.mode) for item in files_value) + if actual != REQUIRED_BUILD_SOURCE_MODES_V1: + _source_fail(BuildSourceReasonV1.NONCANONICAL_SET, "files") + if sum(len(item.contents) for item in files_value) > MAX_BUILD_SOURCE_TOTAL_BYTES_V1: + _source_fail(BuildSourceReasonV1.INVALID_CONTENT, "files") + for item in files_value: + if hashlib.sha256(item.contents).hexdigest() != _PINNED_BUILD_SOURCE_SHA256_V1[item.path]: + _source_fail(BuildSourceReasonV1.CONTENT_DRIFT, item.path) + return AdmittedBuildSourcesV1( + files_value, + _build_sources_identity(files_value), + _token=_BUILD_SOURCES_TOKEN, + ) + + +ARB_BUILD_TRANSPORT_POLICY_V1 = build_transport.DockerBuildPolicyV1( + OCI_IMAGE_REFERENCE_V1, + OCI_PLATFORM_V1, + "labcolors-arb-build-v1", + _BUILD_BOOTSTRAP_V1, + "labcolors-arb-build-bootstrap-v1", + (_BUILD_TMPFS_SPEC_V1, _BUILD_STATE_TMPFS_SPEC_V1), + build_transport.DockerUserModeV1.HOST_EFFECTIVE_IDS, + BUILD_STDOUT_LIMIT_V1, + BUILD_STDERR_LIMIT_V1, + BUILD_TIMEOUT_NS_V1, + DOCKER_PROBE_OUTPUT_LIMIT_V1, + DOCKER_PROBE_TIMEOUT_NS_V1, +) + + +def _arb_input_binding_identity_v2( + source_identity: bytes, + build_input_identity: bytes, + contents: bytes, + exact_policy: build_transport.DockerBuildPolicyV1, +) -> bytes: + if ( + not _valid_digest(source_identity) + or not _valid_digest(build_input_identity) + or type(contents) is not bytes + or not contents + or not build_transport.docker_policy_is_valid_v1(exact_policy) + ): + raise TypeError("invalid Arb build input binding coordinates") + digest = hashlib.sha256(contents).digest() + return _identity( + _BUILD_INPUT_BUNDLE_ID_LABEL_V2, + ( + source_identity, + build_input_identity, + len(contents).to_bytes(8, "big"), + digest, + # This inner identity fixes only the stream-to-tree program. V1 + # never reads shell $0; argv0 instead remains in the outer + # transport identity. A bootstrap that consumes $0 needs a new + # binding schema rather than silently widening this preimage. + hashlib.sha256( + exact_policy.bootstrap.encode("utf-8") + ).digest(), + ), + ) + + +def arb_input_is_bound_v1( + request: object, + exact_policy: object, + value: object, +) -> bool: + """Independently replay public input; never trust its retained identities.""" + + if type(value) is not build_input.SealedInputV1: + return False + try: + snapshot = _snapshot_pipeline_operation_v1(request) + expected = _seal_build_input_from_snapshot_v1( + snapshot, + exact_policy, + ) + return _owned_arb_input_is_bound_v1(value, expected) + except Exception: + return False + + +def _owned_arb_input_is_bound_v1( + value: object, + expected: build_input.SealedInputV1, +) -> bool: + """Cheap transport recheck against one private operation snapshot.""" + + if ( + type(value) is not build_input.SealedInputV1 + or not build_input.sealed_input_is_intact_v1(value) + ): + return False + try: + return ( + value.binding_identity == expected.binding_identity + and value.sha256 == expected.sha256 + and value.length == expected.length + and value.contents == expected.contents + ) + except Exception: + return False + + +def _seal_build_input_from_snapshot_v1( + snapshot: _PipelineOperationSnapshotV1, + exact_policy: build_transport.DockerBuildPolicyV1, +) -> build_input.SealedInputV1: + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") + if not build_transport.docker_policy_is_valid_v1(exact_policy): + raise TypeError("exact_policy must be canonical DockerBuildPolicyV1") + request = snapshot.request + source_closure = snapshot.source_closure + source_entries = tuple( + ( + f"inputs/{lock.root_prefix[:-1]}/{relative}", + mode, + contents, + ) + for lock, materialized in zip( + source_closure.source_lock.sources, + source_closure.sources, + strict=True, + ) + for relative, mode, contents in materialized.files + ) + workspace_entries = tuple( + ( + "inputs/formula.generated.c" + if item.path == GENERATED_FORMULA_PATH_V1 + else f"workspace/{item.path}", + item.mode, + item.contents, + ) + for item in request.build_sources.files + if item.path not in (FORMULA_SPEC_PATH_V1, FORMULA_GENERATOR_PATH_V1) + ) + contents = build_input.canonical_ustar_v1( + tuple(sorted(source_entries + workspace_entries)), + build_input.CanonicalInputLimitsV1( + len(source_entries) + len(workspace_entries) + + sum( + path.count("/") + for path, _mode, _contents in source_entries + workspace_entries + ), + max( + MAX_BUILD_SOURCE_FILE_BYTES_V1, + *( + lock.regular_file_bytes + for lock in source_closure.source_lock.sources + ), + ), + MAX_BUILD_SOURCE_TOTAL_BYTES_V1 + + sum( + lock.regular_file_bytes + for lock in source_closure.source_lock.sources + ), + ), + ) + return build_input.seal_input_v1( + _arb_input_binding_identity_v2( + request.admitted_sources.identity, + request.build_sources.build_input_identity, + contents, + exact_policy, + ), + contents, + ) + + +def _seal_build_input_bundle_v1( + request: "PipelineRequestV1", + exact_policy: build_transport.DockerBuildPolicyV1, +) -> build_input.SealedInputV1: + """Seal one public request through a detached operation snapshot.""" + + return _seal_build_input_from_snapshot_v1( + _snapshot_pipeline_operation_v1(request), + exact_policy, + ) + + +class HostTrustBoundaryV1(StrEnum): + UNSEALED_LINUX_X64_DOCKER_HOST = "unsealed-linux-x64-docker-host" + + +_UNSEALED_LINUX_X64_DOCKER_HOST_WIRE_V1 = b"unsealed-linux-x64-docker-host" + + +def _host_trust_wire_v1(value: object) -> bytes: + """Own the sole V1 host declaration without reading mutable enum storage.""" + + if value is not HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST: + raise TypeError("unknown host trust boundary") + return _UNSEALED_LINUX_X64_DOCKER_HOST_WIRE_V1 + + +def pipeline_policy_identity_v2( + host_trust: HostTrustBoundaryV1, + exact_policy: build_transport.DockerBuildPolicyV1, +) -> bytes: + if not build_transport.docker_policy_is_valid_v1(exact_policy): + raise TypeError("exact_policy must be canonical DockerBuildPolicyV1") + return _identity( + _PIPELINE_POLICY_ID_LABEL_V2, + ( + build_transport.transport_policy_identity_v1(exact_policy), + build_transport.native_command_contract_identity_v1(), + _host_trust_wire_v1(host_trust), + b"build-observation=diagnostic-unsealed-v1", + b"inputs=one-controller-sealed-normalized-tree-ustar", + b"container-admission=exact-length-and-sha256-before-extraction", + b"fresh-container-count=2", + ), + ) + + +class PipelineInputReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + FOREIGN_SOURCE_CAPABILITY = "foreign_source_capability" + INVALID_RETAINED_INPUT = "invalid_retained_input" + FORMULA_MISMATCH = "formula_mismatch" + EXECUTION_LIMIT_MISMATCH = "execution_limit_mismatch" + + +@dataclass(frozen=True) +class PipelineInputErrorV1(ValueError): + reason: PipelineInputReasonV1 + field: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.field}" + + +@dataclass(frozen=True) +class FlintSourceContentPartitionV1: + """Structural FLINT archive partition, not an origin assertion. + + ``commit_content`` names the side which the project lock expects a future + authority to relate to the exact Git tree. ``project_pinned_release_only`` + names the separate release bytes consumed by the build. Neither identity + claims that those release-only bytes were generated from the commit. + """ + + commit_content_identity: bytes + commit_content_file_count: int + project_pinned_release_only_identity: bytes + project_pinned_release_only_file_count: int + + def __post_init__(self) -> None: + if not _valid_digest(self.commit_content_identity): + raise TypeError("invalid FLINT commit-content identity") + if not _valid_digest(self.project_pinned_release_only_identity): + raise TypeError("invalid FLINT project-pinned release-only identity") + if ( + type(self.commit_content_file_count) is not int + or self.commit_content_file_count <= 0 + or type(self.project_pinned_release_only_file_count) is not int + or self.project_pinned_release_only_file_count <= 0 + ): + raise TypeError("FLINT source partition must be nonempty on both sides") + + +def _archive_file_subset_identity( + label: bytes, + files_value: tuple[provenance.ArchiveFileV1, ...], +) -> bytes: + chunks: list[bytes] = [len(files_value).to_bytes(8, "big")] + for item in files_value: + chunks.extend( + ( + item.path.encode("ascii"), + item.mode.to_bytes(4, "big"), + item.length.to_bytes(8, "big"), + item.sha256, + ) + ) + return _identity(label, tuple(chunks)) + + +def _require_bound_source_capability_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, +) -> None: + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "source_lock/admitted_sources", + ) + try: + closure_identity = admitted_sources.source_lock_identity + sources = admitted_sources.sources + if ( + not _valid_digest(closure_identity) + or type(sources) is not tuple + or len(sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not provenance.SafeSourceArchiveV1 for source in sources) + ): + raise TypeError("invalid retained source closure") + except (AttributeError, TypeError, ValueError, OverflowError) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if source_lock.identity != closure_identity: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + for lock, admitted in zip( + source_lock.sources, + sources, + strict=True, + ): + try: + admitted_lock_identity = admitted.source_lock_identity + except AttributeError as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if ( + not _valid_digest(admitted_lock_identity) + or lock.identity != admitted_lock_identity + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + + +def _owned_flint_source_content_partition_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, +) -> FlintSourceContentPartitionV1: + """Derive FLINT partitions from one already-detached source closure.""" + + if type(source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "source_lock") + if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "admitted_sources", + ) + _require_bound_source_capability_v1(source_lock, admitted_sources) + + flint_lock = source_lock.sources[2] + flint_source = admitted_sources.sources[2] + if type(flint_lock.integrity) is not provenance.GitContentRelationPolicyV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "source_lock") + release_only_by_path = { + item.path: item + for item in flint_lock.integrity.project_pinned_release_only_files + } + release_only = tuple( + item for item in flint_source.files if item.path in release_only_by_path + ) + commit_content = tuple( + item for item in flint_source.files if item.path not in release_only_by_path + ) + if ( + len(commit_content) != flint_lock.integrity.common_file_count + or len(release_only) != len(release_only_by_path) + or any( + item.mode != release_only_by_path[item.path].mode + or item.length != release_only_by_path[item.path].length + or item.sha256 != release_only_by_path[item.path].sha256 + for item in release_only + ) + or len(commit_content) + len(release_only) != len(flint_source.files) + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "flint_source_partition", + ) + return FlintSourceContentPartitionV1( + _archive_file_subset_identity( + _FLINT_COMMIT_CONTENT_ID_LABEL_V1, + commit_content, + ), + len(commit_content), + _archive_file_subset_identity( + _FLINT_RELEASE_ONLY_ID_LABEL_V1, + release_only, + ), + len(release_only), + ) + + +def flint_source_content_partition_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, +) -> FlintSourceContentPartitionV1: + """Independently derive FLINT partitions from a public retained closure.""" + + if type(source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "source_lock") + if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "admitted_sources", + ) + try: + canonical_lock = provenance.snapshot_source_closure_lock_v1(source_lock) + canonical_sources = provenance.snapshot_admitted_source_closure_v1( + canonical_lock, + admitted_sources, + ) + if type(canonical_lock) is not provenance.ArbSourceLockV1 or type( + canonical_sources + ) is not provenance.AdmittedArbSourcesV1: + raise TypeError("FLINT requires an Arb source closure") + return _owned_flint_source_content_partition_v1( + canonical_lock, + canonical_sources, + ) + except PipelineInputErrorV1: + raise + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + + +def _comparator_preimage_v1(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + """Encode one independently versioned, ordered comparator preimage.""" + + if ( + type(label) is not bytes + or not label.startswith(b"labcolors.proof-region.arb-comparator.") + or not label.endswith(b".v1\0") + or type(chunks) is not tuple + or not chunks + or any(type(chunk) is not bytes for chunk in chunks) + ): + raise TypeError("invalid comparator preimage coordinates") + return label + b"\x01" + len(chunks).to_bytes(4, "big") + b"".join( + _blob(chunk) for chunk in chunks + ) + + +def _comparator_preimage_v2(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + """Encode one V2 comparator preimage without accepting a V1 label.""" + + if ( + type(label) is not bytes + or not label.startswith(b"labcolors.proof-region.arb-comparator.") + or not label.endswith(b".v2\0") + or type(chunks) is not tuple + or not chunks + or any(type(chunk) is not bytes for chunk in chunks) + ): + raise TypeError("invalid V2 comparator preimage coordinates") + return label + b"\x02" + len(chunks).to_bytes(4, "big") + b"".join( + _blob(chunk) for chunk in chunks + ) + + +def _comparator_build_preimage_v2( + build_sources: AdmittedBuildSourcesV1, + docker_capability_identity: bytes, + pipeline_policy_identity: bytes, + build_processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ], + binary_sha256: bytes, + rebuild_sha256s: tuple[bytes, bytes], + binary_length: int, +) -> bytes: + """Single replay schema for the BUILD coordinate in the comparator.""" + + if type(build_sources) is not AdmittedBuildSourcesV1: + raise TypeError("build_sources must be AdmittedBuildSourcesV1") + for name, value in ( + ("docker_capability_identity", docker_capability_identity), + ("pipeline_policy_identity", pipeline_policy_identity), + ("binary_sha256", binary_sha256), + ): + if not _valid_digest(value): + raise TypeError(f"invalid {name}") + if ( + type(build_processes) is not tuple + or len(build_processes) != 2 + or any( + type(item) is not build_transport.DockerBuildExitedV1 + for item in build_processes + ) + or type(rebuild_sha256s) is not tuple + or rebuild_sha256s != (binary_sha256, binary_sha256) + or type(binary_length) is not int + or binary_length <= 0 + ): + raise TypeError("invalid comparator BUILD observation") + process_bytes = tuple( + build_transport.build_process_bytes_v1(item) for item in build_processes + ) + return _comparator_preimage_v2( + b"labcolors.proof-region.arb-comparator.build-identity.v2\0", + ( + build_sources.contents(BUILD_RECIPE_PATH_V1), + build_sources.build_input_identity, + build_sources.formula_support_identity, + docker_capability_identity, + pipeline_policy_identity, + b"build-observation=diagnostic-unsealed-v1", + len(build_processes).to_bytes(4, "big"), + *process_bytes, + binary_sha256, + rebuild_sha256s[0], + rebuild_sha256s[1], + binary_length.to_bytes(8, "big"), + binary_sha256, + ), + ) + + +def _encoded_build_file_set_v1( + label: bytes, + files_value: tuple[BuildSourceFileV1, ...], +) -> bytes: + chunks: list[bytes] = [len(files_value).to_bytes(4, "big")] + for item in files_value: + chunks.extend( + ( + item.path.encode("ascii"), + item.mode.to_bytes(4, "big"), + len(item.contents).to_bytes(8, "big"), + item.contents, + ) + ) + return _comparator_preimage_v1(label, tuple(chunks)) + + +def _operation_allowlist_preimage_v1(formula_spec: bytes) -> bytes: + """Bind the exact ordered SSA operator contract from the admitted formula.""" + + if type(formula_spec) is not bytes or not formula_spec: + raise TypeError("formula_spec must be nonempty bytes") + lines = formula_spec.splitlines() + declarations: tuple[bytes, ...] | None = None + for index, line in enumerate(lines): + if not line.startswith(b"operators "): + continue + pieces = line.split(b" ") + if len(pieces) != 2 or not pieces[1].isdigit(): + raise ValueError("invalid formula operator count") + count = int(pieces[1]) + candidate = tuple(lines[index + 1 : index + 1 + count]) + if ( + count <= 0 + or len(candidate) != count + or any(not item.startswith(b"operator ") for item in candidate) + or ( + index + 1 + count < len(lines) + and lines[index + 1 + count].startswith(b"operator ") + ) + ): + raise ValueError("formula operator declarations do not match their count") + declarations = candidate + break + if declarations is None: + raise ValueError("formula has no operator contract") + return _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.operation-allowlist.v1\0", + ( + b"exact-real-ssa-operator-declarations", + len(declarations).to_bytes(4, "big"), + *declarations, + ), + ) + + +@dataclass(frozen=True) +class ArbComparatorPreimagesV1: + engine_release: bytes + upstream_source: bytes + arithmetic_input_set: bytes + wrapper_source: bytes + evaluator_source: bytes + build_identity: bytes + operation_allowlist: bytes + test_observation: bytes + legal_file_set: bytes + exclusions: bytes + + def __post_init__(self) -> None: + values = tuple(getattr(self, item.name) for item in fields(self)) + if any(type(value) is not bytes or not value for value in values): + raise TypeError("comparator preimages must be nonempty exact bytes") + if len(set(values)) != len(values): + raise TypeError("comparator preimages must be independently domain-separated") + + +@dataclass(frozen=True, init=False) +class DiagnosticArbComparatorV1: + """Manifest declaration derived from admitted inputs and diagnostic BUILD.""" + + preimages: ArbComparatorPreimagesV1 + manifest: protocol.ContentResolvedComparatorManifestV2 + structural_source_identity: bytes + build_input_identity: bytes + pipeline_policy_identity: bytes + binary_sha256: bytes + rebuild_sha256s: tuple[bytes, bytes] + + def __new__(cls, *args: object, **kwargs: object) -> "DiagnosticArbComparatorV1": + if kwargs.get("_token") is not _COMPARATOR_TOKEN: + raise TypeError("DiagnosticArbComparatorV1 is controller-derived") + return object.__new__(cls) + + def __init__( + self, + preimages: ArbComparatorPreimagesV1, + manifest: protocol.ContentResolvedComparatorManifestV2, + structural_source_identity: bytes, + build_input_identity: bytes, + pipeline_policy_identity: bytes, + binary_sha256: bytes, + rebuild_sha256s: tuple[bytes, bytes], + *, + _token: object, + ) -> None: + if _token is not _COMPARATOR_TOKEN: + raise TypeError("DiagnosticArbComparatorV1 is controller-derived") + if type(preimages) is not ArbComparatorPreimagesV1: + raise TypeError("invalid comparator preimages") + if ( + type(manifest) is not protocol.ContentResolvedComparatorManifestV2 + or manifest.manifest.kind is not protocol.ComparatorKindV1.ARB + ): + raise TypeError("invalid Arb comparator manifest") + manifest_names = tuple( + item.name for item in fields(manifest.manifest) if item.name != "kind" + ) + preimage_names = tuple(item.name for item in fields(preimages)) + if manifest_names != preimage_names: + raise TypeError("comparator manifest/preimage schema drift") + by_digest = { + hashlib.sha256(getattr(preimages, name)).digest(): getattr(preimages, name) + for name in preimage_names + } + replayed = protocol.ContentResolvedComparatorManifestV2.admit( + manifest.manifest, + by_digest.get, + ) + if replayed.identity != manifest.identity: + raise TypeError("comparator manifest replay drift") + for name, value in ( + ("structural_source_identity", structural_source_identity), + ("build_input_identity", build_input_identity), + ("pipeline_policy_identity", pipeline_policy_identity), + ("binary_sha256", binary_sha256), + ): + if not _valid_digest(value): + raise TypeError(f"invalid {name}") + if ( + type(rebuild_sha256s) is not tuple + or rebuild_sha256s != (binary_sha256, binary_sha256) + ): + raise TypeError("invalid comparator rebuild binding") + for field_name, field_value in ( + ("preimages", preimages), + ("manifest", manifest), + ("structural_source_identity", structural_source_identity), + ("build_input_identity", build_input_identity), + ("pipeline_policy_identity", pipeline_policy_identity), + ("binary_sha256", binary_sha256), + ("rebuild_sha256s", rebuild_sha256s), + ): + object.__setattr__(self, field_name, field_value) + + @property + def identity(self) -> bytes: + return self.manifest.identity + + +@dataclass(frozen=True) +class PipelineRequestV1: + source_lock: provenance.ArbSourceLockV1 + admitted_sources: provenance.AdmittedArbSourcesV1 + build_sources: AdmittedBuildSourcesV1 + job: protocol.ProofJobV1 + runtime_binding: arb_runtime.ArbRuntimeBindingV1 + host_trust: HostTrustBoundaryV1 + + def __post_init__(self) -> None: + expected_types = ( + ("source_lock", self.source_lock, provenance.ArbSourceLockV1), + ("admitted_sources", self.admitted_sources, provenance.AdmittedArbSourcesV1), + ("build_sources", self.build_sources, AdmittedBuildSourcesV1), + ("job", self.job, protocol.ProofJobV1), + ("runtime_binding", self.runtime_binding, arb_runtime.ArbRuntimeBindingV1), + ("host_trust", self.host_trust, HostTrustBoundaryV1), + ) + for field_name, value, expected_type in expected_types: + if type(value) is not expected_type: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, field_name) + try: + source_lock = provenance.snapshot_source_closure_lock_v1(self.source_lock) + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "source_lock", + ) from error + if type(source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "source_lock", + ) + try: + admitted_sources = provenance.snapshot_admitted_source_closure_v1( + source_lock, + self.admitted_sources, + ) + if type(admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise TypeError("Arb request retained a non-Arb source closure") + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + try: + build_sources = admit_build_sources_v1(self.build_sources.files) + except ( + BuildSourceAdmissionErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "build_sources", + ) from error + try: + job = protocol.snapshot_proof_job_v1(self.job) + except ( + protocol.ProtocolErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "job", + ) from error + try: + runtime_binding = arb_runtime.ArbRuntimeBindingV1( + *tuple(self.runtime_binding) + ) + except ( + executor.ExecutionRequestErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + ) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "runtime_binding", + ) from error + try: + _host_trust_wire_v1(self.host_trust) + except TypeError as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.INVALID_RETAINED_INPUT, + "host_trust", + ) from error + _validate_pipeline_request_coordinates_v1( + source_lock, + admitted_sources, + build_sources, + job, + runtime_binding, + self.host_trust, + ) + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "admitted_sources", admitted_sources) + object.__setattr__(self, "build_sources", build_sources) + object.__setattr__(self, "job", job) + object.__setattr__(self, "runtime_binding", runtime_binding) + + +_PIPELINE_OWNED_REQUEST_TOKEN = object() + + +def _validate_pipeline_request_coordinates_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + build_sources: AdmittedBuildSourcesV1, + job: protocol.ProofJobV1, + runtime_binding: arb_runtime.ArbRuntimeBindingV1, + host_trust: HostTrustBoundaryV1, +) -> None: + """Check a detached request without reopening its already-owned archives.""" + + expected_types = ( + ("source_lock", source_lock, provenance.ArbSourceLockV1), + ("admitted_sources", admitted_sources, provenance.AdmittedArbSourcesV1), + ("build_sources", build_sources, AdmittedBuildSourcesV1), + ("job", job, protocol.ProofJobV1), + ("runtime_binding", runtime_binding, arb_runtime.ArbRuntimeBindingV1), + ("host_trust", host_trust, HostTrustBoundaryV1), + ) + for field_name, value, expected_type in expected_types: + if type(value) is not expected_type: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, field_name) + try: + _require_bound_source_capability_v1(source_lock, admitted_sources) + _owned_flint_source_content_partition_v1(source_lock, admitted_sources) + except PipelineInputErrorV1: + raise + except (AttributeError, TypeError, ValueError, OverflowError) as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) from error + if build_sources.formula_spec != job.formula_spec: + raise PipelineInputErrorV1(PipelineInputReasonV1.FORMULA_MISMATCH, "job") + job_bytes = protocol.ProofJobV1.encode(job) + invocation_bytes = sum( + len(value) + 1 + for value in ( + b"arb-evaluator", + b"--manifest-identity", + bytes(32).hex().encode("ascii"), + b"--job", + b"/dev/stdin", + ) + ) + sum( + len(key) + len(value) + 2 + for key, value in ((b"LC_ALL", b"C"), (b"TZ", b"UTC")) + ) + execution_limits = runtime_binding.limits + runtime_profile = runtime_binding.profile + allocation_profile_exceeded = ( + job.definition.knot_count > runtime_profile.max_knots + or any( + len(comparator.precision_ladder) > runtime_profile.max_policy_rungs + or comparator.precision_ladder[-1] > runtime_profile.max_precision_bits + for comparator in job.policy.comparators + ) + ) + if ( + execution_limits.max_executable_bytes > BUILD_STDOUT_LIMIT_V1 + or len(job_bytes) > runtime_profile.max_job_bytes + or allocation_profile_exceeded + or invocation_bytes > execution_limits.max_argument_bytes + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.EXECUTION_LIMIT_MISMATCH, + "runtime_binding", + ) + + +def _owned_pipeline_request_v1( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + build_sources: AdmittedBuildSourcesV1, + job: protocol.ProofJobV1, + runtime_binding: arb_runtime.ArbRuntimeBindingV1, + host_trust: HostTrustBoundaryV1, + *, + _token: object, +) -> PipelineRequestV1: + """Mint the request half of a private operation from already-owned values.""" + + if _token is not _PIPELINE_OWNED_REQUEST_TOKEN: + raise TypeError("owned pipeline requests are created only by operation replay") + _validate_pipeline_request_coordinates_v1( + source_lock, + admitted_sources, + build_sources, + job, + runtime_binding, + host_trust, + ) + request = object.__new__(PipelineRequestV1) + for field_name, value in ( + ("source_lock", source_lock), + ("admitted_sources", admitted_sources), + ("build_sources", build_sources), + ("job", job), + ("runtime_binding", runtime_binding), + ("host_trust", host_trust), + ): + object.__setattr__(request, field_name, value) + return request + + +_PIPELINE_OPERATION_SNAPSHOT_TOKEN = object() + + +@dataclass(frozen=True, init=False) +class _PipelineOperationSnapshotV1: + """One private operation capability; its contents never alias caller input.""" + + request: PipelineRequestV1 + source_closure: provenance.ReplayedSourceClosureV1 + + def __init__( + self, + request: PipelineRequestV1, + source_closure: provenance.ReplayedSourceClosureV1, + *, + _token: object, + ) -> None: + if _token is not _PIPELINE_OPERATION_SNAPSHOT_TOKEN: + raise TypeError("PipelineOperationSnapshotV1 is created only by pipeline replay") + if ( + type(request) is not PipelineRequestV1 + or type(source_closure) is not provenance.ReplayedSourceClosureV1 + or type(source_closure.source_lock) is not provenance.ArbSourceLockV1 + or type(source_closure.admitted_sources) is not provenance.AdmittedArbSourcesV1 + or request.source_lock.identity != source_closure.source_lock.identity + or request.admitted_sources.identity != source_closure.admitted_sources.identity + ): + raise TypeError("operation snapshot must retain one coherent Arb closure") + object.__setattr__(self, "request", request) + object.__setattr__(self, "source_closure", source_closure) + + +def _snapshot_pipeline_operation_v1( + request: object, +) -> _PipelineOperationSnapshotV1: + """Rebuild every authority-bearing request coordinate before any probe/spawn.""" + + if type(request) is not PipelineRequestV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "request") + try: + source_lock = request.source_lock + admitted_sources = request.admitted_sources + build_sources = request.build_sources + job = request.job + runtime_binding = request.runtime_binding + host_trust = request.host_trust + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + or type(build_sources) is not AdmittedBuildSourcesV1 + or type(job) is not protocol.ProofJobV1 + or type(runtime_binding) is not arb_runtime.ArbRuntimeBindingV1 + or type(host_trust) is not HostTrustBoundaryV1 + ): + raise PipelineInputErrorV1( + PipelineInputReasonV1.WRONG_TYPE, + "request", + ) + # Copy all non-source coordinates before archive replay can do work or + # trigger a reentrant hostile fixture. The protocol-owned copier reads + # raw fields rather than a mutable instance ``encode`` or cached digest. + canonical_job = protocol.snapshot_proof_job_v1(job) + canonical_build_sources = admit_build_sources_v1(build_sources.files) + canonical_runtime_binding = arb_runtime.ArbRuntimeBindingV1( + *tuple(runtime_binding) + ) + _host_trust_wire_v1(host_trust) + source_closure = provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + if type(source_closure.source_lock) is not provenance.ArbSourceLockV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + if type(source_closure.admitted_sources) is not provenance.AdmittedArbSourcesV1: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + canonical_request = _owned_pipeline_request_v1( + source_closure.source_lock, + source_closure.admitted_sources, + canonical_build_sources, + canonical_job, + canonical_runtime_binding, + host_trust, + _token=_PIPELINE_OWNED_REQUEST_TOKEN, + ) + except PipelineInputErrorV1: + raise + except Exception as error: + raise PipelineInputErrorV1( + PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "request", + ) from error + return _PipelineOperationSnapshotV1( + canonical_request, + source_closure, + _token=_PIPELINE_OPERATION_SNAPSHOT_TOKEN, + ) + + +def _derive_arb_comparator_for_build_v1( + snapshot: _PipelineOperationSnapshotV1, + docker_capability: build_transport.DockerSupportedV1, + binary: bytes, + rebuild_sha256s: tuple[bytes, bytes], + build_processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ], +) -> DiagnosticArbComparatorV1: + """Derive all ten coordinates without accepting a caller digest/resolver.""" + + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") + request = snapshot.request + if type(docker_capability) is not build_transport.DockerSupportedV1: + raise TypeError("docker_capability must be DockerSupportedV1") + if type(binary) is not bytes or not binary: + raise TypeError("binary must be exact nonempty bytes") + binary_sha256 = hashlib.sha256(binary).digest() + if ( + type(build_processes) is not tuple + or len(build_processes) != 2 + or any( + type(item) is not build_transport.DockerBuildExitedV1 + for item in build_processes + ) + or any(item.returncode != 0 for item in build_processes) + or rebuild_sha256s != (binary_sha256, binary_sha256) + ): + raise TypeError("comparator derivation requires two equal successful builds") + docker_capability_identity = build_transport.docker_capability_identity_v1( + docker_capability + ) + pipeline_policy_identity = pipeline_policy_identity_v2( + request.host_trust, + docker_capability.policy, + ) + flint_lock = request.source_lock.sources[2] + flint_source = request.admitted_sources.sources[2] + if type(flint_lock.integrity) is not provenance.GitContentRelationPolicyV1: + raise TypeError("FLINT requires the exact content-relation policy") + + exclusions = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.exclusions.v1\0", + ( + b"gap:host-and-docker-daemon-not-source-bound", + b"gap:unsealed-diagnostic-build-observer", + b"gap:libc-libm-libpthread-libgcc-and-build-utility-source", + b"gap:no-per-test-result-records", + b"gap:no-git-derivation-for-project-pinned-release-only-files", + b"gap:no-origin-authority-reverification", + _host_trust_wire_v1(request.host_trust), + b"build-observation=diagnostic-unsealed-v1", + len(flint_lock.integrity.omitted_paths).to_bytes(4, "big"), + *( + path.encode("ascii") + for path in flint_lock.integrity.omitted_paths + ), + len( + flint_lock.integrity.project_pinned_release_only_files + ).to_bytes(4, "big"), + *( + item.encode() + for item in flint_lock.integrity.project_pinned_release_only_files + ), + ), + ) + + upstream_chunks: list[bytes] = [ + request.source_lock.encode(), + request.admitted_sources.source_lock_identity, + len(request.source_lock.sources).to_bytes(4, "big"), + ] + for materialized in snapshot.source_closure.sources: + upstream_chunks.extend( + provenance._materialized_source_coordinates_v1(materialized) + ) + upstream_source = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.upstream-source.v1\0", + tuple(upstream_chunks), + ) + + operation_allowlist = _operation_allowlist_preimage_v1( + request.build_sources.formula_spec + ) + arithmetic_chunks: list[bytes] = [ + b"exact admitted GMP MPFR FLINT source snapshots and pinned static-build boundary", + len(request.source_lock.sources).to_bytes(4, "big"), + ] + for lock, source in zip( + request.source_lock.sources, + request.admitted_sources.sources, + strict=True, + ): + arithmetic_chunks.extend( + ( + bytes((int(lock.role),)), + lock.identity, + source.archive_sha256, + source.tree_identity, + ) + ) + arithmetic_chunks.extend( + ( + OCI_IMAGE_REFERENCE_V1.encode("ascii"), + OCI_PLATFORM_V1.encode("ascii"), + hashlib.sha256(operation_allowlist).digest(), + hashlib.sha256(exclusions).digest(), + ) + ) + arithmetic_input_set = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.arithmetic-input-set.v1\0", + tuple(arithmetic_chunks), + ) + + wrapper_paths = frozenset( + ( + "proof/region/v1/arb/evaluator/formula.h", + "proof/region/v1/arb/evaluator/interval.c", + "proof/region/v1/arb/evaluator/interval.h", + ) + ) + wrapper_files = tuple( + item for item in request.build_sources.files if item.path in wrapper_paths + ) + evaluator_files = tuple( + item + for item in request.build_sources.files + if item.path not in ( + FORMULA_SPEC_PATH_V1, + FORMULA_GENERATOR_PATH_V1, + BUILD_RECIPE_PATH_V1, + INNER_BUILD_RECIPE_PATH_V1, + ) + and item.path not in wrapper_paths + ) + wrapper_source = _encoded_build_file_set_v1( + b"labcolors.proof-region.arb-comparator.wrapper-source.v1\0", + wrapper_files, + ) + evaluator_source = _encoded_build_file_set_v1( + b"labcolors.proof-region.arb-comparator.evaluator-source.v1\0", + evaluator_files, + ) + + process_bytes = tuple( + build_transport.build_process_bytes_v1(item) for item in build_processes + ) + build_identity = _comparator_build_preimage_v2( + request.build_sources, + docker_capability_identity, + pipeline_policy_identity, + build_processes, + binary_sha256, + rebuild_sha256s, + len(binary), + ) + test_observation = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.test-observation.v1\0", + ( + b"kind:aggregate-outer-process-observation-no-per-test-records", + request.build_sources.contents(BUILD_RECIPE_PATH_V1), + len(build_processes).to_bytes(4, "big"), + *process_bytes, + ), + ) + + legal_chunks: list[bytes] = [ + b"ordered admitted legal-file set; no legal-compliance claim", + len(request.source_lock.sources).to_bytes(4, "big"), + ] + for lock, source in zip( + request.source_lock.sources, + request.admitted_sources.sources, + strict=True, + ): + actual_by_path = {item.path: item for item in source.files} + legal_chunks.extend( + ( + bytes((int(lock.role),)), + lock.identity, + source.archive_sha256, + source.tree_identity, + len(lock.legal_files).to_bytes(4, "big"), + ) + ) + for declaration in lock.legal_files: + actual = actual_by_path.get(declaration.path) + if ( + actual is None + or actual.length != declaration.length + or actual.sha256 != declaration.sha256 + ): + raise TypeError("admitted legal-file set drift") + legal_chunks.extend( + ( + declaration.encode(), + actual.path.encode("ascii"), + actual.mode.to_bytes(4, "big"), + actual.length.to_bytes(8, "big"), + actual.sha256, + ) + ) + legal_file_set = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.legal-file-set.v1\0", + tuple(legal_chunks), + ) + + engine_release = _comparator_preimage_v1( + b"labcolors.proof-region.arb-comparator.engine-release.v1\0", + ( + b"FLINT release lock declaration", + flint_lock.encode(), + flint_source.source_lock_identity, + ), + ) + preimages = ArbComparatorPreimagesV1( + engine_release, + upstream_source, + arithmetic_input_set, + wrapper_source, + evaluator_source, + build_identity, + operation_allowlist, + test_observation, + legal_file_set, + exclusions, + ) + coordinates = tuple( + hashlib.sha256(getattr(preimages, item.name)).digest() + for item in fields(preimages) + ) + manifest_value = protocol.ComparatorManifestV2( + protocol.ComparatorKindV1.ARB, + *coordinates, + ) + by_digest = { + coordinate: getattr(preimages, item.name) + for coordinate, item in zip(coordinates, fields(preimages), strict=True) + } + resolved = protocol.ContentResolvedComparatorManifestV2.admit( + manifest_value, + by_digest.get, + ) + return DiagnosticArbComparatorV1( + preimages, + resolved, + request.admitted_sources.identity, + request.build_sources.build_input_identity, + pipeline_policy_identity, + binary_sha256, + rebuild_sha256s, + _token=_COMPARATOR_TOKEN, + ) + + +@dataclass(frozen=True) +class PipelineBlockedV1: + reason: build_transport.DockerBlockerReasonV1 + detail: str + + +class ExecutionFailureReasonV1(StrEnum): + UNSUPPORTED = "unsupported" + PROCESS_FAILED = "process_failed" + EVALUATOR_INPUT_REJECTED = "evaluator_input_rejected" + EVALUATOR_INPUT_LIMIT = "evaluator_input_limit" + EVALUATOR_OUTPUT_LIMIT = "evaluator_output_limit" + EVALUATOR_RESOURCE_LIMIT = "evaluator_resource_limit" + EVALUATOR_INTERNAL = "evaluator_internal" + EVALUATOR_IO = "evaluator_io" + STDERR_NOT_EMPTY = "stderr_not_empty" + BINARY_MISMATCH = "binary_mismatch" + BACKEND_CONTRACT = "backend_contract" + + +@dataclass(frozen=True) +class ExecutionRejectedV1: + reason: ExecutionFailureReasonV1 + observation: object + + +class TranscriptFailureReasonV1(StrEnum): + INVALID_WIRE = "invalid_wire" + FOREIGN_BINDING = "foreign_binding" + + +@dataclass(frozen=True) +class TranscriptRejectedV1: + reason: TranscriptFailureReasonV1 + detail: str + + +@dataclass(frozen=True, init=False) +class DiagnosticBuildObservationV1: + """Controller-owned two-build observation with no native-evidence claim.""" + + structural_source_identity: bytes + flint_commit_content_identity: bytes + flint_commit_content_file_count: int + flint_project_pinned_release_only_identity: bytes + flint_project_pinned_release_only_file_count: int + build_input_identity: bytes + formula_support_identity: bytes + pipeline_policy_identity: bytes + docker_capability: build_transport.DockerSupportedV1 + binary_sha256: bytes + rebuild_sha256s: tuple[bytes, bytes] + host_trust: HostTrustBoundaryV1 + input_bundle_identity: bytes + input_bundle_sha256: bytes + input_bundle_length: int + build_processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ] + comparator: DiagnosticArbComparatorV1 + _binary: bytes + _rebuild_binaries: tuple[bytes, bytes] + _input_bundle: build_input.SealedInputV1 + + def __init__( + self, + structural_source_identity: bytes, + flint_commit_content_identity: bytes, + flint_commit_content_file_count: int, + flint_project_pinned_release_only_identity: bytes, + flint_project_pinned_release_only_file_count: int, + build_input_identity: bytes, + formula_support_identity: bytes, + pipeline_policy_identity: bytes, + docker_capability: build_transport.DockerSupportedV1, + binary_sha256: bytes, + rebuild_sha256s: tuple[bytes, bytes], + host_trust: HostTrustBoundaryV1, + input_bundle_identity: bytes, + input_bundle_sha256: bytes, + input_bundle_length: int, + build_processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ], + comparator: DiagnosticArbComparatorV1, + rebuild_binaries: tuple[bytes, bytes], + input_bundle: build_input.SealedInputV1, + *, + _token: object, + ) -> None: + if _token is not _BUILD_OBSERVATION_TOKEN: + raise TypeError("DiagnosticBuildObservationV1 is controller-only") + for name, value in ( + ("structural_source_identity", structural_source_identity), + ("flint_commit_content_identity", flint_commit_content_identity), + ( + "flint_project_pinned_release_only_identity", + flint_project_pinned_release_only_identity, + ), + ("build_input_identity", build_input_identity), + ("formula_support_identity", formula_support_identity), + ("pipeline_policy_identity", pipeline_policy_identity), + ("binary_sha256", binary_sha256), + ("input_bundle_identity", input_bundle_identity), + ("input_bundle_sha256", input_bundle_sha256), + ): + if not _valid_digest(value): + raise TypeError(f"invalid {name}") + if ( + type(flint_commit_content_file_count) is not int + or flint_commit_content_file_count <= 0 + or type(flint_project_pinned_release_only_file_count) is not int + or flint_project_pinned_release_only_file_count <= 0 + ): + raise TypeError("FLINT source partition must be nonempty") + if type(docker_capability) is not build_transport.DockerSupportedV1: + raise TypeError("diagnostic build requires DockerSupportedV1") + canonical_capability = build_transport.DockerSupportedV1( + *tuple(docker_capability) + ) + if tuple(canonical_capability) != tuple(docker_capability): + raise TypeError("diagnostic build does not bind the exact Arb capability") + if ( + type(rebuild_sha256s) is not tuple + or len(rebuild_sha256s) != 2 + or any(not _valid_digest(item) for item in rebuild_sha256s) + or rebuild_sha256s != (binary_sha256, binary_sha256) + ): + raise TypeError("invalid observed two-build digests") + _host_trust_wire_v1(host_trust) + if type(input_bundle_length) is not int or input_bundle_length <= 0: + raise TypeError("invalid build input bundle length") + if ( + not build_input.sealed_input_is_intact_v1(input_bundle) + or input_bundle.binding_identity != input_bundle_identity + or input_bundle.sha256 != input_bundle_sha256 + or input_bundle.length != input_bundle_length + or input_bundle.binding_identity + != _arb_input_binding_identity_v2( + structural_source_identity, + build_input_identity, + input_bundle.contents, + canonical_capability.policy, + ) + ): + raise TypeError("diagnostic build lost its sealed input bundle") + if pipeline_policy_identity != pipeline_policy_identity_v2( + host_trust, + canonical_capability.policy, + ): + raise TypeError("pipeline policy is not the fixed diagnostic policy") + if ( + type(build_processes) is not tuple + or len(build_processes) != 2 + or any( + type(item) is not build_transport.DockerBuildExitedV1 + for item in build_processes + ) + or any(item.returncode != 0 for item in build_processes) + ): + raise TypeError("invalid build process observations") + if ( + any( + item.input_transfer.bundle_identity != input_bundle_identity + or item.input_transfer.expected_length != input_bundle_length + or item.input_transfer.expected_sha256 != input_bundle_sha256 + or item.input_transfer.written_length != input_bundle_length + or item.input_transfer.written_sha256 != input_bundle_sha256 + for item in build_processes + ) + ): + raise TypeError("builds did not consume the exact sealed input bundle") + if ( + type(comparator) is not DiagnosticArbComparatorV1 + or comparator.structural_source_identity != structural_source_identity + or comparator.build_input_identity != build_input_identity + or comparator.pipeline_policy_identity != pipeline_policy_identity + or comparator.binary_sha256 != binary_sha256 + or comparator.rebuild_sha256s != rebuild_sha256s + ): + raise TypeError("comparator does not bind this diagnostic build") + if ( + type(rebuild_binaries) is not tuple + or len(rebuild_binaries) != 2 + or any(type(item) is not bytes for item in rebuild_binaries) + or rebuild_binaries[0] != rebuild_binaries[1] + or tuple(hashlib.sha256(item).digest() for item in rebuild_binaries) + != rebuild_sha256s + ): + raise TypeError("invalid owned rebuild binaries") + for field_name, field_value in ( + ("structural_source_identity", structural_source_identity), + ("flint_commit_content_identity", flint_commit_content_identity), + ("flint_commit_content_file_count", flint_commit_content_file_count), + ( + "flint_project_pinned_release_only_identity", + flint_project_pinned_release_only_identity, + ), + ( + "flint_project_pinned_release_only_file_count", + flint_project_pinned_release_only_file_count, + ), + ("build_input_identity", build_input_identity), + ("formula_support_identity", formula_support_identity), + ("pipeline_policy_identity", pipeline_policy_identity), + ("docker_capability", docker_capability), + ("binary_sha256", binary_sha256), + ("rebuild_sha256s", rebuild_sha256s), + ("host_trust", host_trust), + ("input_bundle_identity", input_bundle_identity), + ("input_bundle_sha256", input_bundle_sha256), + ("input_bundle_length", input_bundle_length), + ("build_processes", build_processes), + ("comparator", comparator), + ): + object.__setattr__(self, field_name, field_value) + object.__setattr__(self, "_binary", rebuild_binaries[0]) + object.__setattr__(self, "_rebuild_binaries", rebuild_binaries) + object.__setattr__(self, "_input_bundle", input_bundle) + + @property + def binary(self) -> bytes: + return self._binary + + @property + def rebuild_binaries(self) -> tuple[bytes, bytes]: + return self._rebuild_binaries + + @property + def input_transfers( + self, + ) -> tuple[ + build_transport.BuildInputTransferV1, + build_transport.BuildInputTransferV1, + ]: + first = self.build_processes[0].input_transfer + second = self.build_processes[1].input_transfer + if ( + type(first) is not build_transport.BuildInputTransferV1 + or type(second) is not build_transport.BuildInputTransferV1 + ): + raise RuntimeError("sealed build observation lost its input transfer") + return first, second + + @property + def input_bundle(self) -> build_input.SealedInputV1: + if ( + not build_input.sealed_input_is_intact_v1(self._input_bundle) + or self._input_bundle.binding_identity != self.input_bundle_identity + or self._input_bundle.sha256 != self.input_bundle_sha256 + or self._input_bundle.length != self.input_bundle_length + ): + raise RuntimeError("diagnostic build lost its exact input bytes") + return self._input_bundle + + +BuildResultV1: TypeAlias = ( + DiagnosticBuildObservationV1 + | PipelineBlockedV1 + | build_transport.BuildRejectedV1 + | build_transport.TwoBuildObservationV1 +) + + +class ControlledPipelineV1: + def __init__( + self, + *, + build_backend: build_transport.DockerBuildBackendV1, + ) -> None: + self._transport = build_transport.ControlledBuildTransportV1( + policy=ARB_BUILD_TRANSPORT_POLICY_V1, + backend=build_backend, + ) + + @staticmethod + def _admit_arb_output_v1(binary: bytes) -> bool: + try: + executor.require_static_x86_64_elf_v1(binary) + except executor.ExecutionRequestErrorV1: + return False + return True + + def build(self, request: PipelineRequestV1) -> BuildResultV1: + """Observe two fresh equal builds without requiring a RUN capability.""" + + if type(request) is not PipelineRequestV1: + raise PipelineInputErrorV1(PipelineInputReasonV1.WRONG_TYPE, "request") + try: + snapshot = _snapshot_pipeline_operation_v1(request) + except Exception: + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + return self._build_snapshot_v1(snapshot) + + def _build_snapshot_v1( + self, + snapshot: _PipelineOperationSnapshotV1, + ) -> BuildResultV1: + """Consume one controller-owned snapshot without replaying its closure.""" + + if type(snapshot) is not _PipelineOperationSnapshotV1: + raise TypeError("snapshot must be _PipelineOperationSnapshotV1") + request = snapshot.request + probe_result = self._transport.probe() + if type(probe_result) is build_transport.DockerUnsupportedV1: + return PipelineBlockedV1(probe_result.reason, probe_result.detail) + if type(probe_result) is not build_transport.DockerSupportedV1: + return PipelineBlockedV1( + build_transport.DockerBlockerReasonV1.BACKEND_CONTRACT, + "Docker capability report is not typed", + ) + docker_capability = probe_result + try: + input_bundle = _seal_build_input_from_snapshot_v1( + snapshot, + docker_capability.policy, + ) + except ( + OSError, + TypeError, + ValueError, + BuildSourceAdmissionErrorV1, + provenance.ProvenanceErrorV1, + build_input.InputErrorV1, + ): + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + built = self._transport.build( + docker_capability, + input_bundle, + request.runtime_binding.limits.max_executable_bytes, + input_admission=lambda value: _owned_arb_input_is_bound_v1( + value, + input_bundle, + ), + output_admission=self._admit_arb_output_v1, + ) + if type(built) is build_transport.BuildRejectedV1: + return built + if type(built) is not build_transport.TwoBuildObservationV1: + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + if not build_transport.two_build_observation_matches_v1( + built, + built.session, + ): + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + if built.relation is build_transport.BuildByteRelationV1.DIFFERENT: + return built + if built.relation is not build_transport.BuildByteRelationV1.IDENTICAL: + return build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + + binary = built.outputs[0] + rebuild_sha256s = tuple( + hashlib.sha256(item).digest() for item in built.outputs + ) + build_processes = built.processes + comparator = _derive_arb_comparator_for_build_v1( + snapshot, + docker_capability, + binary, + rebuild_sha256s, + build_processes, + ) + flint_partition = _owned_flint_source_content_partition_v1( + request.source_lock, + request.admitted_sources, + ) + return DiagnosticBuildObservationV1( + request.admitted_sources.identity, + flint_partition.commit_content_identity, + flint_partition.commit_content_file_count, + flint_partition.project_pinned_release_only_identity, + flint_partition.project_pinned_release_only_file_count, + request.build_sources.build_input_identity, + request.build_sources.formula_support_identity, + pipeline_policy_identity_v2( + request.host_trust, + docker_capability.policy, + ), + docker_capability, + rebuild_sha256s[0], + rebuild_sha256s, + request.host_trust, + input_bundle.binding_identity, + input_bundle.sha256, + input_bundle.length, + build_processes, + comparator, + built.outputs, + input_bundle, + _token=_BUILD_OBSERVATION_TOKEN, + ) diff --git a/proof/region/v1/arb/receipt.py b/proof/region/v1/arb/receipt.py new file mode 100644 index 00000000..bd8c8786 --- /dev/null +++ b/proof/region/v1/arb/receipt.py @@ -0,0 +1,1347 @@ +#!/usr/bin/env python3 +"""One controller-owned source → BUILD → RUN evidence boundary for Arb. + +The receipt certifies only the causal observation assembled here. It does not +classify colors, validate interval semantics, or mint a dual proof. The Linux +host and Docker daemon remain declared V1 trust inputs. +""" + +from __future__ import annotations + +import hashlib +import os +import threading +from dataclasses import dataclass, fields +from enum import StrEnum +from pathlib import Path +from typing import TypeAlias + +from arb import pipeline +from arb import runtime as arb_runtime +from build import transport as build_transport + +import executor +import provenance +import region_proof_protocol as protocol + + +_EVIDENCE_TOKEN = object() +_RECEIPT_TOKEN = object() +_NATIVE_BUILD_BACKEND_TYPE = build_transport.NativeDockerBuildBackendV1 +_NATIVE_RUN_BACKEND_TYPE = executor.NativeLinuxBackendV1 + +_SOURCE_ID_LABEL_V1 = b"labcolors.proof-region.arb-source-replay.v1\0" +_BUILD_ID_LABEL_V2 = b"labcolors.proof-region.arb-build-replay.v2\0" +_RUN_ID_LABEL_V1 = b"labcolors.proof-region.arb-run-replay.v1\0" +_EVIDENCE_ID_LABEL_V1 = b"labcolors.proof-region.arb-evaluator-replay.v1\0" +_EVIDENCE_STABILITY_LABEL_V1 = ( + b"labcolors.proof-region.arb-evaluator-stability.v1\0" +) +_SOURCE_BOUND_POLICY_ID_LABEL_V3 = ( + b"labcolors.proof-region.arb-source-bound-policy.v3\0" +) + +_DIAGNOSTIC_BUILD_FIELDS_V1 = ( + "structural_source_identity", + "flint_commit_content_identity", + "flint_commit_content_file_count", + "flint_project_pinned_release_only_identity", + "flint_project_pinned_release_only_file_count", + "build_input_identity", + "formula_support_identity", + "pipeline_policy_identity", + "docker_capability", + "binary_sha256", + "rebuild_sha256s", + "host_trust", + "input_bundle_identity", + "input_bundle_sha256", + "input_bundle_length", + "build_processes", + "comparator", + "_binary", + "_rebuild_binaries", + "_input_bundle", +) +_DIAGNOSTIC_COMPARATOR_FIELDS_V1 = ( + "preimages", + "manifest", + "structural_source_identity", + "build_input_identity", + "pipeline_policy_identity", + "binary_sha256", + "rebuild_sha256s", +) + + +def _blob(value: bytes) -> bytes: + return len(value).to_bytes(8, "big") + value + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join(_blob(chunk) for chunk in chunks) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def source_bound_policy_identity_v3( + capability: build_transport.DockerSupportedV1, + host_trust: pipeline.HostTrustBoundaryV1, + runtime_binding: arb_runtime.ArbRuntimeBindingV1, +) -> bytes: + """Identity of the exact observation rules and observed BUILD capability.""" + + capability_identity = build_transport.docker_capability_identity_v1(capability) + runtime_identity = arb_runtime.runtime_binding_identity_v1(runtime_binding) + if type(runtime_identity) is not bytes: + raise TypeError("runtime binding did not replay") + + return _identity( + _SOURCE_BOUND_POLICY_ID_LABEL_V3, + ( + pipeline.pipeline_policy_identity_v2( + host_trust, + capability.policy, + ), + capability_identity, + runtime_identity, + executor.SANDBOX_POLICY_RELEASE_V1.encode("ascii"), + b"authority=one-shot-native-controller", + b"source=lock-plus-owned-archive-and-build-input-replay", + b"build=one-sealed-bundle-two-fresh-byte-equal-attempts", + b"run=retained-executable-object-one-contained-process", + b"identity=immutable-coordinates-total-rejection-v3", + b"claim=provenance-only-no-numerical-semantics", + b"trust=unsealed-linux-x64-host-native-docker-cli-and-daemon", + ), + ) + + +def _source_identity_from_operation_v1( + snapshot: pipeline._PipelineOperationSnapshotV1, +) -> bytes: + """Derive source identity from one operation-owned materialization.""" + + if type(snapshot) is not pipeline._PipelineOperationSnapshotV1: + raise TypeError("source identity requires a pipeline operation snapshot") + request = snapshot.request + chunks: list[bytes] = [ + request.source_lock.encode(), + request.source_lock.identity, + request.admitted_sources.identity, + ] + for materialized in snapshot.source_closure.sources: + chunks.extend(provenance._materialized_source_coordinates_v1(materialized)) + chunks.extend( + ( + request.build_sources.identity, + request.build_sources.build_input_identity, + request.build_sources.formula_support_identity, + pipeline.build_source_manifest_bytes_v1(request.build_sources), + ) + ) + return _identity(_SOURCE_ID_LABEL_V1, tuple(chunks)) + + +def _source_identity_v1(request: pipeline.PipelineRequestV1) -> bytes: + """Independently derive source identity from a public request.""" + + return _source_identity_from_operation_v1( + pipeline._snapshot_pipeline_operation_v1(request) + ) + + +def _comparator_replays_from_operation_v1( + snapshot: pipeline._PipelineOperationSnapshotV1, + build: pipeline.DiagnosticBuildObservationV1, +) -> bool: + try: + if ( + type(snapshot) is not pipeline._PipelineOperationSnapshotV1 + or type(build) is not pipeline.DiagnosticBuildObservationV1 + ): + return False + expected = pipeline._derive_arb_comparator_for_build_v1( + snapshot, + build.docker_capability, + build.binary, + build.rebuild_sha256s, + build.build_processes, + ) + return build.comparator == expected + except Exception: + return False + + +def _build_identity_from_operation_v2( + snapshot: pipeline._PipelineOperationSnapshotV1, + source_identity: bytes, + build: pipeline.DiagnosticBuildObservationV1, +) -> bytes: + if type(snapshot) is not pipeline._PipelineOperationSnapshotV1: + raise TypeError("build identity requires a pipeline operation snapshot") + if type(build) is not pipeline.DiagnosticBuildObservationV1: + raise TypeError("build replay requires DiagnosticBuildObservationV1") + request = snapshot.request + bundle = build.input_bundle + processes = build.build_processes + binaries = build.rebuild_binaries + capability_identity = build_transport.docker_capability_identity_v1( + build.docker_capability + ) + flint_partition = pipeline._owned_flint_source_content_partition_v1( + request.source_lock, + request.admitted_sources, + ) + expected_bundle = pipeline._seal_build_input_from_snapshot_v1( + snapshot, + build.docker_capability.policy, + ) + if ( + build.structural_source_identity != request.admitted_sources.identity + or build.flint_commit_content_identity + != flint_partition.commit_content_identity + or build.flint_commit_content_file_count + != flint_partition.commit_content_file_count + or build.flint_project_pinned_release_only_identity + != flint_partition.project_pinned_release_only_identity + or build.flint_project_pinned_release_only_file_count + != flint_partition.project_pinned_release_only_file_count + or build.build_input_identity != request.build_sources.build_input_identity + or build.formula_support_identity + != request.build_sources.formula_support_identity + or build.pipeline_policy_identity + != pipeline.pipeline_policy_identity_v2( + request.host_trust, + build.docker_capability.policy, + ) + or build.host_trust is not request.host_trust + or not pipeline._owned_arb_input_is_bound_v1(bundle, expected_bundle) + or build.input_bundle_identity != bundle.binding_identity + or build.input_bundle_sha256 != bundle.sha256 + or build.input_bundle_length != bundle.length + or type(processes) is not tuple + or len(processes) != 2 + or any( + type(item) is not build_transport.DockerBuildExitedV1 + for item in processes + ) + or type(binaries) is not tuple + or len(binaries) != 2 + or binaries[0] is not processes[0].stdout + or binaries[1] is not processes[1].stdout + or binaries[0] != binaries[1] + or build.binary is not binaries[0] + or build.binary_sha256 != hashlib.sha256(build.binary).digest() + or build.rebuild_sha256s != (build.binary_sha256, build.binary_sha256) + or not _comparator_replays_from_operation_v1(snapshot, build) + ): + raise TypeError("controller-observed BUILD did not replay") + for process in processes: + transfer = process.input_transfer + if ( + process.returncode != 0 + or type(transfer) is not build_transport.BuildInputTransferV1 + or transfer.bundle_identity != bundle.binding_identity + or transfer.expected_length != bundle.length + or transfer.expected_sha256 != bundle.sha256 + or transfer.written_length != bundle.length + or transfer.written_sha256 != bundle.sha256 + ): + raise TypeError("BUILD transfer did not consume the sealed bundle") + return _identity( + _BUILD_ID_LABEL_V2, + ( + source_identity, + build.pipeline_policy_identity, + pipeline._host_trust_wire_v1(build.host_trust), + capability_identity, + bundle.binding_identity, + bundle.sha256, + bundle.length.to_bytes(8, "big"), + build_transport.build_process_bytes_v1(processes[0]), + build_transport.build_process_bytes_v1(processes[1]), + build.binary_sha256, + len(build.binary).to_bytes(8, "big"), + build.comparator.identity, + ), + ) + + +def _build_identity_v2( + request: pipeline.PipelineRequestV1, + source_identity: bytes, + build: pipeline.DiagnosticBuildObservationV1, +) -> bytes: + """Independently derive BUILD identity from a public request.""" + + return _build_identity_from_operation_v2( + pipeline._snapshot_pipeline_operation_v1(request), + source_identity, + build, + ) + + +def _run_identity_v1( + request: pipeline.PipelineRequestV1, + build: pipeline.DiagnosticBuildObservationV1, + build_identity: bytes, + invocation: executor.ExecutionRequestV1, + platform_value: executor.SupportedV1, + process: executor.CompletedV1, + transcript: protocol.DecisionTranscriptV1, + run_claim: protocol.RunClaimV1, +) -> bytes: + expected_invocation = executor.ExecutionRequestV1( + executable=build.binary, + argv=( + b"arb-evaluator", + b"--manifest-identity", + build.comparator.identity.hex().encode("ascii"), + b"--job", + b"/dev/stdin", + ), + environment=((b"LC_ALL", b"C"), (b"TZ", b"UTC")), + cwd=b"/", + stdin=request.job.encode(), + umask=0o077, + limits=request.runtime_binding.limits, + ) + if ( + type(invocation) is not executor.ExecutionRequestV1 + or type(platform_value) is not executor.SupportedV1 + or platform_value.platform != executor.EXECUTION_PLATFORM_V1 + or platform_value.sandbox_policy_release + != executor.SANDBOX_POLICY_RELEASE_V1 + or type(process) is not executor.CompletedV1 + or type(transcript) is not protocol.DecisionTranscriptV1 + or type(run_claim) is not protocol.RunClaimV1 + or invocation != expected_invocation + or invocation.executable is not build.binary + or process.binary_sha256 != build.binary_sha256 + or not executor.result_matches_request_v1(process, invocation) + or process.stderr + or transcript.encode() != process.stdout + or transcript.job_identity != request.job.identity + or transcript.domain_identity != request.job.domain.identity + or transcript.comparator_identity != build.comparator.identity + or transcript.point_count != request.job.domain.point_count + ): + raise TypeError("controller-observed RUN did not replay") + parsed = protocol.DecisionTranscriptV1.parse(process.stdout) + if parsed.encode() != process.stdout or parsed.identity != transcript.identity: + raise TypeError("RUN stdout is not the retained canonical transcript") + protocol.validate_witness_alignment_v1( + request.job.domain, + transcript.decision_bits, + transcript.point_count, + transcript.counters, + transcript.witness_store, + ) + invocation_identity = executor.invocation_identity_v1(invocation) + platform_identity = executor.platform_identity_v1(platform_value) + if ( + type(invocation_identity) is not bytes + or type(platform_identity) is not bytes + ): + raise TypeError("execution identity replay was rejected") + expected_claim = protocol.RunClaimV1.for_transcript( + request.job, + build.comparator.manifest, + transcript, + build.binary_sha256, + invocation_identity, + platform_identity, + ) + if expected_claim != run_claim: + raise TypeError("RunClaimV1 did not replay") + process_identity = _identity( + b"labcolors.proof-region.arb-run-process.v1\0", + ( + process.binary_sha256, + process.stdout, + process.stderr, + ), + ) + return _identity( + _RUN_ID_LABEL_V1, + ( + build_identity, + build.comparator.identity, + request.job.identity, + invocation_identity, + platform_identity, + process_identity, + transcript.identity, + run_claim.identity, + ), + ) + + +@dataclass(frozen=True, init=False) +class ContentResolvedEvaluatorReplayV1: + """Immutable DAG whose three identities commit source, BUILD and RUN edges.""" + + request: pipeline.PipelineRequestV1 + build: pipeline.DiagnosticBuildObservationV1 + invocation: executor.ExecutionRequestV1 + platform: executor.SupportedV1 + process: executor.CompletedV1 + transcript: protocol.DecisionTranscriptV1 + run_claim: protocol.RunClaimV1 + source_identity: bytes + build_identity: bytes + run_identity: bytes + _identity: bytes + + def __new__(cls, *args: object, **kwargs: object) -> "ContentResolvedEvaluatorReplayV1": + if kwargs.get("_token") is not _EVIDENCE_TOKEN: + raise TypeError("ContentResolvedEvaluatorReplayV1 is controller-derived") + return object.__new__(cls) + + def __init__( + self, + request: pipeline.PipelineRequestV1, + build: pipeline.DiagnosticBuildObservationV1, + invocation: executor.ExecutionRequestV1, + platform_value: executor.SupportedV1, + process: executor.CompletedV1, + transcript: protocol.DecisionTranscriptV1, + run_claim: protocol.RunClaimV1, + *, + _operation: pipeline._PipelineOperationSnapshotV1, + _token: object, + ) -> None: + if ( + _token is not _EVIDENCE_TOKEN + or type(_operation) is not pipeline._PipelineOperationSnapshotV1 + or _operation.request is not request + ): + raise TypeError("ContentResolvedEvaluatorReplayV1 is controller-derived") + source_identity = _source_identity_from_operation_v1(_operation) + build_identity = _build_identity_from_operation_v2( + _operation, + source_identity, + build, + ) + run_identity = _run_identity_v1( + request, + build, + build_identity, + invocation, + platform_value, + process, + transcript, + run_claim, + ) + identity = _identity( + _EVIDENCE_ID_LABEL_V1, + (source_identity, build_identity, run_identity), + ) + for name, value in ( + ("request", request), + ("build", build), + ("invocation", invocation), + ("platform", platform_value), + ("process", process), + ("transcript", transcript), + ("run_claim", run_claim), + ("source_identity", source_identity), + ("build_identity", build_identity), + ("run_identity", run_identity), + ("_identity", identity), + ): + object.__setattr__(self, name, value) + + @property + def executable(self) -> bytes: + return self.build.binary + + @property + def identity(self) -> bytes: + return self._identity + + +@dataclass(frozen=True) +class _EvidenceFieldsV1: + """One non-reentrant observation of every public evidence coordinate.""" + + request: pipeline.PipelineRequestV1 + build: pipeline.DiagnosticBuildObservationV1 + invocation: executor.ExecutionRequestV1 + platform: executor.SupportedV1 + process: executor.CompletedV1 + transcript: protocol.DecisionTranscriptV1 + run_claim: protocol.RunClaimV1 + source_identity: bytes + build_identity: bytes + run_identity: bytes + identity: bytes + + +def _capture_evidence_fields_v1(value: object) -> _EvidenceFieldsV1: + """Read all public fields before replay can re-enter a hostile fixture.""" + + if type(value) is not ContentResolvedEvaluatorReplayV1: + raise TypeError("evidence must be ContentResolvedEvaluatorReplayV1") + return _EvidenceFieldsV1( + value.request, + value.build, + value.invocation, + value.platform, + value.process, + value.transcript, + value.run_claim, + value.source_identity, + value.build_identity, + value.run_identity, + value._identity, + ) + + +def _same_evidence_references_v1( + first: _EvidenceFieldsV1, + second: _EvidenceFieldsV1, +) -> bool: + """Reject replacement even when an attacker chooses equal-looking values.""" + + return ( + first.request is second.request + and first.build is second.build + and first.invocation is second.invocation + and first.platform is second.platform + and first.process is second.process + and first.transcript is second.transcript + and first.run_claim is second.run_claim + and first.source_identity is second.source_identity + and first.build_identity is second.build_identity + and first.run_identity is second.run_identity + and first.identity is second.identity + ) + + +def _request_replay_coordinates_v1( + request: pipeline.PipelineRequestV1, +) -> tuple[bytes, ...]: + """Project a request without reopening a second materialized source closure.""" + + if type(request) is not pipeline.PipelineRequestV1: + raise TypeError("request must be PipelineRequestV1") + source_lock = request.source_lock + admitted_sources = request.admitted_sources + build_sources = request.build_sources + job = request.job + runtime_binding = request.runtime_binding + if ( + type(source_lock) is not provenance.ArbSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedArbSourcesV1 + or type(build_sources) is not pipeline.AdmittedBuildSourcesV1 + or type(job) is not protocol.ProofJobV1 + or type(runtime_binding) is not arb_runtime.ArbRuntimeBindingV1 + or admitted_sources.source_lock_identity != source_lock.identity + or type(source_lock.sources) is not tuple + or type(admitted_sources.sources) is not tuple + or len(source_lock.sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + or len(admitted_sources.sources) != provenance.SOURCE_CLOSURE_COUNT_V1 + ): + raise TypeError("request coordinates are not canonical") + source_coordinates: list[bytes] = [] + for lock, source in zip( + source_lock.sources, + admitted_sources.sources, + strict=True, + ): + if ( + type(lock) is not provenance.SourceReleaseLockV1 + or type(source) is not provenance.SafeSourceArchiveV1 + ): + raise TypeError("request source coordinates are not canonical") + archive = source.archive_bytes + if ( + type(archive) is not bytes + or type(source.source_lock_identity) is not bytes + or type(source.archive_sha256) is not bytes + or type(source.tree_identity) is not bytes + or type(source.regular_file_count) is not int + or type(source.regular_file_bytes) is not int + or source.source_lock_identity != lock.identity + or source.archive_sha256 != lock.archive_sha256 + or source.regular_file_count != lock.regular_file_count + or source.regular_file_bytes != lock.regular_file_bytes + or source.archive_sha256 != hashlib.sha256(archive).digest() + ): + raise TypeError("retained source coordinates changed") + source_coordinates.extend( + provenance._source_archive_coordinates_from_replayed_v1(lock, source) + ) + canonical_job = _canonical_proof_job_with_coherent_identities_v1(job) + canonical_binding = arb_runtime.ArbRuntimeBindingV1(*tuple(runtime_binding)) + binding_identity = arb_runtime.runtime_binding_identity_v1(canonical_binding) + profile_identity = arb_runtime.runtime_profile_identity_v1( + canonical_binding.profile + ) + if type(binding_identity) is not bytes or type(profile_identity) is not bytes: + raise TypeError("request runtime binding did not replay") + return ( + source_lock.encode(), + source_lock.identity, + admitted_sources.identity, + *source_coordinates, + build_sources.identity, + build_sources.build_input_identity, + build_sources.formula_support_identity, + pipeline.build_source_manifest_bytes_v1(build_sources), + canonical_job.encode(), + canonical_job.identity, + profile_identity, + binding_identity, + pipeline._host_trust_wire_v1(request.host_trust), + ) + + +def _exact_digest_v1(value: object, field_name: str) -> bytes: + if type(value) is not bytes or len(value) != 32 or value == bytes(32): + raise TypeError(f"invalid {field_name}") + return value + + +def _require_canonical_digest_v1( + retained: object, + canonical: object, + field_name: str, +) -> None: + """Reject an observable identity cache that disagrees with fresh wire state.""" + + if _exact_digest_v1(retained, field_name) != _exact_digest_v1( + canonical, + f"canonical {field_name}", + ): + raise TypeError(f"{field_name} does not match canonical wire state") + + +def _canonical_proof_job_with_coherent_identities_v1( + value: object, +) -> protocol.ProofJobV1: + """Detach a job and require every identity cache used by its wire to agree. + + ``cached_property`` is a performance detail, not an authority boundary: + frozen public protocol values still expose a writable ``__dict__`` to + hostile callers. The detached snapshot supplies the cache-free oracle. + """ + + if type(value) is not protocol.ProofJobV1: + raise TypeError("request job must be ProofJobV1") + canonical = protocol.snapshot_proof_job_v1(value) + _require_canonical_digest_v1( + value.definition.definition_digest, + canonical.definition.definition_digest, + "request definition digest", + ) + _require_canonical_digest_v1( + value.domain.identity, + canonical.domain.identity, + "request domain identity", + ) + _require_canonical_digest_v1( + value.policy.identity, + canonical.policy.identity, + "request policy identity", + ) + _require_canonical_digest_v1( + value.identity, + canonical.identity, + "request job identity", + ) + return canonical + + +def _bytes_stability_coordinate_v1(value: object, field_name: str) -> bytes: + if type(value) is not bytes: + raise TypeError(f"invalid {field_name}") + return len(value).to_bytes(8, "big") + hashlib.sha256(value).digest() + + +def _build_stability_coordinates_v1( + build: pipeline.DiagnosticBuildObservationV1, +) -> tuple[bytes, ...]: + """Capture every mutable BUILD observation coordinate without source replay.""" + + if ( + type(build) is not pipeline.DiagnosticBuildObservationV1 + or tuple(field.name for field in fields(build)) + != _DIAGNOSTIC_BUILD_FIELDS_V1 + ): + raise TypeError("diagnostic BUILD schema is not canonical V1") + scalar_digests = tuple( + _exact_digest_v1(getattr(build, name), name) + for name in ( + "structural_source_identity", + "flint_commit_content_identity", + "flint_project_pinned_release_only_identity", + "build_input_identity", + "formula_support_identity", + "pipeline_policy_identity", + "binary_sha256", + "input_bundle_identity", + "input_bundle_sha256", + ) + ) + counts = ( + build.flint_commit_content_file_count, + build.flint_project_pinned_release_only_file_count, + build.input_bundle_length, + ) + if any(type(value) is not int or value <= 0 for value in counts): + raise TypeError("invalid diagnostic BUILD count") + rebuild_sha256s = build.rebuild_sha256s + binary = build.binary + input_bundle = build.input_bundle + processes = build.build_processes + comparator = build.comparator + if ( + type(binary) is not bytes + or type(rebuild_sha256s) is not tuple + or len(rebuild_sha256s) != 2 + or any( + _exact_digest_v1(value, "rebuild_sha256") != build.binary_sha256 + for value in rebuild_sha256s + ) + or type(processes) is not tuple + or len(processes) != 2 + or any( + type(process) is not build_transport.DockerBuildExitedV1 + for process in processes + ) + or type(comparator) is not pipeline.DiagnosticArbComparatorV1 + or tuple(field.name for field in fields(comparator)) + != _DIAGNOSTIC_COMPARATOR_FIELDS_V1 + ): + raise TypeError("invalid diagnostic BUILD observation") + rebuild_binaries = build.rebuild_binaries + if ( + type(rebuild_binaries) is not tuple + or len(rebuild_binaries) != 2 + or any(type(value) is not bytes for value in rebuild_binaries) + ): + raise TypeError("diagnostic BUILD executable binding changed") + if ( + input_bundle.binding_identity != build.input_bundle_identity + or input_bundle.sha256 != build.input_bundle_sha256 + or input_bundle.length != build.input_bundle_length + or type(input_bundle.contents) is not bytes + or hashlib.sha256(input_bundle.contents).digest() != input_bundle.sha256 + ): + raise TypeError("diagnostic BUILD input bundle changed") + preimages = comparator.preimages + manifest = comparator.manifest + if ( + type(preimages) is not pipeline.ArbComparatorPreimagesV1 + or type(manifest) is not protocol.ContentResolvedComparatorManifestV2 + or type(manifest.manifest) is not protocol.ComparatorManifestV2 + or comparator.structural_source_identity != build.structural_source_identity + or comparator.build_input_identity != build.build_input_identity + or comparator.pipeline_policy_identity != build.pipeline_policy_identity + or comparator.binary_sha256 != build.binary_sha256 + or comparator.rebuild_sha256s != rebuild_sha256s + ): + raise TypeError("diagnostic BUILD comparator binding changed") + manifest_bytes = manifest.manifest.encode() + parsed_manifest = protocol.ComparatorManifestV2.parse(manifest_bytes) + preimage_coordinates = tuple( + _bytes_stability_coordinate_v1( + getattr(preimages, field.name), + f"comparator preimage {field.name}", + ) + for field in fields(preimages) + ) + resolved_manifest = protocol.ContentResolvedComparatorManifestV2.admit( + parsed_manifest, + { + hashlib.sha256(getattr(preimages, field.name)).digest(): getattr( + preimages, + field.name, + ) + for field in fields(preimages) + }.get, + ) + if resolved_manifest.manifest.encode() != manifest_bytes: + raise TypeError("diagnostic BUILD manifest changed") + _require_canonical_digest_v1( + manifest.manifest.identity, + parsed_manifest.identity, + "diagnostic BUILD manifest identity", + ) + _require_canonical_digest_v1( + manifest.identity, + resolved_manifest.identity, + "diagnostic BUILD resolved manifest identity", + ) + _require_canonical_digest_v1( + comparator.identity, + resolved_manifest.identity, + "diagnostic BUILD comparator identity", + ) + return ( + *scalar_digests, + *(value.to_bytes(8, "big") for value in counts), + build_transport.docker_capability_identity_v1(build.docker_capability), + pipeline._host_trust_wire_v1(build.host_trust), + _bytes_stability_coordinate_v1(binary, "diagnostic BUILD binary"), + bytes( + ( + binary is rebuild_binaries[0], + binary is processes[0].stdout, + rebuild_binaries[1] is processes[1].stdout, + hashlib.sha256(binary).digest() == build.binary_sha256, + ) + ), + *( + _bytes_stability_coordinate_v1(value, "diagnostic rebuild binary") + for value in rebuild_binaries + ), + input_bundle.binding_identity, + input_bundle.sha256, + input_bundle.length.to_bytes(8, "big"), + _bytes_stability_coordinate_v1( + input_bundle.contents, + "diagnostic BUILD input bytes", + ), + build_transport.build_process_bytes_v1(processes[0]), + build_transport.build_process_bytes_v1(processes[1]), + manifest_bytes, + *preimage_coordinates, + comparator.structural_source_identity, + comparator.build_input_identity, + comparator.pipeline_policy_identity, + comparator.binary_sha256, + *comparator.rebuild_sha256s, + ) + + +def _evidence_stability_coordinates_v1(fields_value: _EvidenceFieldsV1) -> bytes: + """Bind the entry-to-exit value state; this is not a receipt identity.""" + + invocation_identity = executor.invocation_identity_v1(fields_value.invocation) + platform_identity = executor.platform_identity_v1(fields_value.platform) + if type(invocation_identity) is not bytes or type(platform_identity) is not bytes: + raise TypeError("execution coordinates did not replay") + process = fields_value.process + if ( + type(process) is not executor.CompletedV1 + or type(process.stdout) is not bytes + or type(process.stderr) is not bytes + ): + raise TypeError("RUN observation is not structurally bound") + transcript = fields_value.transcript + run_claim = fields_value.run_claim + if ( + type(transcript) is not protocol.DecisionTranscriptV1 + or type(run_claim) is not protocol.RunClaimV1 + ): + raise TypeError("RUN protocol observations are not canonical") + transcript_bytes = transcript.encode() + canonical_transcript = protocol.DecisionTranscriptV1.parse(transcript_bytes) + run_claim_bytes = run_claim.encode() + canonical_claim = protocol.RunClaimV1.parse(run_claim_bytes) + if ( + canonical_transcript.encode() != transcript_bytes + or canonical_claim.encode() != run_claim_bytes + ): + raise TypeError("RUN protocol bindings changed") + _require_canonical_digest_v1( + transcript.identity, + canonical_transcript.identity, + "RUN transcript identity", + ) + _require_canonical_digest_v1( + run_claim.identity, + canonical_claim.identity, + "RUN claim identity", + ) + return _identity( + _EVIDENCE_STABILITY_LABEL_V1, + ( + _identity( + b"labcolors.proof-region.arb-request-stability.v1\0", + _request_replay_coordinates_v1(fields_value.request), + ), + _identity( + b"labcolors.proof-region.arb-build-stability.v1\0", + _build_stability_coordinates_v1(fields_value.build), + ), + invocation_identity, + platform_identity, + process.binary_sha256, + _bytes_stability_coordinate_v1(process.stdout, "RUN stdout"), + _bytes_stability_coordinate_v1(process.stderr, "RUN stderr"), + _bytes_stability_coordinate_v1(transcript_bytes, "RUN transcript"), + _bytes_stability_coordinate_v1(run_claim_bytes, "RUN claim"), + bytes( + ( + fields_value.invocation.executable is fields_value.build.binary, + process.binary_sha256 == fields_value.build.binary_sha256, + transcript_bytes == process.stdout, + canonical_claim.binary_identity + == fields_value.build.binary_sha256, + canonical_claim.invocation_identity == invocation_identity, + canonical_claim.platform_identity == platform_identity, + canonical_claim.transcript_identity + == canonical_transcript.identity, + ) + ), + _exact_digest_v1(fields_value.source_identity, "source identity"), + _exact_digest_v1(fields_value.build_identity, "build identity"), + _exact_digest_v1(fields_value.run_identity, "run identity"), + _exact_digest_v1(fields_value.identity, "evidence identity"), + ), + ) + + +def _replay_evidence_fields_v1( + operation: pipeline._PipelineOperationSnapshotV1, + fields: _EvidenceFieldsV1, +) -> tuple[bytes, bytes, bytes, bytes]: + """Re-derive the three evidence edges from one owned source operation.""" + + if any( + type(value) is not bytes + for value in ( + fields.source_identity, + fields.build_identity, + fields.run_identity, + fields.identity, + ) + ): + raise TypeError("evidence identities must be exact bytes") + request = operation.request + source_identity = _source_identity_from_operation_v1(operation) + build_identity = _build_identity_from_operation_v2( + operation, + source_identity, + fields.build, + ) + run_identity = _run_identity_v1( + request, + fields.build, + build_identity, + fields.invocation, + fields.platform, + fields.process, + fields.transcript, + fields.run_claim, + ) + identity = _identity( + _EVIDENCE_ID_LABEL_V1, + (source_identity, build_identity, run_identity), + ) + if ( + fields.source_identity != source_identity + or fields.build_identity != build_identity + or fields.run_identity != run_identity + or fields.identity != identity + ): + raise TypeError("evidence identity did not replay") + return source_identity, build_identity, run_identity, identity + + +def replay_evidence_is_well_bound_v1(value: object) -> bool: + try: + before = _capture_evidence_fields_v1(value) + before_stability = _evidence_stability_coordinates_v1(before) + operation = pipeline._snapshot_pipeline_operation_v1(before.request) + expected_request = _request_replay_coordinates_v1(operation.request) + first = _replay_evidence_fields_v1(operation, before) + + middle = _capture_evidence_fields_v1(value) + if ( + not _same_evidence_references_v1(before, middle) + or _evidence_stability_coordinates_v1(middle) != before_stability + or _request_replay_coordinates_v1(before.request) != expected_request + ): + return False + + # The source operation has one retained materialization. A second + # edge replay and a second structural projection close the interval in + # which a mutable public object could otherwise change mid-check. + second = _replay_evidence_fields_v1(operation, middle) + after = _capture_evidence_fields_v1(value) + return ( + first == second + and _same_evidence_references_v1(middle, after) + and _evidence_stability_coordinates_v1(after) == before_stability + and _request_replay_coordinates_v1(before.request) == expected_request + ) + except Exception: + return False + + +@dataclass(frozen=True, init=False) +class SourceBoundEvaluatorReceiptV1: + claim: protocol.EvaluatorProvenanceClaimV1 + evidence: ContentResolvedEvaluatorReplayV1 + + def __new__(cls, *args: object, **kwargs: object) -> "SourceBoundEvaluatorReceiptV1": + if kwargs.get("_token") is not _RECEIPT_TOKEN: + raise TypeError("SourceBoundEvaluatorReceiptV1 is controller-sealed") + return object.__new__(cls) + + def __init__( + self, + claim: protocol.EvaluatorProvenanceClaimV1, + evidence: ContentResolvedEvaluatorReplayV1, + *, + _token: object, + ) -> None: + if ( + _token is not _RECEIPT_TOKEN + or type(evidence) is not ContentResolvedEvaluatorReplayV1 + or evidence._identity + != _identity( + _EVIDENCE_ID_LABEL_V1, + ( + evidence.source_identity, + evidence.build_identity, + evidence.run_identity, + ), + ) + ): + raise TypeError("SourceBoundEvaluatorReceiptV1 is controller-sealed") + if ( + claim.provenance_policy_identity + != source_bound_policy_identity_v3( + evidence.build.docker_capability, + evidence.request.host_trust, + evidence.request.runtime_binding, + ) + or claim.run_claim_identity != evidence.run_claim.identity + or claim.replay_evidence_identity != evidence.identity + ): + raise TypeError("provenance claim does not bind replay evidence") + object.__setattr__(self, "claim", claim) + object.__setattr__(self, "evidence", evidence) + + @property + def comparator(self) -> pipeline.DiagnosticArbComparatorV1: + return self.evidence.build.comparator + + @property + def transcript(self) -> protocol.DecisionTranscriptV1: + return self.evidence.transcript + + @property + def run_claim(self) -> protocol.RunClaimV1: + return self.evidence.run_claim + + @property + def executable(self) -> bytes: + return self.evidence.executable + + @property + def identity(self) -> bytes: + return self.claim.identity + + +class SourceBoundFailureReasonV1(StrEnum): + WRONG_REQUEST = "wrong_request" + CONTROLLER_CONSUMED = "controller_consumed" + CONTROLLER_PROCESS_CHANGED = "controller_process_changed" + SOURCE_REPLAY_FAILED = "source_replay_failed" + OBSERVER_PLACEMENT_FAILED = "observer_placement_failed" + REPLAY_BINDING_FAILED = "replay_binding_failed" + + +@dataclass(frozen=True) +class SourceBoundRejectedV1: + reason: SourceBoundFailureReasonV1 + detail: str + + def __post_init__(self) -> None: + if type(self.reason) is not SourceBoundFailureReasonV1: + raise TypeError("invalid source-bound failure reason") + if type(self.detail) is not str or not self.detail or len(self.detail) > 4096: + raise TypeError("invalid source-bound failure detail") + + +SourceBoundResultV1: TypeAlias = ( + SourceBoundEvaluatorReceiptV1 + | SourceBoundRejectedV1 + | pipeline.PipelineBlockedV1 + | build_transport.BuildRejectedV1 + | build_transport.TwoBuildObservationV1 + | pipeline.ExecutionRejectedV1 + | pipeline.TranscriptRejectedV1 +) + + +def _evaluator_process_failure_reason_v1( + observation: executor.ExecutionResultV1, +) -> pipeline.ExecutionFailureReasonV1: + """Classify only versioned evaluator exits; never infer from stderr text.""" + + if type(observation) is executor.OutputLimitExceededV1: + return pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT + if type(observation) is not executor.ExitNonZeroV1: + return pipeline.ExecutionFailureReasonV1.PROCESS_FAILED + by_exit_code = { + arb_runtime.ARB_EXIT_INPUT_REJECTED_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_INPUT_REJECTED, + arb_runtime.ARB_EXIT_INPUT_LIMIT_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_INPUT_LIMIT, + arb_runtime.ARB_EXIT_OUTPUT_LIMIT_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_OUTPUT_LIMIT, + arb_runtime.ARB_EXIT_RESOURCE_LIMIT_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_RESOURCE_LIMIT, + arb_runtime.ARB_EXIT_INTERNAL_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_INTERNAL, + arb_runtime.ARB_EXIT_IO_V1: + pipeline.ExecutionFailureReasonV1.EVALUATOR_IO, + } + reason = by_exit_code.get( + observation.exit_code, + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + ) + if ( + reason is not pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT + and reason is not pipeline.ExecutionFailureReasonV1.EVALUATOR_IO + and observation.stdout + ): + return pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT + return reason + + +def _resolve_request_v1( + request: pipeline.PipelineRequestV1, +) -> pipeline.PipelineRequestV1: + lock = provenance.ArbSourceLockV1.parse(request.source_lock.encode()) + if lock.identity != request.source_lock.identity: + raise TypeError("source lock did not replay") + return pipeline.PipelineRequestV1( + lock, + request.admitted_sources, + pipeline.admit_build_sources_v1(request.build_sources.files), + protocol.ProofJobV1.parse(request.job.encode()), + arb_runtime.ArbRuntimeBindingV1(*tuple(request.runtime_binding)), + request.host_trust, + ) + + +class SourceBoundArbControllerV1: + """One-shot authority that owns native BUILD, RUN, replay and sealing.""" + + def __init__(self, docker_path: Path, cgroup_parent: Path) -> None: + if ( + type(docker_path) is not type(Path("/")) + or type(cgroup_parent) is not type(Path("/")) + ): + raise TypeError( + "controller requires an admitted Docker command Path and canonical cgroup parent" + ) + try: + self._docker_path = build_transport.docker_command_coordinate_v1( + docker_path + ).path + self._cgroup_parent = executor.canonical_cgroup_parent_v1( + cgroup_parent + ) + except TypeError as error: + raise TypeError( + "controller requires an admitted Docker command Path and canonical cgroup parent" + ) from error + self._owner_pid = os.getpid() + self._consumed = False + self._lock = threading.Lock() + + def _consume_v1(self) -> SourceBoundRejectedV1 | None: + if os.getpid() != self._owner_pid: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.CONTROLLER_PROCESS_CHANGED, + "controller authority cannot cross a process boundary", + ) + with self._lock: + if self._consumed: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.CONTROLLER_CONSUMED, + "controller authority is one-shot", + ) + self._consumed = True + return None + + def execute(self, request: pipeline.PipelineRequestV1) -> SourceBoundResultV1: + consumed = self._consume_v1() + if consumed is not None: + return consumed + if ( + type(self) is not SourceBoundArbControllerV1 + or type(request) is not pipeline.PipelineRequestV1 + ): + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.WRONG_REQUEST, + "exact SourceBoundArbControllerV1 and PipelineRequestV1 are required", + ) + try: + operation = pipeline._snapshot_pipeline_operation_v1(request) + replay_request = operation.request + except Exception: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.SOURCE_REPLAY_FAILED, + "exact source, build input, or job replay failed", + ) + + build_backend = _NATIVE_BUILD_BACKEND_TYPE( + self._docker_path, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + if type(build_backend) is not _NATIVE_BUILD_BACKEND_TYPE: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + "native build backend authority changed", + ) + built = pipeline.ControlledPipelineV1( + build_backend=build_backend + )._build_snapshot_v1( + operation + ) + if type(built) is not pipeline.DiagnosticBuildObservationV1: + return built + try: + executor.enter_observer_cgroup_v1(self._cgroup_parent) + except Exception: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.OBSERVER_PLACEMENT_FAILED, + "dedicated controller could not enter the observer cgroup", + ) + run_backend = _NATIVE_RUN_BACKEND_TYPE(self._cgroup_parent) + if type(run_backend) is not _NATIVE_RUN_BACKEND_TYPE: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + "native run backend authority changed", + ) + controller = executor.ControlledExecutorV1(run_backend) + capability = controller.probe() + if type(capability) is executor.UnsupportedV1: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.UNSUPPORTED, + capability, + ) + if type(capability) is not executor.SupportedV1: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + capability, + ) + try: + invocation = executor.ExecutionRequestV1( + executable=built.binary, + argv=( + b"arb-evaluator", + b"--manifest-identity", + built.comparator.identity.hex().encode("ascii"), + b"--job", + b"/dev/stdin", + ), + environment=((b"LC_ALL", b"C"), (b"TZ", b"UTC")), + cwd=b"/", + stdin=replay_request.job.encode(), + umask=0o077, + limits=replay_request.runtime_binding.limits, + ) + except executor.ExecutionRequestErrorV1 as error: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + error, + ) + observed = controller.execute(invocation, capability) + if type(observed) is not executor.CompletedV1: + if not executor.result_matches_request_v1(observed, invocation): + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + observed, + ) + return pipeline.ExecutionRejectedV1( + _evaluator_process_failure_reason_v1(observed), + observed, + ) + if observed.binary_sha256 != built.binary_sha256: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BINARY_MISMATCH, + observed, + ) + if not executor.result_matches_request_v1(observed, invocation): + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + observed, + ) + if observed.stderr: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.STDERR_NOT_EMPTY, + observed, + ) + try: + transcript = protocol.DecisionTranscriptV1.parse(observed.stdout) + except protocol.ProtocolErrorV1 as error: + return pipeline.TranscriptRejectedV1( + pipeline.TranscriptFailureReasonV1.INVALID_WIRE, + str(error), + ) + try: + invocation_identity = executor.invocation_identity_v1(invocation) + if type(invocation_identity) is executor.ExecutionIdentityRejectedV1: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + invocation_identity, + ) + platform_identity = executor.platform_identity_v1(capability) + if type(platform_identity) is executor.ExecutionIdentityRejectedV1: + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + platform_identity, + ) + if ( + type(invocation_identity) is not bytes + or type(platform_identity) is not bytes + ): + return pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + (invocation_identity, platform_identity), + ) + run_claim = protocol.RunClaimV1.for_transcript( + replay_request.job, + built.comparator.manifest, + transcript, + built.binary_sha256, + invocation_identity, + platform_identity, + ) + evidence = ContentResolvedEvaluatorReplayV1( + replay_request, + built, + invocation, + capability, + observed, + transcript, + run_claim, + _operation=operation, + _token=_EVIDENCE_TOKEN, + ) + claim = protocol.EvaluatorProvenanceClaimV1( + source_bound_policy_identity_v3( + built.docker_capability, + replay_request.host_trust, + replay_request.runtime_binding, + ), + run_claim.identity, + evidence.identity, + ) + return SourceBoundEvaluatorReceiptV1( + claim, + evidence, + _token=_RECEIPT_TOKEN, + ) + except protocol.ProtocolErrorV1 as error: + return pipeline.TranscriptRejectedV1( + pipeline.TranscriptFailureReasonV1.FOREIGN_BINDING, + str(error), + ) + except Exception: + return SourceBoundRejectedV1( + SourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + "source/build/run replay DAG did not seal", + ) diff --git a/proof/region/v1/arb/runtime.py b/proof/region/v1/arb/runtime.py new file mode 100644 index 00000000..5e62a6dc --- /dev/null +++ b/proof/region/v1/arb/runtime.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Каноническая связь Arb runtime-профиля с executor limits.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from enum import StrEnum +from typing import TypeAlias + +import executor + + +ARB_RUNTIME_PROFILE_ID_V1 = "LC-ARB-RUNTIME-V1" + +# Это versioned operational boundary прямого evaluator, а не математическая +# граница definition/domain. Те же координаты обязаны жить в wire.h; native +# conformance-тест связывает обе реализации exact-значениями. +ARB_MAX_JOB_BYTES_V1 = 16 * 1024 * 1024 +ARB_MAX_OUTPUT_BYTES_V1 = 16 * 1024 * 1024 +ARB_MAX_PRECISION_BITS_V1 = 4096 +ARB_MAX_POLICY_RUNGS_V1 = 32 +ARB_MAX_KNOTS_V1 = 1024 +ARB_EXIT_USAGE_V1 = 64 +ARB_EXIT_INPUT_REJECTED_V1 = 65 +ARB_EXIT_INPUT_LIMIT_V1 = 66 +ARB_EXIT_OUTPUT_LIMIT_V1 = 67 +ARB_EXIT_RESOURCE_LIMIT_V1 = 68 +ARB_EXIT_INTERNAL_V1 = 70 +ARB_EXIT_IO_V1 = 74 + +_PROFILE_ID_LABEL_V1 = b"labcolors.proof-region.arb-runtime-profile.v1\0" +_BINDING_ID_LABEL_V1 = b"labcolors.proof-region.arb-runtime-binding.v1\0" +_EXECUTION_LIMITS_ID_LABEL_V1 = b"labcolors.proof-region.execution-limits.v1\0" +_PROFILE_VALUES_V1 = ( + ARB_MAX_JOB_BYTES_V1, + ARB_MAX_OUTPUT_BYTES_V1, + ARB_MAX_PRECISION_BITS_V1, + ARB_MAX_POLICY_RUNGS_V1, + ARB_MAX_KNOTS_V1, +) + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join( + len(chunk).to_bytes(8, "big") + chunk + for chunk in chunks + ) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +class ArbRuntimeProfileReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + NONCANONICAL = "noncanonical" + LIMIT_MISMATCH = "limit_mismatch" + + +@dataclass(frozen=True) +class ArbRuntimeIdentityRejectedV1: + reason: ArbRuntimeProfileReasonV1 + + def __post_init__(self) -> None: + if type(self.reason) is not ArbRuntimeProfileReasonV1: + raise TypeError("reason must be ArbRuntimeProfileReasonV1") + + +class ArbRuntimeProfileV1(tuple): + """Один immutable exact-профиль прямого Arb evaluator V1.""" + + __slots__ = () + + def __new__( + cls, + max_job_bytes: int, + max_output_bytes: int, + max_precision_bits: int, + max_policy_rungs: int, + max_knots: int, + ) -> ArbRuntimeProfileV1: + values = ( + max_job_bytes, + max_output_bytes, + max_precision_bits, + max_policy_rungs, + max_knots, + ) + if any(type(value) is not int for value in values): + raise TypeError("Arb runtime profile coordinates must be exact ints") + if values != _PROFILE_VALUES_V1: + raise ValueError("unknown or noncanonical Arb runtime profile") + return tuple.__new__(cls, values) + + max_job_bytes = property(lambda self: self[0]) + max_output_bytes = property(lambda self: self[1]) + max_precision_bits = property(lambda self: self[2]) + max_policy_rungs = property(lambda self: self[3]) + max_knots = property(lambda self: self[4]) + + +def arb_runtime_profile_v1() -> ArbRuntimeProfileV1: + return ArbRuntimeProfileV1(*_PROFILE_VALUES_V1) + + +class ArbRuntimeBindingV1(tuple): + """Профиль и exact immutable executor limits одного RUN.""" + + __slots__ = () + + def __new__( + cls, + profile: ArbRuntimeProfileV1, + limits: executor.ExecutionLimitsV1, + ) -> ArbRuntimeBindingV1: + if type(profile) is not ArbRuntimeProfileV1: + raise TypeError("profile must be ArbRuntimeProfileV1") + if type(limits) is not executor.ExecutionLimitsV1: + raise TypeError("limits must be ExecutionLimitsV1") + canonical_profile = ArbRuntimeProfileV1(*tuple(profile)) + canonical_limits = executor.ExecutionLimitsV1(*tuple(limits)) + if tuple(canonical_profile) != tuple(profile) or tuple(canonical_limits) != tuple(limits): + raise ValueError("runtime binding coordinates are not canonical") + if ( + canonical_limits.max_stdin_bytes != canonical_profile.max_job_bytes + or canonical_limits.max_stdout_bytes != canonical_profile.max_output_bytes + ): + raise ValueError("executor limits do not implement Arb profile") + return tuple.__new__(cls, (canonical_profile, canonical_limits)) + + profile = property(lambda self: self[0]) + limits = property(lambda self: self[1]) + + +ArbRuntimeIdentityResultV1: TypeAlias = bytes | ArbRuntimeIdentityRejectedV1 + + +def runtime_profile_identity_v1(value: object) -> ArbRuntimeIdentityResultV1: + if type(value) is not ArbRuntimeProfileV1: + return ArbRuntimeIdentityRejectedV1(ArbRuntimeProfileReasonV1.WRONG_TYPE) + try: + profile = ArbRuntimeProfileV1(*tuple(value)) + except (TypeError, ValueError, OverflowError): + return ArbRuntimeIdentityRejectedV1(ArbRuntimeProfileReasonV1.NONCANONICAL) + return _identity( + _PROFILE_ID_LABEL_V1, + ( + ARB_RUNTIME_PROFILE_ID_V1.encode("ascii"), + *(item.to_bytes(8, "big") for item in profile), + ), + ) + + +def runtime_binding_identity_v1(value: object) -> ArbRuntimeIdentityResultV1: + if type(value) is not ArbRuntimeBindingV1: + return ArbRuntimeIdentityRejectedV1(ArbRuntimeProfileReasonV1.WRONG_TYPE) + try: + binding = ArbRuntimeBindingV1(*tuple(value)) + profile_identity = runtime_profile_identity_v1(binding.profile) + limits_identity = _identity( + _EXECUTION_LIMITS_ID_LABEL_V1, + tuple(item.to_bytes(8, "big") for item in binding.limits), + ) + except (TypeError, ValueError, OverflowError): + return ArbRuntimeIdentityRejectedV1(ArbRuntimeProfileReasonV1.LIMIT_MISMATCH) + if type(profile_identity) is not bytes: + return ArbRuntimeIdentityRejectedV1(ArbRuntimeProfileReasonV1.LIMIT_MISMATCH) + return _identity( + _BINDING_ID_LABEL_V1, + ( + profile_identity, + limits_identity, + executor.SANDBOX_POLICY_RELEASE_V1.encode("ascii"), + ), + ) diff --git a/proof/region/v1/arb/tests/gate.py b/proof/region/v1/arb/tests/gate.py new file mode 100644 index 00000000..68a995b3 --- /dev/null +++ b/proof/region/v1/arb/tests/gate.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Запускает обязательные быстрые proof-контракты с точным manifest skips.""" + +from __future__ import annotations + +import hashlib +import sys +import unittest +from collections.abc import Iterator +from pathlib import Path + + +TEST_DIRECTORY = Path(__file__).resolve().parent +SHARED_TEST_DIRECTORY = TEST_DIRECTORY.parents[1] / "tests" +REPO = Path(__file__).resolve().parents[5] +sys.path.insert(0, str(REPO)) +SHARED_FAST_TEST_PATTERNS_V1 = ( + "test_executor.py", + "test_mpfi_input.py", +) +EXPECTED_TEST_INVENTORY_SHA256 = ( + "030cd7d43490c3aea5e10ba7d29baa2ab7de61639f05b9e9a98d0007cd990c05" +) +_EVALUATOR_REASON = "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary" +EXPECTED_SKIPS = frozenset( + { + ( + f"test_evaluator_source.ExactBoundaryRuntimeTests.{name}", + _EVALUATOR_REASON, + ) + for name in ( + "test_allocation_profile_boundaries_are_enforced_by_the_native_parser", + "test_black_exact_zero_runs_through_job_parser_formula_and_closed_driver", + "test_cli_requires_one_nonzero_lowercase_manifest_identity", + "test_closed_stdout_is_a_versioned_io_exit_not_an_untyped_signal", + "test_frozen_seam_cube_resolves_one_inside_and_511_outside", + "test_global_pregrant_is_never_transferred_between_points", + "test_job_transport_limit_precedes_wire_parsing", + "test_multisegment_exact_trace_selects_first_canonical_branch", + "test_aggregate_transcript_output_limit_is_exact", + "test_resource_witness_accounts_for_work_consumed_on_earlier_rungs", + "test_spd_admission_is_exact_across_the_full_binary64_exponent_range", + "test_subminimum_precision_is_unresolved_and_a_later_valid_rung_recovers", + "test_zero_grant_emits_canonical_resource_witnesses", + ) + } + | { + ( + "test_executor.NativeLinuxIntegrationTests." + "test_real_kernel_success_output_timeout_signal_oom_and_cleanup", + "requires Linux and an explicit delegated cgroup v2 parent", + ), + ( + "test_receipt.NativeSourceBoundReceiptIntegrationTests." + "test_real_build_run_and_seal_are_one_source_bound_controller_execution", + "requires Linux, Docker, a delegated cgroup, and all three exact source archives", + ), + } +) + + +def _iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: + for item in suite: + if isinstance(item, unittest.TestSuite): + yield from _iter_tests_v1(item) + elif isinstance(item, unittest.TestCase): + yield item + else: + raise TypeError("suite contains a non-test object") + + +def iter_tests_v1(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: + """Expose test enumeration without leaking the gate's private helper.""" + + return _iter_tests_v1(suite) + + +def _inventory_preimage_v1(test_ids: tuple[str, ...]) -> bytes: + return b"".join(test_id.encode("utf-8") + b"\n" for test_id in sorted(test_ids)) + + +def test_inventory_sha256_v1(suite: unittest.TestSuite) -> str: + test_ids = tuple(test.id() for test in _iter_tests_v1(suite)) + return hashlib.sha256(_inventory_preimage_v1(test_ids)).hexdigest() + + +def full_suite_v1() -> unittest.TestSuite: + """Собирает обязательные общие proof-контракты и Arb-only contract.""" + + return unittest.TestSuite( + tuple( + unittest.defaultTestLoader.discover( + str(SHARED_TEST_DIRECTORY), + pattern=pattern, + ) + for pattern in SHARED_FAST_TEST_PATTERNS_V1 + ) + + ( + unittest.defaultTestLoader.discover( + str(TEST_DIRECTORY), + pattern="test_*.py", + ), + ) + ) + + +def run_exact_suite_v1( + suite: unittest.TestSuite, + *, + expected_inventory_sha256: str, + expected_skips: frozenset[tuple[str, str]], + verbosity: int = 2, +) -> int: + tests = tuple(_iter_tests_v1(suite)) + test_ids = tuple(test.id() for test in tests) + actual_inventory_sha256 = hashlib.sha256( + _inventory_preimage_v1(test_ids) + ).hexdigest() + if ( + not tests + or len(set(test_ids)) != len(test_ids) + or actual_inventory_sha256 != expected_inventory_sha256 + ): + print( + "Proof fast gate inventory drift: " + f"count={len(tests)} sha256={actual_inventory_sha256} " + f"expected={expected_inventory_sha256}", + file=sys.stderr, + ) + return 1 + result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + actual_skips = frozenset((test.id(), reason) for test, reason in result.skipped) + if actual_skips != expected_skips: + print(f"unexpected skips: {sorted(actual_skips - expected_skips)!r}", file=sys.stderr) + print(f"missing skips: {sorted(expected_skips - actual_skips)!r}", file=sys.stderr) + return 1 + if ( + result.failures + or result.errors + or result.expectedFailures + or result.unexpectedSuccesses + or not result.wasSuccessful() + ): + print( + "proof suite contains failures, errors, expected failures, or " + "unexpected successes", + file=sys.stderr, + ) + return 1 + print( + f"Proof fast gate: {len(tests)} tests, " + f"inventory {actual_inventory_sha256}, " + f"exact {len(actual_skips)}-skip manifest" + ) + return 0 + + +def main() -> int: + suite = full_suite_v1() + return run_exact_suite_v1( + suite, + expected_inventory_sha256=EXPECTED_TEST_INVENTORY_SHA256, + expected_skips=EXPECTED_SKIPS, + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/region/v1/arb/tests/native_gate.py b/proof/region/v1/arb/tests/native_gate.py new file mode 100644 index 00000000..317f23b4 --- /dev/null +++ b/proof/region/v1/arb/tests/native_gate.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Require one exact native integration lane without skips.""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + + +REPO = Path(__file__).resolve().parents[5] +sys.path.insert(0, str(REPO)) + +from proof.region.v1.arb.tests import gate # noqa: E402 +from proof.region.v1.tests.test_executor import ( # noqa: E402 + NativeLinuxIntegrationTests, +) +from proof.region.v1.arb.tests.test_receipt import ( # noqa: E402 + NativeSourceBoundReceiptIntegrationTests, +) + + +_MODES = { + "executor": ( + (NativeLinuxIntegrationTests,), + "276f45bd831c26288eaa34f1846821a6b8cec3b6d58b9f2c8a6f3136f8ad7869", + ), + "receipt": ( + (NativeSourceBoundReceiptIntegrationTests,), + "d5092e566c23b45f4b81ef850ca8abc8f003fa1a98c15030643660a636b04c6a", + ), +} + + +def main() -> int: + if len(sys.argv) != 2 or sys.argv[1] not in _MODES: + print("usage: native_gate.py {executor|receipt}", file=sys.stderr) + return 64 + test_cases, inventory = _MODES[sys.argv[1]] + suite = unittest.TestSuite( + unittest.defaultTestLoader.loadTestsFromTestCase(test_case) + for test_case in test_cases + ) + return gate.run_exact_suite_v1( + suite, + expected_inventory_sha256=inventory, + expected_skips=frozenset(), + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/region/v1/arb/tests/runtime_gate.py b/proof/region/v1/arb/tests/runtime_gate.py new file mode 100644 index 00000000..4939aad7 --- /dev/null +++ b/proof/region/v1/arb/tests/runtime_gate.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Require the exact evaluator runtime suite with no vacuous outcomes.""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + + +REPO = Path(__file__).resolve().parents[5] +sys.path.insert(0, str(REPO)) + +from proof.region.v1.arb.tests import gate # noqa: E402 +from proof.region.v1.arb.tests.test_evaluator_source import ( # noqa: E402 + ExactBoundaryRuntimeTests, +) + + +EXPECTED_RUNTIME_INVENTORY_SHA256 = ( + "b3694e51281e25a7b2f25fccede2845274cd8e3079d2b8997e2bf105ebdb1043" +) + + +def main() -> int: + suite = unittest.defaultTestLoader.loadTestsFromTestCase( + ExactBoundaryRuntimeTests + ) + return gate.run_exact_suite_v1( + suite, + expected_inventory_sha256=EXPECTED_RUNTIME_INVENTORY_SHA256, + expected_skips=frozenset(), + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/region/v1/arb/tests/test_build_identity_v2.py b/proof/region/v1/arb/tests/test_build_identity_v2.py new file mode 100644 index 00000000..542dfcdb --- /dev/null +++ b/proof/region/v1/arb/tests/test_build_identity_v2.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python3 +"""Causal BUILD policy and capability identity contract.""" + +from __future__ import annotations + +import ast +import hashlib +import inspect +import json +import sys +import tempfile +import unittest +from dataclasses import fields as dataclass_fields +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +sys.path[:0] = [str(PROOF), str(ARB / "tests")] + +from build import transport as build_transport # noqa: E402 + +from arb import pipeline, receipt # noqa: E402 +from test_pipeline import ( # noqa: E402 + _BuildBackend, + _docker_capability, + _request, + _static_elf, +) + + +_POLICY_FIELDS = tuple( + name + for name in inspect.signature( + build_transport.DockerBuildPolicyV1.__new__ + ).parameters + if name != "cls" +) + + +def _called_names(function: object) -> set[str]: + tree = ast.parse(inspect.getsource(function)) + names: set[str] = set() + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if isinstance(node.func, ast.Attribute): + names.add(node.func.attr) + elif isinstance(node.func, ast.Name): + names.add(node.func.id) + return names + + +def _policy_with( + policy: build_transport.DockerBuildPolicyV1, + **changes: object, +) -> build_transport.DockerBuildPolicyV1: + values = {name: getattr(policy, name) for name in _POLICY_FIELDS} + values.update(changes) + return build_transport.DockerBuildPolicyV1( + *(values[name] for name in _POLICY_FIELDS) + ) + + +def _policy_mutants( + policy: build_transport.DockerBuildPolicyV1, +) -> tuple[tuple[str, build_transport.DockerBuildPolicyV1], ...]: + first_tmpfs, *remaining_tmpfs = policy.tmpfs_specs + tmpfs_parts = first_tmpfs.split(",") + size_index = next( + index for index, part in enumerate(tmpfs_parts) if part.startswith("size=") + ) + size_value = int(tmpfs_parts[size_index].removeprefix("size=")) + tmpfs_parts[size_index] = f"size={size_value - 1}" + changed_tmpfs = ",".join(tmpfs_parts) + numeric_fields = ( + "stdout_limit", + "stderr_limit", + "build_timeout_ns", + "probe_output_limit", + "probe_timeout_ns", + ) + mutants: list[tuple[str, build_transport.DockerBuildPolicyV1]] = [ + ( + "image_reference", + _policy_with( + policy, + image_reference="gcc@sha256:" + "ab" * 32, + ), + ), + ("hostname", _policy_with(policy, hostname="labcolors-build-mutant")), + ("bootstrap", _policy_with(policy, bootstrap=policy.bootstrap + "\n:")), + ( + "bootstrap_argv0", + _policy_with(policy, bootstrap_argv0="labcolors-mutant-bootstrap"), + ), + ( + "tmpfs_specs", + _policy_with( + policy, + tmpfs_specs=(changed_tmpfs, *remaining_tmpfs), + ), + ), + ] + for name in numeric_fields: + value = getattr(policy, name) + mutants.append((name, _policy_with(policy, **{name: value - 1}))) + return tuple(mutants) + + +def _arb_input_binding_oracle_v2( + source_identity: bytes, + build_input_identity: bytes, + contents: bytes, + bootstrap: str, +) -> bytes: + """Independent frozen formula for the V2 Arb input binding.""" + + chunks = ( + source_identity, + build_input_identity, + len(contents).to_bytes(8, "big"), + hashlib.sha256(contents).digest(), + hashlib.sha256(bootstrap.encode("utf-8")).digest(), + ) + payload = b"".join( + len(chunk).to_bytes(8, "big") + chunk for chunk in chunks + ) + return hashlib.sha256( + b"labcolors.proof-region.arb-build-input-bundle.v2\0" + + len(payload).to_bytes(8, "big") + + payload + ).digest() + + +def _capability( + docker_path: Path, + policy: build_transport.DockerBuildPolicyV1, + *, + host_user: tuple[int, int] = (501, 20), + daemon_marker: str = "daemon-a", +) -> build_transport.DockerSupportedV1: + backend = build_transport.NativeDockerBuildBackendV1( + docker_path, + policy, + platform_name="linux", + machine_name="x86_64", + host_user=host_user, + ) + image_observation = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + sort_keys=True, + separators=(",", ":"), + ).encode("ascii") + observations = ( + build_transport._docker_command_exited_v1( + 0, + json.dumps( + {"daemon": daemon_marker}, + sort_keys=True, + separators=(",", ":"), + ).encode("ascii"), + b"", + ), + build_transport._docker_command_exited_v1(0, image_observation, b""), + ) + with mock.patch.object(backend, "_observe_command", side_effect=observations): + capability = backend.probe() + if type(capability) is not build_transport.DockerSupportedV1: + raise AssertionError(capability) + return capability + + +def _observed_build_coordinates( + policy: build_transport.DockerBuildPolicyV1, + capability: build_transport.DockerSupportedV1, +) -> tuple[bytes, bytes, bytes, bytes, tuple[bytes, bytes], bytes, bytes]: + request = _request() + binary = _static_elf(b"identity-v2-invariant-output") + with mock.patch.object(pipeline, "ARB_BUILD_TRANSPORT_POLICY_V1", policy): + result = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((binary, binary), probe=capability) + ).build(request) + if type(result) is not pipeline.DiagnosticBuildObservationV1: + raise AssertionError(result) + source_identity = receipt._source_identity_v1(request) + receipt_build_identity = receipt._build_identity_v2( + request, + source_identity, + result, + ) + source_bound_policy = receipt.source_bound_policy_identity_v3( + result.docker_capability, + request.host_trust, + request.runtime_binding, + ) + process_encodings = tuple( + build_transport.build_process_bytes_v1(process) + for process in result.build_processes + ) + return ( + build_transport.docker_capability_identity_v1(result.docker_capability), + result.comparator.preimages.build_identity, + receipt_build_identity, + source_identity, + process_encodings, + result.input_bundle.contents, + source_bound_policy, + ) + + +class BuildIdentityV2Tests(unittest.TestCase): + def test_v2_surface_replaces_v1_aliases_and_preimage_labels(self) -> None: + self.assertFalse(hasattr(pipeline, "pipeline_policy_identity_v1")) + self.assertFalse(hasattr(receipt, "source_bound_policy_identity_v1")) + self.assertFalse(hasattr(receipt, "_build_identity_v1")) + self.assertTrue(callable(pipeline.pipeline_policy_identity_v2)) + self.assertFalse(hasattr(receipt, "source_bound_policy_identity_v2")) + self.assertTrue(callable(receipt.source_bound_policy_identity_v3)) + self.assertTrue(callable(receipt._build_identity_v2)) + + pipeline_source = (ARB / "pipeline.py").read_text(encoding="utf-8") + receipt_source = (ARB / "receipt.py").read_text(encoding="utf-8") + for stale in ( + "labcolors.proof-region.arb-pipeline-policy.v1", + "labcolors.proof-region.arb-comparator.build-identity.v1", + ): + with self.subTest(stale=stale): + self.assertNotIn(stale, pipeline_source) + for stale in ( + "labcolors.proof-region.arb-build-replay.v1", + "labcolors.proof-region.arb-source-bound-policy.v1", + "labcolors.proof-region.arb-source-bound-policy.v2", + ): + with self.subTest(stale=stale): + self.assertNotIn(stale, receipt_source) + + def test_pipeline_policy_consumes_both_owned_transport_identities(self) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + transport_identity = build_transport.transport_policy_identity_v1(policy) + command_identity = build_transport.native_command_contract_identity_v1() + pipeline_identity = pipeline.pipeline_policy_identity_v2(trust, policy) + + for name, value in ( + ("transport", transport_identity), + ("command", command_identity), + ("pipeline", pipeline_identity), + ): + with self.subTest(identity=name): + self.assertIs(type(value), bytes) + self.assertEqual(len(value), hashlib.sha256().digest_size) + self.assertNotEqual(value, bytes(hashlib.sha256().digest_size)) + self.assertNotEqual(transport_identity, command_identity) + self.assertNotEqual(transport_identity, pipeline_identity) + self.assertNotEqual(command_identity, pipeline_identity) + + calls = _called_names(pipeline.pipeline_policy_identity_v2) + self.assertIn("transport_policy_identity_v1", calls) + self.assertIn("native_command_contract_identity_v1", calls) + for surrogate in (tuple(policy), list(policy), object()): + with self.subTest(surrogate=type(surrogate).__name__): + with self.assertRaises(TypeError): + build_transport.transport_policy_identity_v1(surrogate) + with self.assertRaises(TypeError): + pipeline.pipeline_policy_identity_v2(trust, surrogate) + + def test_generic_sealing_preserves_the_frozen_arb_binding_formula(self) -> None: + """Moving byte storage cannot silently change an unchanged protocol ID.""" + + request = _request() + baseline_policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline = pipeline._seal_build_input_bundle_v1(request, baseline_policy) + expected = _arb_input_binding_oracle_v2( + request.admitted_sources.identity, + request.build_sources.build_input_identity, + baseline.contents, + baseline_policy.bootstrap, + ) + self.assertEqual(baseline.binding_identity, expected) + + changed_policy = _policy_with( + baseline_policy, + bootstrap=baseline_policy.bootstrap + "\n:", + ) + with mock.patch.object( + pipeline, + "ARB_BUILD_TRANSPORT_POLICY_V1", + changed_policy, + ): + changed = pipeline._seal_build_input_bundle_v1(request, changed_policy) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + changed_policy, + changed, + ) + ) + expected_changed = _arb_input_binding_oracle_v2( + request.admitted_sources.identity, + request.build_sources.build_input_identity, + changed.contents, + changed_policy.bootstrap, + ) + self.assertEqual(changed.contents, baseline.contents) + self.assertEqual(changed.binding_identity, expected_changed) + self.assertNotEqual(changed.binding_identity, baseline.binding_identity) + + # The fixed V1 bootstrap consumes $1 and $2 only. Its shell argv0 is + # bound by the outer transport identity, so it cannot make identical + # source-to-tree bytes a second inner binding. + argv0_changed_policy = _policy_with( + baseline_policy, + bootstrap_argv0="labcolors-other-bootstrap-argv0", + ) + argv0_changed = pipeline._seal_build_input_bundle_v1( + request, + argv0_changed_policy, + ) + self.assertEqual(argv0_changed.binding_identity, baseline.binding_identity) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + argv0_changed_policy, + baseline, + ) + ) + + def test_input_binding_follows_probed_capability_not_module_global(self) -> None: + """A reentrant backend cannot swap a post-probe sealing dependency.""" + + bound_policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + foreign_policy = _policy_with( + bound_policy, + bootstrap=bound_policy.bootstrap + "\n:", + ) + request = _request() + binary = _static_elf(b"capability-bound-input") + + class _GlobalSwitchingBackend(_BuildBackend): + def __init__(self) -> None: + super().__init__( + (binary, binary), + probe=_docker_capability(bound_policy), + ) + self._attempts = 0 + + def probe(self) -> build_transport.DockerCapabilityReportV1: + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 = foreign_policy + return super().probe() + + def run_build( + self, + value: build_transport.DockerBuildRequestV1, + ) -> build_transport.DockerBuildProcessObservationV1: + observed = super().run_build(value) + self._attempts += 1 + if self._attempts == 2: + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 = bound_policy + return observed + + original_policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + try: + result = pipeline.ControlledPipelineV1( + build_backend=_GlobalSwitchingBackend(), + ).build(request) + finally: + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 = original_policy + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + expected = _arb_input_binding_oracle_v2( + request.admitted_sources.identity, + request.build_sources.build_input_identity, + result.input_bundle.contents, + bound_policy.bootstrap, + ) + self.assertEqual(result.input_bundle.binding_identity, expected) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + bound_policy, + result.input_bundle, + ) + ) + expected_receipt_identity = receipt._build_identity_v2( + request, + receipt._source_identity_v1(request), + result, + ) + with mock.patch.object( + pipeline, + "ARB_BUILD_TRANSPORT_POLICY_V1", + foreign_policy, + ): + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + bound_policy, + result.input_bundle, + ) + ) + self.assertEqual( + receipt._build_identity_v2( + request, + receipt._source_identity_v1(request), + result, + ), + expected_receipt_identity, + ) + + def test_every_admitted_policy_mutation_changes_transport_and_pipeline_identity(self) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline_transport = build_transport.transport_policy_identity_v1(policy) + baseline_pipeline = pipeline.pipeline_policy_identity_v2(trust, policy) + mutants = _policy_mutants(policy) + + self.assertEqual( + {name for name, _mutant in mutants}, + set(_POLICY_FIELDS) - {"platform", "user_mode"}, + ) + for name, mutant in mutants: + with self.subTest(field=name): + self.assertTrue(build_transport.docker_policy_is_valid_v1(mutant)) + self.assertNotEqual( + build_transport.transport_policy_identity_v1(mutant), + baseline_transport, + ) + self.assertNotEqual( + pipeline.pipeline_policy_identity_v2(trust, mutant), + baseline_pipeline, + ) + + # These coordinates currently have singleton admitted domains. Their + # only meaningful mutants are invalid inputs, not a second policy. + self.assertEqual(tuple(build_transport.DockerUserModeV1), (policy.user_mode,)) + with self.assertRaises(TypeError): + _policy_with(policy, platform="linux/arm64") + with self.assertRaises(TypeError): + _policy_with(policy, user_mode="host_effective_ids") + + def test_diagnostic_build_owns_one_capability_and_replayers_consume_its_identity(self) -> None: + field_names = tuple( + field.name for field in dataclass_fields(pipeline.DiagnosticBuildObservationV1) + ) + self.assertEqual(field_names.count("docker_capability"), 1) + for mirror in ( + "docker_daemon_observation_sha256", + "oci_image_reference", + "oci_platform", + "docker_path", + "host_user", + ): + with self.subTest(mirror=mirror): + self.assertNotIn(mirror, field_names) + + comparator_calls = _called_names( + pipeline._derive_arb_comparator_for_build_v1 + ) + comparator_replay = receipt._comparator_replays_from_operation_v1 + comparator_replay_calls = _called_names(comparator_replay) + receipt_build_calls = _called_names(receipt._build_identity_v2) + receipt_owned_build_calls = _called_names( + receipt._build_identity_from_operation_v2 + ) + source_bound_calls = _called_names( + receipt.source_bound_policy_identity_v3 + ) + self.assertIn("docker_capability_identity_v1", comparator_calls) + self.assertIn("_derive_arb_comparator_for_build_v1", comparator_replay_calls) + self.assertIn("build.docker_capability", inspect.getsource(comparator_replay)) + self.assertIn("_build_identity_from_operation_v2", receipt_build_calls) + self.assertIn("docker_capability_identity_v1", receipt_owned_build_calls) + self.assertIn("docker_capability_identity_v1", source_bound_calls) + self.assertIn("runtime_binding_identity_v1", source_bound_calls) + + def test_path_uid_daemon_and_hostname_flow_to_downstream_build_identity_only(self) -> None: + baseline_policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + hostname_policy = _policy_with( + baseline_policy, + hostname="labcolors-build-other-host", + ) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + first_path = root / "docker-a" + second_path = root / "docker-b" + first_path.write_bytes(b"same-docker-cli-fixture") + second_path.write_bytes(b"same-docker-cli-fixture") + first_path.chmod(0o755) + second_path.chmod(0o755) + + baseline = _observed_build_coordinates( + baseline_policy, + _capability(first_path, baseline_policy), + ) + variants = { + "path": _observed_build_coordinates( + baseline_policy, + _capability(second_path, baseline_policy), + ), + "uid": _observed_build_coordinates( + baseline_policy, + _capability(first_path, baseline_policy, host_user=(502, 20)), + ), + "daemon": _observed_build_coordinates( + baseline_policy, + _capability( + first_path, + baseline_policy, + daemon_marker="daemon-b", + ), + ), + "hostname": _observed_build_coordinates( + hostname_policy, + _capability(first_path, hostname_policy), + ), + } + + ( + baseline_capability, + baseline_comparator_build, + baseline_receipt_build, + baseline_source, + baseline_processes, + baseline_bundle, + baseline_source_bound_policy, + ) = baseline + for name, variant in variants.items(): + with self.subTest(mutation=name): + ( + capability_identity, + comparator_build, + receipt_build, + source_identity, + process_encodings, + bundle_bytes, + source_bound_policy, + ) = variant + self.assertNotEqual(capability_identity, baseline_capability) + self.assertNotEqual(comparator_build, baseline_comparator_build) + self.assertNotEqual(receipt_build, baseline_receipt_build) + self.assertNotEqual( + source_bound_policy, + baseline_source_bound_policy, + ) + self.assertEqual(source_identity, baseline_source) + self.assertEqual(process_encodings, baseline_processes) + self.assertEqual(bundle_bytes, baseline_bundle) + + +if __name__ == "__main__": + unittest.main() diff --git a/proof/region/v1/arb/tests/test_build_recipe.py b/proof/region/v1/arb/tests/test_build_recipe.py new file mode 100644 index 00000000..8fb60cc7 --- /dev/null +++ b/proof/region/v1/arb/tests/test_build_recipe.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""Anti-vacuum contract for the offline Arb dependency build.""" + +from __future__ import annotations + +import hashlib +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +from proof.region.v1.arb.tests import gate as arb_gate + + +ARB = Path(__file__).resolve().parents[1] +BUILD = ARB / "build.sh" +INNER_BUILD = ARB / "build-inner.sh" +WORKFLOW = ARB.parents[3] / ".github" / "workflows" / "arb.yml" +RECIPE_REJECTION_TIMEOUT_SECONDS = 5 + + +class ArbBuildRecipeTests(unittest.TestCase): + def test_fast_gate_includes_each_shared_contract_suite_exactly_once(self) -> None: + tests = tuple(arb_gate.iter_tests_v1(arb_gate.full_suite_v1())) + identifiers = tuple(test.id() for test in tests) + for pattern, module_prefix in ( + ("test_executor.py", "test_executor."), + ("test_mpfi_input.py", "test_mpfi_input."), + ): + with self.subTest(pattern=pattern): + included = tuple( + identifier + for identifier in identifiers + if identifier.startswith(module_prefix) + ) + expected = tuple( + test.id() + for test in arb_gate.iter_tests_v1( + unittest.defaultTestLoader.discover( + str(arb_gate.SHARED_TEST_DIRECTORY), + pattern=pattern, + ) + ) + ) + + self.assertTrue(expected) + self.assertEqual(included, expected) + self.assertEqual(len(identifiers), len(set(identifiers))) + + def test_mpfi_input_contract_cannot_green_by_skipping(self) -> None: + suite = unittest.defaultTestLoader.discover( + str(arb_gate.SHARED_TEST_DIRECTORY), + pattern="test_mpfi_input.py", + ) + test_ids = tuple(test.id() for test in arb_gate.iter_tests_v1(suite)) + result = unittest.TestResult() + + suite.run(result) + + self.assertTrue(test_ids) + self.assertTrue( + all(test_id.startswith("test_mpfi_input.") for test_id in test_ids) + ) + expected_skip_ids = { + test_id for test_id, _reason in arb_gate.EXPECTED_SKIPS + } + self.assertTrue(set(test_ids).isdisjoint(expected_skip_ids)) + self.assertEqual(result.testsRun, len(test_ids)) + self.assertFalse(result.skipped) + self.assertFalse(result.expectedFailures) + self.assertFalse(result.unexpectedSuccesses) + self.assertFalse(result.failures) + self.assertFalse(result.errors) + + def test_pr_gate_requires_a_disposable_exact_workflow_runner(self) -> None: + source = WORKFLOW.read_text(encoding="utf-8") + runner_contracts = [ + line.strip() + for line in source.splitlines() + if line.lstrip().startswith("runs-on:") + ] + + self.assertEqual( + runner_contracts, + ["runs-on: [self-hosted, Linux, X64, labcolors-ephemeral]"], + ) + self.assertIn("proof/region/v1/arb/tests/gate.py", source) + self.assertIn("proof/region/v1/arb/tests/native_gate.py", source) + self.assertEqual(source.count("- .github/workflows/arb.yml"), 1) + self.assertNotIn("arb-proof-observation.yml", source) + self.assertIn( + 'echo "LABCOLORS_MPFI_ARCHIVE=$archive" >> "$GITHUB_ENV"', + source, + ) + # The Docker boundary probe must consume the canonical transport + # backend and the retained pipeline policy; a bare pipeline-attribute + # probe drifted once already and failed closed only on a real worker. + for probe_contract in ( + "from build import transport as build_transport", + "build_transport.NativeDockerBuildBackendV1(", + "pipeline.ARB_BUILD_TRANSPORT_POLICY_V1,", + "build_transport.DockerSupportedV1:", + ): + with self.subTest(probe_contract=probe_contract): + self.assertIn(probe_contract, source) + self.assertNotIn("pipeline.NativeDockerBuildBackendV1", source) + for required in ( + 'apparmor_userns=/proc/sys/kernel/apparmor_restrict_unprivileged_userns', + 'if [[ -f "$apparmor_userns" ]]; then', + 'original_userns="$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns)"', + 'echo "LABCOLORS_APPARMOR_USERNS_V1=$original_userns"', + "sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0", + 'echo "LABCOLORS_APPARMOR_USERNS_V1="', + 'kernel.apparmor_restrict_unprivileged_userns=$LABCOLORS_APPARMOR_USERNS_V1', + 'mkdir "$scope/tasks" "$scope/proof"', + "printf '+memory +pids' > \"$scope/cgroup.subtree_control\"", + "printf '+memory +pids' > \"$scope/proof/cgroup.subtree_control\"", + "printf '2' > \"$scope/proof/pids.max\"", + 'mkdir "$scope/proof/observer"', + '"$scope/proof/cgroup.subtree_control"', + 'scope="/sys/fs/cgroup/labcolors-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"', + 'echo "LABCOLORS_CGROUP_SCOPE_V1=$scope"', + 'echo "LABCOLORS_EXECUTOR_CGROUP_V1=$scope/proof"', + '"$LABCOLORS_EXECUTOR_CGROUP_V1/observer/cgroup.procs"', + '"$LABCOLORS_CGROUP_SCOPE_V1/tasks/cgroup.procs"', + "native_gate.py receipt", + "native_gate.py executor", + "exec python3", + '"$LABCOLORS_CGROUP_SCOPE_V1/cgroup.kill"', + "'populated 0'", + "for child in proof/observer proof tasks", + 'sudo rmdir "$LABCOLORS_CGROUP_SCOPE_V1/$child"', + 'sudo rmdir "$LABCOLORS_CGROUP_SCOPE_V1"', + ): + with self.subTest(required=required): + self.assertIn(required, source) + self.assertNotIn("grep --ignore-case --quiet skipped", source) + self.assertNotIn("python3 -m unittest", source) + # The kernel precondition is fail-closed and host-explicit: either the + # restriction sysctl is read, validated, and lifted, or its absence is + # the proof; an unconditional presence test would fail closed on + # kernels without AppArmor userns mediation. + self.assertNotIn( + "test -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns", + source, + ) + self.assertLess( + source.index("proof/region/v1/arb/tests/gate.py"), + source.index("LABCOLORS_EXECUTOR_CGROUP_V1=$scope/proof"), + ) + # Root admits each receipt lane into the delegated subtree before the + # controller's observer self-placement: the kernel rejects a migration + # whose common ancestor with the root-owned runner cgroup is not + # writable by the job, so omitting either admission leaves that lane + # fail-closed on a real runner. + admission = '"$LABCOLORS_CGROUP_SCOPE_V1/tasks/cgroup.procs"' + receipt_gate = "native_gate.py receipt" + self.assertEqual(source.count(admission), 2) + self.assertEqual(source.count(receipt_gate), 2) + first_admission = source.index(admission) + first_gate = source.index(receipt_gate) + second_admission = source.index( + admission, first_admission + len(admission) + ) + second_gate = source.index(receipt_gate, first_gate + len(receipt_gate)) + self.assertLess(first_admission, first_gate) + self.assertLess(second_admission, second_gate) + + def test_pr_gate_cannot_green_skip_a_fork_without_execution(self) -> None: + source = WORKFLOW.read_text(encoding="utf-8") + + self.assertNotIn("github.event.pull_request.head.repo.full_name", source) + + def test_privileged_workflow_has_no_automatic_pull_request_trigger(self) -> None: + source = WORKFLOW.read_text(encoding="utf-8") + + self.assertIn("\n workflow_dispatch:\n", source) + self.assertIn("\n push:\n", source) + self.assertIn("branches: [main]", source) + self.assertNotIn("\n pull_request:\n", source) + self.assertNotIn("pull_request", source) + + def test_exact_suite_gate_rejects_expected_failure(self) -> None: + class BrokenRequiredTest(unittest.TestCase): + @unittest.expectedFailure + def test_required(self) -> None: + self.fail("broken") + + suite = unittest.defaultTestLoader.loadTestsFromTestCase(BrokenRequiredTest) + expected = arb_gate.test_inventory_sha256_v1(suite) + + self.assertEqual( + arb_gate.run_exact_suite_v1( + suite, + expected_inventory_sha256=expected, + expected_skips=frozenset(), + verbosity=0, + ), + 1, + ) + + def test_exact_suite_gate_rejects_empty_or_same_count_replacement(self) -> None: + class RequiredTest(unittest.TestCase): + def test_required(self) -> None: + pass + + class ReplacementTest(unittest.TestCase): + def test_replacement(self) -> None: + pass + + empty = unittest.TestSuite() + self.assertEqual( + arb_gate.run_exact_suite_v1( + empty, + expected_inventory_sha256=hashlib.sha256(b"").hexdigest(), + expected_skips=frozenset(), + verbosity=0, + ), + 1, + ) + required = unittest.defaultTestLoader.loadTestsFromTestCase(RequiredTest) + replacement = unittest.defaultTestLoader.loadTestsFromTestCase(ReplacementTest) + self.assertEqual(required.countTestCases(), replacement.countTestCases()) + self.assertEqual( + arb_gate.run_exact_suite_v1( + replacement, + expected_inventory_sha256=arb_gate.test_inventory_sha256_v1(required), + expected_skips=frozenset(), + verbosity=0, + ), + 1, + ) + + def test_recipe_is_offline_static_and_platform_explicit(self) -> None: + source = INNER_BUILD.read_text(encoding="utf-8") + entrypoint = BUILD.read_text(encoding="utf-8") + self.assertIn("/usr/bin/env -i", entrypoint) + self.assertNotIn("LC_BUILD_ENV_V1", entrypoint) + + for required in ( + 'require_directory "$inputs/gmp-6.3.0"', + 'require_directory "$inputs/mpfr-4.2.2"', + 'require_directory "$inputs/flint-3.6.0"', + 'require_regular "$workspace/proof/region/v1/arb/evaluator/formula.h"', + "9958f20c8ca598625db0593a45f8f8bc79e4b2f22b53263b6c32d78a5e1d2693", + "-I.", + "--build=x86_64-pc-linux-gnu", + "--host=x86_64-pc-linux-gnu", + "--disable-shared", + "--enable-static", + "--disable-assembly", + "--enable-formally-proven-code", + "--disable-lto", + "--enable-assert", + "-fno-fast-math", + "-ffp-contract=off", + "-fno-lto", + "-std=gnu17", + # One strict dialect for the whole chain: the dependency libraries + # are built with gnu17 above, and the evaluator keeps -Wall + # -Wextra -Werror -pedantic fully strict on its own sources. + "-std=gnu17 -Wall -Wextra -Werror -pedantic", + # Assert-enabled FLINT 3.6.0 degrades FLINT_UNUSED(x) to a bare + # parameter, which -Wextra diagnoses inside flint_rand_clear in + # any dialect; the pinned dependency headers therefore enter as + # one system include directory while the evaluator's own headers + # stay under -I. with full diagnostics. + '-I. -isystem "$prefix/include"', + "-march=x86-64", + "-mtune=generic", + "-Wl,--build-id=none", + "make check", + "readelf", + ): + with self.subTest(required=required): + self.assertIn(required, source) + + for forbidden in ( + "curl ", + "wget ", + "git clone", + "apt-get", + "brew ", + "tar --extract", + "-ffast-math", + "-march=native", + "-flto", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, source) + # Reverting the dependency headers to a regular include would turn + # FLINT's assert-mode header diagnostics back into -Werror failures. + self.assertNotIn('-I"$prefix/include"', source) + + self.assertIn( + 'if ! /usr/bin/readelf -l "$build/arb-evaluator-v1" ' + '> "$build/program-headers"; then', + source, + ) + self.assertIn( + 'if ! /usr/bin/readelf -d "$build/arb-evaluator-v1" ' + '> "$build/dynamic-section"; then', + source, + ) + self.assertNotIn("readelf -l \"$build/arb-evaluator-v1\" |", source) + self.assertNotIn("readelf -d \"$build/arb-evaluator-v1\" 2>&1 |", source) + + def test_public_build_entrypoint_strips_hostile_environment_before_recipe(self) -> None: + entrypoint = BUILD.read_text(encoding="utf-8") + recipe = INNER_BUILD.read_text(encoding="utf-8") + self.assertIn("/usr/bin/env -i", entrypoint) + self.assertIn('inner="$script_dir/build-inner.sh"', entrypoint) + self.assertIn('/bin/sh "$inner"', entrypoint) + self.assertNotIn("LC_BUILD_ENV_V1", entrypoint) + self.assertNotIn("LC_BUILD_ENV_V1", recipe) + self.assertNotIn("/usr/bin/env -i", recipe) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + entrypoint_copy = root / "build.sh" + inner = root / "build-inner.sh" + observed = root / "environment" + entrypoint_copy.write_text(entrypoint, encoding="utf-8") + entrypoint_copy.chmod(0o755) + inner.write_text( + "#!/bin/sh\n" + f"/usr/bin/env | /usr/bin/sort > '{observed}'\n", + encoding="utf-8", + ) + result = subprocess.run( + [str(entrypoint_copy)], + check=False, + capture_output=True, + text=True, + env={ + "LC_BUILD_ENV_V1": "1", + "MAKEFLAGS": "--jobserver-auth=spoof", + "PYTHONPATH": "/host-controlled", + "CONFIG_SITE": "/host-controlled/site", + "PATH": "/host-controlled/bin", + }, + ) + self.assertEqual(result.returncode, 0, result.stderr) + environment = observed.read_text(encoding="utf-8") + self.assertIn("PATH=/usr/local/bin:/usr/bin:/bin\n", environment) + for forbidden in ( + "LC_BUILD_ENV_V1=1", + "MAKEFLAGS=--jobserver-auth=spoof", + "PYTHONPATH=/host-controlled", + "CONFIG_SITE=/host-controlled/site", + "PATH=/host-controlled/bin", + ): + self.assertNotIn(forbidden, environment) + + def test_recipe_rejects_ambient_or_incomplete_invocation_before_build(self) -> None: + self.assertTrue(os.access(BUILD, os.X_OK), BUILD) + result = subprocess.run( + [str(BUILD)], + check=False, + capture_output=True, + env={ + "PATH": os.environ.get("PATH", ""), + "UNDECLARED": "must-not-be-observed", + }, + stdin=subprocess.DEVNULL, + timeout=RECIPE_REJECTION_TIMEOUT_SECONDS, + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_evaluator_source.py b/proof/region/v1/arb/tests/test_evaluator_source.py new file mode 100644 index 00000000..0625aaca --- /dev/null +++ b/proof/region/v1/arb/tests/test_evaluator_source.py @@ -0,0 +1,1090 @@ +#!/usr/bin/env python3 +"""Hostile source contract for the standalone Arb evaluator.""" + +from __future__ import annotations + +import hashlib +import os +import struct +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ARB = Path(__file__).resolve().parents[1] +EVALUATOR = ARB / "evaluator" +REPO = ARB.parents[3] +FORMULA = REPO / "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir" +GENERATOR = EVALUATOR / "formula.py" +# CI watchdogs bound broken test processes; they are not performance claims. +# Change them only with a measured exact native-gate workload and its job budget. +GENERATOR_TIMEOUT_SECONDS = 60 +EVALUATOR_TIMEOUT_SECONDS = 300 +sys.path.insert(0, str(REPO / "proof/region/v1")) + +from arb import runtime as arb_runtime # noqa: E402 +from region_proof_protocol import ( # noqa: E402 + BoundaryUnprovenWitnessV1, + ComparatorBudgetV1, + ComparatorKindV1, + ComparatorManifestV2, + ContextualRegionDefinitionV1, + DecisionTranscriptV1, + DecisionV1, + ExactZeroSignalTraceV1, + ProofJobV1, + ProofPolicyV1, + ReducedDomainManifestV1, + ResourceLimitWitnessV1, +) + + +def generate(source: bytes) -> subprocess.CompletedProcess[bytes]: + with tempfile.TemporaryDirectory() as temporary: + formula = Path(temporary) / "formula.lcir" + formula.write_bytes(source) + return subprocess.run( + [sys.executable, str(GENERATOR), str(formula)], + check=False, + capture_output=True, + stdin=subprocess.DEVNULL, + timeout=GENERATOR_TIMEOUT_SECONDS, + env={ + "PATH": os.environ.get("PATH", ""), + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONHASHSEED": "0", + }, + ) + + +def run_evaluator( + command: list[str] | tuple[str, ...], + stdin: bytes, +) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + command, + input=stdin, + check=False, + capture_output=True, + timeout=EVALUATOR_TIMEOUT_SECONDS, + ) + + +def runtime_invocation() -> tuple[str, ...]: + return ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + "ab" + "00" * 31, + "--job", + "/dev/stdin", + ) + + +def runtime_profile_job( + *, + arb_ladder: tuple[int, ...] = (1,), + mpfi_ladder: tuple[int, ...] = (1,), + knot_count: int = 1, +) -> ProofJobV1: + registered = ContextualRegionDefinitionV1.parse( + (REPO / "proof/region/v1/fixtures/v5b2b-definition-0a8d1c3d.bin").read_bytes() + ) + zero = bytes(8) + knots = tuple( + coordinate + for index in range(knot_count) + for coordinate in (struct.pack(">d", float(index)), zero, zero, zero) + ) + definition = ContextualRegionDefinitionV1( + registered.fields[:21] + (knot_count.to_bytes(8, "big"),) + knots, + knot_count, + ) + return ProofJobV1( + definition, + FORMULA.read_bytes(), + ReducedDomainManifestV1.from_ordinals((0,)), + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1( + ComparatorKindV1.ARB, + arb_ladder, + 0, + 0, + ), + ComparatorBudgetV1( + ComparatorKindV1.MPFI, + mpfi_ladder, + 0, + 0, + ), + ), + ), + ) + + +def assert_transcript_wire_coordinates( + case: unittest.TestCase, + wire: bytes, + transcript: DecisionTranscriptV1, + manifest_identity: bytes, +) -> None: + decision_bits = transcript.decision_bits + accounting_digest = transcript.accounting_digest + case.assertEqual(wire[:8], b"LCTRN1\0\0") + case.assertEqual(wire[72:104], manifest_identity) + accounting_offset = 160 + len(decision_bits) + case.assertEqual( + wire[accounting_offset : accounting_offset + 32], + accounting_digest, + ) + + +class FormulaGeneratorTests(unittest.TestCase): + def test_registered_formula_generates_one_deterministic_c_program(self) -> None: + source = FORMULA.read_bytes() + first = generate(source) + second = generate(source) + + self.assertEqual(first.returncode, 0, first.stderr.decode()) + self.assertEqual(second.returncode, 0, second.stderr.decode()) + self.assertEqual(first.stdout, second.stdout) + self.assertEqual( + hashlib.sha256(first.stdout).hexdigest(), + "9958f20c8ca598625db0593a45f8f8bc79e4b2f22b53263b6c32d78a5e1d2693", + ) + self.assertIn(b"lc_formula_point", first.stdout) + self.assertIn(b"lc_formula_segment", first.stdout) + self.assertIn(b"lc_formula_singleton", first.stdout) + self.assertNotIn(b"double", first.stdout) + + def test_generator_rejects_canonical_semantic_and_driver_mutations(self) -> None: + source = FORMULA.read_bytes() + mutations = ( + (b"labcolors_exact_real_ssa 1", b"labcolors_exact_real_ssa 2"), + (b"operator add 2 real exact_x_plus_y", b"operator add 2 real exact_x_minus_y"), + (b"node xyz_x_r real mul srgb_m00 linear_r", b"node xyz_x_r real add srgb_m00 linear_r"), + (b"literal p1_7 3ffb333333333333", b"literal p1_7 3ffb333333333334"), + (b"rule boundary inclusive", b"rule boundary exclusive"), + (b"point_nodes 226", b"point_nodes 225"), + ) + for needle, replacement in mutations: + with self.subTest(replacement=replacement): + self.assertIn(needle, source) + result = generate(source.replace(needle, replacement, 1)) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + + for mutant in (source + b"\n", source.replace(b"\n", b"\r\n", 1)): + result = generate(mutant) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + + def test_generator_is_independent_from_python_protocol_and_controller(self) -> None: + source = GENERATOR.read_text(encoding="utf-8") + for forbidden in ( + "region_proof_protocol", + "controller", + "import numpy", + "import scipy", + ): + self.assertNotIn(forbidden, source) + + +class StandaloneSourceTests(unittest.TestCase): + def test_evaluator_has_an_independent_wire_hash_interval_and_region_path(self) -> None: + required = ( + "main.c", + "wire.c", + "wire.h", + "hash.c", + "hash.h", + "interval.c", + "interval.h", + "region.c", + "region.h", + ) + for name in required: + with self.subTest(name=name): + self.assertTrue((EVALUATOR / name).is_file(), name) + + joined = "\n".join( + (EVALUATOR / name).read_text(encoding="utf-8") + for name in required + ) + for forbidden in ( + "region_proof_protocol", + "controller.py", + "arb_set_d(", + "strtod(", + "#include ", + " pow(", + " sqrt(", + "epsilon", + "midpoint", + "fallback", + ): + self.assertNotIn(forbidden, joined) + self.assertIn("arb_set_fmpz_2exp", joined) + self.assertIn("arb_get_interval_fmpz_2exp", joined) + self.assertIn("LCTRN1", joined) + self.assertNotIn("LCARO1", joined) + self.assertIn("--manifest-identity", joined) + + def test_closed_boundary_and_typed_unresolved_states_are_structural(self) -> None: + region = (EVALUATOR / "region.c").read_text(encoding="utf-8") + header = (EVALUATOR / "region.h").read_text(encoding="utf-8") + + for outcome in ( + "LC_REGION_INSIDE", + "LC_REGION_OUTSIDE", + "LC_REGION_BOUNDARY_UNPROVEN", + "LC_REGION_RESOURCE_LIMIT_REACHED", + ): + self.assertIn(outcome, header) + self.assertIn("arb_is_nonpositive", region) + self.assertIn("arb_is_positive", region) + self.assertIn("arb_intersection", region) + self.assertNotIn("arb_contains_zero(f)", region) + + def test_subminimum_flint_precision_never_enters_the_formula(self) -> None: + region = (EVALUATOR / "region.c").read_text(encoding="utf-8") + evaluator = region[region.index("lc_region_evaluate_rgb(") :] + + guard = evaluator.index("if (precision < 2)") + formula_call = evaluator.index("lc_formula_point(") + self.assertLess(guard, formula_call) + self.assertIn("minimum working precision", evaluator[:formula_call]) + + decision = region[ + region.index("lc_region_decide(") : region.index("lc_region_evaluate_rgb(") + ] + decision_guard = decision.index("if (precision < 2)") + singleton_dispatch = decision.index("if (region->knot_count == 1)") + self.assertLess(decision_guard, singleton_dispatch) + + def test_runtime_profile_bounds_input_and_transcript_before_allocation(self) -> None: + header = (EVALUATOR / "wire.h").read_text(encoding="utf-8") + wire = (EVALUATOR / "wire.c").read_text(encoding="utf-8") + main = (EVALUATOR / "main.c").read_text(encoding="utf-8") + self.assertEqual(arb_runtime.ARB_MAX_JOB_BYTES_V1, 16_777_216) + self.assertEqual(arb_runtime.ARB_MAX_OUTPUT_BYTES_V1, 16_777_216) + self.assertEqual(arb_runtime.ARB_MAX_PRECISION_BITS_V1, 4_096) + self.assertEqual(arb_runtime.ARB_MAX_POLICY_RUNGS_V1, 32) + self.assertEqual(arb_runtime.ARB_MAX_KNOTS_V1, 1_024) + for declaration in ( + "#define LC_ARB_MAX_PRECISION_BITS_V1 UINT32_C(4096)", + "#define LC_ARB_MAX_POLICY_RUNGS_V1 UINT32_C(32)", + "#define LC_ARB_MAX_KNOTS_V1 UINT64_C(1024)", + "#define LC_ARB_EXIT_USAGE_V1 64", + "#define LC_ARB_EXIT_INPUT_REJECTED_V1 65", + "#define LC_ARB_EXIT_INPUT_LIMIT_V1 66", + "#define LC_ARB_EXIT_OUTPUT_LIMIT_V1 67", + "#define LC_ARB_EXIT_RESOURCE_LIMIT_V1 68", + "#define LC_ARB_EXIT_INTERNAL_V1 70", + "#define LC_ARB_EXIT_IO_V1 74", + ): + with self.subTest(declaration=declaration): + self.assertIn(declaration, header) + self.assertIn("UINT64_C(16) * UINT64_C(1024) * UINT64_C(1024)", header) + for name in ( + "LC_ARB_MAX_JOB_BYTES_V1", + "LC_ARB_MAX_OUTPUT_BYTES_V1", + "LC_ARB_MAX_PRECISION_BITS_V1", + "LC_ARB_MAX_POLICY_RUNGS_V1", + "LC_ARB_MAX_KNOTS_V1", + ): + with self.subTest(name=name): + self.assertIn(name, header) + self.assertIn(name, main + wire) + self.assertIn("LC_ARB_EVALUATION_RESOURCE_LIMIT", main) + self.assertIn("limit_exceeded", main) + self.assertIn("input.maximum", main) + self.assertIn("output.maximum", main) + self.assertNotIn("byte_buffer decisions", main) + self.assertNotIn("byte_buffer witnesses", main) + self.assertIn("append_digest_witness(output", main) + self.assertIn("append_resource_witness(\n output", main) + digest_appender = main[ + main.index("append_digest_witness(") : main.index("append_resource_witness(") + ] + resource_appender = main[ + main.index("append_resource_witness(") : main.index("lesser_u64(") + ] + self.assertIn("uint8_t record[37]", digest_appender) + self.assertIn("buffer_append(output, record, sizeof(record))", digest_appender) + self.assertNotIn("buffer_u8", digest_appender) + self.assertIn("uint8_t record[22]", resource_appender) + self.assertIn("buffer_append(output, record, sizeof(record))", resource_appender) + self.assertNotIn("buffer_u8", resource_appender) + self.assertIn("output_limit", main) + reserve = main[main.index("buffer_reserve(") : main.index("buffer_append(")] + self.assertLess( + reserve.index("required > buffer->maximum"), + reserve.index("realloc(buffer->bytes"), + ) + reader = main[main.index("read_stdin(") : main.index("digest_is_nonzero(")] + self.assertLess( + reader.index("input->maximum"), + reader.index("buffer_append(input"), + ) + self.assertLess( + wire.index("knot_count > LC_ARB_MAX_KNOTS_V1"), + wire.index("lc_region_init(&job->region"), + ) + self.assertLess( + wire.index("rung_count > LC_ARB_MAX_POLICY_RUNGS_V1"), + wire.index("policy->precision_ladder = calloc"), + ) + read_failure = main[ + main.index("if (read_status != LC_ARB_READ_OK)") : + main.index("if (!lc_parse_job") + ] + parse_failure_start = main.index("if (!lc_parse_job") + parse_failure = main[ + parse_failure_start : + main.index("lc_arb_evaluation_status", parse_failure_start) + ] + self.assertIn("read_status == LC_ARB_READ_ALLOCATION_FAILED", read_failure) + self.assertIn("status = LC_ARB_EXIT_INTERNAL_V1", read_failure) + self.assertIn("error == LC_WIRE_ALLOCATION_FAILED", parse_failure) + self.assertIn("status = LC_ARB_EXIT_INTERNAL_V1", parse_failure) + + def test_sha256_has_literal_standard_vectors_and_no_external_crypto(self) -> None: + source = (EVALUATOR / "hash.c").read_text(encoding="utf-8") + header = (EVALUATOR / "hash.h").read_text(encoding="utf-8") + self.assertIn("lc_sha256", header) + self.assertIn("0x6a09e667", source) + self.assertNotIn("openssl", source.lower()) + + +class ExactBoundaryRuntimeTests(unittest.TestCase): + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_closed_stdout_is_a_versioned_io_exit_not_an_untyped_signal(self) -> None: + read_descriptor, write_descriptor = os.pipe() + os.close(read_descriptor) + with os.fdopen(write_descriptor, "wb") as output: + process = subprocess.Popen( + runtime_invocation(), + stdin=subprocess.PIPE, + stdout=output, + stderr=subprocess.PIPE, + ) + _stdout, stderr = process.communicate( + runtime_profile_job().encode(), + timeout=EVALUATOR_TIMEOUT_SECONDS, + ) + self.assertEqual(process.returncode, arb_runtime.ARB_EXIT_IO_V1) + self.assertEqual(stderr, b"result write failed\n") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_cli_requires_one_nonzero_lowercase_manifest_identity(self) -> None: + executable = os.environ["LABCOLORS_ARB_EVALUATOR"] + valid = "ab" + "00" * 31 + invalid_invocations = ( + (), + ("--manifest-identity", "0" * 64, "--job", "/dev/stdin"), + ("--manifest-identity", valid.upper(), "--job", "/dev/stdin"), + ("--manifest-identity", "g" + valid[1:], "--job", "/dev/stdin"), + ("--manifest-identity", valid[:-1], "--job", "/dev/stdin"), + ("--manifest-identity", valid + "0", "--job", "/dev/stdin"), + ("--manifest-identity", valid, "--job", "job.bin"), + ("--manifest", valid, "--job", "/dev/stdin"), + ("--manifest-identity", valid, "--job", "/dev/stdin", "extra"), + ) + for arguments in invalid_invocations: + with self.subTest(arguments=arguments): + result = run_evaluator((executable, *arguments), b"") + self.assertEqual(result.returncode, arb_runtime.ARB_EXIT_USAGE_V1) + self.assertEqual(result.stdout, b"") + + accepted = run_evaluator( + ( + executable, + "--manifest-identity", + valid, + "--job", + "/dev/stdin", + ), + b"", + ) + self.assertEqual(accepted.returncode, arb_runtime.ARB_EXIT_INPUT_REJECTED_V1) + self.assertEqual(accepted.stdout, b"") + self.assertEqual(accepted.stderr, b"job read failed: empty_input\n") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_job_transport_limit_precedes_wire_parsing(self) -> None: + at_limit = run_evaluator( + runtime_invocation(), + bytes(arb_runtime.ARB_MAX_JOB_BYTES_V1), + ) + self.assertEqual( + at_limit.returncode, + arb_runtime.ARB_EXIT_INPUT_REJECTED_V1, + ) + self.assertEqual(at_limit.stdout, b"") + self.assertEqual(at_limit.stderr, b"job rejected: bad_magic\n") + + over_limit = run_evaluator( + runtime_invocation(), + bytes(arb_runtime.ARB_MAX_JOB_BYTES_V1 + 1), + ) + self.assertEqual(over_limit.returncode, arb_runtime.ARB_EXIT_INPUT_LIMIT_V1) + self.assertEqual(over_limit.stdout, b"") + self.assertEqual(over_limit.stderr, b"job read failed: input_limit\n") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_allocation_profile_boundaries_are_enforced_by_the_native_parser(self) -> None: + accepted = ( + runtime_profile_job( + arb_ladder=(arb_runtime.ARB_MAX_PRECISION_BITS_V1,), + ), + runtime_profile_job( + mpfi_ladder=(arb_runtime.ARB_MAX_PRECISION_BITS_V1,), + ), + runtime_profile_job( + arb_ladder=tuple(range(1, arb_runtime.ARB_MAX_POLICY_RUNGS_V1 + 1)), + ), + runtime_profile_job( + mpfi_ladder=tuple(range(1, arb_runtime.ARB_MAX_POLICY_RUNGS_V1 + 1)), + ), + runtime_profile_job(knot_count=arb_runtime.ARB_MAX_KNOTS_V1), + ) + for index, job in enumerate(accepted): + with self.subTest(boundary=index): + result = run_evaluator(runtime_invocation(), job.encode()) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(result.stderr, b"") + self.assertEqual(DecisionTranscriptV1.parse(result.stdout).encode(), result.stdout) + + over_rungs = tuple(range(1, arb_runtime.ARB_MAX_POLICY_RUNGS_V1 + 2)) + rejected = ( + runtime_profile_job( + arb_ladder=(arb_runtime.ARB_MAX_PRECISION_BITS_V1 + 1,), + ), + runtime_profile_job( + mpfi_ladder=(arb_runtime.ARB_MAX_PRECISION_BITS_V1 + 1,), + ), + runtime_profile_job(arb_ladder=over_rungs), + runtime_profile_job(mpfi_ladder=over_rungs), + runtime_profile_job(knot_count=arb_runtime.ARB_MAX_KNOTS_V1 + 1), + ) + for index, job in enumerate(rejected): + with self.subTest(over_limit=index): + result = run_evaluator(runtime_invocation(), job.encode()) + self.assertEqual( + result.returncode, + arb_runtime.ARB_EXIT_RESOURCE_LIMIT_V1, + ) + self.assertEqual(result.stdout, b"") + self.assertEqual(result.stderr, b"job rejected: resource_limit\n") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_aggregate_transcript_output_limit_is_exact(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + policy = ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (1,), 0, 0), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (1,), 0, 0), + ), + ) + + def job(point_count: int) -> ProofJobV1: + return ProofJobV1( + frozen.definition, + frozen.formula_spec, + ReducedDomainManifestV1(((0, point_count),), point_count), + policy, + ) + + accepted_points = 450_389 + accepted = run_evaluator(runtime_invocation(), job(accepted_points).encode()) + decision_bytes = (accepted_points + 3) // 4 + counters_offset = 120 + decision_bytes + self.assertEqual(accepted.returncode, 0, accepted.stderr.decode()) + self.assertEqual(accepted.stderr, b"") + self.assertEqual(len(accepted.stdout), 16_777_191) + self.assertEqual( + tuple( + int.from_bytes( + accepted.stdout[offset : offset + 8], + "big", + ) + for offset in range(counters_offset, counters_offset + 32, 8) + ), + (0, 0, accepted_points, 0), + ) + + rejected = run_evaluator(runtime_invocation(), job(450_390).encode()) + self.assertEqual(rejected.returncode, arb_runtime.ARB_EXIT_OUTPUT_LIMIT_V1) + self.assertEqual(rejected.stdout, b"") + self.assertEqual(rejected.stderr, b"evaluation failed: output_limit\n") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_black_exact_zero_runs_through_job_parser_formula_and_closed_driver(self) -> None: + registered = ContextualRegionDefinitionV1.parse( + (REPO / "proof/region/v1/fixtures/v5b2b-definition-0a8d1c3d.bin").read_bytes() + ) + zero = bytes(8) + fields = registered.fields[:21] + ((1).to_bytes(8, "big"),) + (zero,) * 4 + definition = ContextualRegionDefinitionV1(fields, 1) + policy = ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (128,), 1, 1), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (192,), 1, 1), + ), + ) + job = ProofJobV1( + definition, + FORMULA.read_bytes(), + ReducedDomainManifestV1.from_ordinals((0,)), + policy, + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-manifest-{index}".encode()).digest() for index in range(10)), + ) + executable = os.environ["LABCOLORS_ARB_EVALUATOR"] + result = run_evaluator( + [ + executable, + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ], + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(result.stderr, b"") + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(transcript.encode(), result.stdout) + assert_transcript_wire_coordinates( + self, + result.stdout, + transcript, + manifest.identity, + ) + self.assertEqual(transcript.job_identity, job.identity) + self.assertEqual(transcript.domain_identity, job.domain.identity) + self.assertEqual(transcript.comparator_identity, manifest.identity) + self.assertEqual(tuple(transcript.iter_decisions()), (DecisionV1.INSIDE,)) + self.assertEqual(transcript.counters, (1, 0, 0, 0)) + self.assertEqual(transcript.exact_equality_count, 1) + witnesses = tuple(transcript.iter_witnesses()) + self.assertEqual(len(witnesses), 1) + self.assertIs(type(witnesses[0]), ExactZeroSignalTraceV1) + self.assertEqual(witnesses[0].ordinal, 0) + self.assertEqual( + witnesses[0].trace_digest, + hashlib.sha256( + b"labcolors.proof-region.exact-zero-signal-trace.v1\0" + + job.identity + + (0).to_bytes(4, "big") + + (0).to_bytes(8, "big") + ).digest(), + ) + + alternate_manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-alternate-{index}".encode()).digest() for index in range(10)), + ) + alternate = run_evaluator( + [ + executable, + "--manifest-identity", + alternate_manifest.identity.hex(), + "--job", + "/dev/stdin", + ], + job.encode(), + ) + self.assertEqual(alternate.returncode, 0, alternate.stderr.decode()) + alternate_transcript = DecisionTranscriptV1.parse(alternate.stdout) + self.assertEqual( + tuple(alternate_transcript.iter_decisions()), + tuple(transcript.iter_decisions()), + ) + self.assertEqual(alternate_transcript.counters, transcript.counters) + self.assertEqual(tuple(alternate_transcript.iter_witnesses()), witnesses) + self.assertEqual(alternate_transcript.comparator_identity, alternate_manifest.identity) + self.assertNotEqual(alternate_transcript.accounting_digest, transcript.accounting_digest) + self.assertNotEqual(alternate.stdout, result.stdout) + + corrupted = bytearray(job.encode()) + corrupted[-1] ^= 1 + rejected = run_evaluator( + [ + executable, + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ], + bytes(corrupted), + ) + self.assertNotEqual(rejected.returncode, 0) + self.assertEqual(rejected.stdout, b"") + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_multisegment_exact_trace_selects_first_canonical_branch(self) -> None: + registered = ContextualRegionDefinitionV1.parse( + (REPO / "proof/region/v1/fixtures/v5b2b-definition-0a8d1c3d.bin").read_bytes() + ) + zero = bytes(8) + tones = tuple( + bytes.fromhex(bits) + for bits in ( + "c000000000000000", + "bff0000000000000", + "0000000000000000", + "3ff0000000000000", + ) + ) + knots = tuple( + coordinate + for tone in tones + for coordinate in (tone, zero, zero, zero) + ) + definition = ContextualRegionDefinitionV1( + registered.fields[:21] + ((4).to_bytes(8, "big"),) + knots, + 4, + ) + job = ProofJobV1( + definition, + FORMULA.read_bytes(), + ReducedDomainManifestV1.from_ordinals((0,)), + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (128,), 2, 2), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (192,), 2, 2), + ), + ), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-multisegment-{index}".encode()).digest() for index in range(10)), + ) + result = run_evaluator( + ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ), + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(tuple(transcript.iter_decisions()), (DecisionV1.INSIDE,)) + self.assertEqual(transcript.counters, (1, 0, 0, 0)) + self.assertEqual(transcript.exact_equality_count, 1) + witnesses = tuple(transcript.iter_witnesses()) + self.assertEqual(len(witnesses), 1) + self.assertIs(type(witnesses[0]), ExactZeroSignalTraceV1) + self.assertEqual( + witnesses[0].trace_digest, + hashlib.sha256( + b"labcolors.proof-region.exact-zero-signal-trace.v1\0" + + job.identity + + (0).to_bytes(4, "big") + + (1).to_bytes(8, "big") + ).digest(), + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_frozen_seam_cube_resolves_one_inside_and_511_outside(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + budget = ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (64, 128), 4, 2048), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (64, 128), 4, 2048), + ) + job = ProofJobV1( + frozen.definition, + frozen.formula_spec, + frozen.domain, + ProofPolicyV1(1, budget), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-manifest-{index}".encode()).digest() for index in range(10)), + ) + invocation = [ + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ] + first = run_evaluator(invocation, job.encode()) + second = run_evaluator(invocation, job.encode()) + + self.assertEqual(first.returncode, 0, first.stderr.decode()) + self.assertEqual(second.returncode, 0, second.stderr.decode()) + self.assertEqual(first.stdout, second.stdout) + transcript = DecisionTranscriptV1.parse(first.stdout) + self.assertEqual(transcript.encode(), first.stdout) + assert_transcript_wire_coordinates( + self, + first.stdout, + transcript, + manifest.identity, + ) + self.assertEqual(transcript.job_identity, job.identity) + self.assertEqual(transcript.domain_identity, job.domain.identity) + self.assertEqual(transcript.comparator_identity, manifest.identity) + self.assertEqual(len(transcript.decision_bits), 128) + self.assertEqual(transcript.counters, (1, 511, 0, 0)) + self.assertEqual(transcript.exact_equality_count, 0) + self.assertEqual(tuple(transcript.iter_witnesses()), ()) + + low_precision = ProofJobV1( + frozen.definition, + frozen.formula_spec, + frozen.domain, + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (16,), 4, 2_048), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (24,), 4, 2_048), + ), + ), + ) + low_first = run_evaluator(invocation, low_precision.encode()) + low_second = run_evaluator(invocation, low_precision.encode()) + self.assertEqual(low_first.returncode, 0, low_first.stderr.decode()) + self.assertEqual(low_second.returncode, 0, low_second.stderr.decode()) + self.assertEqual(low_first.stdout, low_second.stdout) + low_transcript = DecisionTranscriptV1.parse(low_first.stdout) + self.assertEqual(low_transcript.encode(), low_first.stdout) + assert_transcript_wire_coordinates( + self, + low_first.stdout, + low_transcript, + manifest.identity, + ) + self.assertEqual(low_transcript.counters, (0, 501, 11, 0)) + low_witnesses = tuple(low_transcript.iter_witnesses()) + self.assertTrue( + all(type(witness) is BoundaryUnprovenWitnessV1 for witness in low_witnesses) + ) + self.assertEqual( + tuple(witness.ordinal for witness in low_witnesses), + ( + 65_793, + 657_930, + 723_723, + 8_355_711, + 8_421_247, + 8_421_503, + 8_421_504, + 16_711_422, + 16_776_958, + 16_777_214, + 16_777_215, + ), + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_zero_grant_emits_canonical_resource_witnesses(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + zero_grant = ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (64,), 0, 0), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (64,), 0, 0), + ) + job = ProofJobV1( + frozen.definition, + frozen.formula_spec, + frozen.domain, + ProofPolicyV1(1, zero_grant), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-zero-grant-{index}".encode()).digest() for index in range(10)), + ) + result = run_evaluator( + ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ), + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(transcript.encode(), result.stdout) + assert_transcript_wire_coordinates( + self, + result.stdout, + transcript, + manifest.identity, + ) + self.assertEqual(transcript.counters, (0, 504, 0, 8)) + witnesses = tuple(transcript.iter_witnesses()) + self.assertEqual( + tuple(witness.ordinal for witness in witnesses), + (10, 11, 256, 257, 65_537, 65_546, 65_792, 65_793), + ) + self.assertTrue(all(type(witness) is ResourceLimitWitnessV1 for witness in witnesses)) + self.assertTrue( + all( + (witness.scope, witness.granted, witness.consumed) == (1, 0, 0) + for witness in witnesses + ) + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_global_pregrant_is_never_transferred_between_points(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + job = ProofJobV1( + frozen.definition, + frozen.formula_spec, + ReducedDomainManifestV1.from_ordinals((0, 65_793)), + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (32,), 1, 1), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (40,), 1, 1), + ), + ), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-pregrant-{index}".encode()).digest() for index in range(10)), + ) + result = run_evaluator( + ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ), + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual( + tuple(transcript.iter_decisions()), + (DecisionV1.OUTSIDE, DecisionV1.RESOURCE_LIMIT_REACHED), + ) + self.assertEqual(transcript.counters, (0, 1, 0, 1)) + witnesses = tuple(transcript.iter_witnesses()) + self.assertEqual( + witnesses, + (ResourceLimitWitnessV1(65_793, scope=2, granted=0, consumed=0),), + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_subminimum_precision_is_unresolved_and_a_later_valid_rung_recovers(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + domain = ReducedDomainManifestV1.from_ordinals((0, 65_793)) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-minimum-precision-{index}".encode()).digest() for index in range(10)), + ) + invocation = ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ) + + def run_with(arb_ladder: tuple[int, ...]) -> object: + job = ProofJobV1( + frozen.definition, + frozen.formula_spec, + domain, + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1( + ComparatorKindV1.ARB, + arb_ladder, + 1, + 2, + ), + ComparatorBudgetV1( + ComparatorKindV1.MPFI, + (32,), + 1, + 2, + ), + ), + ), + ) + result = run_evaluator(invocation, job.encode()) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(transcript.encode(), result.stdout) + return transcript + + unresolved = run_with((1,)) + direct = run_with((32,)) + recovered = run_with((1, 32)) + + self.assertEqual( + tuple(unresolved.iter_decisions()), + (DecisionV1.BOUNDARY_UNPROVEN, DecisionV1.BOUNDARY_UNPROVEN), + ) + self.assertEqual(unresolved.counters, (0, 0, 2, 0)) + self.assertEqual( + tuple(recovered.iter_decisions()), + tuple(direct.iter_decisions()), + ) + self.assertEqual(recovered.counters, direct.counters) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_resource_witness_accounts_for_work_consumed_on_earlier_rungs(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + job = ProofJobV1( + frozen.definition, + frozen.formula_spec, + ReducedDomainManifestV1.from_ordinals((257,)), + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (12, 64), 1, 1), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (20, 80), 1, 1), + ), + ), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-cross-rung-{index}".encode()).digest() for index in range(10)), + ) + result = run_evaluator( + ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ), + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual( + tuple(transcript.iter_decisions()), + (DecisionV1.RESOURCE_LIMIT_REACHED,), + ) + self.assertEqual(transcript.counters, (0, 0, 0, 1)) + self.assertEqual( + tuple(transcript.iter_witnesses()), + (ResourceLimitWitnessV1(257, scope=1, granted=1, consumed=1),), + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_ARB_EVALUATOR"), + "set LABCOLORS_ARB_EVALUATOR to the controlled C17 binary", + ) + def test_spd_admission_is_exact_across_the_full_binary64_exponent_range(self) -> None: + frozen = ProofJobV1.parse( + (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + ) + fields = list(frozen.definition.fields) + fields[18] = bytes.fromhex("3ff0000000000000") + fields[19] = bytes.fromhex("0000000000000001") + fields[20] = bytes.fromhex("3ff0000000000000") + definition = ContextualRegionDefinitionV1( + tuple(fields), + frozen.definition.knot_count, + ) + job = ProofJobV1( + definition, + frozen.formula_spec, + ReducedDomainManifestV1.from_ordinals((0,)), + ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (64,), 4, 4), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (80,), 4, 4), + ), + ), + ) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(f"arb-exact-spd-{index}".encode()).digest() for index in range(10)), + ) + result = run_evaluator( + ( + os.environ["LABCOLORS_ARB_EVALUATOR"], + "--manifest-identity", + manifest.identity.hex(), + "--job", + "/dev/stdin", + ), + job.encode(), + ) + + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(transcript.encode(), result.stdout) + self.assertEqual(transcript.job_identity, job.identity) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_native_gate.py b/proof/region/v1/arb/tests/test_native_gate.py new file mode 100644 index 00000000..5ea85529 --- /dev/null +++ b/proof/region/v1/arb/tests/test_native_gate.py @@ -0,0 +1,42 @@ +"""Fail fast in the quick gate when a native lane inventory pin drifts. + +The native containment lane recomputes the exact test inventory at runtime +and refuses any drift, but that verdict arrives only after the disposable +worker rebuilds every sealed archive. A stale literal therefore costs a full +native run before it is visible. This contract recomputes each lane inventory +the same way the native gate does and fails in the quick gate instead. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + + +REPO = Path(__file__).resolve().parents[5] +sys.path.insert(0, str(REPO)) + +import gate # noqa: E402 +import native_gate # noqa: E402 + + +class NativeGateInventoryPinTests(unittest.TestCase): + def test_every_native_lane_pin_matches_its_exact_runtime_suite(self) -> None: + for mode, (test_cases, pinned_inventory) in native_gate._MODES.items(): + with self.subTest(mode=mode): + suite = unittest.TestSuite( + unittest.defaultTestLoader.loadTestsFromTestCase(test_case) + for test_case in test_cases + ) + self.assertEqual( + gate.test_inventory_sha256_v1(suite), + pinned_inventory, + f"native lane {mode!r} inventory pin drifted from the " + "exact runtime suite; recompute the pin from the loaded " + "test ids instead of editing it by hand", + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_origin.py b/proof/region/v1/arb/tests/test_origin.py new file mode 100644 index 00000000..aaf763bd --- /dev/null +++ b/proof/region/v1/arb/tests/test_origin.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python3 +"""Hostile tests for source-origin observations.""" + +from __future__ import annotations + +import hashlib +import gzip +import io +import signal +import sys +import tarfile +import tempfile +import unittest +from dataclasses import replace +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +sys.path.insert(0, str(PROOF)) + +from arb import origin # noqa: E402 +import provenance # noqa: E402 + + +def git_content_relation_fixture() -> tuple[ + provenance.SourceReleaseLockV1, + provenance.SafeSourceArchiveV1, + origin.GitTreeProcessObservationV1, +]: + common_body = b"license" + omitted_body = b"ci" + generated_body = b"config" + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("fixture-1/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + root.mtime = 0 + archive.addfile(root) + for name, body, mode in ( + ("fixture-1/LICENSE", common_body, 0o644), + ("fixture-1/configure", generated_body, 0o755), + ): + member = tarfile.TarInfo(name) + member.mode = mode + member.size = len(body) + member.mtime = 0 + archive.addfile(member, io.BytesIO(body)) + archive_bytes = gzip.compress(raw.getvalue(), compresslevel=9, mtime=0) + lock = provenance.SourceReleaseLockV1( + provenance.SourceRoleV1.FLINT_ARB, + "1", + "https://example.invalid/fixture-1.tar.gz", + provenance.ArchiveFormatV1.TAR_GZIP, + len(archive_bytes), + hashlib.sha256(archive_bytes).digest(), + len(raw.getvalue()), + "fixture-1/", + 2, + len(common_body) + len(generated_body), + ( + provenance.LegalFileV1( + "LICENSE", len(common_body), hashlib.sha256(common_body).digest() + ), + ), + provenance.GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + (".github/ci.yml",), + ( + provenance.ProjectPinnedReleaseOnlyFileV1( + "configure", + 0o755, + len(generated_body), + hashlib.sha256(generated_body).digest(), + ), + ), + ), + ) + process = origin.GitTreeProcessObservationV1( + lock.integrity.commit, + lock.integrity.tree, + bytes.fromhex("55" * 32), + ( + origin.FileCoordinateV1( + ".github/ci.yml", 0o644, len(omitted_body), hashlib.sha256(omitted_body).digest() + ), + origin.FileCoordinateV1( + "LICENSE", 0o644, len(common_body), hashlib.sha256(common_body).digest() + ), + ), + bytes.fromhex("33" * 32), + bytes.fromhex("44" * 32), + _token=origin._GIT_PROCESS_TOKEN, + ) + return lock, provenance.admit_source_archive(lock, archive_bytes), process + + +def signed_source_fixture() -> tuple[ + provenance.SourceReleaseLockV1, + provenance.SafeSourceArchiveV1, +]: + base, admitted, _process = git_content_relation_fixture() + packets = origin.decode_public_key_armour((ARB / "keys/gmp.asc").read_bytes()) + signed = replace( + base, + role=provenance.SourceRoleV1.GMP, + integrity=provenance.DetachedSignaturePolicyV1( + "https://example.invalid/fixture-1.tar.gz.sig", + len(b"signature"), + hashlib.sha256(b"signature").digest(), + hashlib.sha256(packets).digest(), + bytes.fromhex("343c2ff0fbee5ec2edbef399f3599ff828c67298"), + ), + ) + return signed, provenance.admit_source_archive(signed, admitted.archive_bytes) + + +class PublicKeyArmourTests(unittest.TestCase): + def test_pinned_key_armour_decodes_to_exact_openpgp_packets(self) -> None: + cases = ( + ( + "gmp.asc", + "928ac84aa0e2134bbb335cd439110dc3f9b967eb04caff4a44dd5d04a3f13474", + ), + ( + "mpfr.asc", + "3fe00f68bbf3888ae185b950d4db0f708dd01b6159cb03dec77296f9045b6372", + ), + ) + for name, expected in cases: + with self.subTest(name=name): + packets = origin.decode_public_key_armour((ARB / "keys" / name).read_bytes()) + self.assertEqual(hashlib.sha256(packets).hexdigest(), expected) + + def test_armour_is_canonical_and_crc_checked(self) -> None: + valid = (ARB / "keys" / "mpfr.asc").read_bytes() + mutants = ( + valid + b"\n", + valid.replace(b"=3az7", b"=3az8", 1), + valid.replace(b"PUBLIC KEY", b"PRIVATE KEY", 1), + valid.replace(b"\n\n", b"\nComment: ambient\n\n", 1), + valid.replace(b"\n", b"\r\n", 1), + ) + for mutant in mutants: + with self.subTest(mutant=hashlib.sha256(mutant).hexdigest()): + with self.assertRaises(origin.OriginErrorV1): + origin.decode_public_key_armour(mutant) + + +class _DiagnosticRunner: + def __init__( + self, + *observations: origin.DiagnosticProcessObservationV1 | object, + ) -> None: + self.observations = list(observations) + self.requests: list[origin.DiagnosticProcessRequestV1] = [] + + def run( + self, + request: origin.DiagnosticProcessRequestV1, + ) -> origin.DiagnosticProcessObservationV1: + self.requests.append(request) + if not self.observations: + raise RuntimeError("unexpected diagnostic invocation") + return self.observations.pop(0) # type: ignore[return-value] + + +class DiagnosticProcessBoundaryTests(unittest.TestCase): + def test_core_has_no_builtin_process_or_process_group_runner(self) -> None: + self.assertFalse(hasattr(origin, "_run_bounded")) + self.assertFalse(hasattr(origin, "subprocess")) + self.assertFalse(hasattr(origin, "selectors")) + + def test_client_owned_diagnostic_bytes_remain_bounded_and_untrusted(self) -> None: + request = origin.DiagnosticProcessRequestV1( + ("verifier", "--version"), + None, + Path("/"), + {"LANG": "C"}, + (), + 1, + 4, + 4, + ) + cases = ( + ( + _DiagnosticRunner( + origin.DiagnosticProcessObservationV1(0, b"12345", b"") + ), + origin.OriginReasonV1.VERIFIER_OUTPUT_LIMIT, + ), + ( + _DiagnosticRunner(SimpleNamespace(returncode=0, stdout=b"", stderr=b"")), + origin.OriginReasonV1.VERIFIER_UNAVAILABLE, + ), + ) + for runner, reason in cases: + with self.subTest(reason=reason): + with self.assertRaises(origin.OriginErrorV1) as caught: + origin._observe_diagnostic_process_v1(runner, request) + self.assertEqual(caught.exception.reason, reason) + + def test_diagnostic_runner_receives_every_resource_bound_explicitly(self) -> None: + observed = origin.DiagnosticProcessObservationV1(0, b"ok", b"") + runner = _DiagnosticRunner(observed) + request = origin.DiagnosticProcessRequestV1( + ("verifier", "arg"), + b"input", + Path("/tmp"), + {"LANG": "C", "TZ": "UTC"}, + (7,), + 3, + 8, + 9, + ) + + actual = origin._observe_diagnostic_process_v1(runner, request) + + self.assertIs(actual, observed) + self.assertEqual(runner.requests, [request]) +class GpgStatusTests(unittest.TestCase): + FINGERPRINT = bytes.fromhex("343c2ff0fbee5ec2edbef399f3599ff828c67298") + + def test_historical_signature_status_is_accepted_despite_later_key_expiry(self) -> None: + status = b"""[GNUPG:] NEWSIG +[GNUPG:] KEYEXPIRED 1736961163 +[GNUPG:] KEY_CONSIDERED 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 0 +[GNUPG:] EXPKEYSIG F3599FF828C67298 Niels Moller +[GNUPG:] VALIDSIG 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 2023-07-30 1690719513 0 4 0 1 10 00 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 +""" + observed = origin.parse_gpgv_status(status, self.FINGERPRINT) + + self.assertIs(type(observed), origin.AcceptedHistoricalSignatureStatusV1) + self.assertEqual(observed.signer_fingerprint, self.FINGERPRINT) + self.assertEqual(observed.signature_unix_time, 1_690_719_513) + + def test_failure_wrong_signer_or_multiple_signatures_are_rejected(self) -> None: + valid = b"""[GNUPG:] NEWSIG +[GNUPG:] VALIDSIG 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 2023-07-30 1690719513 0 4 0 1 10 00 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 +""" + cases = ( + valid.replace(self.FINGERPRINT.hex().upper().encode(), b"A" * 40), + valid.replace(b"2023-07-30", b"2023-07-31", 1), + valid + valid, + valid.replace(b"VALIDSIG", b"BADSIG ", 1), + valid + b"[GNUPG:] FAILURE verify 17\n", + valid + b"unframed stdout\n", + ) + for status in cases: + with self.subTest(status=hashlib.sha256(status).hexdigest()): + with self.assertRaises(origin.OriginErrorV1): + origin.parse_gpgv_status(status, self.FINGERPRINT) + + def test_unbounded_timestamp_and_zero_fingerprint_fail_as_typed_input(self) -> None: + valid = b"""[GNUPG:] NEWSIG +[GNUPG:] VALIDSIG 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 2023-07-30 1690719513 0 4 0 1 10 00 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 +""" + cases = ( + (valid.replace(b"1690719513", b"9" * 400), self.FINGERPRINT), + (valid.replace(self.FINGERPRINT.hex().upper().encode(), b"0" * 40), bytes(20)), + ) + for status, fingerprint in cases: + with self.subTest(status=hashlib.sha256(status).hexdigest()): + with self.assertRaises(origin.OriginErrorV1): + origin.parse_gpgv_status(status, fingerprint) + + def test_signature_observation_is_explicitly_historical_and_diagnostic(self) -> None: + status = b"""[GNUPG:] NEWSIG +[GNUPG:] VALIDSIG 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 2023-07-30 1690719513 0 4 0 1 10 00 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 +""" + signature = b"signature" + expected, admitted = signed_source_fixture() + process = origin.GpgvProcessObservationV1( + 0, + status, + b"", + admitted.tree_identity, + admitted.archive_sha256, + hashlib.sha256(signature).digest(), + expected.integrity.public_key_packets_sha256, + bytes.fromhex("11" * 32), + bytes.fromhex("22" * 32), + _token=origin._GPGV_PROCESS_TOKEN, + ) + observed = origin.admit_detached_signature_observation( + expected=expected, + admitted=admitted, + signature=signature, + public_key_armour=(ARB / "keys/gmp.asc").read_bytes(), + process=process, + ) + + self.assertIs( + type(observed), + origin.HistoricalPathRecheckedSignatureDiagnosticV1, + ) + for attribute in ( + "authenticated_source", + "current_publisher", + "currently_trusted", + "publisher", + "verified_publisher", + ): + with self.subTest(attribute=attribute): + self.assertFalse(hasattr(observed, attribute)) + + def test_process_observation_cannot_report_other_source_bytes(self) -> None: + status = b"""[GNUPG:] NEWSIG +[GNUPG:] VALIDSIG 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 2023-07-30 1690719513 0 4 0 1 10 00 343C2FF0FBEE5EC2EDBEF399F3599FF828C67298 +""" + expected, admitted = signed_source_fixture() + process = origin.GpgvProcessObservationV1( + 0, + status, + b"", + bytes.fromhex("ff" * 32), + admitted.archive_sha256, + hashlib.sha256(b"signature").digest(), + expected.integrity.public_key_packets_sha256, + bytes.fromhex("11" * 32), + bytes.fromhex("22" * 32), + _token=origin._GPGV_PROCESS_TOKEN, + ) + with self.assertRaises(origin.OriginErrorV1) as caught: + origin.admit_detached_signature_observation( + expected=expected, + admitted=admitted, + signature=b"signature", + public_key_armour=(ARB / "keys/gmp.asc").read_bytes(), + process=process, + ) + self.assertEqual(caught.exception.reason, origin.OriginReasonV1.COORDINATE_MISMATCH) + + def test_crashed_gpgv_is_a_typed_process_failure(self) -> None: + _expected, admitted = signed_source_fixture() + with tempfile.TemporaryDirectory() as temporary: + executable = Path(temporary) / "gpgv" + executable.write_bytes(b"diagnostic executable bytes") + runner = _DiagnosticRunner( + origin.DiagnosticProcessObservationV1(0, b"gpgv fixture\n", b""), + origin.DiagnosticProcessObservationV1(-signal.SIGSEGV, b"", b""), + ) + + with self.assertRaises(origin.OriginErrorV1) as caught: + origin.run_gpgv( + admitted, + b"signature", + (ARB / "keys/gmp.asc").read_bytes(), + executable=executable, + runner=runner, + ) + self.assertEqual(caught.exception.reason, origin.OriginReasonV1.VERIFIER_FAILED) + + def test_process_and_signature_diagnostic_have_no_public_constructor(self) -> None: + with self.assertRaises(TypeError): + origin.GpgvProcessObservationV1( + 0, + b"[GNUPG:] NEWSIG\n", + b"", + bytes.fromhex("88" * 32), + bytes.fromhex("99" * 32), + bytes.fromhex("aa" * 32), + bytes.fromhex("bb" * 32), + bytes.fromhex("11" * 32), + bytes.fromhex("22" * 32), + _token=object(), + ) + with self.assertRaises(TypeError): + origin.admit_detached_signature_observation( + expected=signed_source_fixture()[0], + admitted=signed_source_fixture()[1], + signature=b"fake", + public_key_armour=(ARB / "keys/gmp.asc").read_bytes(), + process=SimpleNamespace(returncode=0, status=b"self report"), + ) + with self.assertRaises(TypeError): + origin.HistoricalPathRecheckedSignatureDiagnosticV1( + bytes.fromhex("11" * 32), + bytes.fromhex("22" * 32), + bytes.fromhex("33" * 32), + bytes.fromhex("77" * 32), + self.FINGERPRINT, + 1, + bytes.fromhex("44" * 32), + bytes.fromhex("55" * 32), + _token=object(), + ) + + def test_old_exact_or_current_authority_symbols_do_not_exist(self) -> None: + for name in ( + "ExactGpgvSignatureObservationV1", + "PathRecheckedSignatureObservationV1", + "ValidSignatureObservationV1", + ): + with self.subTest(name=name): + self.assertFalse(hasattr(origin, name)) + self.assertNotIn( + "same_object_exec", + origin.GpgvProcessObservationV1.__dataclass_fields__, + ) + + +class GitRelationTests(unittest.TestCase): + def test_git_batch_recomputes_blob_object_identity(self) -> None: + body = b"value" + object_id = hashlib.sha1(b"blob 5\0" + body).hexdigest().encode("ascii") + listing = ((object_id, "value", 0o644),) + valid = object_id + b" blob 5\n" + body + b"\n" + + self.assertEqual(origin._parse_git_batch(valid, listing)[0].sha256, hashlib.sha256(body).digest()) + with self.assertRaises(origin.OriginErrorV1): + origin._parse_git_batch(b"2" * 40 + valid[40:], ((b"2" * 40, "value", 0o644),)) + + def test_recursive_tree_identity_has_an_independent_git_golden(self) -> None: + listing = ( + (b"8c7e5a667f1b771847fe88c01c3de34413a1b220", "a.c", 0o644), + (b"7371f47a6f8bd23a8fa1a8b2a9479cdd76380e54", "dir/b", 0o644), + ) + self.assertEqual( + origin._recompute_git_tree_identity(listing).hex(), + "3930f0d390a7a4f2b29fde1dbc8abdc98a282fe0", + ) + + def test_deep_valid_git_tree_is_iterative_not_a_python_stack_overflow(self) -> None: + body = b"z" + object_id = hashlib.sha1(b"blob 1\0" + body).hexdigest().encode("ascii") + path = "/".join(("a",) * 1_500 + ("z",)) + listing = origin._parse_git_listing( + b"100644 blob " + object_id + b"\t" + path.encode("ascii") + b"\0" + ) + + tree = origin._recompute_git_tree_identity(listing) + + self.assertEqual(len(tree), 20) + self.assertNotEqual(tree, bytes(20)) + + def test_malformed_git_output_never_escapes_the_typed_boundary(self) -> None: + for raw in ( + b"100644 blob " + b"0" * 40 + b"\tvalue\0", + b"100644 blob " + b"1" * 40 + b"\t../escape\0", + b"100644 blob " + b"1" * 40 + b"\ta\nb\0", + ): + with self.subTest(raw=raw): + with self.assertRaises(origin.OriginErrorV1): + origin._parse_git_listing(raw) + + def test_git_batch_length_is_canonical_decimal(self) -> None: + body = b"value" + object_id = hashlib.sha1(b"blob 5\0" + body).hexdigest().encode("ascii") + listing = ((object_id, "value", 0o644),) + for length in (b"+5", b"05", b" 5"): + with self.subTest(length=length): + raw = object_id + b" blob " + length + b"\n" + body + b"\n" + with self.assertRaises(origin.OriginErrorV1): + origin._parse_git_batch(raw, listing) + + def test_commit_identity_and_commit_to_tree_edge_are_recomputed(self) -> None: + tree = bytes.fromhex("22" * 20) + body = b"tree " + tree.hex().encode("ascii") + b"\nauthor A 0 +0000\ncommitter A 0 +0000\n\nrelease\n" + commit = hashlib.sha1(b"commit " + str(len(body)).encode("ascii") + b"\0" + body).digest() + + self.assertEqual(origin._admit_git_commit_object(body, commit, tree), hashlib.sha256(body).digest()) + for changed_body, changed_commit, changed_tree in ( + (body + b"x", commit, tree), + (body, bytes.fromhex("ff" * 20), tree), + (body, commit, bytes.fromhex("ff" * 20)), + ): + with self.assertRaises(origin.OriginErrorV1): + origin._admit_git_commit_object(changed_body, changed_commit, changed_tree) + + def test_archive_is_common_tree_plus_project_pinned_release_only_files(self) -> None: + lock, admitted, process = git_content_relation_fixture() + + evidence = origin.admit_git_content_relation_observation( + expected=lock, + admitted=admitted, + process=process, + ) + + self.assertEqual(evidence.common_file_count, 1) + self.assertEqual(evidence.omitted_file_count, 1) + self.assertEqual(evidence.project_pinned_release_only_file_count, 1) + self.assertEqual(evidence.archive_sha256, lock.archive_sha256) + self.assertIs(type(evidence), origin.RecomputedGitContentRelationV1) + + def test_any_relation_edge_mismatch_is_rejected(self) -> None: + lock, admitted, process = git_content_relation_fixture() + base = dict(expected=lock, admitted=admitted, process=process) + mutations = ( + {"expected": replace(lock, version="2")}, + { + "expected": replace( + lock, + integrity=replace( + lock.integrity, + commit=bytes.fromhex("ff" * 20), + ), + ) + }, + {"process": SimpleNamespace(**process.__dict__)}, + ) + for mutation in mutations: + with self.subTest(mutation=mutation): + with self.assertRaises((origin.OriginErrorV1, TypeError)): + origin.admit_git_content_relation_observation(**(base | mutation)) + + def test_git_executable_metadata_has_no_relation_authority(self) -> None: + lock, admitted, process = git_content_relation_fixture() + other_verifier = origin.GitTreeProcessObservationV1( + process.commit, + process.tree, + process.commit_object_sha256, + process.files, + bytes.fromhex("aa" * 32), + bytes.fromhex("bb" * 32), + _token=origin._GIT_PROCESS_TOKEN, + ) + + first = origin.admit_git_content_relation_observation( + expected=lock, admitted=admitted, process=process + ) + second = origin.admit_git_content_relation_observation( + expected=lock, admitted=admitted, process=other_verifier + ) + + self.assertEqual(first, second) + self.assertFalse(hasattr(first, "verifier_executable_sha256")) + self.assertFalse(hasattr(first, "verifier_version_sha256")) + + def test_control_bytes_are_not_source_coordinates(self) -> None: + for path in ("a\0b", "a\nb", "a\x7fb"): + with self.subTest(path=repr(path)): + with self.assertRaises(TypeError): + origin.FileCoordinateV1( + path, + 0o644, + 1, + hashlib.sha256(b"a").digest(), + ) + + def test_process_and_verified_types_have_no_public_constructor(self) -> None: + with self.assertRaises(TypeError): + origin.GitTreeProcessObservationV1( + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + bytes.fromhex("55" * 32), + (origin.FileCoordinateV1("a", 0o644, 1, hashlib.sha256(b"a").digest()),), + bytes.fromhex("33" * 32), + bytes.fromhex("44" * 32), + _token=object(), + ) + with self.assertRaises(TypeError): + origin.RecomputedGitContentRelationV1( + bytes.fromhex("11" * 32), + bytes.fromhex("22" * 32), + bytes.fromhex("33" * 20), + bytes.fromhex("44" * 20), + bytes.fromhex("55" * 32), + bytes.fromhex("66" * 32), + bytes.fromhex("77" * 32), + 1, + 1, + 1, + _token=object(), + ) + + def test_old_git_authority_symbols_do_not_exist(self) -> None: + for name in ( + "ExactGitRelationObservationV1", + "PathRecheckedGitRelationObservationV1", + "admit_git_release_observation", + ): + with self.subTest(name=name): + self.assertFalse(hasattr(origin, name)) + self.assertNotIn( + "same_object_exec", + origin.GitTreeProcessObservationV1.__dataclass_fields__, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_pipeline.py b/proof/region/v1/arb/tests/test_pipeline.py new file mode 100644 index 00000000..443f397a --- /dev/null +++ b/proof/region/v1/arb/tests/test_pipeline.py @@ -0,0 +1,2029 @@ +#!/usr/bin/env python3 +"""Causal, hostile tests for the controlled Arb BUILD/RUN pipeline.""" + +from __future__ import annotations + +import gzip +import hashlib +import io +import inspect +import json +import os +import stat +import struct +import subprocess +import sys +import tarfile +import tempfile +import unittest +from dataclasses import fields as dataclass_fields, replace +from functools import cache +from pathlib import Path +from types import MethodType +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +REPO = PROOF.parents[2] +sys.path.insert(0, str(PROOF)) + +from build import input as build_input # noqa: E402 +from build import transport as build_transport # noqa: E402 +from arb import pipeline # noqa: E402 +from arb import runtime as arb_runtime # noqa: E402 +import executor # noqa: E402 +import provenance # noqa: E402 +from region_proof_protocol import ( # noqa: E402 + ComparatorKindV1, + ComparatorManifestV2, + ContentResolvedComparatorManifestV2, + ContextualRegionDefinitionV1, + ProofJobV1, + ProtocolErrorV1, +) + + +def _digest(label: str) -> bytes: + return hashlib.sha256(label.encode("ascii")).digest() + + +def _static_elf(payload: bytes = b"fixture") -> bytes: + """Return a parseable static ELF64/x86-64 object for executor admission.""" + + code_offset = 64 + 56 + body = payload or b"x" + file_size = code_offset + len(body) + ident = b"\x7fELF\x02\x01\x01" + bytes(9) + header = ident + struct.pack( + " tuple[bytes, int]: + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + directories = {root} + for relative, _body, _mode in files: + parent = Path(relative).parent + while str(parent) not in ("", "."): + directories.add(f"{root}/{parent.as_posix()}") + parent = parent.parent + for name in sorted(directories, key=lambda item: (item.count("/"), item)): + member = tarfile.TarInfo(f"{name}/") + member.type = tarfile.DIRTYPE + member.mode = 0o755 + member.mtime = 0 + archive.addfile(member) + for relative, body, mode in files: + member = tarfile.TarInfo(f"{root}/{relative}") + member.mode = mode + member.size = len(body) + member.mtime = 0 + archive.addfile(member, io.BytesIO(body)) + encoded = gzip.compress(raw.getvalue(), compresslevel=9, mtime=0) + return encoded, len(raw.getvalue()) + + +@cache +def _source_fixture() -> tuple[ + provenance.ArbSourceLockV1, + provenance.AdmittedArbSourcesV1, +]: + locks: list[provenance.SourceReleaseLockV1] = [] + safe: list[provenance.SafeSourceArchiveV1] = [] + coordinates = ( + (provenance.SourceRoleV1.GMP, "gmp-6.3.0", False), + (provenance.SourceRoleV1.MPFR, "mpfr-4.2.2", False), + (provenance.SourceRoleV1.FLINT_ARB, "flint-3.6.0", True), + ) + for index, (role, root, git) in enumerate(coordinates, start=1): + files = (("LICENSE", f"license-{index}".encode(), 0o644),) + if git: + files += (("configure", b"generated", 0o755),) + archive, raw_length = _tar(root, files) + if git: + integrity: provenance.SourceIntegrityPolicyV1 = provenance.GitContentRelationPolicyV1( + "https://example.invalid/flint.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("ci/omitted",), + ( + provenance.ProjectPinnedReleaseOnlyFileV1( + "configure", + 0o755, + len(b"generated"), + hashlib.sha256(b"generated").digest(), + ), + ), + ) + else: + integrity = provenance.DetachedSignaturePolicyV1( + f"https://example.invalid/{root}.tar.gz.sig", + 3, + _digest(f"signature-{index}"), + _digest(f"public-key-{index}"), + bytes((index,)) * 20, + ) + lock = provenance.SourceReleaseLockV1( + role, + "1", + f"https://example.invalid/{root}.tar.gz", + provenance.ArchiveFormatV1.TAR_GZIP, + len(archive), + hashlib.sha256(archive).digest(), + raw_length, + f"{root}/", + len(files), + sum(len(body) for _name, body, _mode in files), + ( + provenance.LegalFileV1( + "LICENSE", + len(files[0][1]), + hashlib.sha256(files[0][1]).digest(), + ), + ), + integrity, + ) + locks.append(lock) + safe.append(provenance.admit_source_archive(lock, archive)) + source_lock = provenance.ArbSourceLockV1(tuple(locks)) + admitted = provenance.admit_arb_sources(source_lock, tuple(safe)) + return source_lock, admitted + + +@cache +def _generated_formula() -> bytes: + result = subprocess.run( + ( + sys.executable, + str(ARB / "evaluator/formula.py"), + str(REPO / "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir"), + ), + check=False, + capture_output=True, + env={"PYTHONDONTWRITEBYTECODE": "1", "PYTHONHASHSEED": "0"}, + timeout=30, + ) + if result.returncode != 0: + raise AssertionError(result.stderr.decode("utf-8", "replace")) + return result.stdout + + +@cache +def _build_sources() -> pipeline.AdmittedBuildSourcesV1: + files = [] + for logical_path, mode in pipeline.REQUIRED_BUILD_SOURCE_MODES_V1: + if logical_path == pipeline.GENERATED_FORMULA_PATH_V1: + body = _generated_formula() + else: + body = (REPO / logical_path).read_bytes() + files.append(pipeline.BuildSourceFileV1(logical_path, mode, body)) + return pipeline.admit_build_sources_v1(tuple(files)) + + +@cache +def _job() -> ProofJobV1: + return ProofJobV1.parse((PROOF / "fixtures/proof-job-v1.bin").read_bytes()) + + +@cache +def _foreign_comparator() -> ContentResolvedComparatorManifestV2: + content = tuple(f"manifest-coordinate-{index}".encode() for index in range(10)) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(item).digest() for item in content), + ) + by_digest = {hashlib.sha256(item).digest(): item for item in content} + return ContentResolvedComparatorManifestV2.admit(manifest, by_digest.get) + + +def _limits(**changes: int) -> executor.ExecutionLimitsV1: + values = { + "max_executable_bytes": 16 * 1024 * 1024, + "max_stdin_bytes": 16 * 1024 * 1024, + "max_argument_bytes": 4096, + "max_stdout_bytes": 16 * 1024 * 1024, + "max_stderr_bytes": 64 * 1024, + "wall_timeout_ns": 60_000_000_000, + "memory_max_bytes": 1024 * 1024 * 1024, + "pids_max": 1, + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +def _runtime_binding(**limit_changes: int) -> arb_runtime.ArbRuntimeBindingV1: + return arb_runtime.ArbRuntimeBindingV1( + arb_runtime.arb_runtime_profile_v1(), + _limits(**limit_changes), + ) + + +def _request(**changes: object) -> pipeline.PipelineRequestV1: + source_lock, admitted = _source_fixture() + values: dict[str, object] = { + "source_lock": source_lock, + "admitted_sources": admitted, + "build_sources": _build_sources(), + "job": _job(), + "runtime_binding": _runtime_binding(), + "host_trust": pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST, + } + values.update(changes) + return pipeline.PipelineRequestV1(**values) + + +def _docker_capability( + policy: build_transport.DockerBuildPolicyV1 | None = None, + *, + docker_path: Path = Path("/usr/bin/docker"), + host_user: tuple[int, int] = (501, 20), + daemon_marker: bytes = b"docker-daemon-fixture", +) -> build_transport.DockerSupportedV1: + owned_policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 if policy is None else policy + return build_transport.DockerSupportedV1( + owned_policy, + build_transport.DockerDaemonObservationV1( + daemon_marker, + b"docker-image-inspection-fixture", + ), + build_transport.native_command_coordinate_v1(docker_path), + host_user, + ) + + +def _probe_native_backend( + backend: build_transport.NativeDockerBuildBackendV1, + policy: build_transport.DockerBuildPolicyV1, +) -> build_transport.DockerSupportedV1: + image_observation = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + separators=(",", ":"), + ).encode("ascii") + with mock.patch.object( + backend, + "_observe_command", + side_effect=( + build_transport._docker_command_exited_v1( + 0, + b'{"Version":"fixture"}', + b"", + ), + build_transport._docker_command_exited_v1( + 0, + image_observation, + b"", + ), + ), + ): + capability = backend.probe() + if type(capability) is not build_transport.DockerSupportedV1: + raise AssertionError(capability) + return capability + + +class _BuildBackend: + def __init__( + self, + outputs: tuple[bytes, ...], + *, + probe: build_transport.DockerCapabilityReportV1 | None = None, + reject_input: bool = False, + omit_transfer: bool = False, + foreign_transfer: bool = False, + reported_stderr: bytes = b"", + ) -> None: + self.outputs = list(outputs) + self.probe_result = probe or _docker_capability() + self.reject_input = reject_input + self.omit_transfer = omit_transfer + self.foreign_transfer = foreign_transfer + self.reported_stderr = reported_stderr + self.requests: list[build_transport.DockerBuildRequestV1] = [] + + def probe(self) -> build_transport.DockerCapabilityReportV1: + return self.probe_result + + def run_build( + self, + request: build_transport.DockerBuildRequestV1, + ) -> build_transport.DockerBuildProcessObservationV1: + self.requests.append(request) + output = self.outputs.pop(0) + if self.reject_input: + return build_transport.DockerBuildInputRejectedV1( + build_transport._build_input_progress_v1( + request.input_bundle, + 1, + hashlib.sha256(request.input_bundle.contents[:1]).digest(), + ), + b"", + b"", + ) + if self.omit_transfer: + return build_transport._docker_command_exited_v1(0, output, self.reported_stderr) + transfer = build_transport._completed_build_input_transfer_v1( + request.input_bundle, + request.input_bundle.length, + request.input_bundle.sha256, + ) + if self.foreign_transfer: + foreign_input = build_input.seal_input_v1( + _digest("foreign-bundle"), + request.input_bundle.contents, + ) + transfer = build_transport._completed_build_input_transfer_v1( + foreign_input, + foreign_input.length, + foreign_input.sha256, + ) + return build_transport._docker_build_exited_v1( + 0, + output, + self.reported_stderr, + transfer, + ) + + +class BuildSourceAdmissionTests(unittest.TestCase): + def test_exact_formula_generator_recipe_and_evaluator_bytes_are_admitted(self) -> None: + admitted = _build_sources() + + self.assertEqual(admitted.formula_spec, _job().formula_spec) + self.assertEqual( + hashlib.sha256(admitted.generated_formula).hexdigest(), + pipeline.GENERATED_FORMULA_SHA256_V1, + ) + self.assertEqual( + tuple(item.path for item in admitted.files), + tuple(path for path, _mode in pipeline.REQUIRED_BUILD_SOURCE_MODES_V1), + ) + self.assertNotEqual(admitted.build_input_identity, admitted.formula_support_identity) + self.assertFalse(hasattr(admitted, "source_path")) + + def test_missing_extra_reordered_or_mutated_source_bytes_are_rejected(self) -> None: + files = _build_sources().files + mutants = ( + files[:-1], + files + (pipeline.BuildSourceFileV1("extra.c", 0o644, b"x"),), + tuple(reversed(files)), + (replace(files[0], contents=files[0].contents + b"x"),) + files[1:], + ) + for mutant in mutants: + with self.subTest(length=len(mutant)): + with self.assertRaises(pipeline.BuildSourceAdmissionErrorV1): + pipeline.admit_build_sources_v1(mutant) + + def test_capabilities_cannot_be_directly_forged(self) -> None: + with self.assertRaises(TypeError): + pipeline.AdmittedBuildSourcesV1( + _build_sources().files, + _digest("forged"), + _token=object(), + ) + + def test_admission_owns_a_fresh_build_file_snapshot(self) -> None: + source_files = tuple( + pipeline.BuildSourceFileV1(item.path, item.mode, item.contents) + for item in _build_sources().files + ) + admitted = pipeline.admit_build_sources_v1(source_files) + original = source_files[0].contents + object.__setattr__(source_files[0], "contents", b"forged") + try: + self.assertEqual(admitted.contents(source_files[0].path), original) + self.assertEqual( + admitted.identity, + pipeline.admit_build_sources_v1(admitted.files).identity, + ) + finally: + object.__setattr__(source_files[0], "contents", original) + + def test_manifest_replay_rejects_forged_retained_identities(self) -> None: + sources = _build_sources() + sentinel = object() + originals = { + name: sources.__dict__.get(name, sentinel) + for name in ( + "identity", + "build_input_identity", + "formula_support_identity", + ) + } + + for name, original in originals.items(): + with self.subTest(retained_coordinate=name): + object.__setattr__(sources, name, _digest(f"forged-{name}")) + try: + with self.assertRaises(TypeError): + pipeline.build_source_manifest_bytes_v1(sources) + finally: + if original is sentinel: + del sources.__dict__[name] + else: + sources.__dict__[name] = original + + +class FlintSourcePartitionTests(unittest.TestCase): + def test_partition_is_nonempty_and_separately_binds_both_content_sets(self) -> None: + source_lock, admitted = _source_fixture() + flint = source_lock.sources[2] + + partition = pipeline.flint_source_content_partition_v1( + source_lock, + admitted, + ) + + self.assertGreater(partition.commit_content_file_count, 0) + self.assertGreater(partition.project_pinned_release_only_file_count, 0) + self.assertEqual( + partition.commit_content_file_count, + flint.integrity.common_file_count, + ) + self.assertEqual( + partition.project_pinned_release_only_file_count, + len(flint.integrity.project_pinned_release_only_files), + ) + self.assertEqual( + partition.commit_content_file_count + + partition.project_pinned_release_only_file_count, + admitted.sources[2].regular_file_count, + ) + self.assertNotEqual( + partition.commit_content_identity, + partition.project_pinned_release_only_identity, + ) + self.assertFalse(hasattr(partition, "commit_derived_identity")) + + def test_partition_rejects_a_foreign_lock_replay(self) -> None: + source_lock, admitted = _source_fixture() + foreign_flint = replace( + source_lock.sources[2], + version="foreign-release", + ) + foreign_lock = provenance.ArbSourceLockV1( + source_lock.sources[:2] + (foreign_flint,) + ) + + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.flint_source_content_partition_v1(foreign_lock, admitted) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + +class ComparatorDerivationTests(unittest.TestCase): + def _result(self) -> pipeline.DiagnosticBuildObservationV1: + binary = _static_elf(b"derived-comparator") + result = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((binary, binary)), + ).build(_request()) + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + return result + + def test_request_cannot_supply_an_arbitrary_comparator(self) -> None: + with self.assertRaises(TypeError): + _request(comparator=_foreign_comparator()) + + def test_all_ten_coordinates_replay_exact_named_preimages(self) -> None: + result = self._result() + admitted = result.comparator + manifest = admitted.manifest.manifest + + names = tuple(field.name for field in dataclass_fields(admitted.preimages)) + self.assertEqual( + names, + ( + "engine_release", + "upstream_source", + "arithmetic_input_set", + "wrapper_source", + "evaluator_source", + "build_identity", + "operation_allowlist", + "test_observation", + "legal_file_set", + "exclusions", + ), + ) + for name in names: + with self.subTest(name=name): + preimage = getattr(admitted.preimages, name) + self.assertGreater(len(preimage), len(name)) + self.assertNotEqual(preimage, name.encode("ascii")) + self.assertEqual( + getattr(manifest, name), + hashlib.sha256(preimage).digest(), + ) + self.assertEqual(admitted.identity, admitted.manifest.identity) + self.assertEqual( + admitted.structural_source_identity, + result.structural_source_identity, + ) + self.assertEqual(admitted.build_input_identity, result.build_input_identity) + self.assertEqual(admitted.pipeline_policy_identity, result.pipeline_policy_identity) + self.assertEqual(admitted.binary_sha256, result.binary_sha256) + self.assertEqual(admitted.rebuild_sha256s, result.rebuild_sha256s) + self.assertIn( + b"gap:host-and-docker-daemon-not-source-bound", + admitted.preimages.exclusions, + ) + self.assertNotIn(b"persistent", admitted.preimages.exclusions) + self.assertNotIn(b"github-hosted", admitted.preimages.exclusions) + + def test_mutated_or_reordered_preimages_cannot_replay_the_manifest(self) -> None: + admitted = self._result().comparator + manifest = admitted.manifest.manifest + original = { + getattr(manifest, field.name): getattr(admitted.preimages, field.name) + for field in dataclass_fields(admitted.preimages) + } + variants = [] + mutated = dict(original) + mutated[manifest.evaluator_source] += b"x" + variants.append(mutated) + reordered = dict(original) + reordered[manifest.wrapper_source], reordered[manifest.evaluator_source] = ( + reordered[manifest.evaluator_source], + reordered[manifest.wrapper_source], + ) + variants.append(reordered) + + for resolver in variants: + with self.subTest(variant=variants.index(resolver)): + with self.assertRaises(ProtocolErrorV1): + ContentResolvedComparatorManifestV2.admit( + manifest, + resolver.get, + ) + + def test_operator_coordinate_is_the_exact_ordered_formula_contract(self) -> None: + original = _build_sources().formula_spec + lines = original.splitlines() + self.assertIn( + b"operators 20", + lines, + "registered formula must retain the exact 20-operator contract", + ) + count_index = lines.index(b"operators 20") + lines[count_index + 1], lines[count_index + 2] = ( + lines[count_index + 2], + lines[count_index + 1], + ) + reordered = b"\n".join(lines) + b"\n" + + original_preimage = pipeline._operation_allowlist_preimage_v1(original) + reordered_preimage = pipeline._operation_allowlist_preimage_v1(reordered) + + self.assertNotEqual(original_preimage, reordered_preimage) + self.assertNotEqual( + hashlib.sha256(original_preimage).digest(), + hashlib.sha256(reordered_preimage).digest(), + ) + + def test_wrapper_and_evaluator_file_sets_are_exact_and_disjoint(self) -> None: + result = self._result() + files = _build_sources().files + wrapper_paths = frozenset( + ( + "proof/region/v1/arb/evaluator/formula.h", + "proof/region/v1/arb/evaluator/interval.c", + "proof/region/v1/arb/evaluator/interval.h", + ) + ) + excluded = wrapper_paths | { + pipeline.FORMULA_SPEC_PATH_V1, + pipeline.FORMULA_GENERATOR_PATH_V1, + pipeline.BUILD_RECIPE_PATH_V1, + pipeline.INNER_BUILD_RECIPE_PATH_V1, + } + wrapper_files = tuple(item for item in files if item.path in wrapper_paths) + evaluator_files = tuple(item for item in files if item.path not in excluded) + + self.assertFalse({item.path for item in wrapper_files} & {item.path for item in evaluator_files}) + self.assertEqual( + result.comparator.preimages.wrapper_source, + pipeline._encoded_build_file_set_v1( + b"labcolors.proof-region.arb-comparator.wrapper-source.v1\0", + wrapper_files, + ), + ) + self.assertEqual( + result.comparator.preimages.evaluator_source, + pipeline._encoded_build_file_set_v1( + b"labcolors.proof-region.arb-comparator.evaluator-source.v1\0", + evaluator_files, + ), + ) + self.assertNotIn( + _build_sources().contents(pipeline.INNER_BUILD_RECIPE_PATH_V1), + result.comparator.preimages.evaluator_source, + ) + + def test_build_stdout_cannot_supply_a_foreign_manifest_or_coordinate(self) -> None: + foreign = _foreign_comparator() + report = b"manifest=" + foreign.identity.hex().encode("ascii") + binary = _static_elf(b"ignore-build-self-report") + + result = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend( + (binary, binary), + reported_stderr=report, + ), + ).build(_request()) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertNotEqual(result.comparator.identity, foreign.identity) + coordinates = tuple( + getattr(result.comparator.manifest.manifest, field.name) + for field in dataclass_fields(result.comparator.manifest.manifest) + if field.name != "kind" + ) + self.assertNotIn(foreign.identity, coordinates) + self.assertEqual(result.build_processes[0].stdout, binary) + self.assertEqual(result.build_processes[0].stderr, report) + + def test_diagnostic_comparator_has_no_public_constructor(self) -> None: + with self.assertRaises(TypeError): + pipeline.DiagnosticArbComparatorV1() + + +class ControlledPipelineTests(unittest.TestCase): + def test_admission_uses_only_explicit_cross_module_verification_api(self) -> None: + source = (ARB / "pipeline.py").read_text(encoding="utf-8") + + for forbidden in ( + "executor._result_matches_request", + "executor._require_static_x86_64_elf", + "protocol._validate_witness_alignment", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, source) + self.assertTrue(callable(executor.invocation_identity_v1)) + self.assertTrue(callable(executor.platform_identity_v1)) + self.assertFalse(hasattr(pipeline, "invocation_identity_v1")) + self.assertFalse(hasattr(pipeline, "platform_identity_v1")) + self.assertFalse(hasattr(pipeline, "comparator_build_preimage_v2")) + + def test_arb_input_keeps_one_source_snapshot_across_reentrant_mutation( + self, + ) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + real_encoder = pipeline.build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, limits) + object.__setattr__(source, "tree_identity", _digest("reentrant-tree")) + return encoded + + try: + with mock.patch.object( + pipeline.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = pipeline._seal_build_input_bundle_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + + def test_arb_input_rejects_reentrant_unadmitted_build_source(self) -> None: + request = _request() + build_file = next( + item + for item in request.build_sources.files + if item.path == pipeline.BUILD_RECIPE_PATH_V1 + ) + original_contents = build_file.contents + real_encoder = pipeline.build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, limits) + object.__setattr__( + build_file, + "contents", + b"#!/bin/sh\nprintf '%s\\n' forged-build-source\n", + ) + return encoded + + try: + with mock.patch.object( + pipeline.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = pipeline._seal_build_input_bundle_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + finally: + object.__setattr__(build_file, "contents", original_contents) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + sealed, + ) + ) + + def test_pipeline_rederives_both_cached_build_coordinates(self) -> None: + for field_name in ("build_input_identity", "formula_support_identity"): + with self.subTest(cached_coordinate=field_name): + request = _request() + fresh = pipeline.admit_build_sources_v1(request.build_sources.files) + original = request.build_sources.__dict__.get(field_name) + forged = _digest(f"forged-{field_name}") + request.build_sources.__dict__[field_name] = forged + binary = _static_elf(b"cached-coordinate") + backend = _BuildBackend((binary, binary)) + try: + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(request) + finally: + if original is None: + request.build_sources.__dict__.pop(field_name, None) + else: + request.build_sources.__dict__[field_name] = original + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertEqual( + getattr(result, field_name), + getattr(fresh, field_name), + ) + self.assertNotEqual(getattr(result, field_name), forged) + + def test_constructor_rejects_a_nominal_forged_build_capability( + self, + ) -> None: + normal = _request() + forged_files = tuple( + replace( + item, + contents=b"#!/bin/sh\nprintf '%s\\n' forged-build-source\n", + ) + if item.path == pipeline.BUILD_RECIPE_PATH_V1 + else item + for item in normal.build_sources.files + ) + forged = object.__new__(pipeline.AdmittedBuildSourcesV1) + object.__setattr__(forged, "files", forged_files) + object.__setattr__(forged, "identity", _digest("forged-build-closure")) + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + _request(build_sources=forged) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.INVALID_RETAINED_INPUT, + ) + self.assertEqual(caught.exception.field, "build_sources") + + def test_private_build_recheck_keeps_the_entry_snapshot_across_attempts( + self, + ) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + binary = _static_elf(b"snapshot-attempt") + backend = _BuildBackend((binary, binary)) + real_run_build = backend.run_build + mutated = False + + def run_then_mutate( + value: build_transport.DockerBuildRequestV1, + ) -> build_transport.DockerBuildProcessObservationV1: + nonlocal mutated + if not mutated: + object.__setattr__(source, "tree_identity", _digest("late-tree")) + mutated = True + return real_run_build(value) + + backend.run_build = run_then_mutate # type: ignore[method-assign] + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + self.assertFalse( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + result.input_bundle, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue(mutated) + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertTrue( + pipeline.arb_input_is_bound_v1( + request, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + result.input_bundle, + ) + ) + + def test_snapshot_captures_job_before_source_replay(self) -> None: + request = _request() + original_domain = request.job.domain + foreign_job = replace( + request.job, + domain=type(original_domain).from_ordinals((0,)), + ) + real_replay = provenance.replay_admitted_source_closure_v1 + + def replay_then_mutate( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + ) -> provenance.ReplayedSourceClosureV1: + snapshot = real_replay(source_lock, admitted_sources) + object.__setattr__(request.job, "domain", foreign_job.domain) + return snapshot + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_mutate, + ): + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + object.__setattr__(request.job, "domain", original_domain) + + self.assertEqual( + snapshot.request.job.domain.point_count, + original_domain.point_count, + ) + + def test_snapshot_ignores_a_proof_job_encoder_shadow(self) -> None: + request = _request() + job = request.job + original_identity = job.identity + foreign_budget = replace( + job.policy.comparators[0], + global_pregrant=job.policy.comparators[0].global_pregrant + 1, + ) + foreign_policy = replace( + job.policy, + comparators=(foreign_budget, job.policy.comparators[1]), + ) + foreign_job = replace(job, policy=foreign_policy) + job.__dict__["encode"] = lambda: ProofJobV1.encode(foreign_job) + try: + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + del job.__dict__["encode"] + + self.assertEqual(snapshot.request.job.identity, original_identity) + + def test_snapshot_uses_raw_nested_job_coordinates_not_caches_or_encoders( + self, + ) -> None: + request = _request() + job = request.job + original_identity = job.identity + sentinel = object() + shadows = ( + (job.definition, "encode"), + (job.definition, "definition_digest"), + (job.domain, "encode"), + (job.domain, "identity"), + (job.policy, "encode"), + (job.policy, "identity"), + ) + originals = [ + (value, name, value.__dict__.get(name, sentinel)) + for value, name in shadows + ] + + def explode() -> bytes: + raise AssertionError("snapshot dispatched caller-owned job state") + + for value, name in shadows: + value.__dict__[name] = explode if name == "encode" else _digest(name) + try: + snapshot = pipeline._snapshot_pipeline_operation_v1(request) + finally: + for value, name, original in originals: + if original is sentinel: + del value.__dict__[name] + else: + value.__dict__[name] = original + + self.assertEqual(snapshot.request.job.identity, original_identity) + + def test_private_operation_snapshot_cannot_be_constructed_by_a_caller(self) -> None: + snapshot = pipeline._snapshot_pipeline_operation_v1(_request()) + + with self.assertRaises(TypeError): + pipeline._PipelineOperationSnapshotV1( + snapshot.request, + snapshot.source_closure, + _token=object(), + ) + + def test_constructor_rejects_a_foreign_admitted_source_closure(self) -> None: + request = _request() + foreign_flint = replace( + request.source_lock.sources[2], + version="foreign-release", + ) + foreign_lock = provenance.ArbSourceLockV1( + request.source_lock.sources[:2] + (foreign_flint,) + ) + + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + foreign_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.runtime_binding, + request.host_trust, + ) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_constructor_rejects_a_noncanonical_retained_source_manifest( + self, + ) -> None: + """A nominal capability cannot retain a mutable manifest container.""" + + request = _request() + source = request.admitted_sources.sources[2] + original_files = source.files + object.__setattr__(source, "files", list(original_files)) + try: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + request.source_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.runtime_binding, + request.host_trust, + ) + finally: + object.__setattr__(source, "files", original_files) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_constructor_totalizes_a_hostile_retained_source_path(self) -> None: + request = _request() + source = request.admitted_sources.sources[2] + archive_file = source.files[0] + original_path = archive_file.path + + class HashBomb: + def __hash__(self) -> int: + raise RuntimeError("unexpected hash dispatch") + + object.__setattr__(archive_file, "path", HashBomb()) + try: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + pipeline.PipelineRequestV1( + request.source_lock, + request.admitted_sources, + request.build_sources, + request.job, + request.runtime_binding, + request.host_trust, + ) + finally: + object.__setattr__(archive_file, "path", original_path) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_build_uses_one_source_operation_snapshot(self) -> None: + request = _request() + binary = _static_elf(b"one-source-operation") + backend = _BuildBackend((binary, binary)) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + + def test_request_admission_rechecks_separately_from_its_operation( + self, + ) -> None: + """Request admission and a later operation must not share mutable evidence.""" + + source_lock, admitted_sources = _source_fixture() + build_sources = _build_sources() + job = _job() + runtime_binding = _runtime_binding() + binary = _static_elf(b"request-then-one-operation") + backend = _BuildBackend((binary, binary)) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + request = pipeline.PipelineRequestV1( + source_lock, + admitted_sources, + build_sources, + job, + runtime_binding, + pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST, + ) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 0) + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(replay.call_count, 6) + self.assertEqual(materialize.call_count, 3) + + def test_build_rejects_mutated_request_before_it_can_start_a_build(self) -> None: + for field_name, replacement in ( + ("host_trust", "foreign"), + ("runtime_binding", "foreign"), + ): + with self.subTest(field=field_name): + request = _request() + original = getattr(request, field_name) + backend = _BuildBackend((_static_elf(b"first"), _static_elf(b"second"))) + object.__setattr__(request, field_name, replacement) + try: + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(request) + finally: + object.__setattr__(request, field_name, original) + self.assertEqual(len(backend.requests), 0) + self.assertEqual( + result, + build_transport.BuildRejectedV1( + 1, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ), + ) + + def test_host_trust_claims_only_backend_observable_facts(self) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + + self.assertEqual(tuple(pipeline.HostTrustBoundaryV1), (trust,)) + self.assertEqual(trust.value, "unsealed-linux-x64-docker-host") + self.assertFalse( + hasattr( + pipeline.HostTrustBoundaryV1, + "PERSISTENT_SELF_HOSTED_DOCKER", + ) + ) + + def test_host_trust_wire_is_private_and_does_not_read_mutable_enum_storage( + self, + ) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline = pipeline.pipeline_policy_identity_v2(trust, policy) + self.assertFalse(hasattr(pipeline, "host_trust_wire_v1")) + original_value = trust._value_ + object.__setattr__(trust, "_value_", "forged-host-boundary") + try: + self.assertEqual(pipeline.pipeline_policy_identity_v2(trust, policy), baseline) + finally: + object.__setattr__(trust, "_value_", original_value) + + def test_pipeline_policy_identity_binds_the_stream_bootstrap(self) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + coordinates = list(policy) + coordinates[3] = policy.bootstrap + "\nexit 1" + changed_policy = build_transport.DockerBuildPolicyV1(*coordinates) + original = pipeline.pipeline_policy_identity_v2(trust, policy) + changed = pipeline.pipeline_policy_identity_v2(trust, changed_policy) + + self.assertNotEqual(original, changed) + + def test_pipeline_policy_identity_binds_the_private_tmpfs_policy(self) -> None: + trust = pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + coordinates = list(policy) + coordinates[5] = ( + "/tmp:rw,exec,suid,dev,size=536870912,mode=1777", + policy.tmpfs_specs[1], + ) + changed_policy = build_transport.DockerBuildPolicyV1(*coordinates) + original = pipeline.pipeline_policy_identity_v2(trust, policy) + changed = pipeline.pipeline_policy_identity_v2(trust, changed_policy) + + self.assertNotEqual(original, changed) + + def test_build_only_does_not_probe_or_execute_run_backend(self) -> None: + binary = _static_elf(b"build-only") + controller = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((binary, binary)), + ) + + result = controller.build(_request()) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(result.binary, binary) + self.assertEqual(result.rebuild_sha256s, (result.binary_sha256,) * 2) + self.assertIs(type(result.comparator), pipeline.DiagnosticArbComparatorV1) + + def test_two_fresh_equal_builds_retain_one_input_and_exact_outputs(self) -> None: + binary = _static_elf(b"observed-output") + build = _BuildBackend((binary, binary)) + controller = pipeline.ControlledPipelineV1(build_backend=build) + + result = controller.build(_request()) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertEqual(len(build.requests), 2) + self.assertEqual(tuple(item.attempt for item in build.requests), (1, 2)) + self.assertTrue( + all( + item.capability is result.docker_capability + for item in build.requests + ) + ) + self.assertIs(build.requests[0].input_bundle, build.requests[1].input_bundle) + self.assertEqual(result.binary, binary) + self.assertEqual(result.binary_sha256, hashlib.sha256(binary).digest()) + self.assertEqual( + result.rebuild_sha256s, + (result.binary_sha256, result.binary_sha256), + ) + self.assertIs(result.rebuild_binaries[0], result.binary) + self.assertEqual(result.rebuild_binaries, (binary, binary)) + self.assertEqual( + result.input_transfers[0].bundle_identity, + result.input_bundle_identity, + ) + self.assertEqual( + result.input_transfers, + tuple(item.input_transfer for item in result.build_processes), + ) + self.assertEqual( + result.structural_source_identity, + _request().admitted_sources.identity, + ) + partition = pipeline.flint_source_content_partition_v1( + _request().source_lock, + _request().admitted_sources, + ) + self.assertEqual( + result.flint_commit_content_identity, + partition.commit_content_identity, + ) + self.assertEqual( + result.flint_project_pinned_release_only_identity, + partition.project_pinned_release_only_identity, + ) + self.assertEqual( + result.flint_commit_content_file_count, + partition.commit_content_file_count, + ) + self.assertEqual( + result.flint_project_pinned_release_only_file_count, + partition.project_pinned_release_only_file_count, + ) + self.assertEqual(result.build_input_identity, _build_sources().build_input_identity) + self.assertEqual( + result.formula_support_identity, + _build_sources().formula_support_identity, + ) + self.assertEqual( + result.pipeline_policy_identity, + pipeline.pipeline_policy_identity_v2( + result.host_trust, + result.docker_capability.policy, + ), + ) + self.assertFalse(hasattr(result, "build_observer_kind")) + self.assertFalse(hasattr(result, "build_source_identity")) + self.assertFalse(hasattr(result, "build_policy_identity")) + self.assertFalse(hasattr(result, "commit_derived_source_identity")) + self.assertEqual( + result.host_trust, + pipeline.HostTrustBoundaryV1.UNSEALED_LINUX_X64_DOCKER_HOST, + ) + self.assertEqual( + result.docker_capability.policy.image_reference, + pipeline.OCI_IMAGE_REFERENCE_V1, + ) + self.assertEqual( + result.docker_capability.policy.platform, + pipeline.OCI_PLATFORM_V1, + ) + self.assertFalse(hasattr(result, "oci_image_reference")) + self.assertFalse(hasattr(result, "oci_platform")) + self.assertFalse(hasattr(result, "slsa_level")) + self.assertFalse(hasattr(result, "fresh_vm")) + + def test_builds_must_be_byte_identical(self) -> None: + first = _static_elf(b"first") + second = _static_elf(b"second") + + result = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((first, second)), + ).build(_request()) + + self.assertIs(type(result), build_transport.TwoBuildObservationV1) + self.assertIs( + result.relation, + build_transport.BuildByteRelationV1.DIFFERENT, + ) + self.assertEqual(result.first_sha256, hashlib.sha256(first).digest()) + self.assertEqual(result.second_sha256, hashlib.sha256(second).digest()) + + def test_input_transport_or_invalid_binary_is_typed_failure(self) -> None: + binary = _static_elf() + cases = ( + ( + _BuildBackend((binary,), reject_input=True), + build_transport.BuildFailureReasonV1.INPUT_TRANSFER_FAILED, + ), + ( + _BuildBackend((binary,), omit_transfer=True), + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ), + ( + _BuildBackend((binary,), foreign_transfer=True), + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ), + ( + _BuildBackend((b"not-an-elf",)), + build_transport.BuildFailureReasonV1.INVALID_OUTPUT, + ), + ) + for backend, reason in cases: + with self.subTest(reason=reason): + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(_request()) + self.assertIs(type(result), build_transport.BuildRejectedV1) + self.assertEqual(result.attempt, 1) + self.assertEqual(result.reason, reason) + + def test_forged_source_coordinate_is_rejected_before_build_without_escape(self) -> None: + request = _request() + source = request.admitted_sources.sources[0] + original_tree_identity = source.tree_identity + object.__setattr__(source, "tree_identity", _digest("foreign-tree")) + backend = _BuildBackend((_static_elf(), _static_elf())) + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + + self.assertIs(type(result), build_transport.BuildRejectedV1) + self.assertEqual(result.attempt, 1) + self.assertIs( + result.reason, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + def test_hostile_nominal_source_coordinate_is_typed_before_build(self) -> None: + request = _request() + source = request.source_lock.sources[0] + original_length = source.archive_length + + class ExplodingCoordinate: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("hostile coordinate comparison") + + object.__setattr__(source, "archive_length", ExplodingCoordinate()) + backend = _BuildBackend((_static_elf(), _static_elf())) + try: + result = pipeline.ControlledPipelineV1(build_backend=backend).build(request) + finally: + object.__setattr__(source, "archive_length", original_length) + + self.assertIs(type(result), build_transport.BuildRejectedV1) + self.assertEqual(result.attempt, 1) + self.assertIs( + result.reason, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + def test_request_rejects_raw_execution_limits_instead_of_a_profile_binding(self) -> None: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + _request( + runtime_binding=_limits() + ) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.WRONG_TYPE, + ) + self.assertEqual(caught.exception.field, "runtime_binding") + + def test_runtime_profile_rejects_all_allocation_coordinates_before_build(self) -> None: + job = _job() + arb_budget, mpfi_budget = job.policy.comparators + over_precision = replace( + job, + policy=replace( + job.policy, + comparators=( + replace( + arb_budget, + precision_ladder=( + arb_runtime.ARB_MAX_PRECISION_BITS_V1 + 1, + ), + ), + mpfi_budget, + ), + ), + ) + over_rungs = replace( + job, + policy=replace( + job.policy, + comparators=( + replace( + arb_budget, + precision_ladder=tuple( + range(2, arb_runtime.ARB_MAX_POLICY_RUNGS_V1 + 3) + ), + ), + mpfi_budget, + ), + ), + ) + over_mpfi_precision = replace( + job, + policy=replace( + job.policy, + comparators=( + arb_budget, + replace( + mpfi_budget, + precision_ladder=( + arb_runtime.ARB_MAX_PRECISION_BITS_V1 + 1, + ), + ), + ), + ), + ) + over_mpfi_rungs = replace( + job, + policy=replace( + job.policy, + comparators=( + arb_budget, + replace( + mpfi_budget, + precision_ladder=tuple( + range(2, arb_runtime.ARB_MAX_POLICY_RUNGS_V1 + 3) + ), + ), + ), + ), + ) + knot_count = arb_runtime.ARB_MAX_KNOTS_V1 + 1 + zero = bytes(8) + knots = tuple( + coordinate + for index in range(knot_count) + for coordinate in (struct.pack(">d", float(index)), zero, zero, zero) + ) + over_knots = replace( + job, + definition=ContextualRegionDefinitionV1( + job.definition.fields[:21] + + (knot_count.to_bytes(8, "big"),) + + knots, + knot_count, + ), + ) + + for field, candidate in ( + ("arb-precision", over_precision), + ("arb-rungs", over_rungs), + ("mpfi-precision", over_mpfi_precision), + ("mpfi-rungs", over_mpfi_rungs), + ("knots", over_knots), + ): + with self.subTest(field=field): + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + _request(job=candidate) + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.EXECUTION_LIMIT_MISMATCH, + ) + self.assertEqual(caught.exception.field, "runtime_binding") + + boundary_ladder = tuple(range(1, arb_runtime.ARB_MAX_POLICY_RUNGS_V1)) + ( + arb_runtime.ARB_MAX_PRECISION_BITS_V1, + ) + boundary_knot_count = arb_runtime.ARB_MAX_KNOTS_V1 + boundary_knots = tuple( + coordinate + for index in range(boundary_knot_count) + for coordinate in (struct.pack(">d", float(index)), zero, zero, zero) + ) + boundary_job = replace( + job, + definition=ContextualRegionDefinitionV1( + job.definition.fields[:21] + + (boundary_knot_count.to_bytes(8, "big"),) + + boundary_knots, + boundary_knot_count, + ), + policy=replace( + job.policy, + comparators=( + replace(arb_budget, precision_ladder=boundary_ladder), + replace(mpfi_budget, precision_ladder=boundary_ladder), + ), + ), + ) + admitted = _request(job=boundary_job) + self.assertEqual(admitted.job, boundary_job) + + def test_build_output_limit_is_rejected_at_pipeline_admission(self) -> None: + with self.assertRaises(pipeline.PipelineInputErrorV1) as caught: + _request( + runtime_binding=_runtime_binding( + max_executable_bytes=pipeline.BUILD_STDOUT_LIMIT_V1 + 1, + ) + ) + + self.assertEqual( + caught.exception.reason, + pipeline.PipelineInputReasonV1.EXECUTION_LIMIT_MISMATCH, + ) + self.assertEqual(caught.exception.field, "runtime_binding") + + def test_snapshot_modes_are_normalized_independently_of_host_umask(self) -> None: + binary = _static_elf(b"umask-independent") + identities: list[tuple[bytes, bytes]] = [] + for mask in (0o077, 0o022): + previous = os.umask(mask) + try: + result = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((binary, binary)), + ).build(_request()) + finally: + os.umask(previous) + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + identities.append( + (result.input_bundle_identity, result.input_bundle_sha256) + ) + + self.assertEqual(identities[0], identities[1]) + + def test_pipeline_exports_no_receipt_or_self_report_admission_api(self) -> None: + names = dir(pipeline) + source = (ARB / "pipeline.py").read_text(encoding="utf-8") + self.assertFalse(any("Receipt" in name for name in names)) + self.assertFalse(hasattr(pipeline, "DiagnosticPipelineObservationV1")) + self.assertFalse(hasattr(pipeline, "PipelineResultV1")) + self.assertFalse(hasattr(pipeline, "ExecutionControllerV1")) + self.assertFalse(hasattr(pipeline.ControlledPipelineV1, "execute")) + self.assertEqual( + tuple(inspect.signature(pipeline.ControlledPipelineV1).parameters), + ("build_backend",), + ) + self.assertFalse(hasattr(pipeline.ControlledPipelineV1, "admit_report")) + self.assertFalse(hasattr(pipeline.ControlledPipelineV1, "mint")) + self.assertNotIn("run-observation=diagnostic", source) + + def test_native_observer_promotion_is_not_representable_in_v1(self) -> None: + for name in ( + "BuildObserverKindV1", + "RunObserverKindV1", + "build_observer_kind_v1", + "run_observer_kind_v1", + "NativePipelineObservationV1", + ): + with self.subTest(name=name): + self.assertFalse(hasattr(pipeline, name)) + + def test_mutable_exact_native_build_backend_cannot_upgrade_fabricated_build(self) -> None: + binary = _static_elf(b"self-mutating-build") + backend = build_transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + ) + + def probe(_self: object) -> build_transport.DockerCapabilityReportV1: + return _docker_capability( + docker_path=Path("/bin/true"), + daemon_marker=b"fabricated-daemon", + ) + + def run_build( + _self: object, + request: build_transport.DockerBuildRequestV1, + ) -> build_transport.DockerBuildProcessObservationV1: + transfer = build_transport._completed_build_input_transfer_v1( + request.input_bundle, + request.input_bundle.length, + request.input_bundle.sha256, + ) + return build_transport._docker_build_exited_v1(0, binary, b"", transfer) + + backend.probe = MethodType(probe, backend) + backend.run_build = MethodType(run_build, backend) + + result = pipeline.ControlledPipelineV1( + build_backend=backend, + ).build(_request()) + + self.assertIs(type(result), pipeline.DiagnosticBuildObservationV1) + self.assertFalse(hasattr(result, "build_observer_kind")) + + +class DockerCommandContractTests(unittest.TestCase): + def test_command_is_exact_digest_offline_read_only_and_capability_bound(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + request = build_transport.DockerBuildRequestV1( + 1, + capability, + pipeline._seal_build_input_bundle_v1( + _request(), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ), + _limits().max_executable_bytes, + ) + lease = backend._next_run_lease_v1(capability) + try: + command = backend._command_for_v1(request, lease) + + joined = " ".join(command) + self.assertEqual(command[0], "/usr/bin/true") + self.assertIn(pipeline.OCI_IMAGE_REFERENCE_V1, command) + self.assertNotIn("gcc:latest", joined) + for fragment in ( + "--pull never", + "--platform linux/amd64", + "--network none", + "--read-only", + "--interactive", + "--cap-drop ALL", + "--security-opt no-new-privileges:true", + f"--cidfile {lease.cid_file}", + "--rm", + ): + with self.subTest(fragment=fragment): + self.assertIn(fragment, joined) + for forbidden in ( + "--name", + "--privileged", + "--network host", + ":latest", + ): + self.assertNotIn(forbidden, joined) + finally: + backend._release_run_lease_v1(lease) + + def test_command_exposes_only_a_private_non_executable_standard_tmp(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + request = build_transport.DockerBuildRequestV1( + 1, + capability, + pipeline._seal_build_input_bundle_v1( + _request(), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ), + _limits().max_executable_bytes, + ) + lease = backend._next_run_lease_v1(capability) + self.addCleanup(backend._release_run_lease_v1, lease) + command = backend._command_for_v1(request, lease) + + tmpfs_indexes = tuple( + index for index, item in enumerate(command) if item == "--tmpfs" + ) + self.assertEqual(len(tmpfs_indexes), 2) + self.assertEqual( + tuple(command[index + 1] for index in tmpfs_indexes), + (pipeline._BUILD_TMPFS_SPEC_V1, pipeline._BUILD_STATE_TMPFS_SPEC_V1), + ) + self.assertTrue( + all("src=" not in command[index + 1] for index in tmpfs_indexes) + ) + mount_indexes = tuple( + index for index, item in enumerate(command) if item == "--mount" + ) + self.assertEqual(mount_indexes, ()) + self.assertNotIn("-v", command) + self.assertNotIn("--volume", command) + + def test_native_probe_fails_closed_without_linux_or_exact_docker(self) -> None: + non_linux = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/docker"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="darwin", + machine_name="arm64", + ).probe() + missing = build_transport.NativeDockerBuildBackendV1( + Path("/definitely/missing/docker"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + ).probe() + + self.assertEqual(non_linux.reason, build_transport.DockerBlockerReasonV1.HOST_NOT_LINUX_AMD64) + self.assertEqual(missing.reason, build_transport.DockerBlockerReasonV1.DOCKER_UNAVAILABLE) + + def test_native_command_observer_caps_probe_output_before_allocation(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/bin/sh"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + ) + + result = backend._observe_command( + ( + sys.executable, + "-c", + "import os; os.write(1, b'x' * 65536)", + ), + stdout_limit=8, + stderr_limit=8, + timeout_ns=5_000_000_000, + ) + + self.assertEqual( + result, + build_transport.DockerBuildOutputLimitV1( + build_transport.DockerOutputStreamV1.STDOUT, + b"x" * 8, + b"", + ), + ) + + def test_cleanup_uses_only_a_docker_issued_id_from_its_private_lease(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + lease = backend._next_run_lease_v1(capability) + lease.cid_file.write_text("b" * 64 + "\n", encoding="ascii") + backend._mark_run_lease_launched_v1(lease) + foreign_id = "a" * 64 + observations = ( + build_transport._docker_command_exited_v1(0, b"b" * 64 + b"\n", b""), + build_transport._docker_command_exited_v1(0, b"b" * 64 + b"\n", b""), + build_transport._docker_command_exited_v1(0, b"", b""), + ) + try: + with mock.patch.object( + backend, + "_observe_cleanup_command", + side_effect=observations, + ) as observe: + detail = backend._cleanup_container(lease) + + self.assertIsNone(detail) + commands = tuple(call.args[1] for call in observe.call_args_list) + self.assertEqual(len(commands), 3) + self.assertEqual(commands[0][-1], "b" * 64) + self.assertEqual(commands[1][-1], "b" * 64) + self.assertIn("id=" + "b" * 64, commands[2]) + self.assertNotIn( + foreign_id, + " ".join(" ".join(command) for command in commands), + ) + self.assertNotIn("name=", " ".join(" ".join(command) for command in commands)) + finally: + backend._release_run_lease_v1(lease) + + def test_absent_cidfile_never_falls_back_to_a_name_or_docker_io(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + lease = backend._next_run_lease_v1(capability) + try: + with mock.patch.object(backend, "_observe_cleanup_command") as observe: + self.assertIsNone(backend._cleanup_container(lease)) + observe.assert_not_called() + backend._mark_run_lease_launched_v1(lease) + with mock.patch.object(backend, "_observe_cleanup_command") as observe: + self.assertEqual( + backend._cleanup_container(lease), + "Docker-issued cleanup ID is unavailable", + ) + observe.assert_not_called() + finally: + backend._release_run_lease_v1(lease) + + def test_malformed_or_aliased_cid_never_reaches_destructive_cleanup(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + for raw, alias in ((b"B" * 64, False), (b"c" * 64, True)): + with self.subTest(alias=alias, raw=raw[:1]): + lease = backend._next_run_lease_v1(capability) + try: + lease.cid_file.write_bytes(raw) + if alias: + os.link(lease.cid_file, lease.cid_file.parent / "cid-alias") + backend._mark_run_lease_launched_v1(lease) + with mock.patch.object( + backend, + "_observe_cleanup_command", + ) as observe: + self.assertEqual( + backend._cleanup_container(lease), + "Docker-issued cleanup ID is unavailable", + ) + observe.assert_not_called() + finally: + backend._release_run_lease_v1(lease) + + def test_foreign_name_matching_a_stale_id_never_reaches_rm(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + lease = backend._next_run_lease_v1(capability) + container_id = "d" * 64 + foreign_id = "e" * 64 + try: + lease.cid_file.write_text(container_id + "\n", encoding="ascii") + backend._mark_run_lease_launched_v1(lease) + with mock.patch.object( + backend, + "_observe_cleanup_command", + return_value=build_transport._docker_command_exited_v1( + 0, + foreign_id.encode("ascii") + b"\n", + b"", + ), + ) as observe: + self.assertEqual( + backend._cleanup_container(lease), + "Docker-issued cleanup ID did not resolve exactly", + ) + commands = tuple(call.args[1] for call in observe.call_args_list) + self.assertEqual(len(commands), 1) + self.assertIn("inspect", commands[0]) + self.assertNotIn("rm", commands[0]) + finally: + backend._release_run_lease_v1(lease) + + def test_cleanup_uses_capability_captured_by_the_run_lease(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + lease = backend._next_run_lease_v1(capability) + try: + lease.cid_file.write_text("f" * 64 + "\n", encoding="ascii") + backend._mark_run_lease_launched_v1(lease) + backend._probed_capability = None + observations = ( + build_transport._docker_command_exited_v1( + 0, + b"f" * 64 + b"\n", + b"", + ), + build_transport._docker_command_exited_v1( + 0, + b"f" * 64 + b"\n", + b"", + ), + build_transport._docker_command_exited_v1(0, b"", b""), + ) + with mock.patch.object( + backend, + "_observe_cleanup_command", + side_effect=observations, + ) as observe: + self.assertIsNone(backend._cleanup_container(lease)) + self.assertEqual( + tuple(call.args[0] for call in observe.call_args_list), + (capability, capability, capability), + ) + finally: + backend._release_run_lease_v1(lease) + + def test_cleanup_rejects_another_adapter_lease_before_docker_io(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + _probe_native_backend(backend, pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + foreign_backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + foreign_capability = _probe_native_backend( + foreign_backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + foreign_lease = foreign_backend._next_run_lease_v1(foreign_capability) + try: + with mock.patch.object(backend, "_observe_cleanup_command") as observe: + with self.assertRaises(TypeError): + backend._cleanup_container(foreign_lease) + + observe.assert_not_called() + finally: + foreign_backend._release_run_lease_v1(foreign_lease) + + def test_unverified_container_removal_is_typed_cleanup_failure(self) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/bin/sh"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + ) + capability = _docker_capability( + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + docker_path=Path("/bin/sh"), + ) + lease = backend._next_run_lease_v1(capability) + self.addCleanup(backend._release_run_lease_v1, lease) + with mock.patch.object( + backend, + "_cleanup_container", + return_value="container absence could not be verified", + ): + result = backend._observe_command( + (sys.executable, "-c", "pass"), + stdout_limit=8, + stderr_limit=8, + timeout_ns=5_000_000_000, + lease=lease, + ) + + self.assertIs(type(result), build_transport.DockerBuildCleanupFailureV1) + self.assertEqual( + result.trigger, + build_transport.DockerCleanupTriggerV1.PROCESS_EXIT, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_receipt.py b/proof/region/v1/arb/tests/test_receipt.py new file mode 100644 index 00000000..c9497b98 --- /dev/null +++ b/proof/region/v1/arb/tests/test_receipt.py @@ -0,0 +1,1501 @@ +#!/usr/bin/env python3 +"""Hostile contract for the controller-owned Arb provenance receipt.""" + +from __future__ import annotations + +import hashlib +import os +import select +import signal +import subprocess +import sys +import tempfile +import time +import unittest +from dataclasses import fields, replace +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +TESTS = ARB / "tests" +sys.path[:0] = [str(PROOF), str(TESTS)] + +from build import transport as build_transport # noqa: E402 + +import executor # noqa: E402 +import provenance # noqa: E402 +from arb import pipeline, receipt # noqa: E402 +from arb import runtime as arb_runtime # noqa: E402 +from region_proof_protocol import ( # noqa: E402 + BoundaryUnprovenWitnessV1, + ComparatorKindV1, + ComparatorManifestV2, + ContentResolvedComparatorManifestV2, + DecisionTranscriptV1, + DecisionV1, + RunClaimV1, +) +from test_pipeline import ( # noqa: E402 + _BuildBackend, + _docker_capability, + _foreign_comparator, + _job, + _request, + _static_elf, +) + + +# Test-hang ceiling only: the child uses local pipe IPC and has no product +# deadline, but a broken fork branch must not occupy CI indefinitely. +_FORK_REPORT_TIMEOUT_SECONDS = 5.0 + + +def _digest(label: str) -> bytes: + return hashlib.sha256(label.encode("ascii")).digest() + + +def _transcript_for_request( + request: executor.ExecutionRequestV1, + *, + unresolved: bool = False, +) -> bytes: + job = _job() + if unresolved: + decisions = ( + DecisionV1.BOUNDARY_UNPROVEN, + *(DecisionV1.OUTSIDE for _ in range(job.domain.point_count - 1)), + ) + witnesses = ( + BoundaryUnprovenWitnessV1( + next(job.domain.iter_ordinals()), + _digest("last-enclosure"), + ), + ) + else: + decisions = tuple(DecisionV1.OUTSIDE for _ in range(job.domain.point_count)) + witnesses = () + transcript = DecisionTranscriptV1.from_decisions( + job, + _foreign_comparator(), + decisions, + witnesses, + _digest("accounting"), + ) + return replace( + transcript, + comparator_identity=bytes.fromhex(request.argv[2].decode("ascii")), + ).encode() + + +class _NativeRunBackend: + def __init__( + self, + *, + unresolved: bool = False, + result: executor.ExecutionResultV1 | None = None, + ) -> None: + self.unresolved = unresolved + self.result = result + self.requests: list[executor.ExecutionRequestV1] = [] + + def probe(self, guard: object) -> executor.CapabilityReportV1: + if not guard.is_current(): + raise AssertionError("controller supplied a stale probe guard") + return executor.SupportedV1("linux-x86_64", executor.SANDBOX_POLICY_RELEASE_V1) + + def run( + self, + request: executor.ExecutionRequestV1, + capability: executor.SupportedV1, + ) -> executor.ExecutionResultV1: + self.requests.append(request) + if self.result is not None: + result = self.result + else: + result = executor.CompletedV1( + hashlib.sha256(request.executable).digest(), + _transcript_for_request(request, unresolved=self.unresolved), + b"", + ) + return result + + +def _controller( + binary: bytes, + run_backend: _NativeRunBackend, +) -> tuple[receipt.SourceBoundArbControllerV1, tuple[object, ...]]: + build_backend = _BuildBackend((binary, binary)) + controller = receipt.SourceBoundArbControllerV1( + Path("/usr/bin/docker"), + Path("/sys/fs/cgroup/labcolors/proof"), + ) + return controller, ( + mock.patch.object( + build_transport.NativeDockerBuildBackendV1, + "probe", + autospec=True, + side_effect=lambda _self: build_backend.probe(), + ), + mock.patch.object( + build_transport.NativeDockerBuildBackendV1, + "run_build", + autospec=True, + side_effect=lambda _self, request: build_backend.run_build(request), + ), + mock.patch.object( + executor.NativeLinuxBackendV1, + "probe", + autospec=True, + side_effect=lambda _self, guard: run_backend.probe(guard), + ), + mock.patch.object( + executor.NativeLinuxBackendV1, + "run", + autospec=True, + side_effect=lambda _self, request, capability: run_backend.run( + request, + capability, + ), + ), + mock.patch.object(executor, "enter_observer_cgroup_v1", return_value=None), + ) + + +def _execute( + *, + unresolved: bool = False, + process_result: executor.ExecutionResultV1 | None = None, +) -> tuple[receipt.SourceBoundResultV1, _NativeRunBackend]: + backend = _NativeRunBackend( + unresolved=unresolved, + result=process_result, + ) + controller, patches = _controller(_static_elf(b"source-bound-receipt"), backend) + with patches[0], patches[1], patches[2], patches[3], patches[4]: + return controller.execute(_request()), backend + + +def _tamper(value: object, field: str, replacement: object) -> object: + clone = object.__new__(type(value)) + for name, current in vars(value).items(): + object.__setattr__(clone, name, current) + object.__setattr__(clone, field, replacement) + return clone + + +def _replace_limits( + value: executor.ExecutionLimitsV1, + **changes: int, +) -> executor.ExecutionLimitsV1: + values = { + name: getattr(value, name) + for name in ( + "max_executable_bytes", + "max_stdin_bytes", + "max_argument_bytes", + "max_stdout_bytes", + "max_stderr_bytes", + "wall_timeout_ns", + "memory_max_bytes", + "pids_max", + ) + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +def _replace_runtime_binding( + value: arb_runtime.ArbRuntimeBindingV1, + **limit_changes: int, +) -> arb_runtime.ArbRuntimeBindingV1: + return arb_runtime.ArbRuntimeBindingV1( + value.profile, + _replace_limits(value.limits, **limit_changes), + ) + + +def _replace_invocation( + value: executor.ExecutionRequestV1, + **changes: object, +) -> executor.ExecutionRequestV1: + values: dict[str, object] = { + "executable": value.executable, + "argv": value.argv, + "environment": value.environment, + "cwd": value.cwd, + "stdin": value.stdin, + "umask": value.umask, + "limits": value.limits, + } + values.update(changes) + return executor.ExecutionRequestV1(**values) + + +class SourceBoundReceiptTests(unittest.TestCase): + def test_public_verifier_rejects_a_top_level_switch_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + switched_request = _request( + runtime_binding=_replace_runtime_binding( + result.evidence.request.runtime_binding, + wall_timeout_ns=result.evidence.request.runtime_binding.limits.wall_timeout_ns + - 1, + ) + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence, "request", switched_request) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_nested_request_switch_during_replay( + self, + ) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + switched_binding = _replace_runtime_binding( + evidence.request.runtime_binding, + wall_timeout_ns=evidence.request.runtime_binding.limits.wall_timeout_ns - 1, + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.request, "runtime_binding", switched_binding) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_source_archive_switch_during_replay( + self, + ) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "request", result.evidence.request) + source = evidence.request.admitted_sources.sources[0] + original_archive = source.archive_bytes + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_switch( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(source, "_archive_bytes", original_archive + b"x") + switched = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_switch, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + finally: + object.__setattr__(source, "_archive_bytes", original_archive) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_build_repair_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + original_binary = result.evidence.build.binary + evidence = _tamper( + result.evidence, + "build", + _tamper(result.evidence.build, "_binary", b"corrupt"), + ) + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.build, "_binary", original_binary) + switched = True + return replayed + + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_process_repair_during_replay(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = _tamper(result.evidence, "process", result.evidence.process) + original_stdout = evidence.process.stdout + object.__setattr__(evidence.process, "stdout", b"corrupt") + real_replay = provenance.replay_admitted_source_closure_v1 + switched = False + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal switched + replayed = real_replay(*args, **kwargs) + object.__setattr__(evidence.process, "stdout", original_stdout) + switched = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse(receipt.replay_evidence_is_well_bound_v1(evidence)) + finally: + object.__setattr__(evidence.process, "stdout", original_stdout) + + self.assertTrue(switched) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_public_verifier_rejects_a_poisoned_cached_identity_before_replay( + self, + ) -> None: + """Cached identities are observable state, not an unguarded speed cache.""" + + targets = ( + ( + "request definition", + lambda evidence: evidence.request.job.definition, + "definition_digest", + ), + ( + "request domain", + lambda evidence: evidence.request.job.domain, + "identity", + ), + ( + "request policy", + lambda evidence: evidence.request.job.policy, + "identity", + ), + ("request job", lambda evidence: evidence.request.job, "identity"), + ( + "inner comparator manifest", + lambda evidence: evidence.build.comparator.manifest.manifest, + "identity", + ), + ( + "resolved comparator manifest", + lambda evidence: evidence.build.comparator.manifest, + "identity", + ), + ("transcript", lambda evidence: evidence.transcript, "identity"), + ("run claim", lambda evidence: evidence.run_claim, "identity"), + ) + for name, target_selector, field_name in targets: + with self.subTest(cache=name): + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + evidence = result.evidence + target = target_selector(evidence) + original_identity = getattr(target, field_name) + forged_identity = _digest(f"forged {name}") + real_replay = provenance.replay_admitted_source_closure_v1 + repaired = False + object.__setattr__(target, field_name, forged_identity) + + def replay_then_repair( + *args: object, + **kwargs: object, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal repaired + replayed = real_replay(*args, **kwargs) + object.__setattr__(target, field_name, original_identity) + repaired = True + return replayed + + try: + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_repair, + ): + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(evidence) + ) + finally: + object.__setattr__(target, field_name, original_identity) + + self.assertFalse(repaired) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(evidence)) + + def test_source_bound_policy_identity_binds_immutable_coordinates(self) -> None: + capability = _docker_capability() + request = _request() + # This golden belongs to the exact observed capability fixture; changing + # its daemon, CLI path or host user must deliberately rederive it. + self.assertEqual( + receipt.source_bound_policy_identity_v3( + capability, + request.host_trust, + request.runtime_binding, + ).hex(), + "a94f16cb61a7a1951457a4254a328bbb5eb07c66cd54d570eaf794eaa5b6bb8e", + ) + + def test_controller_uses_shared_observer_placement_and_fails_closed(self) -> None: + backend = _NativeRunBackend() + controller, patches = _controller( + _static_elf(b"shared-observer-placement"), + backend, + ) + with patches[0], patches[1], patches[2], patches[3], mock.patch.object( + executor, + "enter_observer_cgroup_v1", + return_value=None, + ) as placement: + result = controller.execute(_request()) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + placement.assert_called_once_with(Path("/sys/fs/cgroup/labcolors/proof")) + self.assertFalse(hasattr(receipt, "_enter_observer_cgroup_v1")) + + failed_backend = _NativeRunBackend() + failed_controller, failed_patches = _controller( + _static_elf(b"shared-observer-placement-failure"), + failed_backend, + ) + forbidden_run_backend = mock.Mock( + side_effect=AssertionError( + "RUN backend must not be constructed after placement failure" + ) + ) + placement_build_calls: list[int] = [] + + def fail_placement(_parent: Path) -> None: + placement_build_calls.append(build_runs.call_count) + raise OSError("observer group unavailable") + + with failed_patches[0], failed_patches[1] as build_runs, failed_patches[2], failed_patches[3], mock.patch.object( + executor, + "enter_observer_cgroup_v1", + side_effect=fail_placement, + ) as placement, mock.patch.object( + receipt, + "_NATIVE_RUN_BACKEND_TYPE", + new=forbidden_run_backend, + ), mock.patch.object( + executor, + "NativeLinuxBackendV1", + new=forbidden_run_backend, + ): + failed = failed_controller.execute(_request()) + + placement.assert_called_once_with(Path("/sys/fs/cgroup/labcolors/proof")) + self.assertEqual(placement_build_calls, [2]) + forbidden_run_backend.assert_not_called() + self.assertEqual( + failed, + receipt.SourceBoundRejectedV1( + receipt.SourceBoundFailureReasonV1.OBSERVER_PLACEMENT_FAILED, + "dedicated controller could not enter the observer cgroup", + ), + ) + self.assertEqual(failed_backend.requests, []) + self.assertEqual( + failed_controller.execute(_request()), + receipt.SourceBoundRejectedV1( + receipt.SourceBoundFailureReasonV1.CONTROLLER_CONSUMED, + "controller authority is one-shot", + ), + ) + + def test_controller_rejects_invalid_or_nonexact_native_coordinates_on_construction(self) -> None: + class PathSubclass(type(Path())): + pass + + valid_docker = Path("/usr/bin/docker") + valid_parent = Path("/sys/fs/cgroup/labcolors/proof") + for docker_path, cgroup_parent in ( + (object(), valid_parent), + (Path("relative"), valid_parent), + (Path("/docker\0"), valid_parent), + (Path("/docker\n"), valid_parent), + (Path("/docker,comma"), valid_parent), + (Path("/docker\ud800"), valid_parent), + (PathSubclass("/usr/bin/docker"), valid_parent), + (valid_docker, object()), + (valid_docker, Path("relative")), + (valid_docker, Path("/proof\0")), + (valid_docker, Path("/proof\ud800")), + (valid_docker, Path("//proof")), + (valid_docker, PathSubclass("/proof")), + ): + with self.subTest( + docker_path=type(docker_path).__name__, + cgroup_parent=type(cgroup_parent).__name__, + ): + with self.assertRaises(TypeError): + receipt.SourceBoundArbControllerV1( # type: ignore[arg-type] + docker_path, + cgroup_parent, + ) + + def test_source_bound_policy_identity_consumes_explicit_trust_coordinate(self) -> None: + capability = _docker_capability() + request = _request() + trust = object() + with mock.patch.object( + receipt.pipeline, + "pipeline_policy_identity_v2", + return_value=_digest("pipeline-policy"), + ) as policy_identity: + receipt.source_bound_policy_identity_v3( + capability, + trust, + request.runtime_binding, + ) + policy_identity.assert_called_once_with(trust, capability.policy) + + def test_identity_rejection_remains_typed_at_the_receipt_boundary(self) -> None: + invocation_rejection = executor.ExecutionIdentityRejectedV1( + executor.ExecutionIdentityReasonV1.REQUEST_NOT_ADMITTED, + ) + admitted_invocation_identity = hashlib.sha256(b"admitted invocation").digest() + with mock.patch.object( + receipt.executor, + "invocation_identity_v1", + side_effect=(admitted_invocation_identity, invocation_rejection), + ): + result, _backend = _execute() + self.assertEqual( + result, + pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + invocation_rejection, + ), + ) + + platform_rejection = executor.ExecutionIdentityRejectedV1( + executor.ExecutionIdentityReasonV1.FOREIGN_PLATFORM, + ) + admitted_platform_identity = executor.platform_identity_v1( + executor.SupportedV1( + executor.EXECUTION_PLATFORM_V1, + executor.SANDBOX_POLICY_RELEASE_V1, + ) + ) + with mock.patch.object( + receipt.executor, + "platform_identity_v1", + side_effect=(admitted_platform_identity, platform_rejection), + ): + result, _backend = _execute() + self.assertEqual( + result, + pipeline.ExecutionRejectedV1( + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + platform_rejection, + ), + ) + + def test_only_controller_execution_can_seal_a_receipt(self) -> None: + result, backend = _execute() + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertIs(type(result.comparator), pipeline.DiagnosticArbComparatorV1) + self.assertFalse(hasattr(result.evidence, "source_closure")) + self.assertFalse(hasattr(result.evidence, "operation")) + self.assertEqual(result.run_claim.identity, result.claim.run_claim_identity) + self.assertEqual(result.evidence.identity, result.claim.replay_evidence_identity) + self.assertEqual( + result.claim.provenance_policy_identity, + receipt.source_bound_policy_identity_v3( + result.evidence.build.docker_capability, + result.evidence.request.host_trust, + result.evidence.request.runtime_binding, + ), + ) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(result.evidence)) + self.assertEqual(len(backend.requests), 1) + self.assertIs(backend.requests[0].executable, result.executable) + self.assertIs(result.evidence.invocation.executable, result.executable) + first, second = result.evidence.build.build_processes + self.assertIs(first.stdout, result.evidence.build.rebuild_binaries[0]) + self.assertIs(second.stdout, result.evidence.build.rebuild_binaries[1]) + self.assertEqual( + first.input_transfer.bundle_identity, + second.input_transfer.bundle_identity, + ) + + def test_source_bound_controller_uses_one_source_operation_snapshot(self) -> None: + request = _request() + run_backend = _NativeRunBackend() + controller, patches = _controller( + _static_elf(b"one-source-bound-operation"), + run_backend, + ) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + result = controller.execute(request) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual(len(run_backend.requests), 1) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + + def test_source_bound_snapshot_survives_source_replay_reentrancy(self) -> None: + request = _request() + original_domain = request.job.domain + foreign_domain = type(original_domain).from_ordinals((0,)) + run_backend = _NativeRunBackend() + controller, patches = _controller( + _static_elf(b"source-bound-reentrancy"), + run_backend, + ) + real_replay = provenance.replay_admitted_source_closure_v1 + replay_calls = 0 + + def replay_then_mutate( + source_lock: provenance.ArbSourceLockV1, + admitted_sources: provenance.AdmittedArbSourcesV1, + ) -> provenance.ReplayedSourceClosureV1: + nonlocal replay_calls + replay_calls += 1 + snapshot = real_replay(source_lock, admitted_sources) + object.__setattr__(request.job, "domain", foreign_domain) + return snapshot + + try: + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=replay_then_mutate, + ), + ): + result = controller.execute(request) + finally: + object.__setattr__(request.job, "domain", original_domain) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual(replay_calls, 1) + self.assertEqual(result.evidence.request.job.domain, original_domain) + + def test_evidence_verifier_owns_one_fresh_source_operation_snapshot(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(result.evidence)) + + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 3) + + def test_no_public_object_or_diagnostic_can_mint(self) -> None: + result, _backend = _execute() + with self.assertRaises(TypeError): + receipt.ContentResolvedEvaluatorReplayV1( + result.evidence.request, + result.evidence.build, + result.evidence.invocation, + result.evidence.platform, + result.evidence.process, + result.evidence.transcript, + result.evidence.run_claim, + ) + with self.assertRaises(TypeError): + receipt.SourceBoundEvaluatorReceiptV1(result.claim, result.evidence) + self.assertFalse(hasattr(receipt, "admit_source_bound_receipt_v1")) + self.assertFalse(hasattr(receipt.SourceBoundArbControllerV1, "mint")) + self.assertFalse(hasattr(pipeline, "DiagnosticPipelineObservationV1")) + self.assertFalse(hasattr(receipt.SourceBoundEvaluatorReceiptV1, "parse")) + + def test_receipt_keeps_snapshot_only_on_the_private_operation_path(self) -> None: + source = (ARB / "receipt.py").read_text(encoding="utf-8") + + self.assertNotIn("pipeline._sealed_build_input_bundle_is_well_bound_v1", source) + self.assertNotIn("pipeline._build_process_bytes_v1", source) + self.assertNotIn("executor._execution_identity_v1", source) + self.assertNotIn("executor._enter_observer_cgroup_v1", source) + self.assertNotIn("executor._canonical_cgroup_parent_v1", source) + self.assertNotIn("sealed_build_input_bundle_is_well_bound_v1", source) + self.assertIn("pipeline._seal_build_input_from_snapshot_v1", source) + self.assertIn("pipeline._owned_arb_input_is_bound_v1", source) + self.assertIn("pipeline._derive_arb_comparator_for_build_v1", source) + self.assertIn("build_transport.build_process_bytes_v1", source) + self.assertNotIn("pipeline.replay_pipeline_request_v1", source) + self.assertIn("build_transport.docker_command_coordinate_v1", source) + self.assertTrue(hasattr(pipeline, "arb_input_is_bound_v1")) + self.assertTrue(hasattr(build_transport, "build_process_bytes_v1")) + self.assertTrue(hasattr(build_transport, "docker_command_coordinate_v1")) + self.assertTrue(hasattr(executor, "invocation_identity_v1")) + self.assertTrue(hasattr(executor, "platform_identity_v1")) + self.assertTrue(hasattr(executor, "canonical_cgroup_parent_v1")) + self.assertTrue(hasattr(executor, "enter_observer_cgroup_v1")) + + def test_reference_does_not_describe_shipped_arb_receipt_as_future(self) -> None: + documentation = (PROOF / "PROTOCOL.md").read_text(encoding="utf-8") + prose = " ".join(documentation.split()) + + self.assertIn( + "## Воспроизведение Arb, связанное с источником", + documentation, + ) + self.assertIn("SourceBoundEvaluatorReceiptV1", documentation) + self.assertIn("executor.enter_observer_cgroup_v1", documentation) + for stale_claim in ( + "заявленные результаты будущих Arb/MPFI processes", + "он ещё не строит и не запускает evaluator", + "только controlled-executor slice сможет", + "будущий source-bound receipt", + "predicate с будущей source/build/run цепью", + "V5b2c-0", + "V5b2b", + "в c0", + "c1a", + ): + with self.subTest(stale_claim=stale_claim): + self.assertNotIn(stale_claim, prose) + + def test_job_first_binds_at_run_not_source_or_build(self) -> None: + request = _request() + first_budget, second_budget = request.job.policy.comparators + different_job = replace( + request.job, + policy=replace( + request.job.policy, + comparators=( + replace( + first_budget, + per_point_work=first_budget.per_point_work + 1, + ), + second_budget, + ), + ), + ) + different_request = replace(request, job=different_job) + self.assertNotEqual(request.job.identity, different_request.job.identity) + + first_build = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((_static_elf(b"job-independent"),) * 2) + ).build(request) + second_build = pipeline.ControlledPipelineV1( + build_backend=_BuildBackend((_static_elf(b"job-independent"),) * 2) + ).build(different_request) + self.assertIs(type(first_build), pipeline.DiagnosticBuildObservationV1) + self.assertIs(type(second_build), pipeline.DiagnosticBuildObservationV1) + + first_source = receipt._source_identity_v1(request) + second_source = receipt._source_identity_v1(different_request) + self.assertEqual(first_source, second_source) + self.assertEqual(first_build.input_bundle_identity, second_build.input_bundle_identity) + self.assertEqual( + receipt._build_identity_v2(request, first_source, first_build), + receipt._build_identity_v2( + different_request, + second_source, + second_build, + ), + ) + + def test_root_and_build_coordinates_are_recomputed(self) -> None: + result, _backend = _execute() + dag = result.evidence + for field_name in ( + "source_identity", + "build_identity", + "run_identity", + "_identity", + ): + with self.subTest(root=field_name): + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, field_name, _digest(field_name)) + ) + ) + for field_name in ( + "structural_source_identity", + "build_input_identity", + "formula_support_identity", + "pipeline_policy_identity", + "flint_commit_content_identity", + "flint_project_pinned_release_only_identity", + "binary_sha256", + "input_bundle_identity", + "input_bundle_sha256", + ): + with self.subTest(build=field_name): + build = _tamper(dag.build, field_name, _digest(field_name)) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "build", build) + ) + ) + different_capability = _docker_capability( + daemon_marker=b"different-docker-daemon" + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper( + dag, + "build", + _tamper( + dag.build, + "docker_capability", + different_capability, + ), + ) + ) + ) + for field_name in ( + "flint_commit_content_file_count", + "flint_project_pinned_release_only_file_count", + ): + build = _tamper( + dag.build, + field_name, + getattr(dag.build, field_name) + 1, + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "build", build) + ) + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper( + dag, + "build", + _tamper(dag.build, "host_trust", "foreign-host-trust"), + ) + ) + ) + build = _tamper( + dag.build, + "input_bundle_length", + dag.build.input_bundle_length + 1, + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) + ) + + def test_source_process_transfer_and_comparator_mutations_fail(self) -> None: + result, _backend = _execute() + dag = result.evidence + source = dag.request.admitted_sources.sources[0] + admitted = _tamper( + dag.request.admitted_sources, + "sources", + ( + _tamper(source, "tree_identity", _digest("tree")), + *dag.request.admitted_sources.sources[1:], + ), + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "request", _tamper(dag.request, "admitted_sources", admitted)) + ) + ) + + first = dag.build.build_processes[0] + self.assertFalse(hasattr(first.input_transfer, "__dict__")) + self.assertFalse(hasattr(first, "__dict__")) + forged_transfer = tuple.__new__( + type(first.input_transfer), + ( + first.input_transfer.bundle_identity, + first.input_transfer.expected_length + 1, + first.input_transfer.expected_sha256, + first.input_transfer.written_length, + first.input_transfer.written_sha256, + ), + ) + forged_process = tuple.__new__( + type(first), + ( + first.returncode, + first.stdout, + first.stderr, + forged_transfer, + ), + ) + forged_build = _tamper( + dag.build, + "build_processes", + (forged_process, dag.build.build_processes[1]), + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "build", forged_build) + ) + ) + + forged_process = tuple.__new__( + type(first), + ( + first.returncode + 1, + first.stdout, + first.stderr, + first.input_transfer, + ), + ) + forged_build = _tamper( + dag.build, + "build_processes", + (forged_process, dag.build.build_processes[1]), + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "build", forged_build) + ) + ) + + preimages = _tamper( + dag.build.comparator.preimages, + "engine_release", + b"foreign engine release", + ) + comparator = _tamper(dag.build.comparator, "preimages", preimages) + build = _tamper(dag.build, "comparator", comparator) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) + ) + + manifest = dag.build.comparator.manifest.manifest + _ = manifest.identity + mutated_manifest = _tamper( + manifest, + "engine_release", + manifest.upstream_source, + ) + resolved = _tamper( + dag.build.comparator.manifest, + "manifest", + mutated_manifest, + ) + comparator = _tamper(dag.build.comparator, "manifest", resolved) + build = _tamper(dag.build, "comparator", comparator) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) + ) + + # Keep the BUILD preimage itself intact: a verifier that only checks + # self-consistency would otherwise accept this fully well-formed but + # source-unrelated comparator manifest. + preimage_fields = fields(pipeline.ArbComparatorPreimagesV1) + preimage_values = tuple( + dag.build.comparator.preimages.build_identity + if field.name == "build_identity" + else f"forged-comparator-{index}".encode("ascii") + for index, field in enumerate(preimage_fields) + ) + self.assertEqual(len(set(preimage_values)), len(preimage_values)) + preimages = pipeline.ArbComparatorPreimagesV1(*preimage_values) + manifest = ComparatorManifestV2( + ComparatorKindV1.ARB, + *(hashlib.sha256(value).digest() for value in preimage_values), + ) + by_digest = { + hashlib.sha256(value).digest(): value for value in preimage_values + } + resolved = ContentResolvedComparatorManifestV2.admit( + manifest, + by_digest.get, + ) + comparator = pipeline.DiagnosticArbComparatorV1( + preimages, + resolved, + dag.build.structural_source_identity, + dag.build.build_input_identity, + dag.build.pipeline_policy_identity, + dag.build.binary_sha256, + dag.build.rebuild_sha256s, + _token=pipeline._COMPARATOR_TOKEN, + ) + build = _tamper(dag.build, "comparator", comparator) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1(_tamper(dag, "build", build)) + ) + + def test_source_replay_rejects_a_self_consistent_forged_manifest(self) -> None: + request = _request() + lock = request.source_lock.sources[2] + source = request.admitted_sources.sources[2] + forged_files = list(source.files) + forged_files[1] = replace(forged_files[1], path=forged_files[0].path) + forged_files_value = tuple(sorted(forged_files, key=lambda item: item.path)) + + with self.subTest(boundary="manifest"): + with self.assertRaises(TypeError): + provenance.archive_file_manifest_bytes_v1(forged_files_value) + + case_collision = list(source.files) + case_collision[1] = replace( + case_collision[1], + path=case_collision[0].path.lower(), + ) + with self.subTest(boundary="ASCII case normalization"): + with self.assertRaises(TypeError): + provenance.archive_file_manifest_bytes_v1( + tuple(sorted(case_collision, key=lambda item: item.path)) + ) + + forged_source = _tamper(source, "files", forged_files_value) + forged_source = _tamper( + forged_source, + "tree_identity", + provenance._tree_identity(forged_files_value), + ) + with self.subTest(boundary="archive replay"): + with self.assertRaises(provenance.ProvenanceErrorV1) as caught: + provenance.source_archive_replay_coordinates_v1(lock, forged_source) + self.assertEqual( + caught.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) + + def test_invocation_process_and_same_object_mutations_fail(self) -> None: + result, _backend = _execute() + dag = result.evidence + equal_executable_copy = bytes(bytearray(dag.invocation.executable)) + self.assertEqual(equal_executable_copy, dag.invocation.executable) + self.assertIsNot(equal_executable_copy, dag.invocation.executable) + operation = pipeline._snapshot_pipeline_operation_v1(dag.request) + request = operation.request + baseline = receipt.ContentResolvedEvaluatorReplayV1( + request, + dag.build, + dag.invocation, + dag.platform, + dag.process, + dag.transcript, + dag.run_claim, + _operation=operation, + _token=receipt._EVIDENCE_TOKEN, + ) + self.assertEqual(baseline.identity, dag.identity) + mutants = ( + _replace_invocation(dag.invocation, executable=equal_executable_copy), + _replace_invocation( + dag.invocation, + argv=dag.invocation.argv + (b"ambient",), + ), + _replace_invocation( + dag.invocation, + environment=((b"LC_ALL", b"POSIX"), (b"TZ", b"UTC")), + ), + _replace_invocation(dag.invocation, cwd=b"/tmp"), + _replace_invocation(dag.invocation, stdin=dag.invocation.stdin + b"x"), + _replace_invocation(dag.invocation, umask=0o022), + ) + for invocation in mutants: + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "invocation", invocation) + ) + ) + forged_claim = RunClaimV1.for_transcript( + request.job, + dag.build.comparator.manifest, + dag.transcript, + dag.build.binary_sha256, + executor.invocation_identity_v1(invocation), + executor.platform_identity_v1(dag.platform), + ) + with self.assertRaises(TypeError): + receipt.ContentResolvedEvaluatorReplayV1( + request, + dag.build, + invocation, + dag.platform, + dag.process, + dag.transcript, + forged_claim, + _operation=operation, + _token=receipt._EVIDENCE_TOKEN, + ) + mutated_binding = _replace_runtime_binding( + dag.request.runtime_binding, + wall_timeout_ns=dag.request.runtime_binding.limits.wall_timeout_ns - 1, + ) + request = _tamper(dag.request, "runtime_binding", mutated_binding) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "request", request) + ) + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper( + dag, + "process", + replace(dag.process, stdout=dag.process.stdout + b"x"), + ) + ) + ) + with self.assertRaises(TypeError): + executor.SupportedV1( + "foreign-linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper( + dag, + "platform", + tuple.__new__( + executor.SupportedV1, + ( + "foreign-linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ), + ), + ) + ) + ) + self.assertFalse( + receipt.replay_evidence_is_well_bound_v1( + _tamper(dag, "build", _tamper(dag.build, "_binary", equal_executable_copy)) + ) + ) + + def test_unresolved_typed_transcript_still_gets_provenance_receipt(self) -> None: + result, _backend = _execute(unresolved=True) + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual(result.transcript.counters[2], 1) + self.assertEqual(result.transcript.counters[3], 0) + self.assertFalse(hasattr(result, "mathematical_proof")) + + def test_signal_timeout_and_oom_remain_process_failures(self) -> None: + binary_digest = hashlib.sha256(_static_elf(b"source-bound-receipt")).digest() + outcomes = ( + executor.SignaledV1(binary_digest, b"", b"", 11, True), + executor.TimedOutV1(binary_digest, b"", b"", 60_000_000_000), + executor.OomKilledV1(binary_digest, b"", b"", 1), + ) + for outcome in outcomes: + with self.subTest(outcome=type(outcome).__name__): + result, _backend = _execute(process_result=outcome) + self.assertIs(type(result), pipeline.ExecutionRejectedV1) + self.assertEqual( + result.reason, + pipeline.ExecutionFailureReasonV1.PROCESS_FAILED, + ) + self.assertIs(result.observation, outcome) + + def test_versioned_evaluator_exit_classes_remain_distinct(self) -> None: + binary_digest = hashlib.sha256(_static_elf(b"source-bound-receipt")).digest() + cases = ( + ( + arb_runtime.ARB_EXIT_INPUT_REJECTED_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_INPUT_REJECTED, + ), + ( + arb_runtime.ARB_EXIT_INPUT_LIMIT_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_INPUT_LIMIT, + ), + ( + arb_runtime.ARB_EXIT_OUTPUT_LIMIT_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_OUTPUT_LIMIT, + ), + ( + arb_runtime.ARB_EXIT_RESOURCE_LIMIT_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_RESOURCE_LIMIT, + ), + ( + arb_runtime.ARB_EXIT_INTERNAL_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_INTERNAL, + ), + ( + arb_runtime.ARB_EXIT_IO_V1, + pipeline.ExecutionFailureReasonV1.EVALUATOR_IO, + ), + (99, pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT), + ) + for exit_code, expected in cases: + with self.subTest(exit_code=exit_code): + observed = executor.ExitNonZeroV1( + binary_digest, + b"", + b"typed evaluator failure", + exit_code, + ) + result, _backend = _execute(process_result=observed) + self.assertEqual(result.reason, expected) + self.assertIs(result.observation, observed) + + def test_impossible_evaluator_output_is_a_backend_contract_failure(self) -> None: + binary_digest = hashlib.sha256(_static_elf(b"source-bound-receipt")).digest() + output_limit = _request().runtime_binding.limits.max_stdout_bytes + outcomes = ( + executor.ExitNonZeroV1( + binary_digest, + b"impossible partial transcript", + b"job rejected: bad_magic\n", + arb_runtime.ARB_EXIT_INPUT_REJECTED_V1, + ), + executor.OutputLimitExceededV1( + binary_digest, + bytes(output_limit), + b"", + executor.OutputStreamV1.STDOUT, + output_limit, + ), + ) + for outcome in outcomes: + with self.subTest(outcome=type(outcome).__name__): + result, _backend = _execute(process_result=outcome) + self.assertEqual( + result.reason, + pipeline.ExecutionFailureReasonV1.BACKEND_CONTRACT, + ) + self.assertIs(result.observation, outcome) + + def test_controller_rejects_a_forked_child_without_consuming_parent_authority( + self, + ) -> None: + backend = _NativeRunBackend() + controller, patches = _controller(_static_elf(b"source-bound-receipt"), backend) + request = _request() + read_fd, write_fd = os.pipe() + with patches[0], patches[1], patches[2], patches[3], patches[4]: + child_pid = os.fork() + if child_pid == 0: + exit_status = 1 + try: + os.close(read_fd) + child = controller.execute(request) + payload = ( + f"{type(child).__name__}:" + f"{child.reason.value}:" + f"{child.detail}" + ).encode() + os.write(write_fd, payload) + exit_status = 0 + except Exception as error: + try: + os.write( + write_fd, + f"ERROR:{type(error).__name__}:{error}".encode(), + ) + except OSError: + pass + finally: + try: + os.close(write_fd) + finally: + os._exit(exit_status) + os.close(write_fd) + os.set_blocking(read_fd, False) + child_payload = bytearray() + deadline = time.monotonic() + _FORK_REPORT_TIMEOUT_SECONDS + timed_out = False + try: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + timed_out = True + break + readable, _writable, _exceptional = select.select( + (read_fd,), (), (), remaining + ) + if not readable: + timed_out = True + break + chunk = os.read(read_fd, 4096) + if not chunk: + break + child_payload.extend(chunk) + finally: + os.close(read_fd) + if timed_out: + try: + os.kill(child_pid, signal.SIGKILL) + except ProcessLookupError: + pass + waited_pid, status = os.waitpid(child_pid, 0) + if timed_out: + self.fail( + "forked authority probe did not report within " + f"{_FORK_REPORT_TIMEOUT_SECONDS:g} seconds" + ) + parent = controller.execute(request) + + self.assertEqual(waited_pid, child_pid) + self.assertTrue(os.WIFEXITED(status), status) + self.assertEqual(os.WEXITSTATUS(status), 0) + self.assertEqual( + bytes(child_payload).decode(), + "SourceBoundRejectedV1:controller_process_changed:" + "controller authority cannot cross a process boundary", + ) + self.assertIs(type(parent), receipt.SourceBoundEvaluatorReceiptV1) + + def test_controller_is_one_shot(self) -> None: + backend = _NativeRunBackend() + controller, patches = _controller(_static_elf(b"source-bound-receipt"), backend) + with patches[0], patches[1], patches[2], patches[3], patches[4]: + first = controller.execute(_request()) + second = controller.execute(_request()) + self.assertIs(type(first), receipt.SourceBoundEvaluatorReceiptV1) + self.assertEqual( + second, + receipt.SourceBoundRejectedV1( + receipt.SourceBoundFailureReasonV1.CONTROLLER_CONSUMED, + "controller authority is one-shot", + ), + ) + + +@unittest.skipUnless( + sys.platform == "linux" + and os.environ.get("LABCOLORS_ARB_PIPELINE_DOCKER") + and os.environ.get("LABCOLORS_EXECUTOR_CGROUP_V1") + and os.environ.get("LABCOLORS_GMP_ARCHIVE") + and os.environ.get("LABCOLORS_MPFR_ARCHIVE") + and os.environ.get("LABCOLORS_FLINT_ARCHIVE"), + "requires Linux, Docker, a delegated cgroup, and all three exact source archives", +) +class NativeSourceBoundReceiptIntegrationTests(unittest.TestCase): + def test_real_build_run_and_seal_are_one_source_bound_controller_execution(self) -> None: + source_lock = provenance.arb_source_lock_v1() + archive_names = ( + "LABCOLORS_GMP_ARCHIVE", + "LABCOLORS_MPFR_ARCHIVE", + "LABCOLORS_FLINT_ARCHIVE", + ) + safe = tuple( + provenance.admit_source_archive(lock, Path(os.environ[name]).read_bytes()) + for lock, name in zip(source_lock.sources, archive_names, strict=True) + ) + admitted = provenance.admit_arb_sources(source_lock, safe) + cgroup_parent = Path(os.environ["LABCOLORS_EXECUTOR_CGROUP_V1"]) + result = receipt.SourceBoundArbControllerV1( + Path(os.environ["LABCOLORS_ARB_PIPELINE_DOCKER"]), + cgroup_parent, + ).execute(_request(source_lock=source_lock, admitted_sources=admitted)) + + self.assertIs(type(result), receipt.SourceBoundEvaluatorReceiptV1, result) + self.assertTrue(receipt.replay_evidence_is_well_bound_v1(result.evidence)) + self.assertIs(result.evidence.build.binary, result.executable) + self.assertIs(result.evidence.invocation.executable, result.executable) + first, second = result.evidence.build.build_processes + self.assertEqual( + first.input_transfer.bundle_identity, + second.input_transfer.bundle_identity, + ) + + # Runtime characterization reuses receipt bytes; no third build or host + # output pathname participates in the receipt. + (cgroup_parent.parent / "tasks" / "cgroup.procs").write_text( + str(os.getpid()), + encoding="ascii", + ) + repo = PROOF.parents[2] + with tempfile.TemporaryDirectory(prefix="labcolors-source-bound-runtime-") as temporary: + executable = Path(temporary) / pipeline.EVALUATOR_OUTPUT_NAME_V1 + executable.write_bytes(result.executable) + executable.chmod(0o500) + runtime = subprocess.run( + (sys.executable, str(ARB / "tests" / "runtime_gate.py")), + check=False, + capture_output=True, + cwd=repo, + env={ + "LABCOLORS_ARB_EVALUATOR": str(executable), + "LC_ALL": "C", + "PATH": os.environ.get("PATH", ""), + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONHASHSEED": "0", + "TZ": "UTC", + }, + timeout=300, + ) + self.assertEqual( + runtime.returncode, + 0, + (runtime.stdout + runtime.stderr).decode("utf-8", "replace"), + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_runtime_profile.py b/proof/region/v1/arb/tests/test_runtime_profile.py new file mode 100644 index 00000000..03579360 --- /dev/null +++ b/proof/region/v1/arb/tests/test_runtime_profile.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""RED/green contract for the exact Arb runtime profile binding.""" + +from __future__ import annotations + +import subprocess +import sys +import unittest +from pathlib import Path + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +sys.path.insert(0, str(PROOF)) + +import executor # noqa: E402 +from arb import runtime as arb_runtime # noqa: E402 + + +def _limits(**changes: int) -> executor.ExecutionLimitsV1: + values = { + "max_executable_bytes": 16 * 1024 * 1024, + "max_stdin_bytes": arb_runtime.ARB_MAX_JOB_BYTES_V1, + "max_argument_bytes": 4096, + "max_stdout_bytes": arb_runtime.ARB_MAX_OUTPUT_BYTES_V1, + "max_stderr_bytes": 64 * 1024, + "wall_timeout_ns": 60_000_000_000, + "memory_max_bytes": 1024 * 1024 * 1024, + "pids_max": 1, + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +class ArbRuntimeProfileTests(unittest.TestCase): + def test_profile_rejects_int_subclasses_and_identity_totalizes_forgery(self) -> None: + class EqualInt(int): + def to_bytes(self, *_args: object, **_kwargs: object) -> bytes: + raise RuntimeError("foreign scalar executed") + + values = tuple(arb_runtime.arb_runtime_profile_v1()) + hostile_values = (EqualInt(values[0]), *values[1:]) + with self.assertRaises(TypeError): + arb_runtime.ArbRuntimeProfileV1(*hostile_values) + + forged = tuple.__new__(arb_runtime.ArbRuntimeProfileV1, hostile_values) + result = arb_runtime.runtime_profile_identity_v1(forged) + self.assertIs(type(result), arb_runtime.ArbRuntimeIdentityRejectedV1) + self.assertEqual( + result.reason, + arb_runtime.ArbRuntimeProfileReasonV1.NONCANONICAL, + ) + + def test_arb_package_has_one_pipeline_receipt_and_runtime_identity(self) -> None: + program = f""" +import sys +import types + +sys.path.insert(0, {str(PROOF)!r}) +foreign_runtime = types.ModuleType("runtime") +sys.modules["runtime"] = foreign_runtime +foreign_pipeline = types.ModuleType("pipeline") +sys.modules["pipeline"] = foreign_pipeline + +from arb import pipeline +from arb import receipt +from arb import runtime as expected_runtime + +if pipeline.arb_runtime is not expected_runtime: + raise SystemExit("Arb pipeline accepted a foreign runtime module") +if receipt.pipeline is not pipeline or receipt.arb_runtime is not expected_runtime: + raise SystemExit("Arb receipt split the package module identities") +""" + completed = subprocess.run( + (sys.executable, "-c", program), + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + + def test_profile_is_one_exact_wire_v1_value(self) -> None: + profile = arb_runtime.arb_runtime_profile_v1() + self.assertEqual( + tuple(profile), + ( + 16 * 1024 * 1024, + 16 * 1024 * 1024, + 4096, + 32, + 1024, + ), + ) + self.assertEqual(arb_runtime.ARB_RUNTIME_PROFILE_ID_V1, "LC-ARB-RUNTIME-V1") + self.assertEqual(arb_runtime.ArbRuntimeProfileV1(*tuple(profile)), profile) + with self.assertRaises(ValueError): + arb_runtime.ArbRuntimeProfileV1( + profile.max_job_bytes, + profile.max_output_bytes, + profile.max_precision_bits + 1, + profile.max_policy_rungs, + profile.max_knots, + ) + + def test_profile_identity_is_typed_and_total_for_foreign_input(self) -> None: + profile = arb_runtime.arb_runtime_profile_v1() + identity = arb_runtime.runtime_profile_identity_v1(profile) + self.assertIs(type(identity), bytes) + self.assertEqual(identity, arb_runtime.runtime_profile_identity_v1(profile)) + rejected = arb_runtime.runtime_profile_identity_v1(tuple(profile)) + self.assertIs(type(rejected), arb_runtime.ArbRuntimeIdentityRejectedV1) + self.assertEqual( + rejected.reason, + arb_runtime.ArbRuntimeProfileReasonV1.WRONG_TYPE, + ) + + def test_binding_requires_exact_job_and_output_limits(self) -> None: + profile = arb_runtime.arb_runtime_profile_v1() + binding = arb_runtime.ArbRuntimeBindingV1(profile, _limits()) + identity = arb_runtime.runtime_binding_identity_v1(binding) + self.assertIs(type(identity), bytes) + for field in ("max_stdin_bytes", "max_stdout_bytes"): + exact = getattr(_limits(), field) + for delta in (-1, 1): + with self.subTest(field=field, delta=delta), self.assertRaises(ValueError): + arb_runtime.ArbRuntimeBindingV1( + profile, + _limits(**{field: exact + delta}), + ) + + def test_binding_identity_commits_each_variable_nonprofile_limit(self) -> None: + profile = arb_runtime.arb_runtime_profile_v1() + baseline = _limits() + first = arb_runtime.ArbRuntimeBindingV1(profile, baseline) + first_identity = arb_runtime.runtime_binding_identity_v1(first) + self.assertIs(type(first_identity), bytes) + for field in ( + "max_executable_bytes", + "max_argument_bytes", + "max_stderr_bytes", + "wall_timeout_ns", + "memory_max_bytes", + ): + with self.subTest(field=field): + second = arb_runtime.ArbRuntimeBindingV1( + profile, + _limits(**{field: getattr(baseline, field) - 1}), + ) + second_identity = arb_runtime.runtime_binding_identity_v1(second) + self.assertIs(type(second_identity), bytes) + self.assertNotEqual(first_identity, second_identity) + + def test_protocol_documents_the_lane_specific_arb_binding(self) -> None: + reference = (PROOF / "PROTOCOL.md").read_text(encoding="utf-8") + self.assertIn("ArbRuntimeProfileV1", reference) + self.assertIn("ArbRuntimeBindingV1", reference) + self.assertIn("LC-ARB-RUNTIME-V1", reference) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/arb/tests/test_transport.py b/proof/region/v1/arb/tests/test_transport.py new file mode 100644 index 00000000..08a4a6fc --- /dev/null +++ b/proof/region/v1/arb/tests/test_transport.py @@ -0,0 +1,1326 @@ +#!/usr/bin/env python3 +"""Behavioral contract for the causal controller-to-Docker BUILD transport.""" + +from __future__ import annotations + +import ast +from collections.abc import Callable +import hashlib +import io +import inspect +import json +import os +import subprocess +import sys +import tarfile +import tempfile +import textwrap +import unittest +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +ARB = PROOF / "arb" +TESTS = ARB / "tests" +sys.path.insert(0, str(PROOF)) +sys.path.insert(0, str(TESTS)) + +from build import input as build_input # noqa: E402 +from build import transport as build_transport # noqa: E402 +from arb import pipeline # noqa: E402 +import provenance # noqa: E402 +from test_pipeline import ( # noqa: E402 + _docker_capability, + _probe_native_backend, + _request, +) + + +BUILD_RECIPE = ARB / "build.sh" +INNER_BUILD_RECIPE = ARB / "build-inner.sh" +NATIVE_GATE = ARB / "tests" / "native_gate.py" +_TEST_CANONICAL_LIMITS = build_input.CanonicalInputLimitsV1(64, 1024, 4096) + + +def _digest(label: str) -> bytes: + return hashlib.sha256(label.encode("ascii")).digest() + + +def _bundle(length: int = 1024 * 1024) -> build_input.SealedInputV1: + contents = (b"0123456789abcdef" * ((length + 15) // 16))[:length] + return build_input.seal_input_v1(_digest("opaque-binding"), contents) + + +def _backend() -> build_transport.NativeDockerBuildBackendV1: + return build_transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + ) + + +def _native_backend_with_request( + bundle: build_input.SealedInputV1 | None = None, +) -> tuple[ + build_transport.NativeDockerBuildBackendV1, + build_transport.DockerBuildRequestV1, +]: + """One fixture owns the native request shape used by cleanup tests.""" + + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + return backend, build_transport.DockerBuildRequestV1( + 1, + capability, + _bundle(1024) if bundle is None else bundle, + 1024, + ) + + +def _report_released_cid_root( + backend: build_transport.NativeDockerBuildBackendV1, + detail: object = "forced CID-root cleanup failure", +) -> Callable[[object], object]: + """Keep failure fixtures honest: a reported cleanup failure follows release.""" + + release = backend._release_run_lease_v1 + + def release_then_report(lease: object) -> object: + if release(lease) is not None: + raise AssertionError("native CID-root fixture lease did not release") + return detail + + return release_then_report + + +def _observe( + source: str, + bundle: build_input.SealedInputV1, + *, + stdout_limit: int = 2 * 1024 * 1024, + stderr_limit: int = 2 * 1024 * 1024, + timeout_ns: int = 5_000_000_000, +) -> build_transport.DockerBuildProcessObservationV1: + return _backend()._observe_command( + (sys.executable, "-c", source), + stdout_limit=stdout_limit, + stderr_limit=stderr_limit, + timeout_ns=timeout_ns, + input_bundle=bundle, + ) + + +class CanonicalBuildBundleTests(unittest.TestCase): + def test_public_probe_starts_with_a_typed_terminal_outcome(self) -> None: + """A backend cannot leave the public probe in a non-report state.""" + + source = inspect.getsource(build_transport.ControlledBuildTransportV1.probe) + tree = ast.parse(textwrap.dedent(source)) + function = tree.body[0] + if not isinstance(function, ast.FunctionDef): + self.fail("public probe source must remain one function definition") + typed_initializers = [ + node + for node in function.body + if isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == "outcome" + and isinstance(node.annotation, ast.Name) + and node.annotation.id == "DockerCapabilityReportV1" + and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "DockerUnsupportedV1" + ] + self.assertEqual(len(typed_initializers), 1) + self.assertNotIn('raise RuntimeError("probe outcome was not produced")', source) + + def test_probe_releases_its_transient_lease_after_backend_failure(self) -> None: + report = _docker_capability(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + + class FlakyProbeBackend: + def __init__(self) -> None: + self.calls = 0 + + def probe(self) -> object: + self.calls += 1 + if self.calls == 1: + raise ValueError("forced backend failure") + return report + + backend = FlakyProbeBackend() + controller = build_transport.ControlledBuildTransportV1( + policy=pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + backend=backend, + ) + first = controller.probe() + second = controller.probe() + + self.assertIs(type(first), build_transport.DockerUnsupportedV1) + self.assertEqual( + first.reason, + build_transport.DockerBlockerReasonV1.BACKEND_CONTRACT, + ) + self.assertIs(second, report) + self.assertEqual(backend.calls, 2) + + def test_public_input_constructors_use_typed_value_errors(self) -> None: + def reject( + constructor: Callable[[], object], + reason: build_input.InputReasonV1, + field: str, + ) -> None: + with self.assertRaises(build_input.InputErrorV1) as caught: + constructor() + self.assertEqual(caught.exception.reason, reason) + self.assertEqual(caught.exception.field, field) + + def limits( + max_members: object = 1, + max_file_bytes: object = 1, + max_payload_bytes: object = 1, + max_encoded_bytes: object = None, + ) -> object: + return build_input.CanonicalInputLimitsV1( + max_members, + max_file_bytes, + max_payload_bytes, + max_encoded_bytes, + ) + + def seal( + binding_identity: object = _digest("binding"), + contents: object = b"x", + ) -> object: + return build_input.seal_input_v1(binding_identity, contents) + + limit_fields = ( + "max_members", + "max_file_bytes", + "max_payload_bytes", + "max_encoded_bytes", + ) + self.assertIs( + type(build_input.CanonicalInputLimitsV1(1, 1, 1, None)), + build_input.CanonicalInputLimitsV1, + ) + for field, value in ( + ("max_members", True), + ("max_file_bytes", 1.0), + ("max_payload_bytes", object()), + ("max_encoded_bytes", b"1"), + ): + with self.subTest(kind="wrong_type", field=field): + reject( + lambda field=field, value=value: limits(**{field: value}), + build_input.InputReasonV1.WRONG_TYPE, + field, + ) + for field in limit_fields: + for value in (0, 1 << 64): + with self.subTest(kind="invalid_limit", field=field, value=value): + reject( + lambda field=field, value=value: limits(**{field: value}), + build_input.InputReasonV1.INVALID_VALUE, + field, + ) + reject( + lambda: limits(max_payload_bytes=(1 << 64) - 1), + build_input.InputReasonV1.INVALID_VALUE, + "max_encoded_bytes", + ) + + for field, constructor in ( + ("binding_identity", lambda: seal(bytearray(_digest("binding")))), + ("contents", lambda: seal(contents=bytearray(b"x"))), + ): + with self.subTest(kind="wrong_type", field=field): + reject(constructor, build_input.InputReasonV1.WRONG_TYPE, field) + for field, constructor in ( + ("binding_identity", lambda: seal(bytes(32))), + ("binding_identity", lambda: seal(b"x" * 31)), + ("contents", lambda: seal(contents=b"")), + ): + with self.subTest(kind="invalid_value", field=field): + reject(constructor, build_input.InputReasonV1.INVALID_VALUE, field) + + for constructor in ( + lambda: build_input.CanonicalInputLimitsV1(1, 1), + lambda: build_input.seal_input_v1(_digest("binding")), + lambda: build_input.SealedInputV1( + _digest("binding"), + b"x", + _token=object(), + ), + ): + with self.subTest(kind="private_or_call_shape"): + with self.assertRaises(TypeError): + constructor() + + def test_bundle_is_reproducible_normalized_ustar_with_no_host_authority(self) -> None: + request = _request() + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + first = pipeline._seal_build_input_bundle_v1(request, policy) + second = pipeline._seal_build_input_bundle_v1(request, policy) + + self.assertIsNot(first, second) + self.assertIs(first.contents, first.contents) + self.assertEqual(first.contents, second.contents) + self.assertEqual(first.sha256, second.sha256) + self.assertEqual(first.binding_identity, second.binding_identity) + self.assertTrue(pipeline.arb_input_is_bound_v1(request, policy, first)) + self.assertEqual( + first.sha256.hex(), + "19b32598d41b021a792e54b807f0143940108055591ca5ef6ecb6a826dec576d", + ) + self.assertEqual(first.length, 174_080) + + source_entries = tuple( + ( + f"inputs/{lock.root_prefix[:-1]}/{relative}", + mode, + contents, + ) + for lock, admitted in zip( + request.source_lock.sources, + request.admitted_sources.sources, + strict=True, + ) + for relative, mode, contents in provenance.materialize_admitted_source_files_v1( + lock, + admitted, + ) + ) + workspace_entries = tuple( + ( + "inputs/formula.generated.c" + if item.path == pipeline.GENERATED_FORMULA_PATH_V1 + else f"workspace/{item.path}", + item.mode, + item.contents, + ) + for item in request.build_sources.files + if item.path + not in (pipeline.FORMULA_SPEC_PATH_V1, pipeline.FORMULA_GENERATOR_PATH_V1) + ) + expected_entries = tuple(sorted(source_entries + workspace_entries)) + expected_files = {path: (mode, body) for path, mode, body in expected_entries} + expected_directories = { + "/".join(path.split("/")[:index]) + for path in expected_files + for index in range(1, len(path.split("/"))) + } + expected_order = tuple( + sorted(expected_directories, key=lambda value: (value.count("/"), value)) + ) + tuple(sorted(expected_files)) + + with tarfile.open(fileobj=io.BytesIO(first.contents), mode="r:") as archive: + members = tuple(archive) + self.assertFalse(archive.pax_headers) + self.assertEqual(tuple(member.name for member in members), expected_order) + self.assertEqual(len({member.name for member in members}), len(members)) + for member in members: + with self.subTest(path=member.name): + self.assertEqual(member.uid, 0) + self.assertEqual(member.gid, 0) + self.assertEqual(member.uname, "") + self.assertEqual(member.gname, "") + self.assertEqual(member.mtime, 0) + self.assertFalse(member.pax_headers) + self.assertFalse(member.issym() or member.islnk()) + if member.isdir(): + self.assertIn(member.name, expected_directories) + self.assertEqual(member.mode, 0o755) + self.assertEqual(member.size, 0) + else: + self.assertTrue(member.isreg()) + mode, body = expected_files[member.name] + self.assertEqual(member.mode, mode) + self.assertEqual(member.size, len(body)) + stream = archive.extractfile(member) + self.assertIsNotNone(stream) + self.assertEqual(stream.read(), body) + + def test_canonical_encoder_rejects_reorder_collision_and_unencodable_path(self) -> None: + def reject( + values: object, + reason: build_input.InputReasonV1, + field: str, + limits: build_input.CanonicalInputLimitsV1 = _TEST_CANONICAL_LIMITS, + ) -> None: + with self.assertRaises(build_input.InputErrorV1) as caught: + build_input.canonical_ustar_v1(values, limits) + self.assertEqual(caught.exception.reason, reason) + self.assertEqual(caught.exception.field, field) + + entries = (("a/b", 0o644, b"x"), ("c", 0o755, b"y")) + encoded = build_input.canonical_ustar_v1(entries, _TEST_CANONICAL_LIMITS) + self.assertEqual( + hashlib.sha256(encoded).hexdigest(), + "11bc313cba907e89535876eb8ce46194472367007053ab58b723338676f99427", + ) + private_mode = build_input.canonical_ustar_v1( + (("private", 0o700, b"x"),), + _TEST_CANONICAL_LIMITS, + ) + with tarfile.open(fileobj=io.BytesIO(private_mode), mode="r:") as archive: + member = archive.getmember("private") + self.assertTrue(member.isreg()) + self.assertEqual(member.mode, 0o700) + for hostile, reason, field in ( + ( + tuple(reversed(entries)), + build_input.InputReasonV1.NONCANONICAL_SET, + "entries", + ), + ( + (("a", 0o644, b"x"), ("a/b", 0o644, b"y")), + build_input.InputReasonV1.NONCANONICAL_SET, + "a", + ), + ( + (("A", 0o644, b"x"), ("a", 0o644, b"y")), + build_input.InputReasonV1.NONCANONICAL_SET, + "a", + ), + ( + (("a" * 256, 0o644, b"x"),), + build_input.InputReasonV1.INVALID_PATH, + "a" * 256, + ), + ( + ((1, 0o644, b"x"),), + build_input.InputReasonV1.INVALID_PATH, + "path", + ), + ( + ((["a"], 0o644, b"x"),), + build_input.InputReasonV1.INVALID_PATH, + "path", + ), + ( + (("a" * 101, 0o644, b"x"),), + build_input.InputReasonV1.INVALID_PATH, + "a" * 101, + ), + ( + (("A", 0o644, b"x"), ("a/b", 0o644, b"y")), + build_input.InputReasonV1.NONCANONICAL_SET, + "A", + ), + ): + with self.subTest(hostile=repr(hostile)): + reject(hostile, reason, field) + + resource_cases = ( + ( + (("a", 0o644, b"x"), ("b", 0o644, b"y")), + build_input.CanonicalInputLimitsV1(1, 1, 2), + "max_members", + ), + ( + (("a", 0o644, b"xy"),), + build_input.CanonicalInputLimitsV1(1, 1, 2), + "max_file_bytes", + ), + ( + (("a", 0o644, b"x"), ("b", 0o644, b"y")), + build_input.CanonicalInputLimitsV1(2, 1, 1), + "max_payload_bytes", + ), + ( + (("a/b", 0o644, b"x"),), + build_input.CanonicalInputLimitsV1(1, 1, 1), + "max_members", + ), + ( + (("a", 0o644, b"x"),), + build_input.CanonicalInputLimitsV1(1, 1, 1, 10_239), + "max_encoded_bytes", + ), + ) + for values, limits, field in resource_cases: + with self.subTest(resource=field): + reject( + values, + build_input.InputReasonV1.RESOURCE_LIMIT, + field, + limits, + ) + exact_cap = build_input.CanonicalInputLimitsV1(1, 1, 1, 10_240) + self.assertEqual( + len(build_input.canonical_ustar_v1((("a", 0o644, b"x"),), exact_cap)), + exact_cap.max_encoded_bytes, + ) + + def test_long_implicit_directory_uses_the_ustar_trailing_separator(self) -> None: + directory = "d" * 155 + entries = ((f"{directory}/f", 0o644, b"x"),) + limits = build_input.CanonicalInputLimitsV1(2, 1, 1) + + encoded = build_input.canonical_ustar_v1(entries, limits) + + with tarfile.open(fileobj=io.BytesIO(encoded), mode="r:") as archive: + self.assertEqual( + tuple(member.name for member in archive), + (directory, f"{directory}/f"), + ) + with self.assertRaises(build_input.InputErrorV1) as caught: + build_input.canonical_ustar_v1( + ((f"{'d' * 156}/f", 0o644, b"x"),), + limits, + ) + self.assertEqual(caught.exception.reason, build_input.InputReasonV1.INVALID_PATH) + self.assertEqual(caught.exception.field, f"{'d' * 156}/f") + + def test_omission_or_content_mutation_changes_bundle_identity(self) -> None: + entries = (("a", 0o644, b"x"), ("b", 0o644, b"y")) + original = build_input.canonical_ustar_v1(entries, _TEST_CANONICAL_LIMITS) + omitted = build_input.canonical_ustar_v1( + entries[:1], + _TEST_CANONICAL_LIMITS, + ) + mutated = build_input.canonical_ustar_v1( + (("a", 0o644, b"x"), ("b", 0o644, b"z")), + _TEST_CANONICAL_LIMITS, + ) + identities = { + ( + sealed.binding_identity, + sealed.sha256, + ) + for body in (original, omitted, mutated) + for sealed in ( + build_input.seal_input_v1(_digest("opaque-binding"), body), + ) + } + self.assertEqual(len(identities), 3) + + def test_replayed_source_coordinates_must_match_the_admitted_capability(self) -> None: + request = _request() + admitted = request.admitted_sources.sources[0] + original = admitted.tree_identity + object.__setattr__(admitted, "tree_identity", _digest("mutated-tree")) + try: + with self.assertRaises(provenance.ProvenanceErrorV1): + provenance.materialize_admitted_source_files_v1( + request.source_lock.sources[0], + admitted, + ) + finally: + object.__setattr__(admitted, "tree_identity", original) + + def test_transport_authorities_cannot_be_directly_forged(self) -> None: + bundle = _bundle(1024) + with self.assertRaises(TypeError): + build_transport.BuildInputTransferProgressV1( + bundle.binding_identity, + bundle.length, + bundle.sha256, + bundle.length, + bundle.sha256, + ) + with self.assertRaises(TypeError): + build_transport.BuildInputTransferV1(object()) + with self.assertRaises(TypeError): + build_transport.DockerBuildExitedV1(0, b"binary", b"", object()) + with self.assertRaises(TypeError): + build_transport.DockerBuildPolicyV1( + "gcc@sha256:bad@sha256:" + "0" * 64, + *pipeline.ARB_BUILD_TRANSPORT_POLICY_V1[1:], + ) + report = _docker_capability() + self.assertFalse(hasattr(report, "__dict__")) + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(report, "host_user", (0, 0)) + forged_policy = tuple.__new__(build_transport.DockerBuildPolicyV1, ()) + with self.assertRaises(TypeError): + build_transport.ControlledBuildTransportV1( + policy=forged_policy, + backend=object(), + ) + + class ForgedProbeBackend: + def probe(self) -> object: + return tuple.__new__(build_transport.DockerSupportedV1, ()) + + probed = build_transport.ControlledBuildTransportV1( + policy=pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + backend=ForgedProbeBackend(), + ).probe() + self.assertIs(type(probed), build_transport.DockerUnsupportedV1) + self.assertEqual( + probed.reason, + build_transport.DockerBlockerReasonV1.BACKEND_CONTRACT, + ) + + class ForgedProcessBackend: + def probe(self) -> object: + return report + + def run_build(self, _request: object) -> object: + return tuple.__new__(build_transport.DockerBuildExitedV1, ()) + + controller = build_transport.ControlledBuildTransportV1( + policy=pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + backend=ForgedProcessBackend(), + ) + observed_report = controller.probe() + self.assertIs(type(observed_report), build_transport.DockerSupportedV1) + rejected = controller.build( + observed_report, + bundle, + 1, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + self.assertIs(type(rejected), build_transport.BuildRejectedV1) + self.assertEqual( + rejected.reason, + build_transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + + +class BuildInputObserverTests(unittest.TestCase): + def test_positive_partial_writes_are_normal_and_commit_exact_transfer(self) -> None: + bundle = _bundle(256 * 1024) + real_write = os.write + + def partial_write(descriptor: int, contents: object) -> int: + return real_write(descriptor, contents[:997]) + + with mock.patch.object(build_transport.os, "write", side_effect=partial_write): + result = _observe( + "import hashlib,sys; d=sys.stdin.buffer.read(); " + "sys.stdout.buffer.write(hashlib.sha256(d).digest()); " + "sys.stderr.buffer.write(b'observed')", + bundle, + ) + + self.assertIs(type(result), build_transport.DockerBuildExitedV1, result) + self.assertEqual(result.stdout, bundle.sha256) + self.assertEqual(result.stderr, b"observed") + self.assertEqual(result.input_transfer.bundle_identity, bundle.binding_identity) + self.assertEqual(result.input_transfer.expected_length, bundle.length) + self.assertEqual(result.input_transfer.expected_sha256, bundle.sha256) + self.assertEqual(result.input_transfer.written_length, bundle.length) + self.assertEqual(result.input_transfer.written_sha256, bundle.sha256) + + def test_zero_write_and_epipe_are_typed_with_exact_partial_progress(self) -> None: + bundle = _bundle() + with mock.patch.object(build_transport.os, "write", return_value=0): + zero = _observe("import sys; sys.stdin.buffer.read()", bundle) + self.assertIs(type(zero), build_transport.DockerBuildInputRejectedV1, zero) + self.assertEqual(zero.written_length, 0) + self.assertEqual(zero.written_sha256, hashlib.sha256(b"").digest()) + + closed = _observe("import os,time; os.close(0); time.sleep(1)", bundle) + self.assertIs(type(closed), build_transport.DockerBuildInputRejectedV1, closed) + self.assertLess(closed.written_length, bundle.length) + self.assertEqual( + closed.written_sha256, + hashlib.sha256(bundle.contents[: closed.written_length]).digest(), + ) + + def test_final_stdin_close_failure_is_a_typed_observer_failure(self) -> None: + real_popen = subprocess.Popen + + class CloseFailsOnce: + def __init__(self, stream: object) -> None: + self._stream = stream + + @property + def closed(self) -> bool: + return self._stream.closed + + def fileno(self) -> int: + return self._stream.fileno() + + def close(self) -> None: + self._stream.close() + raise BrokenPipeError("forced close failure") + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + self.assertIsNotNone(process.stdin) + process.stdin = CloseFailsOnce(process.stdin) + return process + + with mock.patch.object(build_transport.subprocess, "Popen", side_effect=spawn): + result = _observe( + "import time; time.sleep(1)", + _bundle(2 * 1024 * 1024), + timeout_ns=100_000_000, + ) + + self.assertIs(type(result), build_transport.DockerBuildObserverFailureV1, result) + + def test_full_duplex_backpressure_does_not_deadlock_or_drop_bytes(self) -> None: + bundle = _bundle(512 * 1024) + result = _observe( + "import os\n" + "while True:\n" + " d=os.read(0,4096)\n" + " if not d: break\n" + " os.write(1,b'o'*len(d))\n" + " os.write(2,b'e'*len(d))\n", + bundle, + ) + self.assertIs(type(result), build_transport.DockerBuildExitedV1, result) + self.assertEqual(len(result.stdout), bundle.length) + self.assertEqual(len(result.stderr), bundle.length) + self.assertEqual(result.input_transfer.written_sha256, bundle.sha256) + + def test_timeout_and_output_limit_preserve_input_progress(self) -> None: + bundle = _bundle() + timed = _observe( + "import os,time; os.read(0,1); time.sleep(2)", + bundle, + timeout_ns=100_000_000, + ) + self.assertIs(type(timed), build_transport.DockerBuildTimedOutV1, timed) + self.assertIs(type(timed.input_progress), build_transport.BuildInputTransferProgressV1) + self.assertGreater(timed.input_progress.written_length, 0) + self.assertLess(timed.input_progress.written_length, bundle.length) + + limited = _observe( + "import os,time; os.write(1,b'x'*65536); time.sleep(2)", + bundle, + stdout_limit=8, + ) + self.assertIs(type(limited), build_transport.DockerBuildOutputLimitV1, limited) + self.assertEqual(limited.stream, build_transport.DockerOutputStreamV1.STDOUT) + self.assertEqual(limited.stdout, b"x" * 8) + self.assertIs(type(limited.input_progress), build_transport.BuildInputTransferProgressV1) + + def test_cleanup_failure_preserves_input_trigger_and_progress(self) -> None: + bundle = _bundle() + backend = _backend() + lease = backend._next_run_lease_v1( + _docker_capability( + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + docker_path=Path("/bin/true"), + ) + ) + try: + with mock.patch.object( + backend, + "_cleanup_container", + return_value="forced cleanup failure", + ): + result = backend._observe_command( + (sys.executable, "-c", "import os,time; os.close(0); time.sleep(1)"), + stdout_limit=1024, + stderr_limit=1024, + timeout_ns=5_000_000_000, + lease=lease, + input_bundle=bundle, + ) + finally: + backend._release_run_lease_v1(lease) + self.assertIs(type(result), build_transport.DockerBuildCleanupFailureV1, result) + self.assertEqual(result.trigger, build_transport.DockerCleanupTriggerV1.INPUT_TRANSFER) + self.assertEqual(result.detail, "forced cleanup failure") + self.assertIs(type(result.input_progress), build_transport.BuildInputTransferProgressV1) + self.assertLess(result.input_progress.written_length, bundle.length) + + +class SealedBuildTransportContractTests(unittest.TestCase): + def test_closed_user_mode_keeps_policy_identity_when_enum_payload_is_tampered( + self, + ) -> None: + """The wire coordinate follows the admitted member, never mutable Enum data.""" + + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + baseline_identity = build_transport.transport_policy_identity_v1(policy) + user_mode = build_transport.DockerUserModeV1.HOST_EFFECTIVE_IDS + original_value = user_mode._value_ + object.__setattr__( + user_mode, + "_value_", + "forged_host_effective_ids", + ) + try: + self.assertEqual(user_mode.value, "forged_host_effective_ids") + self.assertTrue(build_transport.docker_policy_is_valid_v1(policy)) + self.assertEqual( + build_transport.transport_policy_identity_v1(policy), + baseline_identity, + ) + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + self.assertIs(backend._policy.user_mode, user_mode) + finally: + object.__setattr__(user_mode, "_value_", original_value) + + def test_diagnostic_details_have_one_strict_admission_law(self) -> None: + constructors = ( + ( + "unsupported", + lambda detail: build_transport.DockerUnsupportedV1( + build_transport.DockerBlockerReasonV1.BACKEND_CONTRACT, + detail, + ), + ), + ( + "observer-failure", + lambda detail: build_transport.DockerBuildObserverFailureV1( + detail, + b"", + b"", + ), + ), + ( + "cleanup-record", + lambda detail: build_transport.CleanupFailureRecordV1( + build_transport.CleanupResourceV1.DOCKER_CID_ROOT, + detail, + ), + ), + ) + + class DetailSubclass(str): + pass + + for constructor_name, constructor in constructors: + with self.subTest(constructor=constructor_name, detail="valid"): + self.assertIsNotNone(constructor("valid diagnostic detail")) + for name, invalid_detail in ( + ("empty", ""), + ("subclass", DetailSubclass("detail")), + ( + "too-long", + "x" * (build_transport._DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1 + 1), + ), + ("wrong-type", object()), + ): + with self.subTest(constructor=constructor_name, detail=name): + with self.assertRaises(TypeError): + constructor(invalid_detail) + + def test_successful_probe_keeps_machine_readable_stdout_despite_cli_warning(self) -> None: + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + with tempfile.TemporaryDirectory() as temporary: + docker_path = Path(temporary).resolve() / "docker" + docker_path.write_bytes(b"fixture") + docker_path.chmod(0o755) + backend = build_transport.NativeDockerBuildBackendV1( + docker_path, + policy, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + image = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + separators=(",", ":"), + ).encode("ascii") + with mock.patch.object( + backend, + "_observe_command", + side_effect=( + build_transport._docker_command_exited_v1( + 0, + b'{"Version":"fixture"}', + b"warning: CLI hint\n", + ), + build_transport._docker_command_exited_v1( + 0, + image, + b"warning: local metadata\n", + ), + ), + ): + capability = backend.probe() + + self.assertIs(type(capability), build_transport.DockerSupportedV1) + self.assertEqual( + capability.daemon_observation.server_stdout, + b'{"Version":"fixture"}', + ) + + def test_controller_owns_one_sealed_bundle_for_both_builds(self) -> None: + public_pipeline_source = inspect.getsource(pipeline.ControlledPipelineV1.build) + owned_pipeline_source = inspect.getsource( + pipeline.ControlledPipelineV1._build_snapshot_v1 + ) + transport_source = inspect.getsource( + build_transport.ControlledBuildTransportV1.build + ) + self.assertEqual( + public_pipeline_source.count("_snapshot_pipeline_operation_v1("), + 1, + ) + self.assertIn("return self._build_snapshot_v1(snapshot)", public_pipeline_source) + self.assertEqual( + owned_pipeline_source.count("_seal_build_input_from_snapshot_v1("), + 1, + ) + self.assertIn("for attempt in (1, 2)", transport_source) + + def test_docker_request_carries_only_semantic_build_coordinates(self) -> None: + fields = set(inspect.signature(build_transport.DockerBuildRequestV1).parameters) + self.assertEqual( + fields, + { + "attempt", + "capability", + "input_bundle", + "max_output_bytes", + }, + ) + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + request = build_transport.DockerBuildRequestV1( + 1, + capability, + _bundle(1024), + 1024, + ) + self.assertFalse(hasattr(request, "cid_file")) + self.assertFalse(hasattr(request, "container_name")) + # The field assertion above proves no cleanup coordinate is modeled. + # This call separately guards the fixed-arity boundary against extras. + with self.assertRaises(TypeError): + build_transport.DockerBuildRequestV1( + 1, + capability, + _bundle(1024), + 1024, + Path("/tmp/foreign.cid"), + "labcolors-arb-build-v1-foreign", + ) + + def test_native_adapter_mints_private_docker_issued_cleanup_authority(self) -> None: + backend, request = _native_backend_with_request() + capability = request.capability + lease = backend._next_run_lease_v1(capability) + try: + command = backend._command_for_v1(request, lease) + + self.assertIn("--cidfile", command) + self.assertNotIn("--name", command) + self.assertFalse(hasattr(lease, "container_name")) + self.assertTrue(lease.cid_file.is_absolute()) + self.assertFalse(lease.cid_file.exists()) + finally: + backend._release_run_lease_v1(lease) + + def test_native_adapter_rejects_malformed_nominal_observations_typed(self) -> None: + raw_observations = ( + ("unknown", object()), + *( + (kind.__name__, tuple.__new__(kind, ())) + for kind in ( + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildTimedOutV1, + build_transport.DockerBuildOutputLimitV1, + build_transport.DockerBuildObserverFailureV1, + build_transport.DockerBuildInputRejectedV1, + build_transport.DockerBuildCleanupFailureV1, + ) + ), + ( + "DockerBuildObserverFailureV1/invalid-progress", + tuple.__new__( + build_transport.DockerBuildObserverFailureV1, + ("forged", b"untrusted stdout", b"untrusted stderr", object()), + ), + ), + ) + + def observe(raw: object, *, cleanup_fails: bool) -> object: + backend, request = _native_backend_with_request() + if not cleanup_fails: + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ): + return backend.run_build(request) + + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=_report_released_cid_root(backend), + ): + return backend.run_build(request) + + for name, raw in raw_observations: + with self.subTest(observation=name, cleanup_fails=False): + without_cleanup_failure = observe(raw, cleanup_fails=False) + self.assertIs( + type(without_cleanup_failure), + build_transport.DockerBuildObserverFailureV1, + ) + self.assertEqual( + without_cleanup_failure.detail, + "native Docker build observation is not canonical", + ) + self.assertEqual(without_cleanup_failure.stdout, b"") + self.assertEqual(without_cleanup_failure.stderr, b"") + self.assertIsNone(without_cleanup_failure.input_progress) + with self.subTest(observation=name, cleanup_fails=True): + with_cleanup_failure = observe(raw, cleanup_fails=True) + self.assertIs( + type(with_cleanup_failure), + build_transport.DockerBuildObserverFailureV1, + ) + self.assertEqual( + with_cleanup_failure.detail, + "native Docker build observation is not canonical; " + "forced CID-root cleanup failure", + ) + self.assertEqual(with_cleanup_failure.stdout, b"") + self.assertEqual(with_cleanup_failure.stderr, b"") + self.assertIsNone(with_cleanup_failure.input_progress) + + def test_native_adapter_rejects_a_preexisting_cid_root_claim(self) -> None: + bundle = _bundle(1024) + backend, request = _native_backend_with_request(bundle) + raw = build_transport.DockerBuildCleanupFailureV1( + build_transport.DockerCleanupTriggerV1.PROCESS_EXIT, + ( + build_transport.CleanupFailureRecordV1( + build_transport.CleanupResourceV1.DOCKER_CID_ROOT, + "forged prior CID-root cleanup failure", + ), + ), + b"retained stdout", + b"retained stderr", + build_transport._build_input_progress_v1( + bundle, + bundle.length, + bundle.sha256, + ), + ) + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ): + without_cleanup_failure = backend.run_build(request) + + self.assertIs( + type(without_cleanup_failure), + build_transport.DockerBuildObserverFailureV1, + ) + self.assertEqual( + without_cleanup_failure.detail, + "native Docker build observation already contains a CID-root " + "cleanup failure", + ) + self.assertEqual(without_cleanup_failure.stdout, b"retained stdout") + self.assertEqual(without_cleanup_failure.stderr, b"retained stderr") + self.assertIs(without_cleanup_failure.input_progress, raw.input_progress) + + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=_report_released_cid_root(backend), + ): + with_cleanup_failure = backend.run_build(request) + + self.assertIs( + type(with_cleanup_failure), + build_transport.DockerBuildCleanupFailureV1, + ) + self.assertEqual( + with_cleanup_failure.trigger, + build_transport.DockerCleanupTriggerV1.OBSERVER_FAILURE, + ) + self.assertEqual( + with_cleanup_failure.failures, + ( + build_transport.CleanupFailureRecordV1( + build_transport.CleanupResourceV1.DOCKER_CID_ROOT, + "forced CID-root cleanup failure", + ), + ), + ) + self.assertEqual(with_cleanup_failure.stdout, b"retained stdout") + self.assertEqual(with_cleanup_failure.stderr, b"retained stderr") + self.assertIs(with_cleanup_failure.input_progress, raw.input_progress) + + def test_native_adapter_bounds_unknown_observation_cleanup_detail(self) -> None: + prefix = "native Docker build observation is not canonical; " + for name, detail, expected_detail in ( + ( + "retained", + "forced CID-root cleanup failure", + prefix + "forced CID-root cleanup failure", + ), + ( + "bounded", + "x" * build_transport._DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1, + prefix + + "x" + * ( + build_transport._DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1 + - len(prefix) + ), + ), + ): + with self.subTest(detail=name): + result = ( + build_transport.NativeDockerBuildBackendV1._with_cid_root_cleanup_failure_v1( + object(), + detail, + ) + ) + + self.assertIs(type(result), build_transport.DockerBuildObserverFailureV1) + self.assertEqual(result.detail, expected_detail) + self.assertEqual(result.stdout, b"") + self.assertEqual(result.stderr, b"") + self.assertIsNone(result.input_progress) + + def test_native_adapter_appends_cid_root_to_canonical_cleanup_prefix(self) -> None: + bundle = _bundle(1024) + backend, request = _native_backend_with_request(bundle) + progress = build_transport._build_input_progress_v1( + bundle, + bundle.length, + bundle.sha256, + ) + raw = build_transport.DockerBuildCleanupFailureV1( + build_transport.DockerCleanupTriggerV1.PROCESS_EXIT, + ( + build_transport.CleanupFailureRecordV1( + build_transport.CleanupResourceV1.DOCKER_CLI_PROCESS, + "prior CLI cleanup failure", + ), + build_transport.CleanupFailureRecordV1( + build_transport.CleanupResourceV1.DOCKER_CONTAINER, + "prior container cleanup failure", + ), + ), + b"retained stdout", + b"retained stderr", + progress, + ) + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=_report_released_cid_root(backend), + ): + result = backend.run_build(request) + + self.assertIs(type(result), build_transport.DockerBuildCleanupFailureV1) + self.assertEqual(result.trigger, build_transport.DockerCleanupTriggerV1.PROCESS_EXIT) + self.assertEqual( + tuple(record.resource for record in result.failures), + ( + build_transport.CleanupResourceV1.DOCKER_CLI_PROCESS, + build_transport.CleanupResourceV1.DOCKER_CONTAINER, + build_transport.CleanupResourceV1.DOCKER_CID_ROOT, + ), + ) + self.assertEqual( + tuple(record.detail for record in result.failures), + ( + "prior CLI cleanup failure", + "prior container cleanup failure", + "forced CID-root cleanup failure", + ), + ) + self.assertEqual(result.stdout, b"retained stdout") + self.assertEqual(result.stderr, b"retained stderr") + self.assertIs(result.input_progress, progress) + + def test_native_adapter_reowns_invalid_cid_cleanup_details(self) -> None: + bundle = _bundle(1024) + backend, request = _native_backend_with_request(bundle) + transfer = build_transport._completed_build_input_transfer_v1( + bundle, + bundle.length, + bundle.sha256, + ) + raw = build_transport._docker_build_exited_v1( + 0, + b"built stdout", + b"built stderr", + transfer, + ) + for name, invalid_detail in ( + ("empty", ""), + ("wrong-type", object()), + ( + "too-long", + "x" * (build_transport._DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1 + 1), + ), + ): + with self.subTest(detail=name): + + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=_report_released_cid_root( + backend, + invalid_detail, + ), + ): + result = backend.run_build(request) + + self.assertIs(type(result), build_transport.DockerBuildCleanupFailureV1) + self.assertEqual( + result.detail, + "native Docker CID root cleanup detail is not canonical", + ) + self.assertEqual(result.stdout, b"built stdout") + self.assertEqual(result.stderr, b"built stderr") + self.assertEqual( + result.input_progress, + build_transport._build_input_progress_v1( + bundle, + bundle.length, + bundle.sha256, + ), + ) + + def test_native_adapter_keeps_dual_failure_typed_at_detail_limit(self) -> None: + backend, request = _native_backend_with_request() + raw = build_transport.DockerBuildObserverFailureV1( + "x" * build_transport._DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1, + b"retained stdout", + b"retained stderr", + ) + with mock.patch.object( + backend, + "_observe_command", + return_value=raw, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=_report_released_cid_root(backend), + ): + result = backend.run_build(request) + + self.assertIs(type(result), build_transport.DockerBuildObserverFailureV1) + self.assertEqual( + result.detail, + "native Docker build observation and CID root cleanup both failed", + ) + self.assertEqual(result.stdout, b"retained stdout") + self.assertEqual(result.stderr, b"retained stderr") + + def test_native_adapter_releases_before_propagating_first_canonicalization_interruption( + self, + ) -> None: + backend = build_transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + capability = _probe_native_backend( + backend, + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + request = build_transport.DockerBuildRequestV1( + 1, + capability, + _bundle(1024), + 1024, + ) + release = backend._release_run_lease_v1 + released: list[None] = [] + + class FalseyInterrupt(BaseException): + def __bool__(self) -> bool: + return False + + original = FalseyInterrupt("first interruption") + + def release_then_record(lease: object) -> None: + self.assertIsNone(release(lease)) + released.append(None) + raise KeyboardInterrupt("later cleanup interruption") + + with mock.patch.object( + backend, + "_observe_command", + return_value=object(), + ), mock.patch.object( + build_transport, + "_canonical_process_observation_v1", + side_effect=original, + ), mock.patch.object( + backend, + "_release_run_lease_v1", + side_effect=release_then_record, + ): + with self.assertRaises(FalseyInterrupt) as raised: + backend.run_build(request) + + self.assertIs(raised.exception, original) + self.assertEqual(released, [None]) + + def test_recipe_is_transport_agnostic_and_bootstrap_owns_binary_stdout(self) -> None: + source = INNER_BUILD_RECIPE.read_text(encoding="utf-8") + self.assertIn("readonly inputs=/build/snapshot/inputs", source) + self.assertIn("readonly workspace=/build/snapshot/workspace", source) + self.assertIn("readonly build=/build/work", source) + self.assertNotIn("/out", source) + self.assertNotIn(">&3", source) + self.assertIn("exec 3>&1", pipeline._BUILD_BOOTSTRAP_V1) + self.assertIn("/build/work/arb-evaluator-v1 >&3", pipeline._BUILD_BOOTSTRAP_V1) + + def test_native_gate_executes_the_one_shot_receipt_controller(self) -> None: + gate_source = NATIVE_GATE.read_text(encoding="utf-8") + receipt_source = (TESTS / "test_receipt.py").read_text(encoding="utf-8") + self.assertIn('"receipt"', gate_source) + self.assertIn("NativeSourceBoundReceiptIntegrationTests", gate_source) + self.assertIn("SourceBoundArbControllerV1", receipt_source) + self.assertIn("SourceBoundEvaluatorReceiptV1", receipt_source) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/build/__init__.py b/proof/region/v1/build/__init__.py new file mode 100644 index 00000000..dd2df699 --- /dev/null +++ b/proof/region/v1/build/__init__.py @@ -0,0 +1 @@ +"""Owned shared BUILD package; public contracts live in focused leaf modules.""" diff --git a/proof/region/v1/build/input.py b/proof/region/v1/build/input.py new file mode 100644 index 00000000..8352792e --- /dev/null +++ b/proof/region/v1/build/input.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +"""Canonical BUILD input bytes without engine or recipe semantics.""" + +from __future__ import annotations + +import hashlib +import io +import tarfile +from dataclasses import dataclass +from enum import StrEnum +from typing import NoReturn + + +_SEALED_INPUT_TOKEN = object() +_USTAR_BLOCK_BYTES = 512 +_USTAR_RECORD_BYTES = 20 * _USTAR_BLOCK_BYTES +_USTAR_EOF_BLOCKS = 2 +# Канонический input сохраняет privacy bit допущенного regular file: замена +# 0700 на 0755 незаметно меняла бы owned bytes и смысл сборки. +_REGULAR_FILE_MODES_V1 = frozenset((0o644, 0o700, 0o755)) + + +def _valid_digest(value: object) -> bool: + return type(value) is bytes and len(value) == 32 and value != bytes(32) + + +class InputReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + INVALID_VALUE = "invalid_value" + INVALID_PATH = "invalid_path" + INVALID_MODE = "invalid_mode" + NONCANONICAL_SET = "noncanonical_set" + RESOURCE_LIMIT = "resource_limit" + + +@dataclass(frozen=True) +class InputErrorV1(ValueError): + reason: InputReasonV1 + field: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.field}" + + +def _fail(reason: InputReasonV1, field_name: str) -> NoReturn: + raise InputErrorV1(reason, field_name) + + +def _positive_u64(value: object, field_name: str) -> int: + if type(value) is not int: + _fail(InputReasonV1.WRONG_TYPE, field_name) + if value <= 0 or value >= 1 << 64: + _fail(InputReasonV1.INVALID_VALUE, field_name) + return value + + +def _logical_path(value: object) -> str: + if type(value) is not str or not value or value.startswith("/") or "\\" in value: + _fail(InputReasonV1.INVALID_PATH, "path") + try: + encoded = value.encode("ascii") + except UnicodeEncodeError: + _fail(InputReasonV1.INVALID_PATH, "path") + if ( + len(encoded) > 4096 + or any(byte < 0x20 or byte == 0x7F for byte in encoded) + or any(part in ("", ".", "..") for part in value.split("/")) + ): + _fail(InputReasonV1.INVALID_PATH, "path") + return value + + +class CanonicalInputLimitsV1(tuple): + """Caller-owned resource bounds for one in-memory canonical archive.""" + + __slots__ = () + + def __new__( + cls, + max_members: int, + max_file_bytes: int, + max_payload_bytes: int, + max_encoded_bytes: int | None = None, + ) -> CanonicalInputLimitsV1: + max_members = _positive_u64(max_members, "max_members") + max_file_bytes = _positive_u64(max_file_bytes, "max_file_bytes") + max_payload_bytes = _positive_u64(max_payload_bytes, "max_payload_bytes") + if max_encoded_bytes is None: + # USTAR adds one header block per member, at most one partial data + # block per member, two EOF blocks, then pads to one record. This + # is derived from the caller's bounds, not a fixture-specific cap. + maximum_unpadded = ( + max_payload_bytes + + (2 * _USTAR_BLOCK_BYTES - 1) * max_members + + _USTAR_EOF_BLOCKS * _USTAR_BLOCK_BYTES + ) + max_encoded_bytes = _round_up( + maximum_unpadded, + _USTAR_RECORD_BYTES, + ) + max_encoded_bytes = _positive_u64(max_encoded_bytes, "max_encoded_bytes") + return tuple.__new__( + cls, + ( + max_members, + max_file_bytes, + max_payload_bytes, + max_encoded_bytes, + ), + ) + + @property + def max_members(self) -> int: + return self[0] + + @property + def max_file_bytes(self) -> int: + return self[1] + + @property + def max_payload_bytes(self) -> int: + return self[2] + + @property + def max_encoded_bytes(self) -> int: + return self[3] + + +def _limits_are_valid(value: object) -> bool: + if type(value) is not CanonicalInputLimitsV1: + return False + try: + return tuple(CanonicalInputLimitsV1(*tuple(value))) == tuple(value) + except Exception: + return False + + +def _ustar_path_is_encodable(path: str, *, directory: bool = False) -> bool: + encoded = path.encode("ascii") + if directory: + # tarfile writes DIRTYPE without a trailing separator as one with it; + # the USTAR prefix split must validate the exact emitted header name. + encoded += b"/" + if len(encoded) <= 100: + return True + return any( + 0 < index <= 155 and len(encoded) - index - 1 <= 100 + for index, byte in enumerate(encoded) + if byte == ord("/") + ) + + +def _round_up(value: int, quantum: int) -> int: + return ((value + quantum - 1) // quantum) * quantum + + +def _encoded_ustar_length( + entries: tuple[tuple[str, int, bytes], ...], + directory_count: int, +) -> int: + data_bytes = sum( + _round_up(len(contents), _USTAR_BLOCK_BYTES) + for _path, _mode, contents in entries + ) + raw_bytes = ( + (len(entries) + directory_count + _USTAR_EOF_BLOCKS) + * _USTAR_BLOCK_BYTES + + data_bytes + ) + return _round_up(raw_bytes, _USTAR_RECORD_BYTES) + + +class SealedInputV1(tuple): + """Owned exact bytes carrying only integrity and an opaque caller binding.""" + + __slots__ = () + + def __new__( + cls, + binding_identity: bytes, + contents: bytes, + *, + _token: object, + ) -> SealedInputV1: + if _token is not _SEALED_INPUT_TOKEN: + raise TypeError("SealedInputV1 is created only by seal_input_v1") + if not _valid_digest(binding_identity): + raise TypeError("binding_identity must be one opaque nonzero digest") + if type(contents) is not bytes or not contents: + raise TypeError("sealed input must own nonempty exact bytes") + return tuple.__new__( + cls, + ( + binding_identity, + hashlib.sha256(contents).digest(), + len(contents), + contents, + ), + ) + + @property + def binding_identity(self) -> bytes: + return self[0] + + @property + def sha256(self) -> bytes: + return self[1] + + @property + def length(self) -> int: + return self[2] + + @property + def contents(self) -> bytes: + return self[3] + + +def seal_input_v1(binding_identity: bytes, contents: bytes) -> SealedInputV1: + """Seal exact bytes while treating their semantic binding as opaque.""" + + if type(binding_identity) is not bytes: + _fail(InputReasonV1.WRONG_TYPE, "binding_identity") + if not _valid_digest(binding_identity): + _fail(InputReasonV1.INVALID_VALUE, "binding_identity") + if type(contents) is not bytes: + _fail(InputReasonV1.WRONG_TYPE, "contents") + if not contents: + _fail(InputReasonV1.INVALID_VALUE, "contents") + return SealedInputV1( + binding_identity, + contents, + _token=_SEALED_INPUT_TOKEN, + ) + + +def sealed_input_is_intact_v1(value: object) -> bool: + """Recheck byte integrity without interpreting the caller-owned binding.""" + + if type(value) is not SealedInputV1: + return False + try: + return ( + _valid_digest(value.binding_identity) + and type(value.contents) is bytes + and bool(value.contents) + and value.length == len(value.contents) + and value.sha256 == hashlib.sha256(value.contents).digest() + ) + except Exception: + return False + + +def canonical_ustar_v1( + entries: tuple[tuple[str, int, bytes], ...], + limits: CanonicalInputLimitsV1, +) -> bytes: + """Encode one canonical normalized USTAR file tree.""" + + if ( + type(entries) is not tuple + or not entries + or not _limits_are_valid(limits) + ): + _fail(InputReasonV1.NONCANONICAL_SET, "entries") + if len(entries) > limits.max_members: + _fail(InputReasonV1.RESOURCE_LIMIT, "max_members") + parsed: list[tuple[str, int, bytes]] = [] + total_bytes = 0 + for entry in entries: + if type(entry) is not tuple or len(entry) != 3: + _fail(InputReasonV1.WRONG_TYPE, "entries") + path, mode, contents = entry + path = _logical_path(path) + if not _ustar_path_is_encodable(path): + _fail(InputReasonV1.INVALID_PATH, path) + if type(mode) is not int or mode not in _REGULAR_FILE_MODES_V1: + _fail(InputReasonV1.INVALID_MODE, path) + if type(contents) is not bytes: + _fail(InputReasonV1.WRONG_TYPE, path) + total_bytes += len(contents) + if len(contents) > limits.max_file_bytes: + _fail(InputReasonV1.RESOURCE_LIMIT, "max_file_bytes") + if total_bytes > limits.max_payload_bytes: + _fail(InputReasonV1.RESOURCE_LIMIT, "max_payload_bytes") + parsed.append((path, mode, contents)) + owned = tuple(parsed) + paths = tuple(path for path, _mode, _contents in owned) + if paths != tuple(sorted(paths)) or len(set(paths)) != len(entries): + _fail(InputReasonV1.NONCANONICAL_SET, "entries") + directories: set[str] = set() + for path, _mode, _contents in owned: + parts = path.split("/")[:-1] + for length in range(1, len(parts) + 1): + directories.add("/".join(parts[:length])) + for path in directories: + if not _ustar_path_is_encodable(path, directory=True): + _fail(InputReasonV1.INVALID_PATH, path) + namespace: dict[str, tuple[str, str]] = {} + for kind, values in (("directory", tuple(sorted(directories))), ("file", paths)): + for path in values: + folded = path.lower() + prior = namespace.get(folded) + coordinate = (kind, path) + if prior is not None and prior != coordinate: + _fail(InputReasonV1.NONCANONICAL_SET, path) + namespace[folded] = coordinate + if ( + directories.intersection(paths) + or len(directories) + len(owned) > limits.max_members + ): + if directories.intersection(paths): + _fail(InputReasonV1.NONCANONICAL_SET, "file-directory collision") + _fail(InputReasonV1.RESOURCE_LIMIT, "max_members") + encoded_length = _encoded_ustar_length(owned, len(directories)) + if encoded_length > limits.max_encoded_bytes: + _fail(InputReasonV1.RESOURCE_LIMIT, "max_encoded_bytes") + + output = io.BytesIO() + try: + with tarfile.open(fileobj=output, mode="w", format=tarfile.USTAR_FORMAT) as archive: + for path in sorted(directories, key=lambda value: (value.count("/"), value)): + member = tarfile.TarInfo(path) + member.type = tarfile.DIRTYPE + member.mode = 0o755 + member.uid = 0 + member.gid = 0 + member.uname = "" + member.gname = "" + member.mtime = 0 + member.size = 0 + archive.addfile(member) + for path, mode, contents in owned: + member = tarfile.TarInfo(path) + member.type = tarfile.REGTYPE + member.mode = mode + member.uid = 0 + member.gid = 0 + member.uname = "" + member.gname = "" + member.mtime = 0 + member.size = len(contents) + archive.addfile(member, io.BytesIO(contents)) + except (OSError, OverflowError, tarfile.TarError, ValueError): + _fail(InputReasonV1.INVALID_PATH, "USTAR encoding") + encoded = output.getvalue() + if len(encoded) != encoded_length: + _fail(InputReasonV1.NONCANONICAL_SET, "USTAR size mismatch") + return encoded diff --git a/proof/region/v1/build/transport.py b/proof/region/v1/build/transport.py new file mode 100644 index 00000000..436c8248 --- /dev/null +++ b/proof/region/v1/build/transport.py @@ -0,0 +1,3953 @@ +#!/usr/bin/env python3 +"""Engine-neutral, causally observed Docker BUILD transport.""" + +from __future__ import annotations + +import errno as errno_module +import hashlib +import json +import os +import platform +import re +import selectors +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import threading +import time +from enum import StrEnum +from pathlib import Path +from typing import Callable, Protocol, TypeAlias + +from . import input as _build_input + + +# These versioned observer bounds are not physical constants or a claim that +# every client build fits. Changing one requires a transport-version, +# streaming/resource review, and a targeted native high-water gate. A client +# policy may only tighten them. +BUILD_STDOUT_LIMIT_V1 = 16 * 1024 * 1024 +BUILD_STDERR_LIMIT_V1 = 16 * 1024 * 1024 +BUILD_TIMEOUT_NS_V1 = 2 * 60 * 60 * 1_000_000_000 +DOCKER_PROBE_OUTPUT_LIMIT_V1 = 1024 * 1024 +DOCKER_PROBE_TIMEOUT_NS_V1 = 30 * 1_000_000_000 + +# These are observer scheduling/termination mechanics, not successful-build +# evidence coordinates. CPU, RAM and PID containment belong to the declared +# disposable worker, outside this Docker transport. +_IO_CHUNK_BYTES_V1 = 64 * 1024 +_POLL_SLICE_SECONDS_V1 = 0.1 +_PROCESS_STOP_TIMEOUT_SECONDS_V1 = 30 +# Details enter receipts as bounded diagnostic evidence; this avoids allowing +# an adapter error string to become an unbounded transport payload. +_DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1 = 4096 +_INVALID_CID_ROOT_CLEANUP_DETAIL_V1 = ( + "native Docker CID root cleanup detail is not canonical" +) +_OBSERVER_AND_CID_ROOT_CLEANUP_FAILURE_V1 = ( + "native Docker build observation and CID root cleanup both failed" +) +_PATH_TYPE = type(Path("/")) +_NATIVE_CID_ROOT_PREFIX_V1 = "labcolors-docker-cid-" + +_BUILD_INPUT_PROGRESS_TOKEN = object() +_BUILD_INPUT_TRANSFER_TOKEN = object() +_DOCKER_COMMAND_EXITED_TOKEN = object() +_DOCKER_BUILD_EXITED_TOKEN = object() +_NATIVE_RUN_LEASE_TOKEN = object() +_DOCKER_ISSUED_CONTAINER_ID_TOKEN = object() +_BUILD_SESSION_TOKEN = object() +_TWO_BUILD_OBSERVATION_TOKEN = object() + + +class _NativeOwnershipLostV1(RuntimeError): + """A post-fork copy must not act on its creator's native resources.""" + + +def _valid_digest(value: object) -> bool: + return type(value) is bytes and len(value) == 32 and value != bytes(32) + + +def _blob(value: bytes) -> bytes: + return len(value).to_bytes(8, "big") + value + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join(_blob(chunk) for chunk in chunks) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def _retain_first_base_exception_v1( + retained: BaseException | None, + current: BaseException, +) -> BaseException: + return retained if retained is not None else current + + +def _canonical_diagnostic_detail_v1(value: object) -> str | None: + if ( + type(value) is str + and value + and len(value) <= _DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1 + ): + return value + return None + + +def _canonical_cid_root_cleanup_detail_v1(value: object) -> str | None: + if value is None: + return None + detail = _canonical_diagnostic_detail_v1(value) + if detail is not None: + return detail + return _INVALID_CID_ROOT_CLEANUP_DETAIL_V1 + + +def _pinned_image_reference(value: object) -> bool: + if type(value) is not str or value.count("@sha256:") != 1: + return False + repository, digest = value.split("@sha256:", 1) + components = repository.split("/") + if any(not component for component in components): + return False + first, *path_components = components + if ":" in first: + domain, separator, port = first.rpartition(":") + if ( + not separator + or not domain + or not port + or any(character not in "0123456789" for character in port) + ): + return False + first = domain + repository_component = re.compile( + r"[a-z0-9]+(?:[._-]+[a-z0-9]+)*\Z" + ) + return ( + bool(repository) + and repository[0].isalnum() + and repository[-1].isalnum() + and repository == repository.lower() + and repository_component.fullmatch(first) is not None + and all( + repository_component.fullmatch(component) is not None + for component in path_components + ) + and len(digest) == 64 + and all(character in "0123456789abcdef" for character in digest) + ) + + +def _encoded_policy_text( + value: object, + maximum: int, + field_name: str, + *, + allow_newlines: bool = False, +) -> str: + if type(value) is not str or not value or "\0" in value: + raise TypeError(f"invalid {field_name}") + try: + encoded = value.encode("utf-8") + except UnicodeEncodeError as error: + raise TypeError(f"invalid {field_name}") from error + if ( + len(encoded) > maximum + or not allow_newlines and ("\n" in value or "\r" in value) + ): + raise TypeError(f"invalid {field_name}") + return value + + +class DockerUserModeV1(StrEnum): + """Declare which unsealed-host user coordinates Docker observes.""" + + HOST_EFFECTIVE_IDS = "host_effective_ids" + + +_DOCKER_USER_MODE_HOST_EFFECTIVE_IDS_WIRE_V1 = b"host_effective_ids" + + +def _docker_user_mode_wire_v1(value: object) -> bytes: + """Render a closed semantic member without reading mutable Enum payloads.""" + + if value is DockerUserModeV1.HOST_EFFECTIVE_IDS: + return _DOCKER_USER_MODE_HOST_EFFECTIVE_IDS_WIRE_V1 + raise TypeError("invalid Docker user mode") + + +class DockerBuildPolicyV1(tuple): + """Deeply immutable coordinates for one bounded Docker build transport.""" + + __slots__ = () + + def __new__( + cls, + image_reference: str, + platform: str, + hostname: str, + bootstrap: str, + bootstrap_argv0: str, + tmpfs_specs: tuple[str, ...], + user_mode: DockerUserModeV1, + stdout_limit: int, + stderr_limit: int, + build_timeout_ns: int, + probe_output_limit: int, + probe_timeout_ns: int, + ) -> DockerBuildPolicyV1: + strings = tuple( + _encoded_policy_text( + value, + maximum, + field_name, + allow_newlines=field_name == "bootstrap", + ) + for field_name, value, maximum in ( + ("image_reference", image_reference, 512), + ("platform", platform, 64), + ("hostname", hostname, 64), + ("bootstrap", bootstrap, 64 * 1024), + ("bootstrap_argv0", bootstrap_argv0, 128), + ) + ) + ( + image_reference, + platform, + hostname, + bootstrap, + bootstrap_argv0, + ) = strings + if ( + platform != "linux/amd64" + or not _pinned_image_reference(image_reference) + ): + raise TypeError("policy requires one pinned linux/amd64 image") + if ( + any( + character not in "abcdefghijklmnopqrstuvwxyz0123456789-" + for character in hostname + ) + ): + raise TypeError("invalid Docker hostname") + if type(tmpfs_specs) is not tuple or not tmpfs_specs: + raise TypeError("invalid tmpfs_specs") + owned_tmpfs: list[str] = [] + for spec in tmpfs_specs: + parsed = _encoded_policy_text(spec, 4096, "tmpfs_specs") + if not parsed.startswith("/"): + raise TypeError("invalid tmpfs_specs") + owned_tmpfs.append(parsed) + tmpfs_specs = tuple(owned_tmpfs) + if len(set(tmpfs_specs)) != len(tmpfs_specs): + raise TypeError("invalid tmpfs_specs") + _docker_user_mode_wire_v1(user_mode) + limits = ( + (stdout_limit, BUILD_STDOUT_LIMIT_V1, "stdout_limit"), + (stderr_limit, BUILD_STDERR_LIMIT_V1, "stderr_limit"), + (build_timeout_ns, BUILD_TIMEOUT_NS_V1, "build_timeout_ns"), + (probe_output_limit, DOCKER_PROBE_OUTPUT_LIMIT_V1, "probe_output_limit"), + (probe_timeout_ns, DOCKER_PROBE_TIMEOUT_NS_V1, "probe_timeout_ns"), + ) + if any( + type(value) is not int or value <= 0 or value > maximum + for value, maximum, _name in limits + ): + raise TypeError("invalid Docker policy limit") + return tuple.__new__( + cls, + ( + image_reference, + platform, + hostname, + bootstrap, + bootstrap_argv0, + tmpfs_specs, + user_mode, + stdout_limit, + stderr_limit, + build_timeout_ns, + probe_output_limit, + probe_timeout_ns, + ), + ) + + @property + def image_reference(self) -> str: + return self[0] + + @property + def platform(self) -> str: + return self[1] + + @property + def hostname(self) -> str: + return self[2] + + @property + def bootstrap(self) -> str: + return self[3] + + @property + def bootstrap_argv0(self) -> str: + return self[4] + + @property + def tmpfs_specs(self) -> tuple[str, ...]: + return self[5] + + @property + def user_mode(self) -> DockerUserModeV1: + return self[6] + + @property + def stdout_limit(self) -> int: + return self[7] + + @property + def stderr_limit(self) -> int: + return self[8] + + @property + def build_timeout_ns(self) -> int: + return self[9] + + @property + def probe_output_limit(self) -> int: + return self[10] + + @property + def probe_timeout_ns(self) -> int: + return self[11] + + +def docker_policy_is_valid_v1(value: object) -> bool: + if type(value) is not DockerBuildPolicyV1: + return False + try: + return tuple(DockerBuildPolicyV1(*tuple(value))) == tuple(value) + except Exception: + return False + + +def transport_policy_identity_v1(policy: DockerBuildPolicyV1) -> bytes: + """Bind every declared transport-policy coordinate in constructor order.""" + + if not docker_policy_is_valid_v1(policy): + raise TypeError("policy must be canonical DockerBuildPolicyV1") + return _identity( + b"labcolors.proof-region.docker-transport-policy.v1\0", + ( + policy.image_reference.encode("utf-8"), + policy.platform.encode("utf-8"), + policy.hostname.encode("utf-8"), + policy.bootstrap.encode("utf-8"), + policy.bootstrap_argv0.encode("utf-8"), + len(policy.tmpfs_specs).to_bytes(4, "big"), + *(spec.encode("utf-8") for spec in policy.tmpfs_specs), + _docker_user_mode_wire_v1(policy.user_mode), + policy.stdout_limit.to_bytes(8, "big"), + policy.stderr_limit.to_bytes(8, "big"), + policy.build_timeout_ns.to_bytes(8, "big"), + policy.probe_output_limit.to_bytes(8, "big"), + policy.probe_timeout_ns.to_bytes(8, "big"), + ), + ) + + +class _NativeCommandSlotV1(StrEnum): + CLI_PATH = "cli_path" + IMAGE_REFERENCE = "image_reference" + PLATFORM = "platform" + ORDERED_TMPFS_SPECS = "ordered_tmpfs_specs" + HOSTNAME = "hostname" + HOST_USER = "host_user" + CID_FILE = "cid_file" + BOOTSTRAP = "bootstrap" + BOOTSTRAP_ARGV0 = "bootstrap_argv0" + INPUT_LENGTH = "input_length" + INPUT_SHA256 = "input_sha256" + CONTAINER_COORDINATE = "container_coordinate" + CONTAINER_FILTER = "container_filter" + + +class _NativeCommandTokenV1(tuple): + """One tagged literal or named slot in the native command grammar.""" + + __slots__ = () + + def __new__( + cls, + literal: str | None = None, + slot: _NativeCommandSlotV1 | None = None, + ) -> _NativeCommandTokenV1: + if (literal is None) == (slot is None): + raise TypeError("command token must be exactly one literal or slot") + if literal is not None: + if type(literal) is not str or not literal or "\0" in literal: + raise TypeError("invalid native command literal") + return tuple.__new__(cls, (b"literal", literal)) + if type(slot) is not _NativeCommandSlotV1: + raise TypeError("invalid native command slot") + return tuple.__new__(cls, (b"slot", slot)) + + @property + def tag(self) -> bytes: + return self[0] + + @property + def value(self) -> str | _NativeCommandSlotV1: + return self[1] + + +def _literal_v1(value: str) -> _NativeCommandTokenV1: + return _NativeCommandTokenV1(literal=value) + + +def _slot_v1(value: _NativeCommandSlotV1) -> _NativeCommandTokenV1: + return _NativeCommandTokenV1(slot=value) + + +_NATIVE_COMMAND_TEMPLATES_V1: tuple[ + tuple[str, tuple[_NativeCommandTokenV1, ...]], ... +] = ( + ( + "version_probe", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("version"), + _literal_v1("--format"), + _literal_v1("{{json .Server}}"), + ), + ), + ( + "image_inspect", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("image"), + _literal_v1("inspect"), + _slot_v1(_NativeCommandSlotV1.IMAGE_REFERENCE), + ), + ), + ( + "build", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("run"), + _literal_v1("--rm"), + _literal_v1("--interactive"), + _literal_v1("--pull"), + _literal_v1("never"), + _literal_v1("--platform"), + _slot_v1(_NativeCommandSlotV1.PLATFORM), + _literal_v1("--network"), + _literal_v1("none"), + _literal_v1("--read-only"), + _literal_v1("--tmpfs"), + _slot_v1(_NativeCommandSlotV1.ORDERED_TMPFS_SPECS), + _literal_v1("--cap-drop"), + _literal_v1("ALL"), + _literal_v1("--security-opt"), + _literal_v1("no-new-privileges:true"), + _literal_v1("--hostname"), + _slot_v1(_NativeCommandSlotV1.HOSTNAME), + _literal_v1("--user"), + _slot_v1(_NativeCommandSlotV1.HOST_USER), + _literal_v1("--workdir"), + _literal_v1("/"), + _literal_v1("--cidfile"), + _slot_v1(_NativeCommandSlotV1.CID_FILE), + _literal_v1("--entrypoint"), + _literal_v1("/usr/bin/env"), + _slot_v1(_NativeCommandSlotV1.IMAGE_REFERENCE), + _literal_v1("-i"), + _literal_v1("PATH=/usr/local/bin:/usr/bin:/bin"), + _literal_v1("LC_ALL=C"), + _literal_v1("LANG=C"), + _literal_v1("TZ=UTC"), + _literal_v1("HOME=/nonexistent"), + _literal_v1("/bin/sh"), + _literal_v1("-c"), + _slot_v1(_NativeCommandSlotV1.BOOTSTRAP), + _slot_v1(_NativeCommandSlotV1.BOOTSTRAP_ARGV0), + _slot_v1(_NativeCommandSlotV1.INPUT_LENGTH), + _slot_v1(_NativeCommandSlotV1.INPUT_SHA256), + ), + ), + ( + "cleanup_rm", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("container"), + _literal_v1("rm"), + _literal_v1("--force"), + _slot_v1(_NativeCommandSlotV1.CONTAINER_COORDINATE), + ), + ), + ( + "cleanup_inspect", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("container"), + _literal_v1("inspect"), + _literal_v1("--format"), + _literal_v1("{{.Id}}"), + _slot_v1(_NativeCommandSlotV1.CONTAINER_COORDINATE), + ), + ), + ( + "cleanup_ls", + ( + _slot_v1(_NativeCommandSlotV1.CLI_PATH), + _literal_v1("container"), + _literal_v1("ls"), + _literal_v1("--all"), + _literal_v1("--quiet"), + _literal_v1("--no-trunc"), + _literal_v1("--filter"), + _slot_v1(_NativeCommandSlotV1.CONTAINER_FILTER), + ), + ), +) + + +class _NativeStdioModeV1(StrEnum): + PIPE = "pipe" + DEVNULL = "devnull" + + +def _native_stdio_value_v1(mode: _NativeStdioModeV1) -> int: + if type(mode) is not _NativeStdioModeV1: + raise TypeError("invalid native stdio mode") + if mode is _NativeStdioModeV1.PIPE: + return subprocess.PIPE + if mode is _NativeStdioModeV1.DEVNULL: + return subprocess.DEVNULL + raise TypeError("invalid native stdio mode") + + +class _NativeProcessContextV1(tuple): + """One fixed, identity-bound child-launch context for the Docker CLI.""" + + __slots__ = () + + def __new__( + cls, + environment: tuple[tuple[str, str], ...], + cwd: str, + umask: int, + close_fds: bool, + restore_signals: bool, + start_new_session: bool, + stdin_with_input: _NativeStdioModeV1, + stdin_without_input: _NativeStdioModeV1, + stdout: _NativeStdioModeV1, + stderr: _NativeStdioModeV1, + ) -> _NativeProcessContextV1: + if type(environment) is not tuple or not environment: + raise TypeError("invalid native process environment") + owned_environment: list[tuple[str, str]] = [] + for entry in environment: + if type(entry) is not tuple or len(entry) != 2: + raise TypeError("invalid native process environment") + name, value = entry + if ( + type(name) is not str + or not name + or re.fullmatch(r"[A-Z_][A-Z0-9_]*", name) is None + or type(value) is not str + or "\0" in value + or "\n" in value + or "\r" in value + ): + raise TypeError("invalid native process environment") + try: + value.encode("utf-8") + except UnicodeEncodeError as error: + raise TypeError("invalid native process environment") from error + owned_environment.append((name, value)) + canonical_environment = tuple(owned_environment) + if ( + canonical_environment != tuple(sorted(canonical_environment)) + or len({name for name, _value in canonical_environment}) + != len(canonical_environment) + ): + raise TypeError("native process environment must be ordered and unique") + if ( + type(cwd) is not str + or not cwd + or "\0" in cwd + or "\n" in cwd + or "\r" in cwd + or not os.path.isabs(cwd) + ): + raise TypeError("invalid native process cwd") + try: + cwd.encode("utf-8") + except UnicodeEncodeError as error: + raise TypeError("invalid native process cwd") from error + if type(umask) is not int or umask < 0 or umask > 0o777: + raise TypeError("invalid native process umask") + if any( + type(value) is not bool + for value in (close_fds, restore_signals, start_new_session) + ): + raise TypeError("invalid native process launch flags") + if any( + type(value) is not _NativeStdioModeV1 + for value in ( + stdin_with_input, + stdin_without_input, + stdout, + stderr, + ) + ): + raise TypeError("invalid native stdio topology") + return tuple.__new__( + cls, + ( + canonical_environment, + cwd, + umask, + close_fds, + restore_signals, + start_new_session, + stdin_with_input, + stdin_without_input, + stdout, + stderr, + ), + ) + + @property + def environment(self) -> tuple[tuple[str, str], ...]: + return self[0] + + @property + def cwd(self) -> str: + return self[1] + + @property + def umask(self) -> int: + return self[2] + + @property + def close_fds(self) -> bool: + return self[3] + + @property + def restore_signals(self) -> bool: + return self[4] + + @property + def start_new_session(self) -> bool: + return self[5] + + @property + def stdin_with_input(self) -> _NativeStdioModeV1: + return self[6] + + @property + def stdin_without_input(self) -> _NativeStdioModeV1: + return self[7] + + @property + def stdout(self) -> _NativeStdioModeV1: + return self[8] + + @property + def stderr(self) -> _NativeStdioModeV1: + return self[9] + + def identity_chunks_v1(self) -> tuple[bytes, ...]: + return ( + b"native-process-context.v1", + len(self.environment).to_bytes(4, "big"), + *( + chunk + for name, value in self.environment + for chunk in (name.encode("ascii"), value.encode("utf-8")) + ), + self.cwd.encode("utf-8"), + self.umask.to_bytes(4, "big"), + bytes((self.close_fds,)), + bytes((self.restore_signals,)), + bytes((self.start_new_session,)), + b"native-stdio-topology.v1", + b"stdin-with-input", + self.stdin_with_input.value.encode("ascii"), + b"stdin-without-input", + self.stdin_without_input.value.encode("ascii"), + b"stdout", + self.stdout.value.encode("ascii"), + b"stderr", + self.stderr.value.encode("ascii"), + ) + + def popen_kwargs_v1(self, receives_stdin: bool) -> dict[str, object]: + if type(receives_stdin) is not bool: + raise TypeError("receives_stdin must be bool") + return { + "stdin": _native_stdio_value_v1( + self.stdin_with_input + if receives_stdin + else self.stdin_without_input + ), + "stdout": _native_stdio_value_v1(self.stdout), + "stderr": _native_stdio_value_v1(self.stderr), + "cwd": self.cwd, + "env": dict(self.environment), + "close_fds": self.close_fds, + "restore_signals": self.restore_signals, + "start_new_session": self.start_new_session, + "umask": self.umask, + } + + +# The Docker CLI is part of an evidence-producing observation. Its launch +# cannot inherit locale, config, cwd, umask or session state from the host: +# this immutable value both renders Popen kwargs and enters the command +# identity, so a future change cannot silently alter what the observer ran. +_NATIVE_PROCESS_CONTEXT_V1 = _NativeProcessContextV1( + ( + ("DOCKER_CONFIG", "/nonexistent"), + ("HOME", "/nonexistent"), + ("LANG", "C"), + ("LC_ALL", "C"), + ("PATH", "/usr/bin:/bin"), + ("TZ", "UTC"), + ), + "/", + 0o077, + True, + True, + True, + _NativeStdioModeV1.PIPE, + _NativeStdioModeV1.DEVNULL, + _NativeStdioModeV1.PIPE, + _NativeStdioModeV1.PIPE, +) + + +def native_command_contract_identity_v1() -> bytes: + chunks: list[bytes] = [len(_NATIVE_COMMAND_TEMPLATES_V1).to_bytes(4, "big")] + for name, tokens in _NATIVE_COMMAND_TEMPLATES_V1: + chunks.extend((name.encode("ascii"), len(tokens).to_bytes(4, "big"))) + for token in tokens: + value = token.value + chunks.extend( + ( + token.tag, + ( + value.value.encode("ascii") + if type(value) is _NativeCommandSlotV1 + else value.encode("utf-8") + ), + ) + ) + chunks.extend(_NATIVE_PROCESS_CONTEXT_V1.identity_chunks_v1()) + return _identity( + b"labcolors.proof-region.native-command-contract.v1\0", + tuple(chunks), + ) + + +def _native_command_path_v1(value: object) -> tuple[Path, bytes]: + if type(value) is not _PATH_TYPE or not value.is_absolute(): + raise TypeError("native command path must be an absolute Path") + try: + encoded = os.fsencode(value) + except (TypeError, UnicodeEncodeError) as error: + raise TypeError("native command path is not filesystem-encodable") from error + if not encoded or b"\0" in encoded: + raise TypeError("invalid native command path bytes") + return value, encoded + + +class NativeCommandCoordinateV1(tuple): + """Exact filesystem command coordinate bound to the native argv grammar.""" + + __slots__ = () + + def __new__( + cls, + path_bytes: bytes, + command_contract_identity: bytes, + ) -> NativeCommandCoordinateV1: + if type(path_bytes) is not bytes or not path_bytes or b"\0" in path_bytes: + raise TypeError("invalid native command path bytes") + try: + path = Path(os.fsdecode(path_bytes)) + except (TypeError, UnicodeDecodeError) as error: + raise TypeError("invalid native command path bytes") from error + _owned_path, encoded = _native_command_path_v1(path) + if encoded != path_bytes: + raise TypeError("native command path bytes are not canonical") + if command_contract_identity != native_command_contract_identity_v1(): + raise TypeError("foreign native command contract") + return tuple.__new__(cls, (path_bytes, command_contract_identity)) + + @property + def path_bytes(self) -> bytes: + return self[0] + + @property + def path(self) -> Path: + return Path(os.fsdecode(self.path_bytes)) + + @property + def command_contract_identity(self) -> bytes: + return self[1] + + @property + def identity(self) -> bytes: + return _native_command_coordinate_identity_v1(self) + + +def native_command_coordinate_v1(path: Path) -> NativeCommandCoordinateV1: + _owned, encoded = _native_command_path_v1(path) + return NativeCommandCoordinateV1( + encoded, + native_command_contract_identity_v1(), + ) + + +def _native_command_coordinate_identity_v1( + coordinate: NativeCommandCoordinateV1, +) -> bytes: + if type(coordinate) is not NativeCommandCoordinateV1: + raise TypeError("coordinate must be NativeCommandCoordinateV1") + canonical = NativeCommandCoordinateV1(*tuple(coordinate)) + if tuple(canonical) != tuple(coordinate): + raise TypeError("coordinate is not canonical") + return _identity( + b"labcolors.proof-region.native-command-coordinate.v1\0", + ( + coordinate.command_contract_identity, + coordinate.path_bytes, + ), + ) + + +def _render_native_command_v1( + template_name: str, + command_coordinate: NativeCommandCoordinateV1, + values: dict[_NativeCommandSlotV1, tuple[str, ...]], +) -> tuple[str, ...]: + if type(template_name) is not str or type(values) is not dict: + raise TypeError("invalid native command expansion") + canonical_coordinate = NativeCommandCoordinateV1(*tuple(command_coordinate)) + templates = dict(_NATIVE_COMMAND_TEMPLATES_V1) + try: + tokens = templates[template_name] + except KeyError as error: + raise TypeError("unknown native command template") from error + owned_values = dict(values) + if _NativeCommandSlotV1.CLI_PATH in owned_values: + raise TypeError("native command path is owned by its coordinate") + owned_values[_NativeCommandSlotV1.CLI_PATH] = ( + os.fsdecode(canonical_coordinate.path_bytes), + ) + expected_slots = { + token.value + for token in tokens + if token.tag == b"slot" + } + if set(owned_values) != expected_slots: + raise TypeError("native command slots do not match its template") + command: list[str] = [] + for token in tokens: + if token.tag == b"literal": + command.append(token.value) + continue + slot = token.value + expanded = owned_values[slot] + if ( + type(expanded) is not tuple + or ( + slot is not _NativeCommandSlotV1.ORDERED_TMPFS_SPECS + and len(expanded) != 1 + ) + or any( + type(value) is not str or not value or "\0" in value + for value in expanded + ) + ): + raise TypeError("invalid native command slot expansion") + if slot is _NativeCommandSlotV1.ORDERED_TMPFS_SPECS: + if not expanded or not command: + raise TypeError("invalid ordered tmpfs template") + repeated_literal = command.pop() + for value in expanded: + command.extend((repeated_literal, value)) + continue + command.extend(expanded) + try: + tuple(os.fsencode(value) for value in command) + except (TypeError, UnicodeEncodeError) as error: + raise TypeError("native command contains an unencodable coordinate") from error + return tuple(command) + + +class DockerBlockerReasonV1(StrEnum): + HOST_NOT_LINUX_AMD64 = "host_not_linux_amd64" + HOST_USER_UNAVAILABLE = "host_user_unavailable" + DOCKER_UNAVAILABLE = "docker_unavailable" + IMAGE_UNAVAILABLE = "image_unavailable" + IMAGE_IDENTITY_MISMATCH = "image_identity_mismatch" + BACKEND_CONTRACT = "backend_contract" + + +class DockerUnsupportedV1(tuple): + __slots__ = () + + def __new__( + cls, + reason: DockerBlockerReasonV1, + detail: str, + ) -> DockerUnsupportedV1: + if type(reason) is not DockerBlockerReasonV1: + raise TypeError("invalid Docker blocker reason") + if _canonical_diagnostic_detail_v1(detail) is None: + raise TypeError("invalid Docker blocker detail") + return tuple.__new__(cls, (reason, detail)) + + @property + def reason(self) -> DockerBlockerReasonV1: + return self[0] + + @property + def detail(self) -> str: + return self[1] + + +class DockerDaemonObservationV1(tuple): + """Exact stdout bytes observed from the two admitted Docker probes.""" + + __slots__ = () + + def __new__( + cls, + server_stdout: bytes, + image_inspect_stdout: bytes, + ) -> DockerDaemonObservationV1: + for value, field_name in ( + (server_stdout, "server_stdout"), + (image_inspect_stdout, "image_inspect_stdout"), + ): + if ( + type(value) is not bytes + or not value + or len(value) > DOCKER_PROBE_OUTPUT_LIMIT_V1 + ): + raise TypeError(f"invalid Docker daemon {field_name}") + return tuple.__new__(cls, (server_stdout, image_inspect_stdout)) + + @property + def server_stdout(self) -> bytes: + return self[0] + + @property + def image_inspect_stdout(self) -> bytes: + return self[1] + + @property + def identity(self) -> bytes: + return _docker_daemon_observation_identity_v1(self) + + +def _docker_daemon_observation_identity_v1( + observation: DockerDaemonObservationV1, +) -> bytes: + if type(observation) is not DockerDaemonObservationV1: + raise TypeError("observation must be DockerDaemonObservationV1") + canonical = DockerDaemonObservationV1(*tuple(observation)) + if tuple(canonical) != tuple(observation): + raise TypeError("daemon observation is not canonical") + return _identity( + b"labcolors.proof-region.docker-daemon-observation.v1\0", + ( + observation.server_stdout, + observation.image_inspect_stdout, + ), + ) + + +def _host_user_identity_v1(host_user: tuple[int, int]) -> bytes: + owned = _host_user_coordinates(host_user) + return _identity( + b"labcolors.proof-region.host-user.v1\0", + ( + owned[0].to_bytes(4, "big"), + owned[1].to_bytes(4, "big"), + ), + ) + + +class DockerSupportedV1(tuple): + """Canonical capability observed for one exact native Docker coordinate.""" + + __slots__ = () + + def __new__( + cls, + policy: DockerBuildPolicyV1, + daemon_observation: DockerDaemonObservationV1, + command_coordinate: NativeCommandCoordinateV1, + host_user: tuple[int, int], + ) -> DockerSupportedV1: + if not docker_policy_is_valid_v1(policy): + raise TypeError("invalid Docker policy capability") + if type(daemon_observation) is not DockerDaemonObservationV1: + raise TypeError("invalid Docker daemon observation") + canonical_daemon = DockerDaemonObservationV1(*tuple(daemon_observation)) + if ( + tuple(canonical_daemon) != tuple(daemon_observation) + or len(canonical_daemon.server_stdout) > policy.probe_output_limit + or len(canonical_daemon.image_inspect_stdout) + > policy.probe_output_limit + ): + raise TypeError("Docker daemon observation is not canonical") + if type(command_coordinate) is not NativeCommandCoordinateV1: + raise TypeError("invalid native Docker command coordinate") + canonical_command = NativeCommandCoordinateV1(*tuple(command_coordinate)) + if tuple(canonical_command) != tuple(command_coordinate): + raise TypeError("native Docker command coordinate is not canonical") + owned_user = _host_user_coordinates(host_user) + return tuple.__new__( + cls, + (policy, daemon_observation, command_coordinate, owned_user), + ) + + @property + def policy(self) -> DockerBuildPolicyV1: + return self[0] + + @property + def daemon_observation(self) -> DockerDaemonObservationV1: + return self[1] + + @property + def command_coordinate(self) -> NativeCommandCoordinateV1: + return self[2] + + @property + def host_user(self) -> tuple[int, int]: + return self[3] + + @property + def policy_identity(self) -> bytes: + return transport_policy_identity_v1(self.policy) + + @property + def daemon_observation_identity(self) -> bytes: + return _docker_daemon_observation_identity_v1(self.daemon_observation) + + @property + def command_coordinate_identity(self) -> bytes: + return _native_command_coordinate_identity_v1(self.command_coordinate) + + @property + def host_user_identity(self) -> bytes: + return _host_user_identity_v1(self.host_user) + + @property + def identity(self) -> bytes: + return docker_capability_identity_v1(self) + + +def _docker_supported_is_valid_v1(value: object) -> bool: + if type(value) is not DockerSupportedV1: + return False + try: + return tuple(DockerSupportedV1(*tuple(value))) == tuple(value) + except Exception: + return False + + +def docker_capability_identity_v1(capability: DockerSupportedV1) -> bytes: + if not _docker_supported_is_valid_v1(capability): + raise TypeError("capability must be canonical DockerSupportedV1") + return _identity( + b"labcolors.proof-region.docker-capability.v1\0", + ( + capability.policy_identity, + capability.command_coordinate_identity, + capability.daemon_observation_identity, + capability.host_user[0].to_bytes(4, "big"), + capability.host_user[1].to_bytes(4, "big"), + ), + ) + + +DockerCapabilityReportV1: TypeAlias = DockerSupportedV1 | DockerUnsupportedV1 + + +def _docker_unsupported_is_valid_v1(value: object) -> bool: + if type(value) is not DockerUnsupportedV1: + return False + try: + return tuple(DockerUnsupportedV1(*tuple(value))) == tuple(value) + except Exception: + return False + + +def _absolute_path(value: object, field_name: str) -> Path: + if type(value) is not _PATH_TYPE or not value.is_absolute(): + raise TypeError(f"{field_name} must be an absolute Path") + try: + encoded = os.fsencode(value) + except (TypeError, UnicodeEncodeError) as error: + raise TypeError(f"{field_name} is not filesystem-encodable") from error + if ( + not encoded + or b"\0" in encoded + or any(character in str(value) for character in (",", "\n", "\r")) + ): + raise TypeError(f"{field_name} is not Docker-mount-safe") + return value + + +def docker_command_coordinate_v1(value: object) -> NativeCommandCoordinateV1: + """Admit one Docker command coordinate before native authority exists.""" + + return native_command_coordinate_v1(_absolute_path(value, "docker_path")) + + +def _docker_coordinate_metadata_flags_v1() -> int: + """Return the read-free native descriptor mode for one CLI coordinate.""" + + if sys.platform == "linux": + flag = getattr(os, "O_PATH", None) + detail = "Linux Docker coordinate inspection requires O_PATH" + else: + flag = getattr(os, "O_EXEC", None) + detail = "native Docker coordinate inspection requires O_EXEC" + if type(flag) is not int or flag <= 0: + raise OSError(errno_module.ENOTSUP, detail) + return flag + + +def _open_docker_command_v1(coordinate: NativeCommandCoordinateV1) -> int: + """Open the current Docker CLI path without following any symlink segment.""" + + encoded = os.fsencode(coordinate.path) + # Metadata observation must not demand read permission from either an + # executable-only CLI or a search-only parent directory. Linux uses O_PATH + # rather than silently weakening that law when the runtime lacks it. + metadata_flags = _docker_coordinate_metadata_flags_v1() + directory_flags = metadata_flags | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW + command_flags = metadata_flags | os.O_NONBLOCK | os.O_CLOEXEC | os.O_NOFOLLOW + # This coordinate deliberately preserves exact argv spelling. On Linux, + # empty segments are root and `..` is traversed through the pinned parent; + # neither case authorizes filesystem resolution or a symlink transition. + components = tuple(component for component in encoded.split(b"/")[1:] if component) + descriptor = os.open(b"/", directory_flags) + try: + for index, component in enumerate(components): + next_descriptor = os.open( + component, + command_flags if index == len(components) - 1 else directory_flags, + dir_fd=descriptor, + ) + # The Linux close contract consumes its numeric descriptor before + # a late interruption can be observed, so ownership moves first. + previous_descriptor, descriptor = descriptor, next_descriptor + try: + os.close(previous_descriptor) + except BaseException as primary: + cleanup_descriptor, descriptor = descriptor, -1 + try: + os.close(cleanup_descriptor) + except BaseException as cleanup: + raise primary.with_traceback(primary.__traceback__) from cleanup + raise + result = descriptor + descriptor = -1 + return result + finally: + if descriptor >= 0: + os.close(descriptor) + + +def _host_user_coordinates(value: object) -> tuple[int, int]: + if ( + type(value) is not tuple + or len(value) != 2 + or any(type(item) is not int or item < 0 or item >= 1 << 32 for item in value) + ): + raise TypeError("host_user must be one exact Linux uid/gid pair") + return value + + +class DockerBuildRequestV1(tuple): + """Semantic BUILD coordinates; the adapter owns all host resources.""" + + __slots__ = () + + def __new__( + cls, + attempt: int, + capability: DockerSupportedV1, + input_bundle: _build_input.SealedInputV1, + max_output_bytes: int, + ) -> DockerBuildRequestV1: + if type(attempt) is not int or attempt not in (1, 2): + raise TypeError("attempt must be 1 or 2") + if not _docker_supported_is_valid_v1(capability): + raise TypeError("capability must be canonical DockerSupportedV1") + if not _build_input.sealed_input_is_intact_v1(input_bundle): + raise TypeError("input_bundle must preserve exact sealed bytes") + if ( + type(max_output_bytes) is not int + or max_output_bytes <= 0 + or max_output_bytes > capability.policy.stdout_limit + ): + raise TypeError("invalid executable output limit") + return tuple.__new__( + cls, + ( + attempt, + capability, + input_bundle, + max_output_bytes, + ), + ) + + @property + def attempt(self) -> int: + return self[0] + + @property + def capability(self) -> DockerSupportedV1: + return self[1] + + @property + def input_bundle(self) -> _build_input.SealedInputV1: + return self[2] + + @property + def max_output_bytes(self) -> int: + return self[3] + + +def _docker_build_request_is_valid_v1( + value: object, + capability: DockerSupportedV1, +) -> bool: + if ( + type(value) is not DockerBuildRequestV1 + or not _docker_supported_is_valid_v1(capability) + ): + return False + try: + canonical = DockerBuildRequestV1(*tuple(value)) + return ( + tuple(canonical) == tuple(value) + and canonical.capability == capability + and _build_input.sealed_input_is_intact_v1(canonical.input_bundle) + ) + except Exception: + return False + + +class _NativeRunLeaseV1: + """Adapter-owned authority for one Docker-issued container ID file.""" + + __slots__ = ( + "_owner", + "_creator_pid", + "_capability", + "_root", + "_cid_file", + "_launched", + "_released", + ) + + def __init__( + self, + owner: object, + creator_pid: int, + capability: DockerSupportedV1, + root: Path, + cid_file: Path, + *, + _token: object, + ) -> None: + if ( + _token is not _NATIVE_RUN_LEASE_TOKEN + or type(owner) is not object + or type(creator_pid) is not int + or creator_pid <= 0 + or not _docker_supported_is_valid_v1(capability) + or type(root) is not _PATH_TYPE + or not root.is_absolute() + or type(cid_file) is not _PATH_TYPE + or not cid_file.is_absolute() + ): + raise TypeError("native run lease is adapter-owned") + self._owner = owner + self._creator_pid = creator_pid + self._capability = capability + self._root = root + self._cid_file = cid_file + self._launched = False + self._released = False + + @property + def owner(self) -> object: + return self._owner + + @property + def creator_pid(self) -> int: + return self._creator_pid + + @property + def capability(self) -> DockerSupportedV1: + return self._capability + + @property + def cid_file(self) -> Path: + return self._cid_file + + @property + def launched(self) -> bool: + return self._launched + + +class _DockerIssuedContainerIdV1(str): + """A full ID read from the adapter-private CID file Docker created.""" + + def __new__( + cls, + value: str, + *, + _token: object, + ) -> _DockerIssuedContainerIdV1: + if ( + _token is not _DOCKER_ISSUED_CONTAINER_ID_TOKEN + or type(value) is not str + or len(value) != 64 + or any(character not in "0123456789abcdef" for character in value) + ): + raise TypeError("invalid Docker-issued container ID") + return str.__new__(cls, value) + + +def _bounded_bytes(value: object, maximum: int, field_name: str) -> bytes: + if type(value) is not bytes or len(value) > maximum: + raise TypeError(f"invalid {field_name}") + return value + + +class BuildInputTransferProgressV1(tuple): + __slots__ = () + + def __new__( + cls, + bundle_identity: bytes, + expected_length: int, + expected_sha256: bytes, + written_length: int, + written_sha256: bytes, + *, + _token: object, + ) -> BuildInputTransferProgressV1: + if _token is not _BUILD_INPUT_PROGRESS_TOKEN: + raise TypeError("build input progress is controller-observed") + if not _valid_digest(bundle_identity) or not _valid_digest(expected_sha256): + raise TypeError("invalid build input progress coordinates") + if ( + type(expected_length) is not int + or expected_length <= 0 + or expected_length >= 1 << 64 + or type(written_length) is not int + or written_length < 0 + or written_length >= 1 << 64 + or written_length > expected_length + or type(written_sha256) is not bytes + or len(written_sha256) != 32 + ): + raise TypeError("invalid build input progress") + return tuple.__new__( + cls, + ( + bundle_identity, + expected_length, + expected_sha256, + written_length, + written_sha256, + ), + ) + + @property + def bundle_identity(self) -> bytes: + return self[0] + + @property + def expected_length(self) -> int: + return self[1] + + @property + def expected_sha256(self) -> bytes: + return self[2] + + @property + def written_length(self) -> int: + return self[3] + + @property + def written_sha256(self) -> bytes: + return self[4] + + +def _build_input_progress_v1( + bundle: _build_input.SealedInputV1, + written_length: int, + written_sha256: bytes, +) -> BuildInputTransferProgressV1: + if not _build_input.sealed_input_is_intact_v1(bundle): + raise TypeError("build input bytes are not intact") + if ( + type(written_length) is not int + or written_length < 0 + or written_length > bundle.length + or type(written_sha256) is not bytes + or written_sha256 + != hashlib.sha256(bundle.contents[:written_length]).digest() + ): + raise TypeError("build input progress does not match the sealed bytes") + return BuildInputTransferProgressV1( + bundle.binding_identity, + bundle.length, + bundle.sha256, + written_length, + written_sha256, + _token=_BUILD_INPUT_PROGRESS_TOKEN, + ) + + +def _input_progress_matches_v1( + value: object, + bundle: _build_input.SealedInputV1, +) -> bool: + if ( + type(value) is not BuildInputTransferProgressV1 + or not _build_input.sealed_input_is_intact_v1(bundle) + ): + return False + try: + canonical = _build_input_progress_v1( + bundle, + value.written_length, + value.written_sha256, + ) + return tuple(canonical) == tuple(value) + except Exception: + return False + + +class BuildInputTransferV1(tuple): + __slots__ = () + + def __new__( + cls, + progress: BuildInputTransferProgressV1, + *, + _token: object, + ) -> BuildInputTransferV1: + if _token is not _BUILD_INPUT_TRANSFER_TOKEN: + raise TypeError("build input transfer is controller-observed") + if ( + type(progress) is not BuildInputTransferProgressV1 + or progress.written_length != progress.expected_length + or progress.written_sha256 != progress.expected_sha256 + ): + raise TypeError("completed build input transfer must be exact") + return tuple.__new__(cls, tuple(progress)) + + @property + def bundle_identity(self) -> bytes: + return self[0] + + @property + def expected_length(self) -> int: + return self[1] + + @property + def expected_sha256(self) -> bytes: + return self[2] + + @property + def written_length(self) -> int: + return self[3] + + @property + def written_sha256(self) -> bytes: + return self[4] + + +def _input_transfer_is_structurally_valid_v1(value: object) -> bool: + if type(value) is not BuildInputTransferV1: + return False + try: + return ( + len(value) == 5 + and _valid_digest(value.bundle_identity) + and type(value.expected_length) is int + and 0 < value.expected_length < 1 << 64 + and _valid_digest(value.expected_sha256) + and value.written_length == value.expected_length + and value.written_sha256 == value.expected_sha256 + ) + except Exception: + return False + + +def _completed_build_input_transfer_v1( + bundle: _build_input.SealedInputV1, + written_length: int, + written_sha256: bytes, +) -> BuildInputTransferV1: + progress = _build_input_progress_v1(bundle, written_length, written_sha256) + return BuildInputTransferV1( + progress, + _token=_BUILD_INPUT_TRANSFER_TOKEN, + ) + + +class _DockerCommandExitedV1(tuple): + __slots__ = () + + def __new__( + cls, + returncode: int, + stdout: bytes, + stderr: bytes, + *, + _token: object, + ) -> _DockerCommandExitedV1: + if _token is not _DOCKER_COMMAND_EXITED_TOKEN: + raise TypeError("Docker command exit is controller-observed") + if type(returncode) is not int or not -(1 << 31) <= returncode < 1 << 31: + raise TypeError("invalid Docker returncode") + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + return tuple.__new__(cls, (returncode, stdout, stderr)) + + @property + def returncode(self) -> int: + return self[0] + + @property + def stdout(self) -> bytes: + return self[1] + + @property + def stderr(self) -> bytes: + return self[2] + + +def _docker_command_exited_v1( + returncode: int, + stdout: bytes, + stderr: bytes, +) -> _DockerCommandExitedV1: + return _DockerCommandExitedV1( + returncode, + stdout, + stderr, + _token=_DOCKER_COMMAND_EXITED_TOKEN, + ) + + +class DockerBuildExitedV1(tuple): + __slots__ = () + + def __new__( + cls, + returncode: int, + stdout: bytes, + stderr: bytes, + input_transfer: BuildInputTransferV1, + *, + _token: object, + ) -> DockerBuildExitedV1: + if _token is not _DOCKER_BUILD_EXITED_TOKEN: + raise TypeError("Docker build exit is controller-observed") + if type(returncode) is not int or not -(1 << 31) <= returncode < 1 << 31: + raise TypeError("invalid Docker returncode") + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + if not _input_transfer_is_structurally_valid_v1(input_transfer): + raise TypeError("invalid Docker build input transfer") + return tuple.__new__( + cls, + (returncode, stdout, stderr, input_transfer), + ) + + @property + def returncode(self) -> int: + return self[0] + + @property + def stdout(self) -> bytes: + return self[1] + + @property + def stderr(self) -> bytes: + return self[2] + + @property + def input_transfer(self) -> BuildInputTransferV1: + return self[3] + + +def _docker_build_exited_v1( + returncode: int, + stdout: bytes, + stderr: bytes, + input_transfer: BuildInputTransferV1, +) -> DockerBuildExitedV1: + return DockerBuildExitedV1( + returncode, + stdout, + stderr, + input_transfer, + _token=_DOCKER_BUILD_EXITED_TOKEN, + ) + + +def docker_build_exited_is_valid_v1( + value: object, + input_value: _build_input.SealedInputV1, + max_output_bytes: int, + max_stderr_bytes: int, +) -> bool: + if ( + type(value) is not DockerBuildExitedV1 + or not _build_input.sealed_input_is_intact_v1(input_value) + or type(max_output_bytes) is not int + or max_output_bytes <= 0 + or type(max_stderr_bytes) is not int + or max_stderr_bytes <= 0 + ): + return False + try: + return ( + len(value) == 4 + and type(value.returncode) is int + and -(1 << 31) <= value.returncode < 1 << 31 + and type(value.stdout) is bytes + and len(value.stdout) <= max_output_bytes + and type(value.stderr) is bytes + and len(value.stderr) <= max_stderr_bytes + and _input_transfer_is_structurally_valid_v1(value.input_transfer) + and value.input_transfer.bundle_identity + == input_value.binding_identity + and value.input_transfer.expected_length == input_value.length + and value.input_transfer.expected_sha256 == input_value.sha256 + and value.input_transfer.written_length == input_value.length + and value.input_transfer.written_sha256 == input_value.sha256 + ) + except Exception: + return False + + +class DockerBuildTimedOutV1(tuple): + __slots__ = () + + def __new__( + cls, + stdout: bytes, + stderr: bytes, + input_progress: BuildInputTransferProgressV1 | None = None, + ) -> DockerBuildTimedOutV1: + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + if input_progress is not None and type( + input_progress + ) is not BuildInputTransferProgressV1: + raise TypeError("invalid timed-out build input progress") + return tuple.__new__(cls, (stdout, stderr, input_progress)) + + @property + def stdout(self) -> bytes: + return self[0] + + @property + def stderr(self) -> bytes: + return self[1] + + @property + def input_progress(self) -> BuildInputTransferProgressV1 | None: + return self[2] + + +class DockerOutputStreamV1(StrEnum): + STDOUT = "stdout" + STDERR = "stderr" + + +class DockerBuildOutputLimitV1(tuple): + __slots__ = () + + def __new__( + cls, + stream: DockerOutputStreamV1, + stdout: bytes, + stderr: bytes, + input_progress: BuildInputTransferProgressV1 | None = None, + ) -> DockerBuildOutputLimitV1: + if type(stream) is not DockerOutputStreamV1: + raise TypeError("invalid Docker output stream") + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + if input_progress is not None and type( + input_progress + ) is not BuildInputTransferProgressV1: + raise TypeError("invalid output-limited build input progress") + return tuple.__new__(cls, (stream, stdout, stderr, input_progress)) + + @property + def stream(self) -> DockerOutputStreamV1: + return self[0] + + @property + def stdout(self) -> bytes: + return self[1] + + @property + def stderr(self) -> bytes: + return self[2] + + @property + def input_progress(self) -> BuildInputTransferProgressV1 | None: + return self[3] + + +class DockerBuildObserverFailureV1(tuple): + __slots__ = () + + def __new__( + cls, + detail: str, + stdout: bytes, + stderr: bytes, + input_progress: BuildInputTransferProgressV1 | None = None, + ) -> DockerBuildObserverFailureV1: + if _canonical_diagnostic_detail_v1(detail) is None: + raise TypeError("invalid Docker observer failure") + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + if input_progress is not None and type( + input_progress + ) is not BuildInputTransferProgressV1: + raise TypeError("invalid observer-failure build input progress") + return tuple.__new__(cls, (detail, stdout, stderr, input_progress)) + + @property + def detail(self) -> str: + return self[0] + + @property + def stdout(self) -> bytes: + return self[1] + + @property + def stderr(self) -> bytes: + return self[2] + + @property + def input_progress(self) -> BuildInputTransferProgressV1 | None: + return self[3] + + +class DockerBuildInputRejectedV1(tuple): + __slots__ = () + + def __new__( + cls, + input_progress: BuildInputTransferProgressV1, + stdout: bytes, + stderr: bytes, + ) -> DockerBuildInputRejectedV1: + if type(input_progress) is not BuildInputTransferProgressV1: + raise TypeError("invalid partial build input progress") + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + return tuple.__new__(cls, (input_progress, stdout, stderr)) + + @property + def input_progress(self) -> BuildInputTransferProgressV1: + return self[0] + + @property + def stdout(self) -> bytes: + return self[1] + + @property + def stderr(self) -> bytes: + return self[2] + + @property + def written_length(self) -> int: + return self.input_progress.written_length + + @property + def written_sha256(self) -> bytes: + return self.input_progress.written_sha256 + + +class DockerCleanupTriggerV1(StrEnum): + PROCESS_EXIT = "process_exit" + INPUT_TRANSFER = "input_transfer" + TIMEOUT = "timeout" + OUTPUT_LIMIT = "output_limit" + OBSERVER_FAILURE = "observer_failure" + + +class CleanupResourceV1(StrEnum): + DOCKER_CLI_PROCESS = "docker_cli_process" + DOCKER_CONTAINER = "docker_container" + DOCKER_CID_ROOT = "docker_cid_root" + + +class CleanupFailureRecordV1(tuple): + __slots__ = () + + def __new__( + cls, + resource: CleanupResourceV1, + detail: str, + ) -> CleanupFailureRecordV1: + if type(resource) is not CleanupResourceV1: + raise TypeError("invalid cleanup resource") + if _canonical_diagnostic_detail_v1(detail) is None: + raise TypeError("invalid cleanup failure detail") + return tuple.__new__(cls, (resource, detail)) + + @property + def resource(self) -> CleanupResourceV1: + return self[0] + + @property + def detail(self) -> str: + return self[1] + + +def _cleanup_failure_records_v1( + value: object, + allowed_order: tuple[CleanupResourceV1, ...], +) -> tuple[CleanupFailureRecordV1, ...]: + if type(value) is not tuple or not value: + raise TypeError("cleanup failures must be one nonempty tuple") + order = {resource: index for index, resource in enumerate(allowed_order)} + owned: list[CleanupFailureRecordV1] = [] + indexes: list[int] = [] + for record in value: + if type(record) is not CleanupFailureRecordV1: + raise TypeError("cleanup failure record is not canonical") + canonical = CleanupFailureRecordV1(*tuple(record)) + if tuple(canonical) != tuple(record) or canonical.resource not in order: + raise TypeError("cleanup failure record is not canonical") + owned.append(canonical) + indexes.append(order[canonical.resource]) + if indexes != sorted(set(indexes)): + raise TypeError("cleanup failure records are not in stable resource order") + return tuple(owned) + + +class DockerBuildCleanupFailureV1(tuple): + __slots__ = () + + def __new__( + cls, + trigger: DockerCleanupTriggerV1, + failures: tuple[CleanupFailureRecordV1, ...], + stdout: bytes, + stderr: bytes, + input_progress: BuildInputTransferProgressV1 | None = None, + ) -> DockerBuildCleanupFailureV1: + if type(trigger) is not DockerCleanupTriggerV1: + raise TypeError("invalid Docker cleanup trigger") + owned_failures = _cleanup_failure_records_v1( + failures, + ( + CleanupResourceV1.DOCKER_CLI_PROCESS, + CleanupResourceV1.DOCKER_CONTAINER, + CleanupResourceV1.DOCKER_CID_ROOT, + ), + ) + _bounded_bytes(stdout, BUILD_STDOUT_LIMIT_V1, "stdout") + _bounded_bytes(stderr, BUILD_STDERR_LIMIT_V1, "stderr") + if input_progress is not None and type( + input_progress + ) is not BuildInputTransferProgressV1: + raise TypeError("invalid cleanup build input progress") + return tuple.__new__( + cls, + (trigger, owned_failures, stdout, stderr, input_progress), + ) + + @property + def trigger(self) -> DockerCleanupTriggerV1: + return self[0] + + @property + def failures(self) -> tuple[CleanupFailureRecordV1, ...]: + return self[1] + + @property + def detail(self) -> str: + """Render all typed records for diagnostic-only consumers.""" + + return "; ".join(record.detail for record in self.failures) + + @property + def stdout(self) -> bytes: + return self[2] + + @property + def stderr(self) -> bytes: + return self[3] + + @property + def input_progress(self) -> BuildInputTransferProgressV1 | None: + return self[4] + + +DockerBuildProcessObservationV1: TypeAlias = ( + DockerBuildExitedV1 + | DockerBuildTimedOutV1 + | DockerBuildOutputLimitV1 + | DockerBuildObserverFailureV1 + | DockerBuildInputRejectedV1 + | DockerBuildCleanupFailureV1 +) + + +_DockerCommandObservationV1: TypeAlias = ( + _DockerCommandExitedV1 | DockerBuildProcessObservationV1 +) + + +def _canonical_progress_v1( + value: object, + input_value: _build_input.SealedInputV1, +) -> BuildInputTransferProgressV1 | None: + if value is None: + return None + if not _input_progress_matches_v1(value, input_value): + raise TypeError("build input progress is not canonical") + return _build_input_progress_v1( + input_value, + value.written_length, + value.written_sha256, + ) + + +def _canonical_process_observation_v1( + value: object, + input_value: _build_input.SealedInputV1, + max_output_bytes: int, + max_stderr_bytes: int, +) -> DockerBuildProcessObservationV1: + """Own a backend observation before classification or retention.""" + + if ( + not _build_input.sealed_input_is_intact_v1(input_value) + or type(max_output_bytes) is not int + or max_output_bytes <= 0 + or type(max_stderr_bytes) is not int + or max_stderr_bytes <= 0 + ): + raise TypeError("invalid process-observation boundary") + try: + if type(value) is DockerBuildExitedV1: + if not docker_build_exited_is_valid_v1( + value, + input_value, + max_output_bytes, + max_stderr_bytes, + ): + raise TypeError("invalid exited build observation") + transfer = _completed_build_input_transfer_v1( + input_value, + value.input_transfer.written_length, + value.input_transfer.written_sha256, + ) + canonical = _docker_build_exited_v1( + value.returncode, + bytes(value.stdout), + bytes(value.stderr), + transfer, + ) + if tuple(canonical) != tuple(value): + raise TypeError("exited build observation is not canonical") + return value + if type(value) is DockerBuildTimedOutV1: + progress = _canonical_progress_v1(value.input_progress, input_value) + if progress is None: + raise TypeError("build timeout did not retain input progress") + canonical = DockerBuildTimedOutV1( + _bounded_bytes(value.stdout, max_output_bytes, "stdout"), + _bounded_bytes(value.stderr, max_stderr_bytes, "stderr"), + progress, + ) + if tuple(canonical) != tuple(value): + raise TypeError("timeout observation is not canonical") + return value + if type(value) is DockerBuildOutputLimitV1: + progress = _canonical_progress_v1(value.input_progress, input_value) + if progress is None: + raise TypeError("build output limit did not retain input progress") + stdout = _bounded_bytes(value.stdout, max_output_bytes, "stdout") + stderr = _bounded_bytes(value.stderr, max_stderr_bytes, "stderr") + if ( + ( + value.stream is DockerOutputStreamV1.STDOUT + and len(stdout) != max_output_bytes + ) + or ( + value.stream is DockerOutputStreamV1.STDERR + and len(stderr) != max_stderr_bytes + ) + ): + raise TypeError("output-limit observation did not reach its cap") + canonical = DockerBuildOutputLimitV1( + value.stream, + stdout, + stderr, + progress, + ) + if tuple(canonical) != tuple(value): + raise TypeError("output-limit observation is not canonical") + return value + if type(value) is DockerBuildObserverFailureV1: + canonical = DockerBuildObserverFailureV1( + value.detail, + _bounded_bytes(value.stdout, max_output_bytes, "stdout"), + _bounded_bytes(value.stderr, max_stderr_bytes, "stderr"), + _canonical_progress_v1(value.input_progress, input_value), + ) + if tuple(canonical) != tuple(value): + raise TypeError("observer failure is not canonical") + return value + if type(value) is DockerBuildInputRejectedV1: + progress = _canonical_progress_v1(value.input_progress, input_value) + if progress is None or progress.written_length >= progress.expected_length: + raise TypeError("input rejection did not retain partial progress") + canonical = DockerBuildInputRejectedV1( + progress, + _bounded_bytes(value.stdout, max_output_bytes, "stdout"), + _bounded_bytes(value.stderr, max_stderr_bytes, "stderr"), + ) + if tuple(canonical) != tuple(value): + raise TypeError("input rejection is not canonical") + return value + if type(value) is DockerBuildCleanupFailureV1: + progress = _canonical_progress_v1(value.input_progress, input_value) + if progress is None: + raise TypeError("build cleanup failure did not retain input progress") + canonical = DockerBuildCleanupFailureV1( + value.trigger, + value.failures, + _bounded_bytes(value.stdout, max_output_bytes, "stdout"), + _bounded_bytes(value.stderr, max_stderr_bytes, "stderr"), + progress, + ) + if tuple(canonical) != tuple(value): + raise TypeError("cleanup failure is not canonical") + return value + except (AttributeError, IndexError, TypeError, ValueError) as error: + raise TypeError("backend process observation is not canonical") from error + raise TypeError("backend returned an unknown process observation") + + +class DockerBuildBackendV1(Protocol): + def probe(self) -> DockerCapabilityReportV1: ... + + def run_build( + self, + request: DockerBuildRequestV1, + ) -> DockerBuildProcessObservationV1: ... + + +def build_process_bytes_v1(process: DockerBuildExitedV1) -> bytes: + if type(process) is not DockerBuildExitedV1: + raise TypeError("only successful typed build observations are encodable") + try: + transfer = process.input_transfer + if ( + process.returncode != 0 + or not -(1 << 31) <= process.returncode < 1 << 31 + or type(process.stdout) is not bytes + or len(process.stdout) > BUILD_STDOUT_LIMIT_V1 + or type(process.stderr) is not bytes + or len(process.stderr) > BUILD_STDERR_LIMIT_V1 + or not _input_transfer_is_structurally_valid_v1(transfer) + ): + raise TypeError("successful build observation is not canonical") + except (AttributeError, IndexError, OverflowError, TypeError) as error: + raise TypeError( + "only successful canonical build observations are encodable" + ) from error + return b"".join( + ( + process.returncode.to_bytes(4, "big", signed=True), + len(process.stdout).to_bytes(8, "big"), + hashlib.sha256(process.stdout).digest(), + len(process.stderr).to_bytes(8, "big"), + hashlib.sha256(process.stderr).digest(), + transfer.bundle_identity, + transfer.expected_length.to_bytes(8, "big"), + transfer.expected_sha256, + transfer.written_length.to_bytes(8, "big"), + transfer.written_sha256, + ) + ) + + +class NativeDockerBuildBackendV1: + """Docker adapter whose probe observes only Linux x64 and its daemon.""" + + def __init__( + self, + docker_path: Path, + policy: DockerBuildPolicyV1, + *, + platform_name: str | None = None, + machine_name: str | None = None, + monotonic_ns: object = time.monotonic_ns, + host_user: tuple[int, int] | None = None, + ) -> None: + command_coordinate = docker_command_coordinate_v1(docker_path) + if not docker_policy_is_valid_v1(policy): + raise TypeError("policy must be DockerBuildPolicyV1") + if policy.user_mode is not DockerUserModeV1.HOST_EFFECTIVE_IDS: + raise TypeError("unsupported Docker user policy") + observed_user = ( + None if host_user is None else _host_user_coordinates(host_user) + ) + observed_platform = ( + platform.system().lower() if platform_name is None else platform_name + ) + observed_machine = ( + platform.machine() if machine_name is None else machine_name + ) + self._command_coordinate = command_coordinate + self._policy = DockerBuildPolicyV1(*tuple(policy)) + self._platform_name = _encoded_policy_text( + observed_platform, + 64, + "platform_name", + ) + self._machine_name = _encoded_policy_text( + observed_machine, + 64, + "machine_name", + ) + self._monotonic_ns = monotonic_ns + self._configured_host_user = observed_user + self._run_lease_owner = object() + self._owner_pid = os.getpid() + self._probed_capability: DockerSupportedV1 | None = None + + def _in_owner_process_v1(self) -> bool: + return os.getpid() == self._owner_pid + + def probe(self) -> DockerCapabilityReportV1: + self._probed_capability = None + if not self._in_owner_process_v1(): + return DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "native Docker capability belongs to its creator process", + ) + if self._platform_name != "linux" or self._machine_name.lower() not in ( + "x86_64", + "amd64", + ): + return DockerUnsupportedV1( + DockerBlockerReasonV1.HOST_NOT_LINUX_AMD64, + "controlled build requires a Linux amd64 Docker host", + ) + host_user = self._configured_host_user + if host_user is None: + try: + host_user = _host_user_coordinates((os.geteuid(), os.getegid())) + except (AttributeError, OSError, TypeError): + return DockerUnsupportedV1( + DockerBlockerReasonV1.HOST_USER_UNAVAILABLE, + "host effective uid/gid are unavailable", + ) + try: + command_descriptor = _open_docker_command_v1(self._command_coordinate) + try: + metadata = os.fstat(command_descriptor) + finally: + os.close(command_descriptor) + except OSError: + return DockerUnsupportedV1( + DockerBlockerReasonV1.DOCKER_UNAVAILABLE, + "exact Docker CLI path is unavailable", + ) + if not stat.S_ISREG(metadata.st_mode): + return DockerUnsupportedV1( + DockerBlockerReasonV1.DOCKER_UNAVAILABLE, + "Docker CLI must be one regular non-symlink path", + ) + commands = ( + _render_native_command_v1( + "version_probe", + self._command_coordinate, + {}, + ), + _render_native_command_v1( + "image_inspect", + self._command_coordinate, + { + _NativeCommandSlotV1.IMAGE_REFERENCE: ( + self._policy.image_reference, + ), + }, + ), + ) + outputs: list[bytes] = [] + for index, command in enumerate(commands): + result = self._observe_command( + command, + stdout_limit=self._policy.probe_output_limit, + stderr_limit=self._policy.probe_output_limit, + timeout_ns=self._policy.probe_timeout_ns, + ) + # The versioned capability observes machine-readable stdout; + # successful Docker CLI warnings are diagnostic, not absence proof. + if ( + type(result) is not _DockerCommandExitedV1 + or result.returncode != 0 + or not result.stdout + ): + return DockerUnsupportedV1( + DockerBlockerReasonV1.DOCKER_UNAVAILABLE + if index == 0 + else DockerBlockerReasonV1.IMAGE_UNAVAILABLE, + "Docker daemon probe failed" + if index == 0 + else "pinned image is not locally inspectable", + ) + outputs.append(result.stdout) + try: + inspected = json.loads(outputs[1]) + if type(inspected) is not list or len(inspected) != 1: + raise ValueError("wrong image inspection cardinality") + image = inspected[0] + if type(image) is not dict: + raise ValueError("wrong image inspection shape") + repo_digests = image.get("RepoDigests") + if ( + image.get("Os") != "linux" + or image.get("Architecture") not in ("amd64", "x86_64") + or type(repo_digests) is not list + or self._policy.image_reference not in repo_digests + ): + raise ValueError("foreign image coordinate") + except (ValueError, TypeError, json.JSONDecodeError): + return DockerUnsupportedV1( + DockerBlockerReasonV1.IMAGE_IDENTITY_MISMATCH, + "local image does not match pinned linux/amd64 manifest", + ) + daemon_observation = DockerDaemonObservationV1( + outputs[0], + outputs[1], + ) + capability = DockerSupportedV1( + self._policy, + daemon_observation, + self._command_coordinate, + host_user, + ) + self._probed_capability = capability + return capability + + def _bound_request_capability_v1( + self, + request: DockerBuildRequestV1, + ) -> DockerSupportedV1: + if type(request) is not DockerBuildRequestV1: + raise TypeError("request must be DockerBuildRequestV1") + try: + capability = request.capability + except (AttributeError, IndexError) as error: + raise TypeError("request lost its Docker capability") from error + if ( + self._probed_capability is None + or not _docker_build_request_is_valid_v1(request, capability) + or capability is not self._probed_capability + ): + raise TypeError("request capability does not match this backend probe") + return capability + + def _next_run_lease_v1( + self, + capability: DockerSupportedV1, + ) -> _NativeRunLeaseV1: + if not self._in_owner_process_v1(): + raise RuntimeError("native Docker build belongs to its creator process") + if not _docker_supported_is_valid_v1(capability): + raise TypeError("run lease requires one canonical Docker capability") + root = Path( + tempfile.mkdtemp( + prefix=_NATIVE_CID_ROOT_PREFIX_V1, + dir="/tmp", + ) + ) + try: + metadata = root.lstat() + if ( + not root.is_absolute() + or not stat.S_ISDIR(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_mode & 0o077 + ): + raise RuntimeError("native Docker CID root is not private") + cid_file = root / "cid" + if cid_file.exists() or cid_file.is_symlink(): + raise RuntimeError("fresh native Docker CID path already exists") + return _NativeRunLeaseV1( + self._run_lease_owner, + self._owner_pid, + capability, + root, + cid_file, + _token=_NATIVE_RUN_LEASE_TOKEN, + ) + except BaseException: + try: + shutil.rmtree(root) + except BaseException: + pass + raise + + def _owns_run_lease_v1(self, lease: object) -> bool: + if type(lease) is not _NativeRunLeaseV1: + return False + try: + return ( + lease.owner is self._run_lease_owner + and lease.creator_pid == self._owner_pid + and _docker_supported_is_valid_v1(lease.capability) + and lease.cid_file.is_absolute() + ) + except (AttributeError, TypeError): + return False + + def _lease_belongs_to_current_process_v1(self, lease: _NativeRunLeaseV1) -> bool: + return self._owns_run_lease_v1(lease) and os.getpid() == lease.creator_pid + + def _mark_run_lease_launched_v1(self, lease: _NativeRunLeaseV1) -> None: + if not self._lease_belongs_to_current_process_v1(lease): + raise RuntimeError("native Docker run lease belongs to another process") + if lease._released: + raise RuntimeError("native Docker run lease was already released") + lease._launched = True + + def _release_run_lease_v1(self, lease: _NativeRunLeaseV1) -> str | None: + if not self._owns_run_lease_v1(lease): + raise TypeError("native run lease does not belong to this adapter") + if os.getpid() != lease.creator_pid: + return None + if lease._released: + return None + try: + shutil.rmtree(lease._root) + except Exception: + return "native Docker CID root cleanup failed" + # This flag certifies completed removal, not merely an attempted one: + # an interrupted caller may safely retry with the same private lease. + lease._released = True + return None + + def _command_for_v1( + self, + request: DockerBuildRequestV1, + lease: _NativeRunLeaseV1, + ) -> tuple[str, ...]: + if not self._owns_run_lease_v1(lease): + raise TypeError("native run lease does not belong to this adapter") + capability = lease.capability + if ( + not _docker_build_request_is_valid_v1(request, capability) + or request.capability is not capability + ): + raise TypeError("request capability does not match this native run lease") + policy = capability.policy + return _render_native_command_v1( + "build", + capability.command_coordinate, + { + _NativeCommandSlotV1.PLATFORM: (policy.platform,), + _NativeCommandSlotV1.ORDERED_TMPFS_SPECS: policy.tmpfs_specs, + _NativeCommandSlotV1.HOSTNAME: (policy.hostname,), + _NativeCommandSlotV1.HOST_USER: ( + f"{capability.host_user[0]}:{capability.host_user[1]}", + ), + _NativeCommandSlotV1.CID_FILE: (str(lease.cid_file),), + _NativeCommandSlotV1.IMAGE_REFERENCE: (policy.image_reference,), + _NativeCommandSlotV1.BOOTSTRAP: (policy.bootstrap,), + _NativeCommandSlotV1.BOOTSTRAP_ARGV0: (policy.bootstrap_argv0,), + _NativeCommandSlotV1.INPUT_LENGTH: ( + str(request.input_bundle.length), + ), + _NativeCommandSlotV1.INPUT_SHA256: ( + request.input_bundle.sha256.hex(), + ), + }, + ) + + def run_build( + self, + request: DockerBuildRequestV1, + ) -> DockerBuildProcessObservationV1: + if not self._in_owner_process_v1(): + return DockerBuildObserverFailureV1( + "native Docker build belongs to its creator process", + b"", + b"", + ) + lease: _NativeRunLeaseV1 | None = None + observation: DockerBuildProcessObservationV1 | None = None + retained_base_exception: BaseException | None = None + try: + capability = self._bound_request_capability_v1(request) + lease = self._next_run_lease_v1(capability) + command = self._command_for_v1(request, lease) + if not self._lease_belongs_to_current_process_v1(lease): + return DockerBuildObserverFailureV1( + "native Docker build belongs to its creator process", + b"", + b"", + ) + policy = capability.policy + raw_observation = self._observe_command( + command, + stdout_limit=request.max_output_bytes, + stderr_limit=policy.stderr_limit, + timeout_ns=policy.build_timeout_ns, + lease=lease, + input_bundle=request.input_bundle, + ) + try: + canonical_observation = _canonical_process_observation_v1( + raw_observation, + request.input_bundle, + request.max_output_bytes, + policy.stderr_limit, + ) + except Exception: + observation = DockerBuildObserverFailureV1( + "native Docker build observation is not canonical", + b"", + b"", + ) + else: + if ( + type(canonical_observation) is DockerBuildCleanupFailureV1 + and any( + record.resource is CleanupResourceV1.DOCKER_CID_ROOT + for record in canonical_observation.failures + ) + ): + observation = DockerBuildObserverFailureV1( + "native Docker build observation already contains a " + "CID-root cleanup failure", + canonical_observation.stdout, + canonical_observation.stderr, + canonical_observation.input_progress, + ) + else: + observation = canonical_observation + except Exception: + observation = DockerBuildObserverFailureV1( + "native Docker build request could not be materialized", + b"", + b"", + ) + except BaseException as error: + retained_base_exception = error + finally: + release_detail: str | None = None + if lease is not None: + try: + release_detail = _canonical_cid_root_cleanup_detail_v1( + self._release_run_lease_v1(lease) + ) + except Exception: + release_detail = "native Docker CID root cleanup observer raised" + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + release_detail = "native Docker CID root cleanup was interrupted" + if retained_base_exception is None and release_detail is not None: + observation = self._with_cid_root_cleanup_failure_v1( + observation, + release_detail, + ) + if retained_base_exception is not None: + raise retained_base_exception.with_traceback( + retained_base_exception.__traceback__ + ) + if observation is None: + return DockerBuildObserverFailureV1( + "native Docker build observation was unavailable", + b"", + b"", + ) + return observation + + @staticmethod + def _with_cid_root_cleanup_failure_v1( + observation: object, + detail: str, + ) -> DockerBuildProcessObservationV1: + """Retain a canonical prefix, otherwise report a typed observer failure.""" + + if observation is None: + return DockerBuildObserverFailureV1(detail, b"", b"") + if type(observation) is DockerBuildCleanupFailureV1: + return DockerBuildCleanupFailureV1( + observation.trigger, + observation.failures + + ( + CleanupFailureRecordV1( + CleanupResourceV1.DOCKER_CID_ROOT, + detail, + ), + ), + observation.stdout, + observation.stderr, + observation.input_progress, + ) + if type(observation) is DockerBuildExitedV1: + transfer = observation.input_transfer + progress = BuildInputTransferProgressV1( + transfer.bundle_identity, + transfer.expected_length, + transfer.expected_sha256, + transfer.written_length, + transfer.written_sha256, + _token=_BUILD_INPUT_PROGRESS_TOKEN, + ) + trigger = DockerCleanupTriggerV1.PROCESS_EXIT + elif type(observation) is DockerBuildTimedOutV1: + progress = observation.input_progress + trigger = DockerCleanupTriggerV1.TIMEOUT + elif type(observation) is DockerBuildOutputLimitV1: + progress = observation.input_progress + trigger = DockerCleanupTriggerV1.OUTPUT_LIMIT + elif type(observation) is DockerBuildInputRejectedV1: + progress = observation.input_progress + trigger = DockerCleanupTriggerV1.INPUT_TRANSFER + elif type(observation) is DockerBuildObserverFailureV1: + progress = observation.input_progress + trigger = DockerCleanupTriggerV1.OBSERVER_FAILURE + else: + fallback_detail = ( + "native Docker build observation is not canonical; " + detail + ) + # Keep the joined diagnostic typed and bounded even when cleanup + # supplies the full diagnostic budget. + fallback_detail = fallback_detail[:_DIAGNOSTIC_DETAIL_TEXT_LIMIT_V1] + return DockerBuildObserverFailureV1( + fallback_detail, + b"", + b"", + ) + if progress is None: + if type(observation) is DockerBuildObserverFailureV1: + try: + return DockerBuildObserverFailureV1( + observation.detail + "; " + detail, + observation.stdout, + observation.stderr, + ) + except Exception: + return DockerBuildObserverFailureV1( + _OBSERVER_AND_CID_ROOT_CLEANUP_FAILURE_V1, + observation.stdout, + observation.stderr, + ) + return DockerBuildObserverFailureV1( + detail, + observation.stdout, + observation.stderr, + ) + return DockerBuildCleanupFailureV1( + trigger, + ( + CleanupFailureRecordV1( + CleanupResourceV1.DOCKER_CID_ROOT, + detail, + ), + ), + observation.stdout, + observation.stderr, + progress, + ) + + def _observe_command( + self, + command: tuple[str, ...], + *, + stdout_limit: int, + stderr_limit: int, + timeout_ns: int, + lease: _NativeRunLeaseV1 | None = None, + input_bundle: _build_input.SealedInputV1 | None = None, + ) -> _DockerCommandObservationV1: + if ( + type(command) is not tuple + or not command + or any(type(item) is not str or not item or "\0" in item for item in command) + ): + raise TypeError("command must be a nonempty string tuple") + try: + tuple(os.fsencode(item) for item in command) + except (TypeError, UnicodeEncodeError) as error: + raise TypeError("command contains an unencodable coordinate") from error + if ( + type(stdout_limit) is not int + or stdout_limit <= 0 + or stdout_limit > BUILD_STDOUT_LIMIT_V1 + or type(stderr_limit) is not int + or stderr_limit <= 0 + or stderr_limit > BUILD_STDERR_LIMIT_V1 + or type(timeout_ns) is not int + or timeout_ns <= 0 + or timeout_ns > BUILD_TIMEOUT_NS_V1 + ): + raise TypeError("invalid Docker observation limits") + if lease is not None and not self._owns_run_lease_v1(lease): + raise TypeError("native run lease does not belong to this adapter") + if not self._in_owner_process_v1() or ( + lease is not None and not self._lease_belongs_to_current_process_v1(lease) + ): + return DockerBuildObserverFailureV1( + "native Docker observation belongs to its creator process", + b"", + b"", + ) + if input_bundle is not None and type(input_bundle) is not _build_input.SealedInputV1: + raise TypeError("input_bundle must be controller sealed") + if input_bundle is not None and not _build_input.sealed_input_is_intact_v1( + input_bundle + ): + return DockerBuildObserverFailureV1( + "build input bytes are not intact", + b"", + b"", + ) + # The one protected region starts before Popen. Once Popen returns + # its handle, every following Python bytecode has that handle under + # the finalizer; state allocation cannot create a post-spawn gap. + stdout: bytearray | bytes = b"" + stderr: bytearray | bytes = b"" + selector: selectors.BaseSelector | None = None + terminal: DockerOutputStreamV1 | None = None + timed_out = False + observer_failed = False + input_failed = False + written = 0 + input_hasher = hashlib.sha256() + bundle_view: memoryview | None = None + input_progress: BuildInputTransferProgressV1 | None = None + stop_detail: str | None = None + cleanup_detail: str | None = None + input_descriptor: int | None = None + stdout_descriptor: int | None = None + stderr_descriptor: int | None = None + retained_base_exception: BaseException | None = None + ownership_lost = False + process: subprocess.Popen[bytes] | None = None + try: + try: + process = subprocess.Popen( + command, + **_NATIVE_PROCESS_CONTEXT_V1.popen_kwargs_v1( + input_bundle is not None + ), + ) + except (OSError, UnicodeEncodeError): + return DockerBuildObserverFailureV1( + "cannot start Docker CLI", + b"", + b"", + ) + stdout = bytearray() + stderr = bytearray() + if lease is not None: + self._mark_run_lease_launched_v1(lease) + if ( + process.stdout is None + or process.stderr is None + or (input_bundle is not None and process.stdin is None) + ): + observer_failed = True + raise RuntimeError("Docker pipes unavailable") + stdout_descriptor = process.stdout.fileno() + stderr_descriptor = process.stderr.fileno() + if process.stdin is not None: + input_descriptor = process.stdin.fileno() + selector = selectors.DefaultSelector() + bundle_view = ( + memoryview(input_bundle.contents) + if input_bundle is not None + else None + ) + streams = ( + ( + stdout_descriptor, + DockerOutputStreamV1.STDOUT, + stdout, + stdout_limit, + ), + ( + stderr_descriptor, + DockerOutputStreamV1.STDERR, + stderr, + stderr_limit, + ), + ) + for descriptor, stream, target, maximum in streams: + os.set_blocking(descriptor, False) + selector.register( + descriptor, + selectors.EVENT_READ, + ("read", stream, target, maximum), + ) + if process.stdin is not None: + os.set_blocking(input_descriptor, False) + selector.register( + input_descriptor, + selectors.EVENT_WRITE, + ("write",), + ) + start = self._clock() + deadline = start + timeout_ns + while selector.get_map() or process.poll() is None: + now = self._clock() + if now >= deadline: + timed_out = True + break + timeout = min( + (deadline - now) / 1_000_000_000, + _POLL_SLICE_SECONDS_V1, + ) + for key, _events in selector.select(timeout): + if key.data[0] == "read": + _kind, stream, target, maximum = key.data + try: + chunk = os.read( + key.fd, + min( + _IO_CHUNK_BYTES_V1, + maximum + 1 - len(target), + ), + ) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fd) + continue + target.extend(chunk) + if len(target) > maximum: + del target[maximum:] + terminal = stream + break + continue + if input_bundle is None or bundle_view is None: + observer_failed = True + break + try: + count = os.write( + key.fd, + bundle_view[written : written + _IO_CHUNK_BYTES_V1], + ) + except BlockingIOError: + continue + except BrokenPipeError: + input_failed = True + break + if count <= 0: + input_failed = True + break + input_hasher.update(bundle_view[written : written + count]) + written += count + if written == input_bundle.length: + selector.unregister(key.fd) + if process.stdin is not None: + process.stdin.close() + if terminal is not None or input_failed or observer_failed: + break + except _NativeOwnershipLostV1: + ownership_lost = True + observer_failed = True + except Exception: + observer_failed = True + except BaseException as error: + # Cancellation is not an excuse to leak a child or a container. It + # is re-raised only after every independently-owned resource got a + # best-effort deterministic release attempt. + retained_base_exception = error + finally: + if process is None: + # Popen can itself be interrupted after the daemon received a + # launch request but before Python returned a handle. There + # is no safe CLI PID to reap then, yet a CID cleanup attempt + # can still release a Docker container without replacing the + # caller's original interruption. + if retained_base_exception is not None and lease is not None: + try: + self._cleanup_container( + lease, + spawn_may_have_started=True, + ) + except BaseException: + pass + else: + if selector is not None: + try: + selector.close() + except Exception: + observer_failed = True + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + observer_failed = True + if process.stdin is not None: + close_failed, close_interrupt = self._close_owned_stream( + process.stdin, + ) + if close_failed: + observer_failed = True + if close_interrupt is not None: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + close_interrupt, + ) + if bundle_view is not None: + try: + bundle_view.release() + except Exception: + observer_failed = True + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + observer_failed = True + if input_bundle is not None: + try: + input_progress = _build_input_progress_v1( + input_bundle, + written, + input_hasher.digest(), + ) + except Exception: + observer_failed = True + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + observer_failed = True + ownership_lost = ownership_lost or not self._in_owner_process_v1() + if not ownership_lost: + try: + process_running = process.poll() is None + except Exception: + process_running = True + stop_detail = "Docker CLI process state could not be observed" + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + process_running = True + stop_detail = "Docker CLI process state observation was interrupted" + if process_running: + if not ( + timed_out + or terminal is not None + or observer_failed + or input_failed + ): + timed_out = True + try: + observed_stop = self._stop_process(process) + except Exception: + observed_stop = "Docker CLI process termination raised" + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + observed_stop = "Docker CLI process termination was interrupted" + fallback_stop = self._force_reap_after_interruption_v1(process) + stop_detail = stop_detail or observed_stop or fallback_stop + for stream in (process.stdout, process.stderr): + if stream is None: + continue + close_failed, close_interrupt = self._close_owned_stream( + stream, + ) + if close_failed: + observer_failed = True + if close_interrupt is not None: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + close_interrupt, + ) + if lease is not None and not ownership_lost: + try: + cleanup_detail = self._cleanup_container( + lease, + spawn_may_have_started=True, + ) + except Exception: + cleanup_detail = "Docker container cleanup observer raised" + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + cleanup_detail = "Docker container cleanup was interrupted" + if retained_base_exception is not None: + raise retained_base_exception.with_traceback( + retained_base_exception.__traceback__ + ) + if ownership_lost: + return DockerBuildObserverFailureV1( + "native Docker observation left its creator process", + bytes(stdout), + bytes(stderr), + input_progress, + ) + if stop_detail is not None or cleanup_detail is not None: + trigger = DockerCleanupTriggerV1.PROCESS_EXIT + if observer_failed: + trigger = DockerCleanupTriggerV1.OBSERVER_FAILURE + elif terminal is not None: + trigger = DockerCleanupTriggerV1.OUTPUT_LIMIT + elif timed_out: + trigger = DockerCleanupTriggerV1.TIMEOUT + elif input_failed: + trigger = DockerCleanupTriggerV1.INPUT_TRANSFER + failures: list[CleanupFailureRecordV1] = [] + if stop_detail is not None: + failures.append( + CleanupFailureRecordV1( + CleanupResourceV1.DOCKER_CLI_PROCESS, + stop_detail, + ) + ) + if cleanup_detail is not None: + failures.append( + CleanupFailureRecordV1( + CleanupResourceV1.DOCKER_CONTAINER, + cleanup_detail, + ) + ) + return DockerBuildCleanupFailureV1( + trigger, + tuple(failures), + bytes(stdout), + bytes(stderr), + input_progress, + ) + if input_failed: + if input_progress is None: + return DockerBuildObserverFailureV1( + "build input progress could not be retained", + bytes(stdout), + bytes(stderr), + input_progress, + ) + return DockerBuildInputRejectedV1( + input_progress, + bytes(stdout), + bytes(stderr), + ) + if observer_failed: + return DockerBuildObserverFailureV1( + "Docker output observation failed", + bytes(stdout), + bytes(stderr), + input_progress, + ) + if terminal is not None: + return DockerBuildOutputLimitV1( + terminal, + bytes(stdout), + bytes(stderr), + input_progress, + ) + if timed_out: + return DockerBuildTimedOutV1( + bytes(stdout), + bytes(stderr), + input_progress, + ) + if type(process.returncode) is not int: + return DockerBuildObserverFailureV1( + "Docker returncode unavailable", + bytes(stdout), + bytes(stderr), + input_progress, + ) + if input_bundle is not None: + if ( + input_progress is None + or written != input_bundle.length + or input_hasher.digest() != input_bundle.sha256 + ): + return DockerBuildObserverFailureV1( + "completed build input transfer invariant failed", + bytes(stdout), + bytes(stderr), + input_progress, + ) + input_transfer = _completed_build_input_transfer_v1( + input_bundle, + written, + input_hasher.digest(), + ) + return _docker_build_exited_v1( + process.returncode, + bytes(stdout), + bytes(stderr), + input_transfer, + ) + return _docker_command_exited_v1( + process.returncode, + bytes(stdout), + bytes(stderr), + ) + + @staticmethod + def _close_owned_stream( + stream: object, + ) -> tuple[bool, BaseException | None]: + """Release through the stream owner without aliasing its descriptor.""" + + close_failed = False + close_raised = False + retained_base_exception: BaseException | None = None + try: + closed = stream.closed is True + except Exception: + closed = False + close_failed = True + except BaseException as error: + closed = False + close_failed = True + retained_base_exception = error + if not closed: + try: + stream.close() + except Exception: + close_failed = True + close_raised = True + except BaseException as error: + close_failed = True + close_raised = True + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + if close_raised: + try: + still_open = stream.closed is False + except Exception: + still_open = False + except BaseException as error: + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + still_open = False + if still_open: + # close() may fail before it releases the resource, but a + # saved FD can already name another resource. Retrying the + # same owner is the only bounded release attempt that keeps + # ownership unambiguous. + try: + stream.close() + except Exception: + close_failed = True + except BaseException as error: + close_failed = True + retained_base_exception = _retain_first_base_exception_v1( + retained_base_exception, + error, + ) + return close_failed, retained_base_exception + + def _clock(self) -> int: + if not self._in_owner_process_v1(): + raise _NativeOwnershipLostV1( + "native Docker observation belongs to its creator process" + ) + value = self._monotonic_ns() + if not self._in_owner_process_v1(): + raise _NativeOwnershipLostV1( + "native Docker observation left its creator process" + ) + if type(value) is not int or value < 0: + raise RuntimeError("invalid monotonic clock") + return value + + def _stop_process( + self, + process: subprocess.Popen[bytes], + ) -> str | None: + failed = False + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except OSError: + try: + process.kill() + except ProcessLookupError: + pass + except OSError: + failed = True + try: + process.wait(timeout=_PROCESS_STOP_TIMEOUT_SECONDS_V1) + except subprocess.TimeoutExpired: + failed = True + if process.poll() is None: + failed = True + return "Docker CLI process could not be terminated" if failed else None + + @staticmethod + def _force_reap_after_interruption_v1( + process: subprocess.Popen[bytes], + ) -> str | None: + """Use an independent, interruption-safe kill/reap path after stop fails.""" + + failed = False + try: + running = process.poll() is None + except BaseException: + running = True + failed = True + if running: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except BaseException: + try: + process.kill() + except ProcessLookupError: + pass + except BaseException: + failed = True + try: + process.wait(timeout=_PROCESS_STOP_TIMEOUT_SECONDS_V1) + except BaseException: + failed = True + try: + if process.poll() is None: + failed = True + except BaseException: + failed = True + return "Docker CLI process could not be force-reaped" if failed else None + + @staticmethod + def _docker_issued_container_id_v1( + lease: _NativeRunLeaseV1, + ) -> _DockerIssuedContainerIdV1 | None: + """Admit only the exact ID written into this fresh private CID path.""" + + try: + descriptor = os.open( + lease.cid_file, + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_NOFOLLOW", 0), + ) + except OSError: + return None + try: + metadata = os.fstat(descriptor) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_nlink != 1 + or metadata.st_size not in (64, 65) + ): + return None + raw = os.read(descriptor, 66) + except OSError: + return None + finally: + try: + os.close(descriptor) + except OSError: + pass + if len(raw) == 65 and raw.endswith(b"\n"): + raw = raw[:-1] + if len(raw) != 64 or any(byte not in b"0123456789abcdef" for byte in raw): + return None + return _DockerIssuedContainerIdV1( + raw.decode("ascii"), + _token=_DOCKER_ISSUED_CONTAINER_ID_TOKEN, + ) + + def _observe_cleanup_command( + self, + capability: DockerSupportedV1, + command: tuple[str, ...], + ) -> _DockerCommandObservationV1: + if not self._in_owner_process_v1(): + raise RuntimeError("native Docker cleanup belongs to its creator process") + if not _docker_supported_is_valid_v1(capability): + raise TypeError("Docker cleanup requires one observed capability") + policy = capability.policy + return self._observe_command( + command, + stdout_limit=policy.probe_output_limit, + stderr_limit=policy.probe_output_limit, + timeout_ns=policy.probe_timeout_ns, + ) + + def _container_is_absent_v1( + self, + capability: DockerSupportedV1, + container_id: _DockerIssuedContainerIdV1, + ) -> bool: + observation = self._observe_cleanup_command( + capability, + _render_native_command_v1( + "cleanup_ls", + capability.command_coordinate, + { + _NativeCommandSlotV1.CONTAINER_FILTER: ( + f"id={str(container_id)}", + ), + }, + ), + ) + return ( + type(observation) is _DockerCommandExitedV1 + and observation.returncode == 0 + and observation.stdout == b"" + and observation.stderr == b"" + ) + + def _cleanup_container( + self, + lease: _NativeRunLeaseV1, + *, + spawn_may_have_started: bool = False, + ) -> str | None: + if not self._owns_run_lease_v1(lease): + raise TypeError("native run lease does not belong to this adapter") + if type(spawn_may_have_started) is not bool: + raise TypeError("native Docker spawn state must be bool") + if not self._lease_belongs_to_current_process_v1(lease): + return "native Docker run lease belongs to another process" + if not lease.launched and not spawn_may_have_started: + return None + capability = lease.capability + container_id = self._docker_issued_container_id_v1(lease) + if container_id is None: + return "Docker-issued cleanup ID is unavailable" + try: + inspection = self._observe_cleanup_command( + capability, + _render_native_command_v1( + "cleanup_inspect", + capability.command_coordinate, + { + _NativeCommandSlotV1.CONTAINER_COORDINATE: ( + str(container_id), + ), + }, + ), + ) + if ( + type(inspection) is _DockerCommandExitedV1 + and inspection.returncode != 0 + ): + return ( + None + if self._container_is_absent_v1(capability, container_id) + else "Docker container absence could not be verified" + ) + if ( + type(inspection) is not _DockerCommandExitedV1 + or inspection.returncode != 0 + or inspection.stdout != container_id.encode("ascii") + b"\n" + or inspection.stderr + ): + return "Docker-issued cleanup ID did not resolve exactly" + self._observe_cleanup_command( + capability, + _render_native_command_v1( + "cleanup_rm", + capability.command_coordinate, + { + _NativeCommandSlotV1.CONTAINER_COORDINATE: ( + str(container_id), + ), + }, + ) + ) + if not self._container_is_absent_v1(capability, container_id): + return "Docker container absence could not be verified" + except Exception: + return "Docker container cleanup observer raised" + return None + + +class BuildFailureReasonV1(StrEnum): + CONTRACT_VIOLATION = "contract_violation" + PROCESS_FAILED = "process_failed" + CLEANUP_FAILED = "cleanup_failed" + INPUT_TRANSFER_FAILED = "input_transfer_failed" + TIMEOUT = "timeout" + OUTPUT_LIMIT = "output_limit" + OBSERVER_FAILURE = "observer_failure" + INVALID_OUTPUT = "invalid_output" + + +BuildAttemptObservationV1: TypeAlias = DockerBuildProcessObservationV1 + + +class BuildSessionV1(tuple): + """Owned coordinates shared by every attempt in one two-build session.""" + + __slots__ = () + + def __new__( + cls, + capability: DockerSupportedV1, + input_value: _build_input.SealedInputV1, + max_output_bytes: int, + *, + _token: object, + ) -> BuildSessionV1: + if ( + _token is not _BUILD_SESSION_TOKEN + or not _docker_supported_is_valid_v1(capability) + or not _build_input.sealed_input_is_intact_v1(input_value) + or type(max_output_bytes) is not int + or max_output_bytes <= 0 + or max_output_bytes > capability.policy.stdout_limit + ): + raise TypeError("invalid two-build session coordinates") + return tuple.__new__( + cls, + ( + capability, + input_value, + max_output_bytes, + ), + ) + + @property + def policy(self) -> DockerBuildPolicyV1: + return self.capability.policy + + @property + def capability(self) -> DockerSupportedV1: + return self[0] + + @property + def input_value(self) -> _build_input.SealedInputV1: + return self[1] + + @property + def max_output_bytes(self) -> int: + return self[2] + + +def _build_session_v1( + capability: DockerSupportedV1, + input_value: _build_input.SealedInputV1, + max_output_bytes: int, +) -> BuildSessionV1: + return BuildSessionV1( + capability, + input_value, + max_output_bytes, + _token=_BUILD_SESSION_TOKEN, + ) + + +def _build_session_is_valid_v1(value: object) -> bool: + if type(value) is not BuildSessionV1: + return False + try: + canonical = _build_session_v1( + value.capability, + value.input_value, + value.max_output_bytes, + ) + return tuple(canonical) == tuple(value) + except Exception: + return False + + +class BuildByteRelationV1(StrEnum): + IDENTICAL = "identical" + DIFFERENT = "different" + + +class TwoBuildObservationV1(tuple): + """Two fresh successful attempts and their observed byte relation.""" + + __slots__ = () + + def __new__( + cls, + session: BuildSessionV1, + processes: tuple[DockerBuildExitedV1, DockerBuildExitedV1], + *, + _token: object, + ) -> TwoBuildObservationV1: + if ( + _token is not _TWO_BUILD_OBSERVATION_TOKEN + or not _build_session_is_valid_v1(session) + or type(processes) is not tuple + or len(processes) != 2 + ): + raise TypeError("invalid two-build observation") + owned: list[DockerBuildExitedV1] = [] + for process in processes: + canonical = _canonical_process_observation_v1( + process, + session.input_value, + session.max_output_bytes, + session.policy.stderr_limit, + ) + if type(canonical) is not DockerBuildExitedV1 or canonical.returncode != 0: + raise TypeError("two-build observation requires successful exits") + owned.append(canonical) + owned_processes = (owned[0], owned[1]) + relation = ( + BuildByteRelationV1.IDENTICAL + if owned_processes[0].stdout == owned_processes[1].stdout + else BuildByteRelationV1.DIFFERENT + ) + return tuple.__new__(cls, (session, relation, owned_processes)) + + @property + def session(self) -> BuildSessionV1: + return self[0] + + @property + def relation(self) -> BuildByteRelationV1: + return self[1] + + @property + def policy(self) -> DockerBuildPolicyV1: + return self.session.policy + + @property + def capability(self) -> DockerSupportedV1: + return self.session.capability + + @property + def input_value(self) -> _build_input.SealedInputV1: + return self.session.input_value + + @property + def max_output_bytes(self) -> int: + return self.session.max_output_bytes + + @property + def processes( + self, + ) -> tuple[DockerBuildExitedV1, DockerBuildExitedV1]: + return self[2] + + @property + def outputs(self) -> tuple[bytes, bytes]: + return self.processes[0].stdout, self.processes[1].stdout + + @property + def first_sha256(self) -> bytes: + return hashlib.sha256(self.outputs[0]).digest() + + @property + def second_sha256(self) -> bytes: + return hashlib.sha256(self.outputs[1]).digest() + + +class BuildRejectedV1(tuple): + """Typed failed attempt retaining the successful causal prefix.""" + + __slots__ = () + + def __new__( + cls, + attempt: int, + reason: BuildFailureReasonV1, + process: BuildAttemptObservationV1 | None = None, + *, + session: BuildSessionV1 | None = None, + completed_processes: tuple[DockerBuildExitedV1, ...] = (), + ) -> BuildRejectedV1: + if ( + type(attempt) is not int + or attempt not in (1, 2) + or type(reason) is not BuildFailureReasonV1 + or type(completed_processes) is not tuple + ): + raise TypeError("invalid build rejection") + if session is None: + if ( + reason is not BuildFailureReasonV1.CONTRACT_VIOLATION + or process is not None + or completed_processes + ): + raise TypeError("context-free rejection must be a contract violation") + return tuple.__new__(cls, (attempt, reason, None, None, ())) + if ( + not _build_session_is_valid_v1(session) + or len(completed_processes) != attempt - 1 + ): + raise TypeError("build rejection lost its causal prefix") + owned_completed: list[DockerBuildExitedV1] = [] + for completed in completed_processes: + canonical = _canonical_process_observation_v1( + completed, + session.input_value, + session.max_output_bytes, + session.policy.stderr_limit, + ) + if type(canonical) is not DockerBuildExitedV1 or canonical.returncode != 0: + raise TypeError("causal prefix contains a failed attempt") + owned_completed.append(canonical) + if process is None: + owned_process: BuildAttemptObservationV1 | None = None + else: + owned_process = _canonical_process_observation_v1( + process, + session.input_value, + session.max_output_bytes, + session.policy.stderr_limit, + ) + expected_process_types: dict[BuildFailureReasonV1, tuple[type, ...]] = { + BuildFailureReasonV1.CONTRACT_VIOLATION: (), + BuildFailureReasonV1.PROCESS_FAILED: (DockerBuildExitedV1,), + BuildFailureReasonV1.CLEANUP_FAILED: (DockerBuildCleanupFailureV1,), + BuildFailureReasonV1.INPUT_TRANSFER_FAILED: ( + DockerBuildInputRejectedV1, + ), + BuildFailureReasonV1.TIMEOUT: (DockerBuildTimedOutV1,), + BuildFailureReasonV1.OUTPUT_LIMIT: (DockerBuildOutputLimitV1,), + BuildFailureReasonV1.OBSERVER_FAILURE: ( + DockerBuildObserverFailureV1, + ), + BuildFailureReasonV1.INVALID_OUTPUT: (DockerBuildExitedV1,), + } + expected = expected_process_types[reason] + if not expected: + if owned_process is not None: + raise TypeError("contract-violation rejection cannot retain authority") + elif type(owned_process) not in expected: + raise TypeError("build rejection reason and observation disagree") + if ( + ( + reason is BuildFailureReasonV1.PROCESS_FAILED + and owned_process.returncode == 0 + ) + or ( + reason is BuildFailureReasonV1.INVALID_OUTPUT + and owned_process.returncode != 0 + ) + ): + raise TypeError("build rejection exit status disagrees with reason") + return tuple.__new__( + cls, + ( + attempt, + reason, + owned_process, + session, + tuple(owned_completed), + ), + ) + + @property + def attempt(self) -> int: + return self[0] + + @property + def reason(self) -> BuildFailureReasonV1: + return self[1] + + @property + def process(self) -> BuildAttemptObservationV1 | None: + return self[2] + + @property + def session(self) -> BuildSessionV1 | None: + return self[3] + + @property + def completed_processes(self) -> tuple[DockerBuildExitedV1, ...]: + return self[4] + + +def two_build_observation_matches_v1( + value: object, + session: BuildSessionV1, +) -> bool: + if ( + type(value) is not TwoBuildObservationV1 + or not _build_session_is_valid_v1(session) + ): + return False + try: + replayed = TwoBuildObservationV1( + session, + value.processes, + _token=_TWO_BUILD_OBSERVATION_TOKEN, + ) + return tuple(replayed) == tuple(value) + except Exception: + return False + + +BuildTransportResultV1: TypeAlias = ( + TwoBuildObservationV1 | BuildRejectedV1 +) + + +class ControlledBuildTransportV1: + """Own two fresh attempts; callers own semantic input and output admission.""" + + def __init__( + self, + *, + policy: DockerBuildPolicyV1, + backend: DockerBuildBackendV1, + ) -> None: + if not docker_policy_is_valid_v1(policy): + raise TypeError("policy must be DockerBuildPolicyV1") + self._policy = DockerBuildPolicyV1(*tuple(policy)) + self._backend = backend + # Fork copies Python object state and may copy a locked mutex. This + # controller's capability is therefore valid only in its creator; + # every public operation checks PID before it can touch that mutex. + self._owner_pid = os.getpid() + self._probed_capability: DockerSupportedV1 | None = None + self._consumed = False + # Probe result and its one-shot BUILD right are one causal state. A + # lock makes the state transition indivisible across reentrant or + # concurrent callers without holding it during caller/backend IO. + self._lease_lock = threading.Lock() + self._probe_in_flight = False + + def _in_owner_process_v1(self) -> bool: + return os.getpid() == self._owner_pid + + def probe(self) -> DockerCapabilityReportV1: + if not self._in_owner_process_v1(): + return DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "build transport capability belongs to its creator process", + ) + with self._lease_lock: + if ( + self._consumed + or self._probed_capability is not None + or self._probe_in_flight + ): + return DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "build transport capability is one-shot", + ) + self._probe_in_flight = True + # Start at a typed rejection before foreign IO so every ordinary exit + # has a public report. A BaseException still propagates unchanged. + outcome: DockerCapabilityReportV1 = DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "Docker capability probe produced no canonical report", + ) + try: + report = self._backend.probe() + except Exception: + outcome = DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "Docker capability probe raised", + ) + else: + if type(report) is DockerUnsupportedV1: + if _docker_unsupported_is_valid_v1(report): + outcome = DockerUnsupportedV1(*tuple(report)) + else: + outcome = DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "Docker capability rejection is not canonical", + ) + elif ( + not _docker_supported_is_valid_v1(report) + or report.policy != self._policy + ): + outcome = DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "Docker capability report does not match build policy", + ) + else: + outcome = report + finally: + # BaseException must not permanently leave this controller in the + # transient PROBING state. It still propagates to the caller; the + # cleanup only revokes that incomplete external observation. + if not self._in_owner_process_v1(): + outcome = DockerUnsupportedV1( + DockerBlockerReasonV1.BACKEND_CONTRACT, + "build transport capability belongs to its creator process", + ) + else: + with self._lease_lock: + self._probe_in_flight = False + if type(outcome) is DockerSupportedV1: + self._probed_capability = outcome + return outcome + + def build( + self, + capability: DockerSupportedV1, + input_value: _build_input.SealedInputV1, + max_output_bytes: int, + *, + input_admission: Callable[[_build_input.SealedInputV1], bool], + output_admission: Callable[[bytes], bool], + ) -> BuildTransportResultV1: + if not self._in_owner_process_v1(): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + if ( + not _docker_supported_is_valid_v1(capability) + or capability.policy != self._policy + or type(max_output_bytes) is not int + or max_output_bytes <= 0 + or max_output_bytes > capability.policy.stdout_limit + or not callable(input_admission) + or not callable(output_admission) + or not _build_input.sealed_input_is_intact_v1(input_value) + ): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + if not self._in_owner_process_v1(): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + with self._lease_lock: + if ( + self._consumed + or capability is not self._probed_capability + or not _docker_supported_is_valid_v1(capability) + or capability.policy != self._policy + ): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + # A rejected declaration has not reached an owned execution attempt, + # so it must not burn the lease. Recheck after claim below because a + # callback is external and may be reentrant or mutate hostile input. + if not self._admitted(input_admission, input_value): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + if not self._in_owner_process_v1(): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + with self._lease_lock: + if ( + self._consumed + or capability is not self._probed_capability + or not _docker_supported_is_valid_v1(capability) + or capability.policy != self._policy + ): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + self._consumed = True + if not _build_input.sealed_input_is_intact_v1(input_value): + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + try: + session = _build_session_v1( + capability, + input_value, + max_output_bytes, + ) + except Exception: + return BuildRejectedV1(1, BuildFailureReasonV1.CONTRACT_VIOLATION) + completed: list[DockerBuildExitedV1] = [] + for attempt in (1, 2): + if ( + not self._in_owner_process_v1() + or not _build_input.sealed_input_is_intact_v1(input_value) + or not self._admitted(input_admission, input_value) + or not self._in_owner_process_v1() + ): + return BuildRejectedV1( + attempt, + BuildFailureReasonV1.CONTRACT_VIOLATION, + session=session, + completed_processes=tuple(completed), + ) + built = self._build_once( + attempt, + session, + output_admission, + tuple(completed), + ) + if not self._in_owner_process_v1(): + return BuildRejectedV1( + attempt, + BuildFailureReasonV1.CONTRACT_VIOLATION, + session=session, + completed_processes=tuple(completed), + ) + if type(built) is BuildRejectedV1: + return built + completed.append(built) + return TwoBuildObservationV1( + session, + (completed[0], completed[1]), + _token=_TWO_BUILD_OBSERVATION_TOKEN, + ) + + @staticmethod + def _admitted( + admission: Callable[[object], bool], + value: object, + ) -> bool: + try: + return admission(value) is True + except Exception: + return False + + def _build_once( + self, + attempt: int, + session: BuildSessionV1, + output_admission: Callable[[bytes], bool], + completed_processes: tuple[DockerBuildExitedV1, ...], + ) -> DockerBuildExitedV1 | BuildRejectedV1: + if not _build_session_is_valid_v1(session): + return BuildRejectedV1( + attempt, + BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + contract_rejection = BuildRejectedV1( + attempt, + BuildFailureReasonV1.CONTRACT_VIOLATION, + session=session, + completed_processes=completed_processes, + ) + try: + return self._observe_build_attempt_v1( + attempt, + session, + output_admission, + completed_processes, + ) + except Exception: + return contract_rejection + + def _observe_build_attempt_v1( + self, + attempt: int, + session: BuildSessionV1, + output_admission: Callable[[bytes], bool], + completed_processes: tuple[DockerBuildExitedV1, ...], + ) -> DockerBuildExitedV1 | BuildRejectedV1: + contract_rejection = BuildRejectedV1( + attempt, + BuildFailureReasonV1.CONTRACT_VIOLATION, + session=session, + completed_processes=completed_processes, + ) + request = DockerBuildRequestV1( + attempt, + session.capability, + session.input_value, + session.max_output_bytes, + ) + try: + observed = self._backend.run_build(request) + except Exception: + return contract_rejection + if not self._in_owner_process_v1(): + return contract_rejection + try: + process = _canonical_process_observation_v1( + observed, + session.input_value, + session.max_output_bytes, + session.policy.stderr_limit, + ) + except TypeError: + return contract_rejection + reason_by_type: dict[type, BuildFailureReasonV1] = { + DockerBuildCleanupFailureV1: BuildFailureReasonV1.CLEANUP_FAILED, + DockerBuildInputRejectedV1: BuildFailureReasonV1.INPUT_TRANSFER_FAILED, + DockerBuildTimedOutV1: BuildFailureReasonV1.TIMEOUT, + DockerBuildOutputLimitV1: BuildFailureReasonV1.OUTPUT_LIMIT, + DockerBuildObserverFailureV1: BuildFailureReasonV1.OBSERVER_FAILURE, + } + failure_reason = reason_by_type.get(type(process)) + if failure_reason is not None: + return BuildRejectedV1( + attempt, + failure_reason, + process, + session=session, + completed_processes=completed_processes, + ) + if type(process) is not DockerBuildExitedV1: + return contract_rejection + if not docker_build_exited_is_valid_v1( + process, + session.input_value, + session.max_output_bytes, + session.policy.stderr_limit, + ): + return contract_rejection + if process.returncode != 0: + return BuildRejectedV1( + attempt, + BuildFailureReasonV1.PROCESS_FAILED, + process, + session=session, + completed_processes=completed_processes, + ) + transfer = process.input_transfer + if ( + type(transfer) is not BuildInputTransferV1 + or transfer.bundle_identity != session.input_value.binding_identity + or transfer.expected_length != session.input_value.length + or transfer.expected_sha256 != session.input_value.sha256 + or transfer.written_length != session.input_value.length + or transfer.written_sha256 != session.input_value.sha256 + ): + return contract_rejection + if not self._admitted(output_admission, process.stdout): + return BuildRejectedV1( + attempt, + BuildFailureReasonV1.INVALID_OUTPUT, + process, + session=session, + completed_processes=completed_processes, + ) + if not self._in_owner_process_v1(): + return contract_rejection + return process diff --git a/proof/region/v1/executor.py b/proof/region/v1/executor.py new file mode 100644 index 00000000..b94a5659 --- /dev/null +++ b/proof/region/v1/executor.py @@ -0,0 +1,2553 @@ +#!/usr/bin/env python3 +"""Fail-closed Linux process boundary for proof evaluators. + +This module returns process observations only. A caller must bind those +observations to source/build evidence elsewhere; no value here can certify that +provenance relationship. +""" + +from __future__ import annotations + +import ctypes +import errno as errno_module +import fcntl +import hashlib +import itertools +import os +import platform +import posixpath +import resource +import selectors +import signal +import struct +import sys +import threading +import time +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Callable, NoReturn, Protocol, TypeAlias + + +EXECUTION_PLATFORM_V1 = "linux-x86_64" +SANDBOX_POLICY_RELEASE_V1 = "labcolors.proof-region.executor.linux-x86_64.v1" + +_INVOCATION_ID_LABEL_V1 = b"labcolors.proof-region.execution-invocation.v1\0" +_PLATFORM_ID_LABEL_V1 = b"labcolors.proof-region.execution-platform.v1\0" + +# Linux UAPI values are fixed by fcntl.h. Requiring F_SEAL_EXEC makes an older +# kernel an explicit Unsupported host instead of silently weakening the object. +F_SEAL_SEAL_V1 = 0x0001 +F_SEAL_SHRINK_V1 = 0x0002 +F_SEAL_GROW_V1 = 0x0004 +F_SEAL_WRITE_V1 = 0x0008 +F_SEAL_EXEC_V1 = 0x0020 +REQUIRED_FILE_SEALS_V1 = ( + F_SEAL_SEAL_V1 + | F_SEAL_SHRINK_V1 + | F_SEAL_GROW_V1 + | F_SEAL_WRITE_V1 + | F_SEAL_EXEC_V1 +) + +_MFD_CLOEXEC = 0x0001 +_MFD_ALLOW_SEALING = 0x0002 +_MFD_EXEC = 0x0010 +_F_ADD_SEALS = 1033 +_F_GET_SEALS = 1034 +_AT_EMPTY_PATH = 0x1000 + +_SYS_SECCOMP_X86_64 = 317 +_SYS_EXECVEAT_X86_64 = 322 +_SYS_CLOSE_RANGE_X86_64 = 436 +_SYS_PRLIMIT64_X86_64 = 302 + +_CLONE_NEWNS = 0x00020000 +_CLONE_NEWUSER = 0x10000000 +_CLONE_NEWNET = 0x40000000 +_MS_REC = 0x4000 +_MS_PRIVATE = 1 << 18 + +_PR_SET_NO_NEW_PRIVS = 38 +_SECCOMP_SET_MODE_FILTER = 1 +_SECCOMP_FILTER_FLAG_TSYNC = 1 +_SECCOMP_RET_KILL_PROCESS = 0x80000000 +_SECCOMP_RET_ALLOW = 0x7FFF0000 +_AUDIT_ARCH_X86_64 = 0xC000003E + +_BPF_LD_W_ABS = 0x20 +_BPF_JMP_JEQ_K = 0x15 +_BPF_RET_K = 0x06 + +_CHILD_PACKET = struct.Struct(">4sBBI") +_CHILD_PACKET_MAGIC = b"LCXE" +_ELF_HEADER = struct.Struct("<16sHHIQQQIHHHHHH") +_ELF_PROGRAM_HEADER = struct.Struct(" bytes: + payload = b"".join(len(chunk).to_bytes(8, "big") + chunk for chunk in chunks) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def _sequence_count_v1(value: tuple[object, ...]) -> int: + return len(value) + + +class RequestReasonV1(str, Enum): + WRONG_TYPE = "wrong_type" + INVALID_LIMIT = "invalid_limit" + EMPTY_ARGV_ZERO = "empty_argv_zero" + NUL_BYTE = "nul_byte" + INVALID_ENVIRONMENT_KEY = "invalid_environment_key" + DUPLICATE_ENVIRONMENT = "duplicate_environment" + NONCANONICAL_ENVIRONMENT = "noncanonical_environment" + RELATIVE_CWD = "relative_cwd" + NONCANONICAL_CWD = "noncanonical_cwd" + LIMIT_EXCEEDED = "limit_exceeded" + INVALID_ELF = "invalid_elf" + DYNAMIC_EXECUTABLE = "dynamic_executable" + + +class ExecutionRequestErrorV1(ValueError): + def __init__(self, reason: RequestReasonV1, field: str) -> None: + super().__init__(f"{field}: {reason.value}") + self.reason = reason + self.field = field + + +class ExecutionIdentityReasonV1(str, Enum): + WRONG_REQUEST_TYPE = "wrong_request_type" + REQUEST_NOT_ADMITTED = "request_not_admitted" + FOREIGN_PLATFORM = "foreign_platform" + + +@dataclass(frozen=True) +class ExecutionIdentityRejectedV1: + reason: ExecutionIdentityReasonV1 + + def __post_init__(self) -> None: + if type(self.reason) is not ExecutionIdentityReasonV1: + raise TypeError("reason must be ExecutionIdentityReasonV1") + + +ExecutionIdentityResultV1: TypeAlias = bytes | ExecutionIdentityRejectedV1 + + +class CapabilityReasonV1(str, Enum): + HOST_NOT_LINUX = "host_not_linux" + ARCHITECTURE_NOT_SUPPORTED = "architecture_not_supported" + CGROUP_PARENT_NOT_DECLARED = "cgroup_parent_not_declared" + CGROUP_V2_UNAVAILABLE = "cgroup_v2_unavailable" + EXECUTABLE_MEMFD_UNAVAILABLE = "executable_memfd_unavailable" + FILE_SEALS_UNAVAILABLE = "file_seals_unavailable" + EXECVEAT_UNAVAILABLE = "execveat_unavailable" + CLOSE_RANGE_UNAVAILABLE = "close_range_unavailable" + NETWORK_NAMESPACE_UNAVAILABLE = "network_namespace_unavailable" + SECCOMP_FILTER_UNAVAILABLE = "seccomp_filter_unavailable" + STANDARD_FDS_UNAVAILABLE = "standard_fds_unavailable" + OBSERVER_NOT_SINGLE_THREADED = "observer_not_single_threaded" + OBSERVER_TASK_BUDGET_UNAVAILABLE = "observer_task_budget_unavailable" + OBSERVATION_INVALIDATED = "observation_invalidated" + KERNEL_API_UNAVAILABLE = "kernel_api_unavailable" + + +@dataclass(frozen=True) +class CapabilityFailureV1: + reason: CapabilityReasonV1 + errno: int | None + + def __post_init__(self) -> None: + if type(self.reason) is not CapabilityReasonV1: + raise TypeError("reason must be CapabilityReasonV1") + if self.errno is not None and (type(self.errno) is not int or self.errno <= 0): + raise TypeError("errno must be a positive int or None") + + +@dataclass(frozen=True) +class UnsupportedV1: + failures: tuple[CapabilityFailureV1, ...] + + def __post_init__(self) -> None: + if ( + type(self.failures) is not tuple + or not self.failures + or any(type(item) is not CapabilityFailureV1 for item in self.failures) + or len(set(self.failures)) != len(self.failures) + ): + raise TypeError("failures must be a nonempty unique tuple") + + +class SupportedV1(tuple): + """Exact immutable coordinates of one supported execution platform.""" + + __slots__ = () + + def __new__( + cls, + platform: str, + sandbox_policy_release: str, + ) -> SupportedV1: + if type(platform) is not str or platform != EXECUTION_PLATFORM_V1: + raise TypeError("unknown execution platform") + if ( + type(sandbox_policy_release) is not str + or sandbox_policy_release != SANDBOX_POLICY_RELEASE_V1 + ): + raise TypeError("unknown sandbox policy release") + return tuple.__new__(cls, (platform, sandbox_policy_release)) + + @property + def platform(self) -> str: + return self[0] + + @property + def sandbox_policy_release(self) -> str: + return self[1] + + +CapabilityReportV1: TypeAlias = SupportedV1 | UnsupportedV1 + + +def _invalidated_capability_report_v1() -> UnsupportedV1: + return UnsupportedV1( + ( + CapabilityFailureV1( + CapabilityReasonV1.OBSERVATION_INVALIDATED, + errno_module.EBUSY, + ), + ) + ) + + +def _kernel_api_unavailable_report_v1() -> UnsupportedV1: + return UnsupportedV1( + ( + CapabilityFailureV1( + CapabilityReasonV1.KERNEL_API_UNAVAILABLE, + None, + ), + ) + ) + + +_EXECUTION_LIMIT_FIELDS_V1 = ( + "max_executable_bytes", + "max_stdin_bytes", + "max_argument_bytes", + "max_stdout_bytes", + "max_stderr_bytes", + "wall_timeout_ns", + "memory_max_bytes", + "pids_max", +) + + +class ExecutionLimitsV1(tuple): + """Immutable resource coordinates admitted by the execution wire.""" + + __slots__ = () + + def __new__( + cls, + max_executable_bytes: int, + max_stdin_bytes: int, + max_argument_bytes: int, + max_stdout_bytes: int, + max_stderr_bytes: int, + wall_timeout_ns: int, + memory_max_bytes: int, + pids_max: int, + ) -> ExecutionLimitsV1: + values = ( + max_executable_bytes, + max_stdin_bytes, + max_argument_bytes, + max_stdout_bytes, + max_stderr_bytes, + wall_timeout_ns, + memory_max_bytes, + pids_max, + ) + # Every limit is encoded as u64 in the invocation identity. Admission + # owns that representability boundary so identity derivation is total. + positive = frozenset((0, 1, 2, 5, 6, 7)) + for index, (field_name, value) in enumerate( + zip(_EXECUTION_LIMIT_FIELDS_V1, values, strict=True) + ): + minimum = 1 if index in positive else 0 + if type(value) is not int or value < minimum or value >= 1 << 64: + raise ExecutionRequestErrorV1(RequestReasonV1.INVALID_LIMIT, field_name) + # V1's syscall policy denies clone/fork/vfork; a larger cgroup task + # budget would advertise a concurrency capability the executor lacks. + if pids_max != 1: + raise ExecutionRequestErrorV1(RequestReasonV1.INVALID_LIMIT, "pids_max") + return tuple.__new__(cls, values) + + max_executable_bytes = property(lambda self: self[0]) + max_stdin_bytes = property(lambda self: self[1]) + max_argument_bytes = property(lambda self: self[2]) + max_stdout_bytes = property(lambda self: self[3]) + max_stderr_bytes = property(lambda self: self[4]) + wall_timeout_ns = property(lambda self: self[5]) + memory_max_bytes = property(lambda self: self[6]) + pids_max = property(lambda self: self[7]) + + +class ExecutionRequestV1(tuple): + """Deeply immutable invocation coordinates admitted as one value.""" + + __slots__ = () + + def __new__( + cls, + executable: bytes, + argv: tuple[bytes, ...], + environment: tuple[tuple[bytes, bytes], ...], + cwd: bytes, + stdin: bytes, + umask: int, + limits: ExecutionLimitsV1, + ) -> ExecutionRequestV1: + if type(limits) is not ExecutionLimitsV1: + _request_fail(RequestReasonV1.WRONG_TYPE, "limits") + try: + limits = ExecutionLimitsV1(*limits) + except ExecutionRequestErrorV1: + raise + except Exception: + _request_fail(RequestReasonV1.WRONG_TYPE, "limits") + if type(executable) is not bytes: + _request_fail(RequestReasonV1.WRONG_TYPE, "executable") + if not executable or len(executable) > limits.max_executable_bytes: + _request_fail(RequestReasonV1.LIMIT_EXCEEDED, "executable") + require_static_x86_64_elf_v1(executable) + + if type(argv) is not tuple or not argv: + _request_fail(RequestReasonV1.WRONG_TYPE, "argv") + if _sequence_count_v1(argv) >= _U32_CARDINALITY_LIMIT_V1: + _request_fail(RequestReasonV1.LIMIT_EXCEEDED, "argv") + for index, item in enumerate(argv): + _require_bytes_without_nul(item, f"argv[{index}]") + if not argv[0]: + _request_fail(RequestReasonV1.EMPTY_ARGV_ZERO, "argv[0]") + + if type(environment) is not tuple: + _request_fail(RequestReasonV1.WRONG_TYPE, "environment") + if _sequence_count_v1(environment) >= _U32_CARDINALITY_LIMIT_V1: + _request_fail(RequestReasonV1.LIMIT_EXCEEDED, "environment") + previous: bytes | None = None + argument_bytes = sum(len(item) + 1 for item in argv) + for index, item in enumerate(environment): + if type(item) is not tuple or len(item) != 2: + _request_fail(RequestReasonV1.WRONG_TYPE, f"environment[{index}]") + key, value = item + _require_bytes_without_nul(key, f"environment[{index}].key") + _require_bytes_without_nul(value, f"environment[{index}].value") + if not key or b"=" in key: + _request_fail( + RequestReasonV1.INVALID_ENVIRONMENT_KEY, + f"environment[{index}].key", + ) + if previous == key: + _request_fail(RequestReasonV1.DUPLICATE_ENVIRONMENT, "environment") + if previous is not None and previous > key: + _request_fail(RequestReasonV1.NONCANONICAL_ENVIRONMENT, "environment") + previous = key + argument_bytes += len(key) + len(value) + 2 + if argument_bytes > limits.max_argument_bytes: + _request_fail(RequestReasonV1.LIMIT_EXCEEDED, "argv+environment") + + _require_bytes_without_nul(cwd, "cwd") + if not cwd.startswith(b"/"): + _request_fail(RequestReasonV1.RELATIVE_CWD, "cwd") + if ( + posixpath.normpath(cwd) != cwd + or cwd.startswith(b"//") + or (cwd != b"/" and cwd.endswith(b"/")) + ): + _request_fail(RequestReasonV1.NONCANONICAL_CWD, "cwd") + + if type(stdin) is not bytes: + _request_fail(RequestReasonV1.WRONG_TYPE, "stdin") + if len(stdin) > limits.max_stdin_bytes: + _request_fail(RequestReasonV1.LIMIT_EXCEEDED, "stdin") + if type(umask) is not int or not 0 <= umask <= 0o777: + _request_fail(RequestReasonV1.INVALID_LIMIT, "umask") + return tuple.__new__( + cls, + (executable, argv, environment, cwd, stdin, umask, limits), + ) + + executable = property(lambda self: self[0]) + argv = property(lambda self: self[1]) + environment = property(lambda self: self[2]) + cwd = property(lambda self: self[3]) + stdin = property(lambda self: self[4]) + umask = property(lambda self: self[5]) + limits = property(lambda self: self[6]) + + +def _invocation_identity_from_fields_v1(request: ExecutionRequestV1) -> bytes: + chunks: list[bytes] = [hashlib.sha256(request.executable).digest()] + chunks.append(len(request.argv).to_bytes(4, "big")) + chunks.extend(request.argv) + chunks.append(len(request.environment).to_bytes(4, "big")) + for key, value in request.environment: + chunks.extend((key, value)) + chunks.extend( + ( + request.cwd, + hashlib.sha256(request.stdin).digest(), + len(request.stdin).to_bytes(8, "big"), + request.umask.to_bytes(4, "big"), + ) + ) + for value in request.limits: + chunks.append(value.to_bytes(8, "big")) + return _execution_identity_v1(_INVOCATION_ID_LABEL_V1, tuple(chunks)) + + +def invocation_identity_v1(request: object) -> ExecutionIdentityResultV1: + """Bind exactly the invocation state that passed request admission.""" + + if type(request) is not ExecutionRequestV1: + return ExecutionIdentityRejectedV1( + ExecutionIdentityReasonV1.WRONG_REQUEST_TYPE + ) + try: + if type(request.limits) is not ExecutionLimitsV1: + raise TypeError("foreign execution limits") + replayed_limits = ExecutionLimitsV1(*request.limits) + replayed = ExecutionRequestV1( + request.executable, + request.argv, + request.environment, + request.cwd, + request.stdin, + request.umask, + replayed_limits, + ) + except Exception: + return ExecutionIdentityRejectedV1( + ExecutionIdentityReasonV1.REQUEST_NOT_ADMITTED + ) + if replayed != request: + return ExecutionIdentityRejectedV1( + ExecutionIdentityReasonV1.REQUEST_NOT_ADMITTED + ) + return _invocation_identity_from_fields_v1(replayed) + + +def platform_identity_v1(report: object) -> ExecutionIdentityResultV1: + """Bind the exact admitted execution platform and sandbox policy.""" + + if type(report) is not SupportedV1: + return ExecutionIdentityRejectedV1( + ExecutionIdentityReasonV1.FOREIGN_PLATFORM + ) + try: + replayed = SupportedV1(report.platform, report.sandbox_policy_release) + if replayed != report: + raise TypeError("platform coordinates did not replay") + return _execution_identity_v1( + _PLATFORM_ID_LABEL_V1, + ( + replayed.platform.encode("ascii"), + replayed.sandbox_policy_release.encode("ascii"), + ), + ) + except Exception: + return ExecutionIdentityRejectedV1( + ExecutionIdentityReasonV1.FOREIGN_PLATFORM + ) + + +def _request_fail(reason: RequestReasonV1, field: str) -> NoReturn: + raise ExecutionRequestErrorV1(reason, field) + + +def _require_bytes_without_nul(value: object, field: str) -> None: + if type(value) is not bytes: + _request_fail(RequestReasonV1.WRONG_TYPE, field) + if b"\0" in value: + _request_fail(RequestReasonV1.NUL_BYTE, field) + + +def require_static_x86_64_elf_v1(data: bytes) -> None: + if type(data) is not bytes: + _request_fail(RequestReasonV1.WRONG_TYPE, "executable") + if len(data) < _ELF_HEADER.size: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + try: + ( + ident, + elf_type, + machine, + version, + _entry, + program_offset, + _section_offset, + _flags, + header_size, + program_entry_size, + program_count, + _section_entry_size, + _section_count, + _section_names, + ) = _ELF_HEADER.unpack_from(data) + except struct.error: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + if ( + ident[:7] != b"\x7fELF\x02\x01\x01" + or ident[7] not in (0, 3) + or elf_type not in (2, 3) + or machine != 62 + or version != 1 + or header_size != _ELF_HEADER.size + or program_entry_size != _ELF_PROGRAM_HEADER.size + or program_count == 0 + or program_offset < header_size + ): + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + table_end = program_offset + program_count * program_entry_size + if table_end > len(data): + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + + saw_load = False + dynamic_ranges: list[tuple[int, int]] = [] + for index in range(program_count): + offset = program_offset + index * program_entry_size + try: + ( + segment_type, + _segment_flags, + file_offset, + _virtual_address, + _physical_address, + file_size, + memory_size, + _alignment, + ) = _ELF_PROGRAM_HEADER.unpack_from(data, offset) + except struct.error: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + if file_size > memory_size or file_offset + file_size > len(data): + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + if segment_type == 1: + saw_load = True + elif segment_type == 3: + _request_fail(RequestReasonV1.DYNAMIC_EXECUTABLE, "executable") + elif segment_type == 2: + dynamic_ranges.append((file_offset, file_size)) + if not saw_load: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + + for start, size in dynamic_ranges: + if size % _ELF_DYNAMIC_ENTRY.size != 0: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + saw_terminator = False + for offset in range(start, start + size, _ELF_DYNAMIC_ENTRY.size): + tag, _value = _ELF_DYNAMIC_ENTRY.unpack_from(data, offset) + if tag == 0: + saw_terminator = True + break + if tag == 1: + _request_fail(RequestReasonV1.DYNAMIC_EXECUTABLE, "executable") + if not saw_terminator: + _request_fail(RequestReasonV1.INVALID_ELF, "executable") + + +class OutputStreamV1(str, Enum): + STDOUT = "stdout" + STDERR = "stderr" + + +class SetupStageV1(int, Enum): + SEALED_EXECUTABLE = 1 + CGROUP_CREATE = 2 + CGROUP_ATTACH = 3 + CWD = 4 + NAMESPACE = 5 + MOUNT_PROPAGATION = 6 + FILE_DESCRIPTORS = 7 + SIGNAL_STATE = 8 + NO_NEW_PRIVILEGES = 9 + SECCOMP = 10 + EXECVEAT = 11 + OBSERVER_PRECONDITION = 12 + + +class ObserverReasonV1(str, Enum): + REQUEST_NOT_ADMITTED = "request_not_admitted" + PROBE_FAILED = "probe_failed" + BACKEND_EXCEPTION = "backend_exception" + BACKEND_CONTRACT = "backend_contract" + CHILD_PROTOCOL = "child_protocol" + CGROUP_OBSERVATION = "cgroup_observation" + CLEANUP_FAILED = "cleanup_failed" + + +@dataclass(frozen=True) +class CompletedV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + + +@dataclass(frozen=True) +class ExitNonZeroV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + exit_code: int + + +@dataclass(frozen=True) +class SignaledV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + signal_number: int + core_dumped: bool + + +@dataclass(frozen=True) +class TimedOutV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + deadline_ns: int + + +@dataclass(frozen=True) +class OomKilledV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + oom_kill_delta: int + + +@dataclass(frozen=True) +class OutputLimitExceededV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + stream: OutputStreamV1 + limit: int + + +@dataclass(frozen=True) +class SandboxSetupFailedV1: + binary_sha256: bytes | None + stdout: bytes + stderr: bytes + stage: SetupStageV1 + errno: int + + +@dataclass(frozen=True) +class ResidualProcessesV1: + binary_sha256: bytes + stdout: bytes + stderr: bytes + + +@dataclass(frozen=True) +class ObserverFailureV1: + reason: ObserverReasonV1 + + +ExecutionResultV1: TypeAlias = ( + CompletedV1 + | ExitNonZeroV1 + | SignaledV1 + | TimedOutV1 + | OomKilledV1 + | OutputLimitExceededV1 + | SandboxSetupFailedV1 + | ResidualProcessesV1 + | ObserverFailureV1 + | UnsupportedV1 +) + + +@dataclass(frozen=True) +class _ProbeGuardV1: + """One controller-owned lease; a backend may observe but never renew it.""" + + _is_current: Callable[[], bool] + + def is_current(self) -> bool: + try: + return self._is_current() + except Exception: + return False + + +class ExecutionBackendV1(Protocol): + def probe(self, guard: _ProbeGuardV1) -> CapabilityReportV1: ... + + def run( + self, + request: ExecutionRequestV1, + capability: SupportedV1, + ) -> ExecutionResultV1: ... + + +class ControlledExecutorV1: + def __init__(self, backend: ExecutionBackendV1 | None = None) -> None: + self._backend = backend if backend is not None else NativeLinuxBackendV1() + # A fork snapshots Python locks and object identity, so an inherited + # controller cannot share the creator process's one-shot authority. + self._owner_pid = os.getpid() + self._capability_lock = threading.Lock() + self._capability_generation = 0 + self._capability_conflict_generation = 0 + self._active_capability_probes = 0 + self._issued_capability: SupportedV1 | None = None + self._issued_backend: ExecutionBackendV1 | None = None + + def _probe_is_current_v1( + self, + generation: int, + conflict_generation: int, + backend: ExecutionBackendV1, + ) -> bool: + with self._capability_lock: + return ( + os.getpid() == self._owner_pid + and generation == self._capability_generation + and conflict_generation == self._capability_conflict_generation + and backend is self._backend + ) + + def probe(self) -> CapabilityReportV1: + if os.getpid() != self._owner_pid: + return _invalidated_capability_report_v1() + with self._capability_lock: + self._capability_generation += 1 + generation = self._capability_generation + if self._active_capability_probes != 0: + self._capability_conflict_generation += 1 + self._issued_capability = None + self._issued_backend = None + return _invalidated_capability_report_v1() + conflict_generation = self._capability_conflict_generation + self._active_capability_probes += 1 + self._issued_capability = None + self._issued_backend = None + backend = self._backend + guard = _ProbeGuardV1( + lambda: self._probe_is_current_v1( + generation, + conflict_generation, + backend, + ) + ) + try: + report = backend.probe(guard) + except Exception: + report = _kernel_api_unavailable_report_v1() + except BaseException: + with self._capability_lock: + self._active_capability_probes -= 1 + self._capability_conflict_generation += 1 + self._issued_capability = None + self._issued_backend = None + raise + if type(report) not in (SupportedV1, UnsupportedV1): + report = _kernel_api_unavailable_report_v1() + elif ( + type(report) is SupportedV1 + and type(platform_identity_v1(report)) is not bytes + ): + report = _kernel_api_unavailable_report_v1() + elif type(report) is UnsupportedV1: + try: + if not _unsupported_is_well_typed_v1(report): + raise TypeError("unsupported capability report did not replay") + report = UnsupportedV1( + tuple( + CapabilityFailureV1(failure.reason, failure.errno) + for failure in report.failures + ) + ) + except Exception: + report = _kernel_api_unavailable_report_v1() + with self._capability_lock: + self._active_capability_probes -= 1 + invalidated = ( + generation != self._capability_generation + or conflict_generation != self._capability_conflict_generation + or backend is not self._backend + ) + if invalidated: + self._issued_capability = None + self._issued_backend = None + return _invalidated_capability_report_v1() + if type(report) is SupportedV1: + # The backend reports host facts; it cannot mint authority. + # A fresh controller-owned object binds this exact successful + # probe generation, even when a backend reuses its report. + issued = SupportedV1( + report.platform, + report.sandbox_policy_release, + ) + self._issued_capability = issued + self._issued_backend = backend + return issued + return report + + def execute( + self, + request: object, + capability: SupportedV1 | None = None, + ) -> ExecutionResultV1: + if os.getpid() != self._owner_pid: + return ObserverFailureV1(ObserverReasonV1.PROBE_FAILED) + request_identity = invocation_identity_v1(request) + if ( + type(request) is not ExecutionRequestV1 + or type(request_identity) is not bytes + ): + return ObserverFailureV1(ObserverReasonV1.REQUEST_NOT_ADMITTED) + if capability is None: + report = self.probe() + if type(report) is UnsupportedV1: + return report + capability = report + if type(capability) is not SupportedV1: + return ObserverFailureV1(ObserverReasonV1.PROBE_FAILED) + with self._capability_lock: + backend = self._issued_backend + if ( + capability is not self._issued_capability + or backend is None + or backend is not self._backend + ): + return ObserverFailureV1(ObserverReasonV1.PROBE_FAILED) + # The controller is the sole owner. Consumption precedes every + # backend operation, so retries and backend replacement fail shut. + self._issued_capability = None + self._issued_backend = None + try: + result = backend.run(request, capability) + except Exception: + return ObserverFailureV1(ObserverReasonV1.BACKEND_EXCEPTION) + if not result_matches_request_v1(result, request): + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + return result + + +def _unsupported_is_well_typed_v1(report: UnsupportedV1) -> bool: + failures = report.failures + return ( + type(failures) is tuple + and bool(failures) + and all( + type(failure) is CapabilityFailureV1 + and type(failure.reason) is CapabilityReasonV1 + and ( + failure.errno is None + or (type(failure.errno) is int and failure.errno > 0) + ) + for failure in failures + ) + ) + + +def _result_matches_request_v1(result: object, request: ExecutionRequestV1) -> bool: + if type(request) is not ExecutionRequestV1: + return False + known = ( + CompletedV1, + ExitNonZeroV1, + SignaledV1, + TimedOutV1, + OomKilledV1, + OutputLimitExceededV1, + SandboxSetupFailedV1, + ResidualProcessesV1, + ObserverFailureV1, + UnsupportedV1, + ) + if type(result) not in known: + return False + if type(result) is ObserverFailureV1: + return type(result.reason) is ObserverReasonV1 + if type(result) is UnsupportedV1: + return _unsupported_is_well_typed_v1(result) + + stdout = result.stdout + stderr = result.stderr + if ( + type(stdout) is not bytes + or type(stderr) is not bytes + or len(stdout) > request.limits.max_stdout_bytes + or len(stderr) > request.limits.max_stderr_bytes + ): + return False + expected_digest = hashlib.sha256(request.executable).digest() + if type(result) is SandboxSetupFailedV1: + if result.binary_sha256 is not None and ( + type(result.binary_sha256) is not bytes + or len(result.binary_sha256) != len(expected_digest) + or result.binary_sha256 != expected_digest + ): + return False + return ( + type(result.stage) is SetupStageV1 + and type(result.errno) is int + and result.errno > 0 + ) + if ( + type(result.binary_sha256) is not bytes + or len(result.binary_sha256) != len(expected_digest) + or result.binary_sha256 != expected_digest + ): + return False + if type(result) is ExitNonZeroV1: + return type(result.exit_code) is int and result.exit_code > 0 + if type(result) is SignaledV1: + return ( + type(result.signal_number) is int + and result.signal_number > 0 + and type(result.core_dumped) is bool + ) + if type(result) is TimedOutV1: + return ( + type(result.deadline_ns) is int + and result.deadline_ns == request.limits.wall_timeout_ns + ) + if type(result) is OomKilledV1: + return type(result.oom_kill_delta) is int and result.oom_kill_delta > 0 + if type(result) is OutputLimitExceededV1: + expected_limit = ( + request.limits.max_stdout_bytes + if result.stream is OutputStreamV1.STDOUT + else request.limits.max_stderr_bytes + if result.stream is OutputStreamV1.STDERR + else None + ) + captured = ( + result.stdout + if result.stream is OutputStreamV1.STDOUT + else result.stderr + ) + return ( + expected_limit is not None + and type(result.limit) is int + and result.limit == expected_limit + and len(captured) == expected_limit + ) + return True + + +def result_matches_request_v1(result: object, request: ExecutionRequestV1) -> bool: + """Total validation for observations returned by an injected backend.""" + + try: + return _result_matches_request_v1(result, request) + except Exception: + return False + + +class _MemfdOperationsV1(Protocol): + def create_executable_memfd(self) -> int: ... + + def write_all(self, fd: int, data: bytes) -> None: ... + + def make_executable(self, fd: int) -> None: ... + + def add_seals(self, fd: int, seals: int) -> None: ... + + def get_seals(self, fd: int) -> int: ... + + def pread(self, fd: int, size: int, offset: int) -> bytes: ... + + def execveat( + self, + fd: int, + argv: tuple[bytes, ...], + environment: tuple[tuple[bytes, bytes], ...], + ) -> None: ... + + def close(self, fd: int) -> None: ... + + +@dataclass(frozen=True) +class _SealedExecutableV1: + fd: int + size: int + sha256: bytes + + def execveat( + self, + argv: tuple[bytes, ...], + environment: tuple[tuple[bytes, bytes], ...], + operations: _MemfdOperationsV1, + ) -> None: + operations.execveat(self.fd, argv, environment) + + +def _seal_executable_v1( + executable: bytes, + operations: _MemfdOperationsV1, +) -> _SealedExecutableV1: + fd = operations.create_executable_memfd() + try: + operations.write_all(fd, executable) + operations.make_executable(fd) + operations.add_seals(fd, REQUIRED_FILE_SEALS_V1) + actual_seals = operations.get_seals(fd) + if actual_seals & REQUIRED_FILE_SEALS_V1 != REQUIRED_FILE_SEALS_V1: + raise OSError(errno_module.ENOTSUP, "required file seals did not stick") + digest = hashlib.sha256() + offset = 0 + while offset < len(executable): + chunk = operations.pread(fd, min(1 << 20, len(executable) - offset), offset) + if not chunk: + raise OSError(errno_module.EIO, "sealed executable shortened while hashing") + digest.update(chunk) + offset += len(chunk) + if offset != len(executable): + raise OSError(errno_module.EIO, "sealed executable length changed") + return _SealedExecutableV1(fd, len(executable), digest.digest()) + except BaseException: + operations.close(fd) + raise + + +@dataclass(frozen=True) +class _ChildErrorV1: + stage: SetupStageV1 + errno: int + + +class ObserverProtocolErrorV1(ValueError): + pass + + +def _encode_child_error_packet_v1(stage: SetupStageV1, error_number: int) -> bytes: + if type(stage) is not SetupStageV1 or type(error_number) is not int or not 0 < error_number <= 0xFFFFFFFF: + raise ValueError("invalid child error") + return _CHILD_PACKET.pack(_CHILD_PACKET_MAGIC, 1, stage.value, error_number) + + +def _parse_child_error_packet_v1(packet: bytes) -> _ChildErrorV1: + if type(packet) is not bytes or len(packet) != _CHILD_PACKET.size: + raise ObserverProtocolErrorV1("noncanonical child packet length") + try: + magic, release, raw_stage, error_number = _CHILD_PACKET.unpack(packet) + stage = SetupStageV1(raw_stage) + except (struct.error, ValueError) as error: + raise ObserverProtocolErrorV1("invalid child packet") from error + if magic != _CHILD_PACKET_MAGIC or release != 1 or error_number == 0: + raise ObserverProtocolErrorV1("invalid child packet") + return _ChildErrorV1(stage, error_number) + + +class _SockFilter(ctypes.Structure): + _fields_ = ( + ("code", ctypes.c_ushort), + ("jt", ctypes.c_ubyte), + ("jf", ctypes.c_ubyte), + ("k", ctypes.c_uint32), + ) + + +class _SockFprog(ctypes.Structure): + _fields_ = ( + ("length", ctypes.c_ushort), + ("filters", ctypes.POINTER(_SockFilter)), + ) + + +class _NativeLinuxOperationsV1: + _runtime_syscalls = ( + 0, # read: only inherited stdin remains readable + 1, # write: only inherited stdout/stderr remain writable + 3, # close + 5, # fstat + 8, # lseek + 9, # mmap + 10, # mprotect + 11, # munmap + 12, # brk + 13, # rt_sigaction + 14, # rt_sigprocmask + 15, # rt_sigreturn + 25, # mremap + 28, # madvise + 60, # exit + 89, # readlink: static glibc resolves /proc/self/exe once before main + 131, # sigaltstack + 158, # arch_prctl + 202, # futex + 218, # set_tid_address + 231, # exit_group + 273, # set_robust_list + 318, # getrandom: static glibc seeds its stack protector canary + 334, # rseq + ) + + def __init__(self) -> None: + self._libc = ctypes.CDLL(None, use_errno=True) + + def create_executable_memfd(self) -> int: + if not hasattr(os, "memfd_create"): + raise OSError(errno_module.ENOSYS, "memfd_create unavailable") + return os.memfd_create( + "labcolors-proof-evaluator", + _MFD_CLOEXEC | _MFD_ALLOW_SEALING | _MFD_EXEC, + ) + + def pipe_cloexec(self) -> tuple[int, int]: + return os.pipe2(os.O_CLOEXEC) + + def write_all(self, fd: int, data: bytes) -> None: + view = memoryview(data) + offset = 0 + while offset < len(view): + try: + written = os.write(fd, view[offset:]) + except InterruptedError: + continue + if written <= 0: + raise OSError(errno_module.EIO, "short memfd write") + offset += written + + def make_executable(self, fd: int) -> None: + os.fchmod(fd, 0o500) + + def add_seals(self, fd: int, seals: int) -> None: + fcntl.fcntl(fd, _F_ADD_SEALS, seals) + + def get_seals(self, fd: int) -> int: + return int(fcntl.fcntl(fd, _F_GET_SEALS)) + + def pread(self, fd: int, size: int, offset: int) -> bytes: + return os.pread(fd, size, offset) + + def close(self, fd: int) -> None: + os.close(fd) + + def execveat( + self, + fd: int, + argv: tuple[bytes, ...], + environment: tuple[tuple[bytes, bytes], ...], + ) -> None: + argv_array = (ctypes.c_char_p * (len(argv) + 1))(*argv, None) + environment_bytes = tuple(key + b"=" + value for key, value in environment) + environment_array = (ctypes.c_char_p * (len(environment_bytes) + 1))( + *environment_bytes, + None, + ) + ctypes.set_errno(0) + result = self._libc.syscall( + _SYS_EXECVEAT_X86_64, + fd, + ctypes.c_char_p(b""), + argv_array, + environment_array, + _AT_EMPTY_PATH, + ) + error_number = ctypes.get_errno() + if result == -1: + raise OSError(error_number or errno_module.EIO, "execveat failed") + raise OSError(errno_module.EIO, "execveat unexpectedly returned") + + def probe_execveat(self) -> None: + ctypes.set_errno(0) + result = self._libc.syscall( + _SYS_EXECVEAT_X86_64, + -1, + ctypes.c_char_p(b""), + ctypes.c_void_p(), + ctypes.c_void_p(), + _AT_EMPTY_PATH, + ) + error_number = ctypes.get_errno() + if result != -1 or error_number != errno_module.EBADF: + raise OSError(error_number or errno_module.ENOSYS, "execveat unavailable") + + def probe_single_threaded(self) -> None: + try: + task_count = len(os.listdir("/proc/self/task")) + except OSError as error: + raise OSError(error.errno or errno_module.EIO, "cannot inspect observer tasks") from error + if task_count != 1: + raise OSError(errno_module.EBUSY, "observer is not single-threaded") + + def probe_standard_fds(self) -> None: + for descriptor in (0, 1, 2): + os.fstat(descriptor) + + def close_range_after_setup(self) -> None: + ctypes.set_errno(0) + result = self._libc.syscall( + _SYS_CLOSE_RANGE_X86_64, + 5, + ctypes.c_uint(0xFFFFFFFF), + 0, + ) + if result == -1: + raise OSError(ctypes.get_errno() or errno_module.EIO, "close_range failed") + + def probe_close_range(self) -> None: + ctypes.set_errno(0) + result = self._libc.syscall( + _SYS_CLOSE_RANGE_X86_64, + ctypes.c_uint(0xFFFFFFFF), + ctypes.c_uint(0xFFFFFFFF), + 0, + ) + if result == -1: + raise OSError(ctypes.get_errno() or errno_module.ENOSYS, "close_range unavailable") + + def enter_namespaces(self) -> None: + flags = _CLONE_NEWUSER | _CLONE_NEWNET | _CLONE_NEWNS + ctypes.set_errno(0) + if self._libc.unshare(flags) == -1: + raise OSError(ctypes.get_errno() or errno_module.EIO, "unshare failed") + + def make_mounts_private(self) -> None: + ctypes.set_errno(0) + result = self._libc.mount( + ctypes.c_void_p(), + ctypes.c_char_p(b"/"), + ctypes.c_void_p(), + ctypes.c_ulong(_MS_REC | _MS_PRIVATE), + ctypes.c_void_p(), + ) + if result == -1: + raise OSError(ctypes.get_errno() or errno_module.EIO, "mount propagation failed") + + def probe_namespaces(self) -> None: + read_fd, write_fd = os.pipe2(os.O_CLOEXEC) + pid = os.fork() + if pid == 0: + os.close(read_fd) + error_number = 0 + try: + self.enter_namespaces() + self.make_mounts_private() + except OSError as error: + error_number = error.errno or errno_module.EIO + try: + os.write(write_fd, error_number.to_bytes(4, "big")) + finally: + os._exit(0 if error_number == 0 else 1) + os.close(write_fd) + try: + packet = _read_exact_fd(read_fd, 4) + finally: + os.close(read_fd) + _wait_exact_child(pid) + if len(packet) != 4: + raise OSError(errno_module.EIO, "namespace probe lost") + error_number = int.from_bytes(packet, "big") + if error_number: + raise OSError(error_number, "namespace probe failed") + + def set_no_new_privileges(self) -> None: + ctypes.set_errno(0) + if self._libc.prctl(_PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1: + raise OSError(ctypes.get_errno() or errno_module.EIO, "no_new_privs failed") + + def set_not_dumpable(self) -> None: + ctypes.set_errno(0) + if self._libc.prctl(4, 0, 0, 0, 0) == -1: # PR_SET_DUMPABLE + raise OSError(ctypes.get_errno() or errno_module.EIO, "PR_SET_DUMPABLE failed") + + def install_seccomp(self, exec_fd: int, setup_error_fd: int) -> None: + instructions = self._seccomp_program(exec_fd, setup_error_fd) + array = (_SockFilter * len(instructions))(*instructions) + program = _SockFprog(len(instructions), array) + ctypes.set_errno(0) + result = self._libc.syscall( + _SYS_SECCOMP_X86_64, + _SECCOMP_SET_MODE_FILTER, + _SECCOMP_FILTER_FLAG_TSYNC, + ctypes.byref(program), + ) + if result == -1: + raise OSError(ctypes.get_errno() or errno_module.EIO, "seccomp failed") + + def probe_seccomp(self) -> None: + pid = os.fork() + if pid == 0: + try: + self.set_no_new_privileges() + self.install_seccomp(3, 4) + except OSError as error: + os._exit(min(error.errno or errno_module.EIO, 255)) + os._exit(0) + status = _wait_exact_child(pid) + if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0: + code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else errno_module.EIO + raise OSError(code or errno_module.EIO, "seccomp probe failed") + + def _seccomp_program(self, exec_fd: int, setup_error_fd: int) -> list[_SockFilter]: + instructions = [ + _bpf(_BPF_LD_W_ABS, 0, 0, 4), + _bpf(_BPF_JMP_JEQ_K, 1, 0, _AUDIT_ARCH_X86_64), + _bpf(_BPF_RET_K, 0, 0, _SECCOMP_RET_KILL_PROCESS), + _bpf(_BPF_LD_W_ABS, 0, 0, 0), + _bpf(_BPF_JMP_JEQ_K, 0, 9, _SYS_EXECVEAT_X86_64), + _bpf(_BPF_LD_W_ABS, 0, 0, 16), + _bpf(_BPF_JMP_JEQ_K, 0, 0, exec_fd), + _bpf(_BPF_LD_W_ABS, 0, 0, 20), + _bpf(_BPF_JMP_JEQ_K, 0, 0, 0), + _bpf(_BPF_LD_W_ABS, 0, 0, 48), + _bpf(_BPF_JMP_JEQ_K, 0, 0, _AT_EMPTY_PATH), + _bpf(_BPF_LD_W_ABS, 0, 0, 52), + _bpf(_BPF_JMP_JEQ_K, 0, 0, 0), + _bpf(_BPF_RET_K, 0, 0, _SECCOMP_RET_ALLOW), + ] + restricted_start = len(instructions) + # glibc may query or tighten this process's own limits after exec. A + # foreign PID would instead give the evaluator authority over another + # same-UID process, so both words of pid_t must encode the kernel's + # canonical self selector (zero). + instructions.extend( + ( + _bpf(_BPF_JMP_JEQ_K, 0, 5, _SYS_PRLIMIT64_X86_64), + _bpf(_BPF_LD_W_ABS, 0, 0, 16), + _bpf(_BPF_JMP_JEQ_K, 0, 0, 0), + _bpf(_BPF_LD_W_ABS, 0, 0, 20), + _bpf(_BPF_JMP_JEQ_K, 0, 0, 0), + _bpf(_BPF_RET_K, 0, 0, _SECCOMP_RET_ALLOW), + ) + ) + generic_start = len(instructions) + # setup_error_fd is CLOEXEC, so this write capability disappears at the + # successful exec boundary together with the descriptor itself. + generic = tuple(self._runtime_syscalls) + for syscall_number in generic: + instructions.extend( + ( + _bpf(_BPF_JMP_JEQ_K, 0, 1, syscall_number), + _bpf(_BPF_RET_K, 0, 0, _SECCOMP_RET_ALLOW), + ) + ) + final_kill = len(instructions) + instructions.append(_bpf(_BPF_RET_K, 0, 0, _SECCOMP_RET_KILL_PROCESS)) + for index in (6, 8, 10, 12, restricted_start + 2, restricted_start + 4): + distance = final_kill - index - 1 + instructions[index].jf = distance + + # A plain write rule is safe only because close_range leaves fd 1, 2 + # and the CLOEXEC setup fd. No executable or filesystem fd survives. + if setup_error_fd not in (4,): + raise OSError(errno_module.EINVAL, "noncanonical setup fd") + if restricted_start != 14 or generic_start != 20: + raise AssertionError("seccomp branch offset drift") + return instructions + + +def _bpf(code: int, jt: int, jf: int, value: int) -> _SockFilter: + if not 0 <= jt <= 255 or not 0 <= jf <= 255: + raise ValueError("BPF jump exceeds classic filter encoding") + return _SockFilter(code, jt, jf, value) + + +def _read_exact_fd(fd: int, size: int) -> bytes: + chunks = bytearray() + while len(chunks) < size: + try: + chunk = os.read(fd, size - len(chunks)) + except InterruptedError: + continue + if not chunk: + break + chunks.extend(chunk) + return bytes(chunks) + + +def _wait_exact_child(pid: int) -> int: + while True: + try: + waited, status = os.waitpid(pid, 0) + except InterruptedError: + continue + if waited != pid: + raise OSError(errno_module.ECHILD, "wrong child reaped") + return status + + +_CGROUP_NAMES = itertools.count() +_CGROUP_ROOT_V1 = Path("/sys/fs/cgroup") +# One observer plus one child is the whole process tree. The kernel pids +# controller makes thread creation and fork contend for the same final slot. +_OBSERVER_SUBTREE_TASK_LIMIT_V1 = 2 + + +def _current_unified_cgroup_v1() -> Path: + descriptor = os.open( + "/proc/self/cgroup", + os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW, + ) + try: + raw = os.read(descriptor, 4097) + finally: + os.close(descriptor) + if len(raw) > 4096 or not raw.endswith(b"\n") or raw.count(b"\n") != 1: + raise OSError(errno_module.EPROTO, "noncanonical unified cgroup record") + prefix = b"0::" + if not raw.startswith(prefix): + raise OSError(errno_module.ENOTSUP, "unified cgroup v2 is required") + try: + relative = raw[len(prefix) : -1].decode("ascii") + except UnicodeDecodeError as error: + raise OSError(errno_module.EPROTO, "non-ASCII cgroup path") from error + if ( + not relative.startswith("/") + or relative != posixpath.normpath(relative) + or any(part in ("", ".", "..") for part in relative[1:].split("/")) + ): + raise OSError(errno_module.EPROTO, "noncanonical cgroup path") + return _CGROUP_ROOT_V1 / relative[1:] + + +def canonical_cgroup_parent_v1(value: object) -> Path: + """Parse one declared cgroup parent without resolving symbolic links.""" + + try: + raw = os.fspath(value) + except Exception as error: + raise TypeError( + "cgroup parent must be an absolute canonical Path" + ) from error + if ( + type(raw) is not str + or "\0" in raw + or not raw.startswith("/") + or raw.startswith("//") + or raw != posixpath.normpath(raw) + or ( + raw != "/" + and any(part in ("", ".", "..") for part in raw[1:].split("/")) + ) + ): + raise TypeError("cgroup parent must be an absolute canonical Path") + try: + os.fsencode(raw) + except (TypeError, UnicodeError) as error: + raise TypeError("cgroup parent must be filesystem-encodable") from error + return Path(raw) + + +def _cgroup_coordinate_metadata_flags_v1() -> int: + """Return the read-free native descriptor mode for cgroup coordinates.""" + + if sys.platform == "linux": + flag = getattr(os, "O_PATH", None) + detail = "Linux cgroup coordinate inspection requires O_PATH" + else: + flag = getattr(os, "O_EXEC", None) + detail = "native cgroup coordinate inspection requires O_EXEC" + if type(flag) is not int or flag <= 0: + raise OSError(errno_module.ENOTSUP, detail) + return flag + + +def _cgroup_coordinate_directory_flags_v1() -> int: + """Derive the only descriptor mode used for a cgroup directory coordinate.""" + + return ( + _cgroup_coordinate_metadata_flags_v1() + | os.O_DIRECTORY + | os.O_CLOEXEC + | os.O_NOFOLLOW + ) + + +def _open_cgroup_child_directory_v1(parent_fd: int, component: bytes) -> int: + """Open one named cgroup child without changing its coordinate semantics.""" + + return os.open( + component, + _cgroup_coordinate_directory_flags_v1(), + dir_fd=parent_fd, + ) + + +def _open_cgroup_directory_v1(parent: object) -> int: + """Open an absolute canonical cgroup directory without following symlinks.""" + + encoded = os.fsencode(canonical_cgroup_parent_v1(parent)) + flags = _cgroup_coordinate_directory_flags_v1() + descriptor = os.open(b"/", flags) + try: + for component in encoded.split(b"/")[1:]: + if not component: + continue + next_descriptor = _open_cgroup_child_directory_v1(descriptor, component) + # Linux releases a descriptor number even when close reports a + # late interruption. Transfer ownership first: retrying that + # number could close an unrelated descriptor that reused it. + previous_descriptor, descriptor = descriptor, next_descriptor + try: + os.close(previous_descriptor) + except BaseException as primary: + cleanup_descriptor, descriptor = descriptor, -1 + try: + os.close(cleanup_descriptor) + except BaseException as cleanup: + raise primary.with_traceback(primary.__traceback__) from cleanup + raise + result = descriptor + descriptor = -1 + return result + finally: + if descriptor >= 0: + os.close(descriptor) + + +def enter_observer_cgroup_v1(parent: Path) -> None: + """Move the dedicated controller into the declared observer group.""" + + if not isinstance(parent, Path): + raise TypeError("cgroup parent must be an absolute canonical Path") + # Descriptors pin every path component; resolving a pathname would follow + # a symlink before this controller can prove which cgroup it entered. + parent_fd = _open_cgroup_directory_v1(parent) + try: + directory_fd = _open_cgroup_child_directory_v1(parent_fd, b"observer") + try: + procs_fd = os.open( + b"cgroup.procs", + os.O_WRONLY | os.O_CLOEXEC | os.O_NOFOLLOW, + dir_fd=directory_fd, + ) + try: + payload = str(os.getpid()).encode("ascii") + if os.write(procs_fd, payload) != len(payload): + raise OSError(errno_module.EIO, "short cgroup placement write") + finally: + os.close(procs_fd) + finally: + os.close(directory_fd) + finally: + os.close(parent_fd) + + +class _CgroupV2V1: + def __init__(self, parent_fd: int, directory_fd: int, name: bytes) -> None: + self._parent_fd = parent_fd + self._directory_fd = directory_fd + self._name = name + + @classmethod + def probe_observer_task_budget(cls, parent: Path) -> None: + parent_fd = _open_cgroup_directory_v1(parent) + current_fd = -1 + current_parent_fd = -1 + try: + current = _current_unified_cgroup_v1() + current_fd = _open_cgroup_directory_v1(current) + current_parent_fd = _open_cgroup_directory_v1(current.parent) + parent_stat = os.fstat(parent_fd) + current_parent_stat = os.fstat(current_parent_fd) + if ( + parent_stat.st_dev != current_parent_stat.st_dev + or parent_stat.st_ino != current_parent_stat.st_ino + ): + raise OSError( + errno_module.EXDEV, + "observer must be in a direct child of the delegated parent", + ) + expected_limit = f"{_OBSERVER_SUBTREE_TASK_LIMIT_V1}\n".encode("ascii") + if ( + _read_cgroup_file(parent_fd, b"pids.max") != expected_limit + or _read_cgroup_file(parent_fd, b"pids.current") != b"1\n" + or _read_cgroup_file(current_fd, b"pids.current") != b"1\n" + ): + raise OSError( + errno_module.EBUSY, + "observer subtree must contain exactly one of two permitted tasks", + ) + finally: + if current_parent_fd >= 0: + os.close(current_parent_fd) + if current_fd >= 0: + os.close(current_fd) + os.close(parent_fd) + + @classmethod + def create( + cls, + parent: Path, + *, + memory_max: int | None, + pids_max: int, + ) -> "_CgroupV2V1": + parent_fd = _open_cgroup_directory_v1(parent) + name = f"labcolors-executor-{os.getpid()}-{next(_CGROUP_NAMES)}".encode("ascii") + directory_fd = -1 + try: + controllers = set(_read_cgroup_file(parent_fd, b"cgroup.controllers").split()) + subtree = set(_read_cgroup_file(parent_fd, b"cgroup.subtree_control").split()) + if not {b"memory", b"pids"} <= controllers or not {b"memory", b"pids"} <= subtree: + raise OSError(errno_module.ENOTSUP, "memory/pids controllers are not delegated") + os.mkdir(name, mode=0o700, dir_fd=parent_fd) + directory_fd = _open_cgroup_child_directory_v1(parent_fd, name) + group = cls(parent_fd, directory_fd, name) + group._write(b"memory.max", b"max" if memory_max is None else str(memory_max).encode("ascii")) + group._write(b"memory.swap.max", b"0") + group._write(b"memory.oom.group", b"1") + group._write(b"pids.max", str(pids_max).encode("ascii")) + group._require_applied_limits( + memory_max=memory_max, + pids_max=pids_max, + ) + group._require_writable(b"cgroup.kill") + group.oom_kill_count() + group.populated() + return group + except BaseException: + if directory_fd >= 0: + os.close(directory_fd) + try: + os.rmdir(name, dir_fd=parent_fd) + except OSError: + pass + os.close(parent_fd) + raise + + @classmethod + def probe(cls, parent: Path) -> None: + group = cls.create(parent, memory_max=None, pids_max=1) + group.close() + + def attach(self, pid: int) -> None: + self._write(b"cgroup.procs", str(pid).encode("ascii")) + + def kill_all(self) -> None: + self._write(b"cgroup.kill", b"1") + + def oom_kill_count(self) -> int: + values = _parse_cgroup_kv(self._read_required(b"memory.events.local")) + try: + return values[b"oom_kill"] + except KeyError as error: + raise OSError(errno_module.EPROTO, "oom_kill counter missing") from error + + def populated(self) -> bool: + values = _parse_cgroup_kv(self._read_required(b"cgroup.events")) + value = values.get(b"populated") + if value not in (0, 1): + raise OSError(errno_module.EPROTO, "invalid populated counter") + return bool(value) + + def close(self) -> None: + directory_fd, parent_fd = self._directory_fd, self._parent_fd + self._directory_fd = -1 + self._parent_fd = -1 + try: + os.close(directory_fd) + os.rmdir(self._name, dir_fd=parent_fd) + finally: + os.close(parent_fd) + + def _write(self, name: bytes, value: bytes) -> None: + fd = os.open(name, os.O_WRONLY | os.O_CLOEXEC | os.O_NOFOLLOW, dir_fd=self._directory_fd) + try: + written = os.write(fd, value) + if written != len(value): + raise OSError(errno_module.EIO, "short cgroup write") + finally: + os.close(fd) + + def _read_required(self, name: bytes) -> bytes: + return _read_cgroup_file(self._directory_fd, name) + + def _require_writable(self, name: bytes) -> None: + fd = os.open( + name, + os.O_WRONLY | os.O_CLOEXEC | os.O_NOFOLLOW, + dir_fd=self._directory_fd, + ) + os.close(fd) + + def _require_applied_limits( + self, + *, + memory_max: int | None, + pids_max: int, + ) -> None: + expected = { + b"memory.max": b"max" if memory_max is None else str(memory_max).encode("ascii"), + b"memory.swap.max": b"0", + b"memory.oom.group": b"1", + b"pids.max": str(pids_max).encode("ascii"), + } + for name, value in expected.items(): + if self._read_required(name) != value + b"\n": + raise OSError(errno_module.EPROTO, f"cgroup rejected exact {name!r}") + + +def _read_cgroup_file(directory_fd: int, name: bytes) -> bytes: + fd = os.open(name, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW, dir_fd=directory_fd) + try: + chunks = bytearray() + while True: + chunk = os.read(fd, 4096) + if not chunk: + return bytes(chunks) + chunks.extend(chunk) + if len(chunks) > 65536: + raise OSError(errno_module.EOVERFLOW, "cgroup control file too large") + finally: + os.close(fd) + + +def _parse_cgroup_kv(data: bytes) -> dict[bytes, int]: + result: dict[bytes, int] = {} + for line in data.splitlines(): + parts = line.split(b" ") + if len(parts) != 2 or not parts[0] or not parts[1].isdigit() or parts[0] in result: + raise OSError(errno_module.EPROTO, "invalid cgroup counter file") + result[parts[0]] = int(parts[1]) + if not result: + raise OSError(errno_module.EPROTO, "empty cgroup counter file") + return result + + +def _append_bounded_v1(captured: bytearray, chunk: bytes, limit: int) -> bool: + if ( + type(captured) is not bytearray + or type(chunk) is not bytes + or type(limit) is not int + or limit < 0 + or len(captured) > limit + ): + raise ValueError("invalid bounded capture state") + remaining = limit - len(captured) + captured.extend(chunk[:remaining]) + return len(chunk) > remaining + + +def _classify_process_v1( + *, + digest: bytes, + stdout: bytes, + stderr: bytes, + child_status: int | None, + oom_kill_delta: int, + residual: bool, + setup_packet: bytes, + terminal: tuple[str, OutputStreamV1 | None] | None, + limits: ExecutionLimitsV1, +) -> ExecutionResultV1: + if ( + type(digest) is not bytes + or len(digest) != 32 + or type(stdout) is not bytes + or type(stderr) is not bytes + or len(stdout) > limits.max_stdout_bytes + or len(stderr) > limits.max_stderr_bytes + or type(oom_kill_delta) is not int + or oom_kill_delta < 0 + or type(residual) is not bool + or type(setup_packet) is not bytes + ): + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + if terminal is not None: + if terminal == ("timeout", None): + return TimedOutV1(digest, stdout, stderr, limits.wall_timeout_ns) + kind, stream = terminal + if kind != "output" or type(stream) is not OutputStreamV1: + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + limit = ( + limits.max_stdout_bytes + if stream is OutputStreamV1.STDOUT + else limits.max_stderr_bytes + ) + captured = stdout if stream is OutputStreamV1.STDOUT else stderr + if len(captured) != limit: + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + return OutputLimitExceededV1(digest, stdout, stderr, stream, limit) + if setup_packet: + try: + child_error = _parse_child_error_packet_v1(setup_packet) + except ObserverProtocolErrorV1: + return ObserverFailureV1(ObserverReasonV1.CHILD_PROTOCOL) + return SandboxSetupFailedV1( + digest, + stdout, + stderr, + child_error.stage, + child_error.errno, + ) + if residual: + return ResidualProcessesV1(digest, stdout, stderr) + if oom_kill_delta > 0: + return OomKilledV1(digest, stdout, stderr, oom_kill_delta) + if child_status is None: + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + if os.WIFSIGNALED(child_status): + core_dumped = bool(os.WCOREDUMP(child_status)) if hasattr(os, "WCOREDUMP") else False + return SignaledV1( + digest, + stdout, + stderr, + os.WTERMSIG(child_status), + core_dumped, + ) + if not os.WIFEXITED(child_status): + return ObserverFailureV1(ObserverReasonV1.BACKEND_CONTRACT) + exit_code = os.WEXITSTATUS(child_status) + if exit_code: + return ExitNonZeroV1(digest, stdout, stderr, exit_code) + return CompletedV1(digest, stdout, stderr) + + +class NativeLinuxBackendV1: + """Native backend for a dedicated, single-threaded Linux helper process. + + Correctness requires a dedicated helper whose delegated cgroup permits + exactly the observer and one controlled child across the whole subtree. + The kernel pids controller then arbitrates thread creation against fork, + eliminating the observation-to-fork race rather than timing around it. + Native threads created outside CPython and instruction-level inputs such as + CPUID/RDTSC or auxv remain outside this observation boundary, so this result + alone cannot establish ambient-free reproducibility. + """ + + def __init__( + self, + cgroup_parent: str | os.PathLike[str] | None = None, + *, + platform_name: str | None = None, + machine_name: str | None = None, + operations: _NativeLinuxOperationsV1 | None = None, + cgroup_factory: object = _CgroupV2V1, + monotonic_ns: object = time.monotonic_ns, + ) -> None: + self._cgroup_parent = ( + None + if cgroup_parent is None + else canonical_cgroup_parent_v1(cgroup_parent) + ) + self._platform_name = sys.platform if platform_name is None else platform_name + self._machine_name = platform.machine() if machine_name is None else machine_name + self._operations = operations + self._cgroup_factory = cgroup_factory + self._monotonic_ns = monotonic_ns + + def probe(self, guard: _ProbeGuardV1) -> CapabilityReportV1: + if type(guard) is not _ProbeGuardV1 or not guard.is_current(): + return _invalidated_capability_report_v1() + try: + return self._probe_capability_v1(guard) + except Exception: + return _kernel_api_unavailable_report_v1() + + def _probe_capability_v1(self, guard: _ProbeGuardV1) -> CapabilityReportV1: + if self._platform_name != "linux": + return UnsupportedV1( + (CapabilityFailureV1(CapabilityReasonV1.HOST_NOT_LINUX, None),) + ) + if self._machine_name.lower() not in ("x86_64", "amd64"): + return UnsupportedV1( + ( + CapabilityFailureV1( + CapabilityReasonV1.ARCHITECTURE_NOT_SUPPORTED, + None, + ), + ) + ) + if self._cgroup_parent is None: + return UnsupportedV1( + ( + CapabilityFailureV1( + CapabilityReasonV1.CGROUP_PARENT_NOT_DECLARED, + None, + ), + ) + ) + operations = self._operations + if operations is None: + if sys.platform != "linux": + return _kernel_api_unavailable_report_v1() + operations = _NativeLinuxOperationsV1() + self._operations = operations + + failures: list[CapabilityFailureV1] = [] + _probe_operation( + operations.probe_standard_fds, + CapabilityReasonV1.STANDARD_FDS_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_single_threaded, + CapabilityReasonV1.OBSERVER_NOT_SINGLE_THREADED, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + lambda: self._cgroup_factory.probe_observer_task_budget( + self._cgroup_parent + ), + CapabilityReasonV1.OBSERVER_TASK_BUDGET_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + self._probe_sealed_memfd(operations, failures) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_execveat, + CapabilityReasonV1.EXECVEAT_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_close_range, + CapabilityReasonV1.CLOSE_RANGE_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_single_threaded, + CapabilityReasonV1.OBSERVER_NOT_SINGLE_THREADED, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_namespaces, + CapabilityReasonV1.NETWORK_NAMESPACE_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_single_threaded, + CapabilityReasonV1.OBSERVER_NOT_SINGLE_THREADED, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + operations.probe_seccomp, + CapabilityReasonV1.SECCOMP_FILTER_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + _probe_operation( + lambda: self._cgroup_factory.probe(self._cgroup_parent), + CapabilityReasonV1.CGROUP_V2_UNAVAILABLE, + failures, + ) + if failures or not guard.is_current(): + if not failures: + return _invalidated_capability_report_v1() + return UnsupportedV1(tuple(failures)) + return SupportedV1(EXECUTION_PLATFORM_V1, SANDBOX_POLICY_RELEASE_V1) + + def _probe_sealed_memfd( + self, + operations: _NativeLinuxOperationsV1, + failures: list[CapabilityFailureV1], + ) -> None: + try: + fd = operations.create_executable_memfd() + except OSError as error: + failures.append( + CapabilityFailureV1( + CapabilityReasonV1.EXECUTABLE_MEMFD_UNAVAILABLE, + error.errno or None, + ) + ) + return + try: + operations.write_all(fd, b"probe") + operations.make_executable(fd) + operations.add_seals(fd, REQUIRED_FILE_SEALS_V1) + if operations.get_seals(fd) & REQUIRED_FILE_SEALS_V1 != REQUIRED_FILE_SEALS_V1: + raise OSError(errno_module.ENOTSUP, "required file seals missing") + except OSError as error: + failures.append( + CapabilityFailureV1( + CapabilityReasonV1.FILE_SEALS_UNAVAILABLE, + error.errno or None, + ) + ) + finally: + operations.close(fd) + + def run( + self, + request: ExecutionRequestV1, + capability: SupportedV1, + ) -> ExecutionResultV1: + operations = self._operations + if operations is None or self._cgroup_parent is None: + return ObserverFailureV1(ObserverReasonV1.PROBE_FAILED) + + try: + sealed = _seal_executable_v1(request.executable, operations) + except OSError as error: + return SandboxSetupFailedV1( + None, + b"", + b"", + SetupStageV1.SEALED_EXECUTABLE, + error.errno or errno_module.EIO, + ) + try: + return self._run_sealed(request, sealed, operations) + finally: + operations.close(sealed.fd) + + def _run_sealed( + self, + request: ExecutionRequestV1, + sealed: _SealedExecutableV1, + operations: _NativeLinuxOperationsV1, + ) -> ExecutionResultV1: + try: + cwd_fd = os.open( + request.cwd, + os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW, + ) + except OSError as error: + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.CWD, + error.errno or errno_module.EIO, + ) + try: + group = self._cgroup_factory.create( + self._cgroup_parent, + memory_max=request.limits.memory_max_bytes, + pids_max=request.limits.pids_max, + ) + except OSError as error: + os.close(cwd_fd) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.CGROUP_CREATE, + error.errno or errno_module.EIO, + ) + try: + try: + result = self._fork_and_observe(request, sealed, operations, cwd_fd, group) + except Exception: + result = ObserverFailureV1(ObserverReasonV1.BACKEND_EXCEPTION) + finally: + os.close(cwd_fd) + cleanup_failed = False + try: + if group.populated(): + group.kill_all() + cleanup_deadline = self._clock() + 1_000_000_000 + while group.populated() and self._clock() < cleanup_deadline: + time.sleep(0.001) + if group.populated(): + cleanup_failed = True + except OSError: + cleanup_failed = True + try: + group.close() + except OSError: + cleanup_failed = True + if cleanup_failed: + return ObserverFailureV1(ObserverReasonV1.CLEANUP_FAILED) + return result + + def _fork_and_observe( + self, + request: ExecutionRequestV1, + sealed: _SealedExecutableV1, + operations: _NativeLinuxOperationsV1, + cwd_fd: int, + group: _CgroupV2V1, + ) -> ExecutionResultV1: + all_fds: list[int] = [] + try: + for _ in range(5): + all_fds.extend(operations.pipe_cloexec()) + except OSError as error: + _close_many(all_fds) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.FILE_DESCRIPTORS, + error.errno or errno_module.EIO, + ) + ( + stdin_read, + stdin_write, + stdout_read, + stdout_write, + stderr_read, + stderr_write, + setup_read, + setup_write, + start_read, + start_write, + ) = all_fds + try: + # The task count is observational; the delegated pids.max=2 + # subtree is the atomic law. Once the observer occupies one slot, + # either a new thread or the controlled child can claim the other, + # never both. + operations.probe_single_threaded() + if self._cgroup_parent is None: + raise OSError(errno_module.EINVAL, "missing cgroup parent") + self._cgroup_factory.probe_observer_task_budget( + self._cgroup_parent + ) + except OSError as error: + _close_many(all_fds) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.OBSERVER_PRECONDITION, + error.errno or errno_module.EIO, + ) + try: + pid = os.fork() + except OSError as error: + _close_many(all_fds) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.CGROUP_ATTACH, + error.errno or errno_module.EIO, + ) + if pid == 0: + self._child( + request, + sealed, + operations, + cwd_fd, + stdin_read, + stdout_write, + stderr_write, + setup_write, + start_read, + ) + os._exit(127) + + _close_many((stdin_read, stdout_write, stderr_write, setup_write, start_read)) + try: + baseline_oom = group.oom_kill_count() + group.attach(pid) + except OSError as error: + _close_many((stdin_write, stdout_read, stderr_read, setup_read, start_write)) + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + _wait_exact_child(pid) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.CGROUP_ATTACH, + error.errno or errno_module.EIO, + ) + try: + os.write(start_write, b"1") + except OSError as error: + try: + group.kill_all() + finally: + _close_many((stdin_write, stdout_read, stderr_read, setup_read, start_write)) + _wait_exact_child(pid) + return SandboxSetupFailedV1( + sealed.sha256, + b"", + b"", + SetupStageV1.CGROUP_ATTACH, + error.errno or errno_module.EIO, + ) + os.close(start_write) + return self._observe( + request, + sealed.sha256, + pid, + group, + baseline_oom, + stdin_write, + stdout_read, + stderr_read, + setup_read, + ) + + def _child( + self, + request: ExecutionRequestV1, + sealed: _SealedExecutableV1, + operations: _NativeLinuxOperationsV1, + cwd_fd: int, + stdin_read: int, + stdout_write: int, + stderr_write: int, + setup_write: int, + start_read: int, + ) -> None: + try: + if _read_exact_fd(start_read, 1) != b"1": + _child_fail(setup_write, SetupStageV1.CGROUP_ATTACH, errno_module.EPIPE) + os.fchdir(cwd_fd) + os.umask(request.umask) + except OSError as error: + _child_fail(setup_write, SetupStageV1.CWD, error.errno or errno_module.EIO) + try: + operations.enter_namespaces() + except OSError as error: + _child_fail(setup_write, SetupStageV1.NAMESPACE, error.errno or errno_module.EIO) + try: + operations.make_mounts_private() + except OSError as error: + _child_fail( + setup_write, + SetupStageV1.MOUNT_PROPAGATION, + error.errno or errno_module.EIO, + ) + try: + protected = tuple( + fcntl.fcntl(fd, fcntl.F_DUPFD_CLOEXEC, 10) + for fd in (sealed.fd, setup_write) + ) + os.dup2(stdin_read, 0) + os.dup2(stdout_write, 1) + os.dup2(stderr_write, 2) + os.dup2(protected[0], 3, inheritable=False) + os.dup2(protected[1], 4, inheritable=False) + operations.close_range_after_setup() + except OSError as error: + _child_fail(setup_write, SetupStageV1.FILE_DESCRIPTORS, error.errno or errno_module.EIO) + try: + _reset_signal_state() + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + operations.set_not_dumpable() + except OSError as error: + _child_fail(4, SetupStageV1.SIGNAL_STATE, error.errno or errno_module.EIO) + except (ValueError, RuntimeError): + _child_fail(4, SetupStageV1.SIGNAL_STATE, errno_module.EINVAL) + try: + operations.set_no_new_privileges() + except OSError as error: + _child_fail(4, SetupStageV1.NO_NEW_PRIVILEGES, error.errno or errno_module.EIO) + try: + operations.install_seccomp(3, 4) + except OSError as error: + _child_fail(4, SetupStageV1.SECCOMP, error.errno or errno_module.EIO) + try: + _SealedExecutableV1(3, sealed.size, sealed.sha256).execveat( + request.argv, + request.environment, + operations, + ) + except OSError as error: + _child_fail(4, SetupStageV1.EXECVEAT, error.errno or errno_module.EIO) + + def _observe( + self, + request: ExecutionRequestV1, + digest: bytes, + pid: int, + group: _CgroupV2V1, + baseline_oom: int, + stdin_fd: int, + stdout_fd: int, + stderr_fd: int, + setup_fd: int, + ) -> ExecutionResultV1: + streams = { + "stdout": bytearray(), + "stderr": bytearray(), + "setup": bytearray(), + } + limits = { + "stdout": request.limits.max_stdout_bytes, + "stderr": request.limits.max_stderr_bytes, + "setup": _CHILD_PACKET.size, + } + fd_by_tag = {"stdin": stdin_fd, "stdout": stdout_fd, "stderr": stderr_fd, "setup": setup_fd} + input_offset = 0 + selector: selectors.BaseSelector | None = None + child_status: int | None = None + terminal: tuple[str, OutputStreamV1 | None] | None = None + observer_failure: ObserverReasonV1 | None = None + killed = False + + try: + selector = selectors.DefaultSelector() + for fd in fd_by_tag.values(): + os.set_blocking(fd, False) + selector.register(stdout_fd, selectors.EVENT_READ, "stdout") + selector.register(stderr_fd, selectors.EVENT_READ, "stderr") + selector.register(setup_fd, selectors.EVENT_READ, "setup") + if request.stdin: + selector.register(stdin_fd, selectors.EVENT_WRITE, "stdin") + else: + os.close(stdin_fd) + fd_by_tag["stdin"] = -1 + deadline_ns = self._clock() + request.limits.wall_timeout_ns + + while child_status is None or any(fd_by_tag[tag] >= 0 for tag in ("stdout", "stderr", "setup")): + if child_status is None: + waited, status = os.waitpid(pid, os.WNOHANG) + if waited == pid: + child_status = status + if fd_by_tag["stdin"] >= 0: + _selector_close(selector, fd_by_tag, "stdin") + now = self._clock() + if child_status is None and terminal is None and now >= deadline_ns: + terminal = ("timeout", None) + try: + group.kill_all() + killed = True + except OSError: + observer_failure = ObserverReasonV1.CGROUP_OBSERVATION + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + wait_seconds = max(0.0, min((deadline_ns - now) / 1_000_000_000, 0.05)) + events = sorted(selector.select(wait_seconds), key=lambda item: str(item[0].data)) + for key, _mask in events: + tag = key.data + if tag == "stdin": + try: + written = os.write(stdin_fd, request.stdin[input_offset:]) + except BlockingIOError: + continue + except BrokenPipeError: + _selector_close(selector, fd_by_tag, "stdin") + continue + input_offset += written + if input_offset == len(request.stdin): + _selector_close(selector, fd_by_tag, "stdin") + continue + + limit = limits[tag] + remaining = max(0, limit - len(streams[tag])) + try: + chunk = os.read(key.fd, min(65536, remaining + 1)) + except BlockingIOError: + continue + if not chunk: + _selector_close(selector, fd_by_tag, tag) + continue + exceeded = _append_bounded_v1(streams[tag], chunk, limit) + if exceeded: + if tag == "setup": + observer_failure = ObserverReasonV1.CHILD_PROTOCOL + elif terminal is None: + terminal = ( + "output", + OutputStreamV1.STDOUT if tag == "stdout" else OutputStreamV1.STDERR, + ) + if not killed: + try: + group.kill_all() + killed = True + except OSError: + observer_failure = ObserverReasonV1.CGROUP_OBSERVATION + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + if observer_failure is not None and child_status is None and not killed: + try: + group.kill_all() + killed = True + except OSError: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + if child_status is not None and not events: + for tag in ("stdout", "stderr", "setup"): + if fd_by_tag[tag] >= 0: + try: + chunk = os.read(fd_by_tag[tag], 1) + except BlockingIOError: + continue + if not chunk: + _selector_close(selector, fd_by_tag, tag) + else: + exceeded = _append_bounded_v1( + streams[tag], + chunk, + limits[tag], + ) + if exceeded and tag == "setup": + observer_failure = ObserverReasonV1.CHILD_PROTOCOL + elif exceeded and terminal is None: + terminal = ( + "output", + OutputStreamV1.STDOUT if tag == "stdout" else OutputStreamV1.STDERR, + ) + except Exception: + observer_failure = ObserverReasonV1.BACKEND_EXCEPTION + if not killed: + try: + group.kill_all() + killed = True + except OSError: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + finally: + if selector is not None: + selector.close() + _close_many(fd for fd in fd_by_tag.values() if fd >= 0) + if child_status is None: + try: + waited, status = os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + waited = pid + status = 0 + if waited == pid: + child_status = status + if child_status is None: + try: + group.kill_all() + except OSError: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + child_status = _wait_exact_child(pid) + + stdout = bytes(streams["stdout"]) + stderr = bytes(streams["stderr"]) + if observer_failure is not None: + return ObserverFailureV1(observer_failure) + try: + oom_delta = group.oom_kill_count() - baseline_oom + residual = group.populated() + except OSError: + return ObserverFailureV1(ObserverReasonV1.CGROUP_OBSERVATION) + if residual: + try: + group.kill_all() + except OSError: + return ObserverFailureV1(ObserverReasonV1.CGROUP_OBSERVATION) + return _classify_process_v1( + digest=digest, + stdout=stdout, + stderr=stderr, + child_status=child_status, + oom_kill_delta=oom_delta, + residual=residual, + setup_packet=bytes(streams["setup"]), + terminal=terminal, + limits=request.limits, + ) + + def _clock(self) -> int: + value = self._monotonic_ns() + if type(value) is not int or value < 0: + raise OSError(errno_module.EIO, "invalid monotonic clock") + return value + + +def _probe_operation( + operation: object, + reason: CapabilityReasonV1, + failures: list[CapabilityFailureV1], +) -> None: + try: + operation() + except OSError as error: + failures.append(CapabilityFailureV1(reason, error.errno or None)) + except Exception: + failures.append(CapabilityFailureV1(reason, None)) + + +def _child_fail(fd: int, stage: SetupStageV1, error_number: int) -> None: + packet = _encode_child_error_packet_v1(stage, error_number) + try: + offset = 0 + while offset < len(packet): + try: + written = os.write(fd, packet[offset:]) + except InterruptedError: + continue + if written <= 0: + break + offset += written + finally: + os._exit(127) + + +def _reset_signal_state() -> None: + for number in signal.valid_signals(): + if number in (signal.SIGKILL, signal.SIGSTOP): + continue + signal.signal(number, signal.SIG_DFL) + signal.pthread_sigmask(signal.SIG_SETMASK, set()) + + +def _selector_close( + selector: selectors.BaseSelector, + fd_by_tag: dict[str, int], + tag: str, +) -> None: + fd = fd_by_tag[tag] + if fd < 0: + return + try: + selector.unregister(fd) + except KeyError: + pass + os.close(fd) + fd_by_tag[tag] = -1 + + +def _close_many(fds: object) -> None: + for fd in tuple(fds): + try: + os.close(fd) + except OSError: + pass diff --git a/proof/region/v1/mpfi/__init__.py b/proof/region/v1/mpfi/__init__.py new file mode 100644 index 00000000..82942371 --- /dev/null +++ b/proof/region/v1/mpfi/__init__.py @@ -0,0 +1 @@ +"""MPFI-специфичные границы proof V1.""" diff --git a/proof/region/v1/mpfi/build-inner.sh b/proof/region/v1/mpfi/build-inner.sh new file mode 100644 index 00000000..cef567dd --- /dev/null +++ b/proof/region/v1/mpfi/build-inner.sh @@ -0,0 +1,222 @@ +#!/bin/sh +# Internal MPFI recipe. The source-bound transport dispatches this file only +# after establishing its clean child environment; it is not a standalone API. +set -eu + +if [ "$#" -ne 0 ]; then + printf '%s\n' 'mpfi build takes no arguments' >&2 + exit 64 +fi + +umask 022 + +readonly inputs=/build/snapshot/inputs +readonly workspace=/build/snapshot/workspace +readonly build=/build/work +readonly compiler=/usr/bin/clang-19 +readonly common_cflags='-O2 -g0 -fno-ident -fno-fast-math -ffp-contract=off -fno-lto -std=gnu17 -march=x86-64 -mtune=generic -ffile-prefix-map=/build=. -fdebug-prefix-map=/build=.' +readonly evaluator_cflags='-O2 -g0 -fno-fast-math -ffp-contract=off -fno-lto -march=x86-64 -mtune=generic -ffile-prefix-map=/build=. -fdebug-prefix-map=/build=. -std=c17 -Wall -Wextra -Werror -pedantic' +readonly prefix="$build/prefix" +readonly evaluator_sources='main.c wire.c hash.c interval.c region.c' +readonly mpfi_test_exclusions='^(tdiv_ext|texp10|trec_sqrt)$' + +require_regular() { + if [ ! -f "$1" ] || [ -L "$1" ]; then + printf 'missing regular build input: %s\n' "$1" >&2 + exit 66 + fi +} + +require_executable() { + if [ ! -f "$1" ] || [ ! -x "$1" ]; then + printf 'missing executable build tool: %s\n' "$1" >&2 + exit 66 + fi +} + +require_clang_19() { + version=$("$1" --version) || { + printf '%s\n' 'cannot inspect the admitted Clang compiler' >&2 + exit 67 + } + if ! printf '%s\n' "$version" | /usr/bin/grep -q 'clang version 19\.'; then + printf '%s\n' 'MPFI build requires the admitted Clang 19 compiler family' >&2 + exit 67 + fi +} + +require_directory() { + if [ ! -d "$1" ] || [ -L "$1" ]; then + printf 'missing normalized source directory: %s\n' "$1" >&2 + exit 66 + fi +} + +require_empty_directory() { + if [ ! -d "$1" ] || [ -L "$1" ]; then + printf 'missing build directory: %s\n' "$1" >&2 + exit 66 + fi + if [ -n "$(find "$1" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + printf 'build directory is not empty: %s\n' "$1" >&2 + exit 65 + fi +} + +require_absent_pattern() { + pattern=$1 + path=$2 + message=$3 + inspection_error=$4 + if /usr/bin/grep -q "$pattern" "$path"; then + printf '%s\n' "$message" >&2 + exit 70 + else + grep_status=$? + if [ "$grep_status" -ne 1 ]; then + printf '%s\n' "$inspection_error" >&2 + exit 70 + fi + fi +} + +require_regular "$inputs/formula.generated.c" +require_directory "$inputs/sources/gmp" +require_directory "$inputs/sources/mpfr" +require_directory "$inputs/sources/mpfi" +require_regular "$workspace/proof/region/v1/mpfi/operations.py" +for source in main.c wire.c hash.c interval.c region.c; do + require_regular "$workspace/proof/region/v1/mpfi/evaluator/$source" +done +for header in wire.h hash.h interval.h region.h formula.h; do + require_regular "$workspace/proof/region/v1/mpfi/evaluator/$header" +done +printf '%s %s\n' \ + 'a8df7529261ba68e8fbf591cff283ec88a35cb98958b293bc7885d9fb4dd0fb6' \ + "$inputs/formula.generated.c" \ + | /usr/bin/sha256sum --check --strict - +/usr/bin/python3 "$workspace/proof/region/v1/mpfi/operations.py" \ + "$workspace/proof/region/v1/mpfi/evaluator" +require_executable "$compiler" +require_clang_19 "$compiler" +require_empty_directory "$build" + +/usr/bin/mkdir "$build/prefix" "$build/gmp" "$build/mpfr" "$build/mpfi" "$build/tmp" + +cd "$build/gmp" +ABI=64 CC="$compiler" CFLAGS="$common_cflags" \ + "$inputs/sources/gmp/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --disable-shared \ + --enable-static \ + --disable-assembly \ + --disable-cxx +/usr/bin/make -j1 +/usr/bin/make check -j1 +/usr/bin/make install + +cd "$build/mpfr" +CC="$compiler" CFLAGS="$common_cflags" \ + "$inputs/sources/mpfr/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --with-gmp="$prefix" \ + --disable-shared \ + --enable-static \ + --enable-formally-proven-code +/usr/bin/make -j1 +/usr/bin/make check -j1 +/usr/bin/make install + +cd "$build/mpfi" +CC="$compiler" CFLAGS="$common_cflags" \ + "$inputs/sources/mpfi/configure" \ + --build=x86_64-pc-linux-gnu \ + --host=x86_64-pc-linux-gnu \ + --prefix="$prefix" \ + --with-gmp="$prefix" \ + --with-mpfr="$prefix" \ + --disable-shared \ + --enable-static +/usr/bin/make -j1 +# MPFI 1.5.4 ships three defective tests: two pass incompatible function +# pointers to the generic harness, which Clang 19 rejects at compile time, +# while texp10 names a fixture absent from the sealed source archive. +# Exclude those upstream defects from compilation and from the run alike; +# every other shipped test remains part of this source-bound library check. +make_database="$build/mpfi-check-database" +if ! /usr/bin/make -pn > "$make_database"; then + printf '%s\n' 'cannot inspect MPFI upstream test inventory' >&2 + exit 70 +fi +mpfi_tests=$( + /usr/bin/awk -v exclusions="$mpfi_test_exclusions" ' + /^check_PROGRAMS =/ && !found { + found = 1 + for (i = 3; i <= NF; i++) { + gsub(/\$\(EXEEXT\)/, "", $i) + if ($i !~ exclusions) + printf "%s ", $i + } + } + ' "$make_database" +) +if [ -z "$mpfi_tests" ]; then + printf '%s\n' 'MPFI upstream test inventory is empty after exclusions' >&2 + exit 70 +fi +/usr/bin/make check -j1 TESTS="$mpfi_tests" check_PROGRAMS="$mpfi_tests" CFLAGS="$common_cflags" +/usr/bin/make install + +cd "$workspace/proof/region/v1/mpfi/evaluator" +for source in $evaluator_sources; do + object="$build/${source%.c}.o" + # shellcheck disable=SC2086 + "$compiler" $evaluator_cflags \ + -I. -I"$prefix/include" \ + -c "$source" \ + -o "$object" +done +# shellcheck disable=SC2086 +"$compiler" $evaluator_cflags \ + -I. -I"$prefix/include" \ + -c "$inputs/formula.generated.c" \ + -o "$build/formula.generated.o" +if ! /usr/bin/nm --undefined-only "$build"/*.o > "$build/evaluator-undefined-symbols"; then + printf '%s\n' 'cannot inspect evaluator undefined symbols' >&2 + exit 70 +fi +/usr/bin/python3 "$workspace/proof/region/v1/mpfi/operations.py" \ + --undefined-symbols "$build/evaluator-undefined-symbols" +# shellcheck disable=SC2086 +"$compiler" $evaluator_cflags \ + "$build/main.o" "$build/wire.o" "$build/hash.o" "$build/interval.o" \ + "$build/region.o" "$build/formula.generated.o" \ + -static -Wl,--build-id=none -fno-lto \ + "$prefix/lib/libmpfi.a" "$prefix/lib/libmpfr.a" "$prefix/lib/libgmp.a" \ + -lm -lpthread \ + -o "$build/mpfi-evaluator-v1" + +if ! /usr/bin/readelf -l "$build/mpfi-evaluator-v1" > "$build/program-headers"; then + printf '%s\n' 'cannot inspect evaluator program headers' >&2 + exit 70 +fi +require_absent_pattern \ + INTERP \ + "$build/program-headers" \ + 'evaluator unexpectedly contains PT_INTERP' \ + 'cannot inspect evaluator program headers' +if ! /usr/bin/readelf -d "$build/mpfi-evaluator-v1" > "$build/dynamic-section"; then + printf '%s\n' 'cannot inspect evaluator dynamic section' >&2 + exit 70 +fi +require_absent_pattern \ + NEEDED \ + "$build/dynamic-section" \ + 'evaluator unexpectedly contains DT_NEEDED' \ + 'cannot inspect evaluator dynamic section' + +/usr/bin/sha256sum "$build/mpfi-evaluator-v1" diff --git a/proof/region/v1/mpfi/build.py b/proof/region/v1/mpfi/build.py new file mode 100644 index 00000000..42f40ca5 --- /dev/null +++ b/proof/region/v1/mpfi/build.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +"""MPFI source-owned BUILD input and transport policy. + +Это только BUILD-граница: она не создаёт receipt и не запускает evaluator. +Source-bound controller M2a обязан передать sealed input в общий transport, +а затем independently bind его к двум свежим build observations. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from enum import StrEnum +from typing import NoReturn + +import provenance +from build import input as build_input +from build import transport as build_transport + + +MPFI_BUILD_IMAGE_REFERENCE_V1 = ( + "silkeh/clang@sha256:f1d693e7af5ee954370e1f3605830d8cabc05f9731226fc99aa5e26127797c11" +) +MPFI_BUILD_PLATFORM_V1 = "linux/amd64" +MPFI_BUILD_OUTPUT_NAME_V1 = "mpfi-evaluator-v1" +MPFI_GENERATED_FORMULA_PATH_V1 = "generated/mpfi-formula.generated.c" +MPFI_FORMULA_SPEC_PATH_V1 = ( + "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir" +) +MPFI_FORMULA_GENERATOR_PATH_V1 = "proof/region/v1/mpfi/evaluator/formula.py" +MPFI_BUILD_RECIPE_PATH_V1 = "proof/region/v1/mpfi/build.sh" +MPFI_BUILD_INNER_RECIPE_PATH_V1 = "proof/region/v1/mpfi/build-inner.sh" + +MPFI_GENERATED_FORMULA_SHA256_V1 = ( + "a8df7529261ba68e8fbf591cff283ec88a35cb98958b293bc7885d9fb4dd0fb6" +) +MPFI_FORMULA_SPEC_SHA256_V1 = ( + "a6f77ac462f226453b1c27bbd8637b62780b9a640c317a6f50028dacd1de8540" +) + +_MPFI_SOURCE_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-build-sources.v1\0" +_MPFI_SOURCE_REPLAY_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-source-replay.v1\0" +_MPFI_INPUT_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-build-input.v1\0" + +_BUILD_BOOTSTRAP_V1 = r"""set -eu +exec 3>&1 +exec 1>&2 +umask 077 +readonly bundle=/build/input.bundle +readonly snapshot=/build/snapshot +/usr/bin/cat > "$bundle" +actual_length=$(/usr/bin/wc -c < "$bundle") +if [ "$actual_length" != "$1" ]; then + printf '%s\n' 'build input bundle length mismatch' >&2 + exit 65 +fi +printf '%s %s\n' "$2" "$bundle" | /usr/bin/sha256sum --check --strict - +/usr/bin/mkdir "$snapshot" /build/work +umask 022 +/usr/bin/tar --extract --file "$bundle" --directory "$snapshot" --no-same-owner +/usr/bin/rm "$bundle" +umask 077 +/bin/sh "$snapshot/workspace/proof/region/v1/mpfi/build.sh" +/usr/bin/cat /build/work/mpfi-evaluator-v1 >&3 +""" + +MPFI_BUILD_STDOUT_LIMIT_V1 = build_transport.BUILD_STDOUT_LIMIT_V1 +MPFI_BUILD_STDERR_LIMIT_V1 = build_transport.BUILD_STDERR_LIMIT_V1 +MPFI_BUILD_TIMEOUT_NS_V1 = build_transport.BUILD_TIMEOUT_NS_V1 +MPFI_DOCKER_PROBE_OUTPUT_LIMIT_V1 = build_transport.DOCKER_PROBE_OUTPUT_LIMIT_V1 +MPFI_DOCKER_PROBE_TIMEOUT_NS_V1 = build_transport.DOCKER_PROBE_TIMEOUT_NS_V1 +MPFI_BUILD_TMP_LIMIT_BYTES_V1 = 512 * 1024 * 1024 +MPFI_BUILD_STATE_LIMIT_BYTES_V1 = 4 * 1024 * 1024 * 1024 +_MPFI_BUILD_TMPFS_SPEC_V1 = ( + f"/tmp:rw,noexec,nosuid,nodev,size={MPFI_BUILD_TMP_LIMIT_BYTES_V1},mode=1777" +) +_MPFI_BUILD_STATE_TMPFS_SPEC_V1 = ( + f"/build:rw,exec,nosuid,nodev,size={MPFI_BUILD_STATE_LIMIT_BYTES_V1},mode=0777" +) + +MPFI_BUILD_TRANSPORT_POLICY_V1 = build_transport.DockerBuildPolicyV1( + MPFI_BUILD_IMAGE_REFERENCE_V1, + MPFI_BUILD_PLATFORM_V1, + "labcolors-mpfi-build-v1", + _BUILD_BOOTSTRAP_V1, + "labcolors-mpfi-build-bootstrap-v1", + (_MPFI_BUILD_TMPFS_SPEC_V1, _MPFI_BUILD_STATE_TMPFS_SPEC_V1), + build_transport.DockerUserModeV1.HOST_EFFECTIVE_IDS, + MPFI_BUILD_STDOUT_LIMIT_V1, + MPFI_BUILD_STDERR_LIMIT_V1, + MPFI_BUILD_TIMEOUT_NS_V1, + MPFI_DOCKER_PROBE_OUTPUT_LIMIT_V1, + MPFI_DOCKER_PROBE_TIMEOUT_NS_V1, +) + +_PINNED_WORKSPACE_SHA256_V1 = { + MPFI_BUILD_RECIPE_PATH_V1: "ae7ab236d323d694e0d627b7fcb07f272c351290da10f78f9f7d6cf63b6cf571", + MPFI_BUILD_INNER_RECIPE_PATH_V1: "95d2cde6649f0bf138a3acfee774a49294f2515f683c62f4234bd75b7a558d60", + "proof/region/v1/mpfi/operations.py": "61c977e9373788d141ac89dbdc70fba0fb853cb175052975916c21506689eaf3", + MPFI_FORMULA_GENERATOR_PATH_V1: "961d488a2e9f539518d9a2b7223230495a617cbb50497f3482ad90a5819e4a6a", + "proof/region/v1/mpfi/evaluator/formula.h": "84794cec2cbc73f73948f4c411c78f6495546879a95bf76a401df8dd24c3b794", + "proof/region/v1/mpfi/evaluator/hash.c": "9adf78d50c7cbaa25befa4ab745df8f5e0b9de0d8a06cc208bd9cf30f31aa8ce", + "proof/region/v1/mpfi/evaluator/hash.h": "605a14a0ad221a7e43c5d65c72154793d735d461c53407790dc1dcb78c7f111a", + "proof/region/v1/mpfi/evaluator/interval.c": "9e146aba9467c0386dd40ada686727039a150afb37f65b8cbafbfa5c1fcfb017", + "proof/region/v1/mpfi/evaluator/interval.h": "12eb0563f481898bbf6b8add3c7a52e47fd8e6d9ff4d796f2a9bea4f07238e8b", + "proof/region/v1/mpfi/evaluator/main.c": "7cb30c89fd4b54a1b3b9fbff1b22f7242371b6ef716a176bac8b558181c0205d", + "proof/region/v1/mpfi/evaluator/region.c": "8a0308f951b9ba681b1d537229ba5fbd1390a1ca863c082b4e24e4fa1a69345f", + "proof/region/v1/mpfi/evaluator/region.h": "940680c5201393232bec58f4fc45d2db9a3ed3b02d237d8d6e8aa59f6168fa4d", + "proof/region/v1/mpfi/evaluator/wire.c": "fc9cd817a64b50499f6eb822cfa013bd9557dbf633fb9619e8c30349371643ed", + "proof/region/v1/mpfi/evaluator/wire.h": "3be98141e9e3ef67b03f5e535e523810fb10e00526fe638bfddbee9d4bbf1710", + MPFI_FORMULA_SPEC_PATH_V1: MPFI_FORMULA_SPEC_SHA256_V1, +} + +REQUIRED_WORKSPACE_MODES_V1 = tuple( + (path, 0o755 if path == MPFI_BUILD_RECIPE_PATH_V1 else 0o644) + for path in sorted(_PINNED_WORKSPACE_SHA256_V1) +) + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join( + len(chunk).to_bytes(8, "big") + chunk + for chunk in chunks + ) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def _valid_digest(value: object) -> bool: + return type(value) is bytes and len(value) == 32 and value != bytes(32) + + +class MpfiBuildSourceReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + NONCANONICAL_SET = "noncanonical_set" + INVALID_PATH = "invalid_path" + INVALID_MODE = "invalid_mode" + INVALID_CONTENT = "invalid_content" + CONTENT_DRIFT = "content_drift" + + +@dataclass(frozen=True) +class MpfiBuildSourceErrorV1(ValueError): + reason: MpfiBuildSourceReasonV1 + path: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.path}" + + +def _fail(reason: MpfiBuildSourceReasonV1, path: str) -> NoReturn: + raise MpfiBuildSourceErrorV1(reason, path) + + +@dataclass(frozen=True) +class MpfiBuildSourceFileV1: + path: str + mode: int + contents: bytes + + def __post_init__(self) -> None: + if ( + type(self.path) is not str + or self.path not in _PINNED_WORKSPACE_SHA256_V1 + ): + _fail(MpfiBuildSourceReasonV1.INVALID_PATH, str(self.path)) + if type(self.mode) is not int or self.mode not in (0o644, 0o755): + _fail(MpfiBuildSourceReasonV1.INVALID_MODE, self.path) + if type(self.contents) is not bytes or not self.contents: + _fail(MpfiBuildSourceReasonV1.INVALID_CONTENT, self.path) + + +@dataclass(frozen=True) +class AdmittedMpfiBuildSourcesV1: + files: tuple[MpfiBuildSourceFileV1, ...] + identity: bytes + + def __post_init__(self) -> None: + if ( + type(self.files) is not tuple + or any(type(item) is not MpfiBuildSourceFileV1 for item in self.files) + or tuple((item.path, item.mode) for item in self.files) + != REQUIRED_WORKSPACE_MODES_V1 + or not _valid_digest(self.identity) + ): + raise TypeError("invalid admitted MPFI build sources") + + def contents(self, path: str) -> bytes: + for item in self.files: + if item.path == path: + return item.contents + raise KeyError(path) + + +def _workspace_identity(files: tuple[MpfiBuildSourceFileV1, ...]) -> bytes: + chunks: list[bytes] = [len(files).to_bytes(4, "big")] + for item in files: + chunks.extend( + ( + item.path.encode("ascii"), + item.mode.to_bytes(4, "big"), + len(item.contents).to_bytes(8, "big"), + hashlib.sha256(item.contents).digest(), + ) + ) + return _identity(_MPFI_SOURCE_ID_LABEL_V1, tuple(chunks)) + + +def admit_mpfi_build_sources_v1( + files: tuple[MpfiBuildSourceFileV1, ...], +) -> AdmittedMpfiBuildSourcesV1: + if type(files) is not tuple or any( + type(item) is not MpfiBuildSourceFileV1 for item in files + ): + _fail(MpfiBuildSourceReasonV1.WRONG_TYPE, "files") + try: + owned = tuple( + MpfiBuildSourceFileV1(item.path, item.mode, item.contents) + for item in files + ) + except MpfiBuildSourceErrorV1: + raise + except Exception: + _fail(MpfiBuildSourceReasonV1.WRONG_TYPE, "files") + actual = tuple((item.path, item.mode) for item in owned) + if actual != REQUIRED_WORKSPACE_MODES_V1: + _fail(MpfiBuildSourceReasonV1.NONCANONICAL_SET, "files") + for item in owned: + if hashlib.sha256(item.contents).hexdigest() != _PINNED_WORKSPACE_SHA256_V1[item.path]: + _fail(MpfiBuildSourceReasonV1.CONTENT_DRIFT, item.path) + return AdmittedMpfiBuildSourcesV1(owned, _workspace_identity(owned)) + + +def canonical_build_sources_v1( + value: object, +) -> AdmittedMpfiBuildSourcesV1: + if type(value) is not AdmittedMpfiBuildSourcesV1: + raise TypeError("build_sources must be AdmittedMpfiBuildSourcesV1") + canonical = admit_mpfi_build_sources_v1(value.files) + if value.identity != canonical.identity: + raise ValueError("retained MPFI build-source identity drift") + return canonical + + +def _source_entries_v1( + snapshot: provenance.ReplayedSourceClosureV1, +) -> tuple[tuple[str, int, bytes], ...]: + entries = tuple( + ( + f"inputs/sources/{lock.role.name.lower()}/{relative}", + mode, + contents, + ) + for lock, materialized in zip( + snapshot.source_lock.sources, + snapshot.sources, + strict=True, + ) + for relative, mode, contents in materialized.files + ) + if not entries: + raise ValueError("MPFI source closure is empty") + return tuple(sorted(entries)) + + +def source_identity_v1( + snapshot: provenance.ReplayedSourceClosureV1, +) -> bytes: + chunks: list[bytes] = [ + snapshot.source_lock.identity, + snapshot.admitted_sources.identity, + ] + for path, mode, contents in _source_entries_v1(snapshot): + chunks.extend( + ( + path.encode("ascii"), + mode.to_bytes(4, "big"), + len(contents).to_bytes(8, "big"), + hashlib.sha256(contents).digest(), + ) + ) + return _identity(_MPFI_SOURCE_REPLAY_ID_LABEL_V1, tuple(chunks)) + + +def _canonical_input_entries_from_owned_v1( + snapshot: provenance.ReplayedSourceClosureV1, + build_sources: AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, +) -> tuple[tuple[str, int, bytes], ...]: + source_entries = _source_entries_v1(snapshot) + workspace_entries = tuple( + (f"workspace/{item.path}", item.mode, item.contents) + for item in build_sources.files + ) + return tuple( + sorted( + source_entries + + (("inputs/formula.generated.c", 0o644, generated_formula),) + + workspace_entries + ) + ) + + +def canonical_input_entries_v1( + snapshot: provenance.ReplayedSourceClosureV1, + build_sources: AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, +) -> tuple[tuple[str, int, bytes], ...]: + """Return the canonical source, formula and workspace file set.""" + + if type(snapshot) is not provenance.ReplayedSourceClosureV1: + raise TypeError("snapshot must be ReplayedSourceClosureV1") + if ( + type(snapshot.source_lock) is not provenance.MpfiSourceLockV1 + or type(snapshot.admitted_sources) is not provenance.AdmittedMpfiSourcesV1 + or snapshot.admitted_sources.source_lock_identity + != snapshot.source_lock.identity + ): + raise TypeError("snapshot must retain MPFI source lock") + canonical = canonical_build_sources_v1(build_sources) + if type(generated_formula) is not bytes or not generated_formula: + raise TypeError("generated_formula must be nonempty bytes") + if hashlib.sha256(generated_formula).hexdigest() != MPFI_GENERATED_FORMULA_SHA256_V1: + raise ValueError("generated MPFI formula drift") + return _canonical_input_entries_from_owned_v1( + snapshot, + canonical, + generated_formula, + ) + + +def _input_binding_identity_v1( + source_identity: bytes, + build_sources: AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, + policy: build_transport.DockerBuildPolicyV1, + contents: bytes, +) -> bytes: + return _identity( + _MPFI_INPUT_ID_LABEL_V1, + ( + source_identity, + build_sources.identity, + hashlib.sha256(generated_formula).digest(), + build_transport.transport_policy_identity_v1(policy), + len(contents).to_bytes(8, "big"), + hashlib.sha256(contents).digest(), + ), + ) + + +def seal_mpfi_build_input_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + build_sources: AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, + limits: build_input.CanonicalInputLimitsV1, + policy: build_transport.DockerBuildPolicyV1 = MPFI_BUILD_TRANSPORT_POLICY_V1, +) -> build_input.SealedInputV1: + if type(source_lock) is not provenance.MpfiSourceLockV1: + raise TypeError("source_lock must be MpfiSourceLockV1") + if type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1: + raise TypeError("admitted_sources must be AdmittedMpfiSourcesV1") + if type(limits) is not build_input.CanonicalInputLimitsV1: + raise TypeError("limits must be CanonicalInputLimitsV1") + build_sources = canonical_build_sources_v1(build_sources) + if type(generated_formula) is not bytes or not generated_formula: + raise TypeError("generated_formula must be nonempty bytes") + if hashlib.sha256(generated_formula).hexdigest() != MPFI_GENERATED_FORMULA_SHA256_V1: + raise ValueError("generated MPFI formula drift") + if not build_transport.docker_policy_is_valid_v1(policy): + raise TypeError("policy must be canonical DockerBuildPolicyV1") + snapshot = provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + return seal_mpfi_build_input_from_snapshot_v1( + snapshot, + build_sources, + generated_formula, + limits, + policy, + ) + + +def seal_mpfi_build_input_from_snapshot_v1( + snapshot: provenance.ReplayedSourceClosureV1, + build_sources: AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, + limits: build_input.CanonicalInputLimitsV1, + policy: build_transport.DockerBuildPolicyV1 = MPFI_BUILD_TRANSPORT_POLICY_V1, +) -> build_input.SealedInputV1: + """Seal from one already-owned source replay without a second materialization.""" + + if type(snapshot) is not provenance.ReplayedSourceClosureV1: + raise TypeError("snapshot must be ReplayedSourceClosureV1") + if ( + type(snapshot.source_lock) is not provenance.MpfiSourceLockV1 + or type(snapshot.admitted_sources) + is not provenance.AdmittedMpfiSourcesV1 + or snapshot.admitted_sources.source_lock_identity + != snapshot.source_lock.identity + ): + raise TypeError("snapshot must retain MPFI source lock") + build_sources = canonical_build_sources_v1(build_sources) + if type(generated_formula) is not bytes or not generated_formula: + raise TypeError("generated_formula must be nonempty bytes") + if hashlib.sha256(generated_formula).hexdigest() != MPFI_GENERATED_FORMULA_SHA256_V1: + raise ValueError("generated MPFI formula drift") + if not build_transport.docker_policy_is_valid_v1(policy): + raise TypeError("policy must be canonical DockerBuildPolicyV1") + if type(limits) is not build_input.CanonicalInputLimitsV1: + raise TypeError("limits must be CanonicalInputLimitsV1") + entries = _canonical_input_entries_from_owned_v1( + snapshot, + build_sources, + generated_formula, + ) + canonical = build_input.canonical_ustar_v1(entries, limits) + return build_input.seal_input_v1( + _input_binding_identity_v1( + source_identity_v1(snapshot), + build_sources, + generated_formula, + policy, + canonical, + ), + canonical, + ) + + +def mpfi_build_input_is_bound_v1( + source_lock: object, + admitted_sources: object, + build_sources: object, + generated_formula: object, + limits: object, + value: object, + policy: object = MPFI_BUILD_TRANSPORT_POLICY_V1, +) -> bool: + if ( + type(value) is not build_input.SealedInputV1 + or not build_input.sealed_input_is_intact_v1(value) + or type(source_lock) is not provenance.MpfiSourceLockV1 + or type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1 + or type(build_sources) is not AdmittedMpfiBuildSourcesV1 + or type(generated_formula) is not bytes + or type(limits) is not build_input.CanonicalInputLimitsV1 + or type(policy) is not build_transport.DockerBuildPolicyV1 + ): + return False + try: + snapshot = provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + return mpfi_build_input_is_bound_from_snapshot_v1( + snapshot, + build_sources, + generated_formula, + limits, + value, + policy, + ) + except Exception: + return False + + +def mpfi_build_input_is_bound_from_snapshot_v1( + snapshot: object, + build_sources: object, + generated_formula: object, + limits: object, + value: object, + policy: object = MPFI_BUILD_TRANSPORT_POLICY_V1, +) -> bool: + if ( + type(snapshot) is not provenance.ReplayedSourceClosureV1 + or type(value) is not build_input.SealedInputV1 + or not build_input.sealed_input_is_intact_v1(value) + or type(build_sources) is not AdmittedMpfiBuildSourcesV1 + or type(generated_formula) is not bytes + or type(limits) is not build_input.CanonicalInputLimitsV1 + or type(policy) is not build_transport.DockerBuildPolicyV1 + ): + return False + try: + canonical_build_sources = canonical_build_sources_v1(build_sources) + expected = seal_mpfi_build_input_from_snapshot_v1( + snapshot, + canonical_build_sources, + generated_formula, + limits, + policy, + ) + except Exception: + return False + return ( + value.binding_identity == expected.binding_identity + and value.contents == expected.contents + ) diff --git a/proof/region/v1/mpfi/build.sh b/proof/region/v1/mpfi/build.sh new file mode 100755 index 00000000..3c8e59ce --- /dev/null +++ b/proof/region/v1/mpfi/build.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Source-owned MPFI dispatcher. The trusted Docker/CI transport invokes this +# file from the fixed bundle path with a clean environment. Keep the outer +# shell limited to builtins: path resolution must happen only in the clean child. +# shellcheck disable=SC2016 +set -eu + +if [ "$#" -ne 0 ]; then + printf '%s\n' 'mpfi build takes no arguments' >&2 + exit 64 +fi + +exec /usr/bin/env -i \ + PATH=/usr/bin:/bin \ + LC_ALL=C \ + LANG=C \ + TZ=UTC \ + HOME=/nonexistent \ + TMPDIR=/build/work/tmp \ + SOURCE_DATE_EPOCH=0 \ + ZERO_AR_DATE=1 \ + ARFLAGS=crD \ + /bin/sh -c ' + set -eu + script_path=$(/usr/bin/readlink -f -- "$1") + script_dir=$(/usr/bin/dirname -- "$script_path") + inner="$script_dir/build-inner.sh" + if [ ! -f "$inner" ] || [ -L "$inner" ]; then + printf "%s\\n" "missing regular MPFI inner build recipe" >&2 + exit 66 + fi + exec /bin/sh "$inner" + ' /bin/sh "$0" diff --git a/proof/region/v1/mpfi/evaluator/formula.h b/proof/region/v1/mpfi/evaluator/formula.h new file mode 100644 index 00000000..87926a78 --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/formula.h @@ -0,0 +1,19 @@ +#ifndef LABCOLOR_MPFI_FORMULA_H +#define LABCOLOR_MPFI_FORMULA_H + +#include + +#include + +#include "interval.h" + +lc_mpfi_status lc_mpfi_formula_point( + mpfi_ptr output, + const uint8_t rgb[3], + mpfi_srcptr context, + uint8_t surround +); +lc_mpfi_status lc_mpfi_formula_segment(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_formula_singleton(mpfi_ptr output, mpfi_srcptr input); + +#endif diff --git a/proof/region/v1/mpfi/evaluator/formula.py b/proof/region/v1/mpfi/evaluator/formula.py new file mode 100755 index 00000000..7344cf4f --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/formula.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +"""Generate the MPFI evaluator from the registered exact-real SSA. + +This is a separate parser and emitter from the Arb implementation. The two +engines intentionally consume the same immutable mathematical contract while +owning different C types, adapters and source identities. +""" + +from __future__ import annotations + +import hashlib +import sys +from dataclasses import dataclass +from pathlib import Path + + +SOURCE_SHA256 = "a6f77ac462f226453b1c27bbd8637b62780b9a640c317a6f50028dacd1de8540" +RELEASE_DOMAIN = b"labcolors.nominal-exact-real-lift.ascii-ssa.v1\0" +RELEASE_SHA256 = "2c626d8ee60eeb62ae4db53660d61bbc25e0efd4e557f0dc1e77565c130b6e52" + +_UNARY = frozenset(("root3", "sqrt", "exp", "log", "sin", "cos", "abs", "sign")) +_BINARY = frozenset(("add", "sub", "mul", "div", "min", "max", "pow_pos", "pow_nn", "ratio0")) +_EXPECTED = { + "point": ( + (("r8", "u8"), ("g8", "u8"), ("b8", "u8"), + ("adapting_luminance", "real"), ("background_ratio", "real"), + ("surround", "surround_profile")), + 226, + ("jp", "ap", "bp"), + 39, + ), + "segment": (tuple((name, "real") for name in ( + "segment_t", "segment_a", "segment_b", "segment_t0", "segment_t1", + "segment_c0a", "segment_c0b", "segment_c1a", "segment_c1b", + "segment_rho0", "segment_rho1", "segment_g00", "segment_g01", "segment_g11", + )), 27, ("segment_f",), 0), + "singleton": (tuple((name, "real") for name in ( + "singleton_a", "singleton_b", "singleton_ca", "singleton_cb", + "singleton_rho", "singleton_g00", "singleton_g01", "singleton_g11", + )), 12, ("singleton_f",), 0), +} + + +class FormulaError(ValueError): + pass + + +@dataclass(frozen=True) +class Node: + name: str + result: str + operator: str + arguments: tuple[str, ...] + + +@dataclass(frozen=True) +class Program: + name: str + inputs: tuple[tuple[str, str], ...] + nodes: tuple[Node, ...] + outputs: tuple[str, ...] + + +@dataclass(frozen=True) +class Formula: + decode: tuple[int, ...] + literals: tuple[tuple[str, int], ...] + programs: tuple[Program, ...] + + +class Cursor: + def __init__(self, lines: tuple[str, ...]): + self.lines = lines + self.index = 0 + + def take(self) -> str: + if self.index >= len(self.lines): + raise FormulaError(f"unexpected end at line {self.index + 1}") + value = self.lines[self.index] + self.index += 1 + return value + + def expect(self, value: str) -> None: + actual = self.take() + if actual != value: + raise FormulaError(f"expected {value!r}, got {actual!r}") + + +def _record(line: str, count: int) -> tuple[str, ...]: + values = tuple(line.split(" ")) + if len(values) != count: + raise FormulaError(f"record arity {len(values)} != {count}") + return values + + +def _bits(value: str) -> int: + if len(value) != 16 or any(ch not in "0123456789abcdef" for ch in value): + raise FormulaError("noncanonical binary64 payload") + bits = int(value, 16) + if bits & 0x7FF0000000000000 == 0x7FF0000000000000: + raise FormulaError("nonfinite binary64 payload") + if bits == 0x8000000000000000: + raise FormulaError("negative zero") + return bits + + +def _node_type_check(node: Node, symbols: dict[str, str]) -> None: + try: + argument_types = tuple(symbols[name] for name in node.arguments) + except KeyError as error: + raise FormulaError(f"unknown or forward reference {error.args[0]}") from None + if node.operator == "lookup": + valid = node.result == "real" and argument_types == ("decode_table", "u8") + elif node.operator == "eq": + valid = node.result == "bool" and len(argument_types) == 2 and argument_types[0] == argument_types[1] == "surround_profile" + elif node.operator == "select": + valid = len(argument_types) == 3 and argument_types[0] == "bool" and argument_types[1] == argument_types[2] == node.result == "real" + elif node.operator in _UNARY: + valid = node.result == "real" and argument_types == ("real",) + elif node.operator in _BINARY: + valid = node.result == "real" and argument_types == ("real", "real") + else: + valid = False + if not valid: + raise FormulaError(f"operator/type mismatch for {node.name}") + + +def _program(cursor: Cursor, name: str, globals_: dict[str, str]) -> Program: + expected_inputs, expected_nodes, expected_outputs, checkpoints = _EXPECTED[name] + cursor.expect(f"{name}_inputs {len(expected_inputs)}") + symbols = dict(globals_) + inputs: list[tuple[str, str]] = [] + for expected in expected_inputs: + record = _record(cursor.take(), 3) + if record != ("input", *expected) or record[1] in symbols: + raise FormulaError(f"foreign {name} input") + symbols[record[1]] = record[2] + inputs.append((record[1], record[2])) + cursor.expect(f"{name}_nodes {expected_nodes}") + nodes: list[Node] = [] + for _ in range(expected_nodes): + record = tuple(cursor.take().split(" ")) + if len(record) < 5 or record[0] != "node" or not record[1].islower(): + raise FormulaError("invalid node") + node = Node(record[1], record[2], record[3], record[4:]) + _node_type_check(node, symbols) + if node.name in symbols: + raise FormulaError("shadowed node") + symbols[node.name] = node.result + nodes.append(node) + if checkpoints: + cursor.expect(f"{name}_checkpoints {checkpoints}") + for _ in range(checkpoints): + checkpoint = _record(cursor.take(), 3) + if checkpoint[0] != "checkpoint" or checkpoint[2] not in {node.name for node in nodes}: + raise FormulaError("invalid checkpoint") + cursor.expect(f"{name}_outputs {len(expected_outputs)}") + outputs: list[str] = [] + for expected in expected_outputs: + record = _record(cursor.take(), 3) + if record != ("output", expected, "real") or symbols.get(expected) != "real": + raise FormulaError(f"foreign {name} output") + outputs.append(expected) + return Program(name, tuple(inputs), tuple(nodes), tuple(outputs)) + + +def parse(source: bytes) -> Formula: + if hashlib.sha256(source).hexdigest() != SOURCE_SHA256: + raise FormulaError("formula source is not the registered V1 content") + release = hashlib.sha256(RELEASE_DOMAIN + len(source).to_bytes(8, "big") + source).hexdigest() + if release != RELEASE_SHA256: + raise FormulaError("formula release mismatch") + if not source.isascii() or not source.endswith(b"\n") or source.endswith(b"\n\n"): + raise FormulaError("formula is not canonical ASCII with one final LF") + lines = tuple(source.decode("ascii")[:-1].split("\n")) + if any(not line or line.startswith(" ") or line.endswith(" ") or " " in line or "\t" in line or "\r" in line or "#" in line for line in lines): + raise FormulaError("formula contains a noncanonical line") + cursor = Cursor(lines) + cursor.expect("labcolors_exact_real_ssa 1") + cursor.expect("arithmetic exact_real_v1") + cursor.expect("types 4") + for declaration in ("type u8 unsigned_integer_0_255", "type real mathematical_real", "type bool exact_boolean", "type surround_profile closed_enum"): + cursor.expect(declaration) + cursor.expect("operators 20") + operators = tuple(cursor.take() for _ in range(20)) + if operators != ( + "operator lookup 2 real table_u8_exact_dyadic_at_ordinal", + "operator eq 2 bool exact_same_type_equality", + "operator select 3 same bool_true_second_else_third", + "operator add 2 real exact_x_plus_y", + "operator sub 2 real exact_x_minus_y", + "operator mul 2 real exact_x_times_y", + "operator div 2 real domain_y_ne_zero_x_div_y_else_domain_unproven", + "operator min 2 real exact_lesser_real", + "operator max 2 real exact_greater_real", + "operator root3 1 real domain_x_ge_zero_unique_y_ge_zero_y_cubed_eq_x_else_domain_unproven", + "operator sqrt 1 real domain_x_ge_zero_unique_y_ge_zero_y_squared_eq_x_else_domain_unproven", + "operator exp 1 real analytic_natural_exponential", + "operator log 1 real domain_x_gt_zero_analytic_natural_logarithm_else_domain_unproven", + "operator sin 1 real analytic_sine_radians", + "operator cos 1 real analytic_cosine_radians", + "operator abs 1 real exact_absolute_value", + "operator sign 1 real negative_minus_one_zero_zero_positive_one", + "operator pow_pos 2 real domain_x_gt_zero_exp_y_mul_log_x_else_domain_unproven", + "operator pow_nn 2 real if_x_eq_zero_and_y_gt_zero_zero_else_pow_pos", + "operator ratio0 2 real if_x_eq_zero_and_y_eq_zero_zero_else_domain_y_gt_zero_x_div_y", + ): + raise FormulaError("operator contract drift") + cursor.expect("decode_table decode_srgb8 256") + decode: list[int] = [] + for ordinal in range(256): + record = _record(cursor.take(), 3) + if record[:2] != ("decode", f"{ordinal:02x}"): + raise FormulaError("decode order drift") + decode.append(_bits(record[2])) + cursor.expect("literals 56") + literals: list[tuple[str, int]] = [] + names: set[str] = set() + values: set[int] = set() + for _ in range(56): + record = _record(cursor.take(), 3) + bits = _bits(record[2]) + if record[0] != "literal" or not record[1].islower() or record[1] in names or bits in values: + raise FormulaError("invalid literal") + names.add(record[1]) + values.add(bits) + literals.append((record[1], bits)) + cursor.expect("enum_type surround_profile 3") + enums = (("surround_average", 1), ("surround_dim", 2), ("surround_dark", 3)) + for name, tag in enums: + if _record(cursor.take(), 4) != ("enum", "surround_profile", name, f"{tag:02x}"): + raise FormulaError("surround enum drift") + globals_ = {"decode_srgb8": "decode_table"} + globals_.update({name: "real" for name, _ in literals}) + globals_.update({name: "surround_profile" for name, _ in enums}) + programs = tuple(_program(cursor, name, globals_) for name in ("point", "segment", "singleton")) + cursor.expect("driver 6") + for rule in ("rule tone_domain closed_first_last", "rule out_of_tone_domain outside", "rule one_knot_tone exact_equality_required", "rule one_knot_predicate singleton_f_le_zero", "rule multi_knot_predicate piecewise_linear_segment_f_le_zero", "rule boundary inclusive"): + cursor.expect(rule) + cursor.expect("end") + if cursor.index != len(lines): + raise FormulaError("trailing records") + return Formula(tuple(decode), tuple(literals), programs) + + +def _real_expr(name: str, slots: dict[str, int]) -> str: + return f"real + {slots[name]}" + + +def _emit_program(formula: Formula, program: Program) -> list[str]: + slots: dict[str, int] = {name: index for index, (name, _bits_value) in enumerate(formula.literals)} + surround = {"surround_average": "1", "surround_dim": "2", "surround_dark": "3"} + booleans: dict[str, str] = {} + for name, kind in program.inputs: + if kind == "real": + slots[name] = len(slots) + elif kind == "surround_profile": + surround[name] = "surround" + for node in program.nodes: + if node.result == "real": + slots[node.name] = len(slots) + else: + booleans[node.name] = f"condition_{len(booleans)}" + signatures = { + "point": "lc_mpfi_status lc_mpfi_formula_point(mpfi_ptr output, const uint8_t rgb[3], mpfi_srcptr context, uint8_t surround)", + "segment": "lc_mpfi_status lc_mpfi_formula_segment(mpfi_ptr output, mpfi_srcptr input)", + "singleton": "lc_mpfi_status lc_mpfi_formula_singleton(mpfi_ptr output, mpfi_srcptr input)", + } + lines = [signatures[program.name], "{", " lc_mpfi_status status = LC_MPFI_OK;", f" __mpfi_struct real[{len(slots)}];"] + lines.extend(f" mpfi_init2(real + {index}, mpfi_get_prec(output));" for index in range(len(slots))) + for name, bits in formula.literals: + lines.append(f" status = lc_mpfi_set_dyadic_bits(real + {slots[name]}, UINT64_C(0x{bits:016x}));") + lines.append(" if (status != LC_MPFI_OK) goto cleanup;") + real_cursor = 0 + u8_cursor = 0 + u8_values: dict[str, str] = {} + for name, kind in program.inputs: + if kind == "real": + source = "context" if program.name == "point" else "input" + lines.append(f" mpfi_set(real + {slots[name]}, {source} + {real_cursor});") + real_cursor += 1 + elif kind == "u8": + u8_values[name] = f"rgb[{u8_cursor}]" + u8_cursor += 1 + adapters = { + "add": "lc_mpfi_add", "sub": "lc_mpfi_sub", "mul": "lc_mpfi_mul", "div": "lc_mpfi_div", + "min": "lc_mpfi_min", "max": "lc_mpfi_max", "root3": "lc_mpfi_root3", "sqrt": "lc_mpfi_sqrt", + "exp": "lc_mpfi_exp", "log": "lc_mpfi_log", "sin": "lc_mpfi_sin", "cos": "lc_mpfi_cos", + "abs": "lc_mpfi_abs", "sign": "lc_mpfi_sign", "pow_pos": "lc_mpfi_pow_pos", + "pow_nn": "lc_mpfi_pow_nn", "ratio0": "lc_mpfi_ratio0", + } + for node in program.nodes: + target = _real_expr(node.name, slots) if node.result == "real" else "" + if node.operator == "lookup": + lines.append(f" status = lc_mpfi_set_dyadic_bits({target}, LC_MPFI_DECODE_BITS[(size_t){u8_values[node.arguments[1]]}]);") + lines.append(" if (status != LC_MPFI_OK) goto cleanup;") + elif node.operator == "eq": + lines.append(f" int {booleans[node.name]} = ({surround[node.arguments[0]]} == {surround[node.arguments[1]]});") + elif node.operator == "select": + condition = booleans[node.arguments[0]] + lines.append(f" mpfi_set({target}, {condition} ? {_real_expr(node.arguments[1], slots)} : {_real_expr(node.arguments[2], slots)});") + else: + arguments = ", ".join(_real_expr(argument, slots) for argument in node.arguments) + lines.append(f" status = {adapters[node.operator]}({target}, {arguments});") + lines.append(" if (status != LC_MPFI_OK) goto cleanup;") + for index, name in enumerate(program.outputs): + destination = f"output + {index}" if len(program.outputs) > 1 else "output" + lines.append(f" mpfi_set({destination}, {_real_expr(name, slots)});") + lines.append("cleanup:") + lines.extend(f" mpfi_clear(real + {index});" for index in range(len(slots) - 1, -1, -1)) + lines.extend((" return status;", "}", "")) + return lines + + +def emit(formula: Formula) -> bytes: + lines = [ + "/* Generated from the registered exact-real SSA; do not edit. */", + "#include ", + "#include ", + "#include \"formula.h\"", + "", + "static const uint64_t LC_MPFI_DECODE_BITS[256] = {", + ] + for index in range(0, 256, 4): + values = ", ".join(f"UINT64_C(0x{value:016x})" for value in formula.decode[index : index + 4]) + lines.append(f" {values},") + lines.append("};") + lines.append("") + for program in formula.programs: + lines.extend(_emit_program(formula, program)) + return ("\n".join(lines) + "\n").encode("ascii") + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: formula.py FORMULA", file=sys.stderr) + return 2 + try: + output = emit(parse(Path(argv[1]).read_bytes())) + except (OSError, FormulaError) as error: + print(f"formula rejected: {error}", file=sys.stderr) + return 1 + sys.stdout.buffer.write(output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/proof/region/v1/mpfi/evaluator/hash.c b/proof/region/v1/mpfi/evaluator/hash.c new file mode 100644 index 00000000..b902bd9a --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/hash.c @@ -0,0 +1,169 @@ +#include "hash.h" + +#include + +static const uint32_t constants[64] = { + UINT32_C(0x428a2f98), UINT32_C(0x71374491), UINT32_C(0xb5c0fbcf), UINT32_C(0xe9b5dba5), + UINT32_C(0x3956c25b), UINT32_C(0x59f111f1), UINT32_C(0x923f82a4), UINT32_C(0xab1c5ed5), + UINT32_C(0xd807aa98), UINT32_C(0x12835b01), UINT32_C(0x243185be), UINT32_C(0x550c7dc3), + UINT32_C(0x72be5d74), UINT32_C(0x80deb1fe), UINT32_C(0x9bdc06a7), UINT32_C(0xc19bf174), + UINT32_C(0xe49b69c1), UINT32_C(0xefbe4786), UINT32_C(0x0fc19dc6), UINT32_C(0x240ca1cc), + UINT32_C(0x2de92c6f), UINT32_C(0x4a7484aa), UINT32_C(0x5cb0a9dc), UINT32_C(0x76f988da), + UINT32_C(0x983e5152), UINT32_C(0xa831c66d), UINT32_C(0xb00327c8), UINT32_C(0xbf597fc7), + UINT32_C(0xc6e00bf3), UINT32_C(0xd5a79147), UINT32_C(0x06ca6351), UINT32_C(0x14292967), + UINT32_C(0x27b70a85), UINT32_C(0x2e1b2138), UINT32_C(0x4d2c6dfc), UINT32_C(0x53380d13), + UINT32_C(0x650a7354), UINT32_C(0x766a0abb), UINT32_C(0x81c2c92e), UINT32_C(0x92722c85), + UINT32_C(0xa2bfe8a1), UINT32_C(0xa81a664b), UINT32_C(0xc24b8b70), UINT32_C(0xc76c51a3), + UINT32_C(0xd192e819), UINT32_C(0xd6990624), UINT32_C(0xf40e3585), UINT32_C(0x106aa070), + UINT32_C(0x19a4c116), UINT32_C(0x1e376c08), UINT32_C(0x2748774c), UINT32_C(0x34b0bcb5), + UINT32_C(0x391c0cb3), UINT32_C(0x4ed8aa4a), UINT32_C(0x5b9cca4f), UINT32_C(0x682e6ff3), + UINT32_C(0x748f82ee), UINT32_C(0x78a5636f), UINT32_C(0x84c87814), UINT32_C(0x8cc70208), + UINT32_C(0x90befffa), UINT32_C(0xa4506ceb), UINT32_C(0xbef9a3f7), UINT32_C(0xc67178f2), +}; + +static uint32_t +load_word(const uint8_t *source) +{ + return ((uint32_t) source[0] << 24) + | ((uint32_t) source[1] << 16) + | ((uint32_t) source[2] << 8) + | (uint32_t) source[3]; +} + +static void +store_word(uint8_t *destination, uint32_t value) +{ + destination[0] = (uint8_t) (value >> 24); + destination[1] = (uint8_t) (value >> 16); + destination[2] = (uint8_t) (value >> 8); + destination[3] = (uint8_t) value; +} + +static uint32_t +rotate(uint32_t value, unsigned distance) +{ + return (value >> distance) | (value << (32U - distance)); +} + +static void +compress(lc_mpfi_sha256 *state, const uint8_t block[64]) +{ + uint32_t schedule[64]; + uint32_t a = state->words[0]; + uint32_t b = state->words[1]; + uint32_t c = state->words[2]; + uint32_t d = state->words[3]; + uint32_t e = state->words[4]; + uint32_t f = state->words[5]; + uint32_t g = state->words[6]; + uint32_t h = state->words[7]; + + for (size_t index = 0; index < 16; ++index) { + schedule[index] = load_word(block + index * 4); + } + for (size_t index = 16; index < 64; ++index) { + uint32_t older = schedule[index - 15]; + uint32_t newer = schedule[index - 2]; + uint32_t sigma0 = rotate(older, 7) ^ rotate(older, 18) ^ (older >> 3); + uint32_t sigma1 = rotate(newer, 17) ^ rotate(newer, 19) ^ (newer >> 10); + + schedule[index] = schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1; + } + for (size_t index = 0; index < 64; ++index) { + uint32_t upper = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25); + uint32_t choose = (e & f) ^ ((~e) & g); + uint32_t first = h + upper + choose + constants[index] + schedule[index]; + uint32_t lower = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22); + uint32_t majority = (a & b) ^ (a & c) ^ (b & c); + uint32_t second = lower + majority; + + h = g; + g = f; + f = e; + e = d + first; + d = c; + c = b; + b = a; + a = first + second; + } + state->words[0] += a; + state->words[1] += b; + state->words[2] += c; + state->words[3] += d; + state->words[4] += e; + state->words[5] += f; + state->words[6] += g; + state->words[7] += h; +} + +void +lc_mpfi_sha256_init(lc_mpfi_sha256 *state) +{ + static const uint32_t initial[8] = { + UINT32_C(0x6a09e667), UINT32_C(0xbb67ae85), UINT32_C(0x3c6ef372), UINT32_C(0xa54ff53a), + UINT32_C(0x510e527f), UINT32_C(0x9b05688c), UINT32_C(0x1f83d9ab), UINT32_C(0x5be0cd19), + }; + + memcpy(state->words, initial, sizeof(initial)); + state->bits = 0; + state->used = 0; +} + +void +lc_mpfi_sha256_update( + lc_mpfi_sha256 *state, + const uint8_t *bytes, + size_t length +) +{ + while (length != 0) { + size_t available = sizeof(state->block) - state->used; + size_t take = length < available ? length : available; + + memcpy(state->block + state->used, bytes, take); + state->used += take; + bytes += take; + length -= take; + if (state->used == sizeof(state->block)) { + compress(state, state->block); + state->bits += UINT64_C(512); + state->used = 0; + } + } +} + +void +lc_mpfi_sha256_finish(lc_mpfi_sha256 *state, uint8_t digest[32]) +{ + uint64_t length = state->bits + (uint64_t) state->used * 8; + + state->block[state->used++] = UINT8_C(0x80); + if (state->used > 56) { + memset(state->block + state->used, 0, sizeof(state->block) - state->used); + compress(state, state->block); + state->used = 0; + } + memset(state->block + state->used, 0, 56 - state->used); + for (size_t index = 0; index < 8; ++index) { + state->block[63 - index] = (uint8_t) (length >> (index * 8)); + } + compress(state, state->block); + for (size_t index = 0; index < 8; ++index) { + store_word(digest + index * 4, state->words[index]); + } + memset(state, 0, sizeof(*state)); +} + +void +lc_mpfi_sha256_bytes( + const uint8_t *bytes, + size_t length, + uint8_t digest[32] +) +{ + lc_mpfi_sha256 state; + + lc_mpfi_sha256_init(&state); + lc_mpfi_sha256_update(&state, bytes, length); + lc_mpfi_sha256_finish(&state, digest); +} diff --git a/proof/region/v1/mpfi/evaluator/hash.h b/proof/region/v1/mpfi/evaluator/hash.h new file mode 100644 index 00000000..a833f61c --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/hash.h @@ -0,0 +1,27 @@ +#ifndef LABCOLOR_MPFI_HASH_H +#define LABCOLOR_MPFI_HASH_H + +#include +#include + +typedef struct { + uint32_t words[8]; + uint64_t bits; + uint8_t block[64]; + size_t used; +} lc_mpfi_sha256; + +void lc_mpfi_sha256_init(lc_mpfi_sha256 *state); +void lc_mpfi_sha256_update( + lc_mpfi_sha256 *state, + const uint8_t *bytes, + size_t length +); +void lc_mpfi_sha256_finish(lc_mpfi_sha256 *state, uint8_t digest[32]); +void lc_mpfi_sha256_bytes( + const uint8_t *bytes, + size_t length, + uint8_t digest[32] +); + +#endif diff --git a/proof/region/v1/mpfi/evaluator/interval.c b/proof/region/v1/mpfi/evaluator/interval.c new file mode 100644 index 00000000..9affb726 --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/interval.c @@ -0,0 +1,242 @@ +#include "interval.h" + +#include + +static lc_mpfi_status +lc_mpfi_set_rational_bits(mpfi_ptr output, uint64_t bits) +{ + uint64_t exponent_bits = (bits >> 52) & UINT64_C(0x7ff); + uint64_t significand = bits & UINT64_C(0x000fffffffffffff); + mpz_t numerator; + mpz_t denominator; + mpq_t rational; + long exponent; + + if (exponent_bits == UINT64_C(0x7ff) + || bits == UINT64_C(0x8000000000000000)) { + return LC_MPFI_INVALID_DYADIC; + } + if (exponent_bits == 0) { + exponent = -1074; + } else { + significand |= UINT64_C(0x0010000000000000); + exponent = (long) exponent_bits - 1075; + } + + mpz_init_set_ui(numerator, significand); + mpz_init_set_ui(denominator, 1); + if ((bits >> 63) != 0 && significand != 0) { + mpz_neg(numerator, numerator); + } + if (exponent >= 0) { + mpz_mul_2exp(numerator, numerator, (unsigned long) exponent); + } else { + mpz_mul_2exp(denominator, denominator, (unsigned long) -exponent); + } + mpq_init(rational); + mpq_set_num(rational, numerator); + mpq_set_den(rational, denominator); + mpq_canonicalize(rational); + mpfi_set_q(output, rational); + mpq_clear(rational); + mpz_clear(denominator); + mpz_clear(numerator); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_set_dyadic_bits(mpfi_ptr output, uint64_t bits) +{ + return lc_mpfi_set_rational_bits(output, bits); +} + +lc_mpfi_status +lc_mpfi_add(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + mpfi_add(output, left, right); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_sub(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + mpfi_sub(output, left, right); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_mul(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + mpfi_mul(output, left, right); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_div(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + if (mpfi_has_zero(right)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_div(output, left, right); + return LC_MPFI_OK; +} + +static lc_mpfi_status +lc_mpfi_endpoint_select( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right, + int choose_lower +) +{ + mpfr_prec_t precision = mpfi_get_prec(output); + mpfr_t left_endpoint; + mpfr_t right_endpoint; + mpfr_t left_other; + mpfr_t right_other; + + mpfr_inits2(precision, left_endpoint, right_endpoint, left_other, right_other, (mpfr_ptr) 0); + mpfi_get_left(left_endpoint, left); + mpfi_get_right(right_endpoint, left); + mpfi_get_left(left_other, right); + mpfi_get_right(right_other, right); + if (choose_lower) { + mpfr_min(left_endpoint, left_endpoint, left_other, MPFR_RNDD); + mpfr_min(right_endpoint, right_endpoint, right_other, MPFR_RNDU); + } else { + mpfr_max(left_endpoint, left_endpoint, left_other, MPFR_RNDD); + mpfr_max(right_endpoint, right_endpoint, right_other, MPFR_RNDU); + } + mpfi_interv_fr(output, left_endpoint, right_endpoint); + mpfr_clears(left_endpoint, right_endpoint, left_other, right_other, (mpfr_ptr) 0); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_min(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + return lc_mpfi_endpoint_select(output, left, right, 1); +} + +lc_mpfi_status +lc_mpfi_max(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) +{ + return lc_mpfi_endpoint_select(output, left, right, 0); +} + +lc_mpfi_status +lc_mpfi_root3(mpfi_ptr output, mpfi_srcptr input) +{ + if (!mpfi_is_nonneg(input)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_cbrt(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_sqrt(mpfi_ptr output, mpfi_srcptr input) +{ + if (!mpfi_is_nonneg(input)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_sqrt(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_exp(mpfi_ptr output, mpfi_srcptr input) +{ + mpfi_exp(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_log(mpfi_ptr output, mpfi_srcptr input) +{ + if (!mpfi_is_pos(input)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_log(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_sin(mpfi_ptr output, mpfi_srcptr input) +{ + mpfi_sin(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_cos(mpfi_ptr output, mpfi_srcptr input) +{ + mpfi_cos(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_abs(mpfi_ptr output, mpfi_srcptr input) +{ + mpfi_abs(output, input); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_sign(mpfi_ptr output, mpfi_srcptr input) +{ + if (mpfi_is_strictly_neg(input)) { + mpfi_set_si(output, -1); + } else if (mpfi_is_strictly_pos(input)) { + mpfi_set_si(output, 1); + } else if (mpfi_is_zero(input)) { + mpfi_set_si(output, 0); + } else { + mpfi_interv_si(output, -1, 1); + } + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_pow_pos(mpfi_ptr output, mpfi_srcptr base, mpfi_srcptr exponent) +{ + mpfi_t logarithm; + + if (!mpfi_is_pos(base)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_init2(logarithm, mpfi_get_prec(output)); + mpfi_log(logarithm, base); + mpfi_mul(logarithm, logarithm, exponent); + mpfi_exp(output, logarithm); + mpfi_clear(logarithm); + return LC_MPFI_OK; +} + +lc_mpfi_status +lc_mpfi_pow_nn(mpfi_ptr output, mpfi_srcptr base, mpfi_srcptr exponent) +{ + if (mpfi_is_zero(base) && mpfi_is_pos(exponent)) { + mpfi_set_ui(output, 0); + return LC_MPFI_OK; + } + return lc_mpfi_pow_pos(output, base, exponent); +} + +lc_mpfi_status +lc_mpfi_ratio0( + mpfi_ptr output, + mpfi_srcptr numerator, + mpfi_srcptr denominator +) +{ + if (mpfi_is_zero(numerator) && mpfi_is_zero(denominator)) { + mpfi_set_ui(output, 0); + return LC_MPFI_OK; + } + if (!mpfi_is_pos(denominator)) { + return LC_MPFI_DOMAIN_UNPROVEN; + } + mpfi_div(output, numerator, denominator); + return LC_MPFI_OK; +} diff --git a/proof/region/v1/mpfi/evaluator/interval.h b/proof/region/v1/mpfi/evaluator/interval.h new file mode 100644 index 00000000..9bf9f01c --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/interval.h @@ -0,0 +1,70 @@ +#ifndef LABCOLOR_MPFI_INTERVAL_H +#define LABCOLOR_MPFI_INTERVAL_H + +#include + +#include + +typedef enum { + LC_MPFI_OK = 0, + LC_MPFI_DOMAIN_UNPROVEN = 1, + LC_MPFI_INVALID_DYADIC = 2 +} lc_mpfi_status; + +lc_mpfi_status lc_mpfi_set_dyadic_bits(mpfi_ptr output, uint64_t bits); + +lc_mpfi_status lc_mpfi_add( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_sub( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_mul( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_div( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_min( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_max( + mpfi_ptr output, + mpfi_srcptr left, + mpfi_srcptr right +); +lc_mpfi_status lc_mpfi_root3(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_sqrt(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_exp(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_log(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_sin(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_cos(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_abs(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_sign(mpfi_ptr output, mpfi_srcptr input); +lc_mpfi_status lc_mpfi_pow_pos( + mpfi_ptr output, + mpfi_srcptr base, + mpfi_srcptr exponent +); +lc_mpfi_status lc_mpfi_pow_nn( + mpfi_ptr output, + mpfi_srcptr base, + mpfi_srcptr exponent +); +lc_mpfi_status lc_mpfi_ratio0( + mpfi_ptr output, + mpfi_srcptr numerator, + mpfi_srcptr denominator +); + +#endif diff --git a/proof/region/v1/mpfi/evaluator/main.c b/proof/region/v1/mpfi/evaluator/main.c new file mode 100644 index 00000000..9d9906ce --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/main.c @@ -0,0 +1,570 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include "hash.h" +#include "wire.h" + +typedef struct { + uint8_t *bytes; + size_t length; + size_t capacity; + size_t maximum; + bool limit_exceeded; +} mpfi_buffer; + +typedef enum { + LC_MPFI_READ_OK = 0, + LC_MPFI_READ_EMPTY = 1, + LC_MPFI_READ_TOO_LARGE = 2, + LC_MPFI_READ_FAILED = 3 +} lc_mpfi_read_status; + +typedef enum { + LC_MPFI_EVALUATION_OK = 0, + LC_MPFI_EVALUATION_RESOURCE_LIMIT = 1, + LC_MPFI_EVALUATION_FAILED = 2 +} lc_mpfi_evaluation_status; + +static const uint8_t transcript_magic[8] = {'L', 'C', 'T', 'R', 'N', '1', 0, 0}; +static const uint8_t accounting_domain[] = + "labcolors.mpfi-evaluation-accounting.v1\0"; +static const uint8_t exact_trace_domain[] = + "labcolors.proof-region.exact-zero-signal-trace.v1\0"; +static const uint8_t boundary_domain[] = + "labcolors.mpfi-boundary-enclosure.v1\0"; + +static void +buffer_clear(mpfi_buffer *buffer) +{ + free(buffer->bytes); + memset(buffer, 0, sizeof(*buffer)); +} + +static bool +buffer_reserve(mpfi_buffer *buffer, size_t additional) +{ + size_t required; + size_t capacity; + uint8_t *replacement; + + if (additional > SIZE_MAX - buffer->length) { + return false; + } + required = buffer->length + additional; + if (buffer->maximum != 0 && required > buffer->maximum) { + buffer->limit_exceeded = true; + return false; + } + if (required <= buffer->capacity) { + return required == 0 || buffer->bytes != NULL; + } + capacity = buffer->capacity == 0 ? 4096 : buffer->capacity; + while (capacity < required) { + if (capacity > SIZE_MAX / 2) { + capacity = required; + break; + } + capacity *= 2; + } + replacement = realloc(buffer->bytes, capacity); + if (replacement == NULL) { + return false; + } + buffer->bytes = replacement; + buffer->capacity = capacity; + return true; +} + +static bool +buffer_append(mpfi_buffer *buffer, const uint8_t *bytes, size_t length) +{ + if (length == 0) { + return true; + } + if (!buffer_reserve(buffer, length)) { + return false; + } + memcpy(buffer->bytes + buffer->length, bytes, length); + buffer->length += length; + return true; +} + +static bool +buffer_u8(mpfi_buffer *buffer, uint8_t value) +{ + return buffer_append(buffer, &value, 1); +} + +static bool +buffer_u32(mpfi_buffer *buffer, uint32_t value) +{ + uint8_t encoded[4]; + + lc_mpfi_write_u32_be(encoded, value); + return buffer_append(buffer, encoded, sizeof(encoded)); +} + +static bool +buffer_u64(mpfi_buffer *buffer, uint64_t value) +{ + uint8_t encoded[8]; + + lc_mpfi_write_u64_be(encoded, value); + return buffer_append(buffer, encoded, sizeof(encoded)); +} + +static lc_mpfi_read_status +read_stdin(mpfi_buffer *input) +{ + uint8_t chunk[16384]; + + for (;;) { + ssize_t count = read(STDIN_FILENO, chunk, sizeof(chunk)); + + if (count < 0) { + if (errno == EINTR) { + continue; + } + return LC_MPFI_READ_FAILED; + } + if (count == 0) { + return input->length == 0 ? LC_MPFI_READ_EMPTY : LC_MPFI_READ_OK; + } + if (input->maximum != 0 + && (size_t) count > input->maximum - input->length) { + return LC_MPFI_READ_TOO_LARGE; + } + if (!buffer_append(input, chunk, (size_t) count)) { + return LC_MPFI_READ_FAILED; + } + } +} + +static bool +nonzero_digest(const uint8_t digest[32]) +{ + uint8_t value = 0; + + for (size_t index = 0; index < 32; ++index) { + value |= digest[index]; + } + return value != 0; +} + +static bool +parse_identity(const char *text, uint8_t identity[32]) +{ + uint8_t aggregate = 0; + + if (strlen(text) != 64) { + return false; + } + for (size_t index = 0; index < 32; ++index) { + uint8_t value = 0; + + for (size_t nibble = 0; nibble < 2; ++nibble) { + unsigned char character = (unsigned char) text[index * 2 + nibble]; + + value <<= 4; + if (character >= '0' && character <= '9') { + value |= (uint8_t) (character - '0'); + } else if (character >= 'a' && character <= 'f') { + value |= (uint8_t) (character - 'a' + 10); + } else { + return false; + } + } + identity[index] = value; + aggregate |= value; + } + return aggregate != 0; +} + +static void +hash_common_prefix( + lc_mpfi_sha256 *state, + const uint8_t *domain, + size_t domain_length, + const lc_mpfi_job *job, + uint32_t ordinal +) +{ + uint8_t ordinal_bytes[4]; + + lc_mpfi_write_u32_be(ordinal_bytes, ordinal); + lc_mpfi_sha256_init(state); + lc_mpfi_sha256_update(state, domain, domain_length); + lc_mpfi_sha256_update(state, job->job_identity, sizeof(job->job_identity)); + lc_mpfi_sha256_update(state, ordinal_bytes, sizeof(ordinal_bytes)); +} + +static bool +exact_trace_digest( + const lc_mpfi_job *job, + uint32_t ordinal, + uint64_t exact_branch, + uint8_t digest[32] +) +{ + lc_mpfi_sha256 state; + uint8_t branch_bytes[8]; + + hash_common_prefix( + &state, + exact_trace_domain, + sizeof(exact_trace_domain) - 1, + job, + ordinal + ); + lc_mpfi_write_u64_be(branch_bytes, exact_branch); + lc_mpfi_sha256_update(&state, branch_bytes, sizeof(branch_bytes)); + lc_mpfi_sha256_finish(&state, digest); + return nonzero_digest(digest); +} + +static bool +hash_mpfr_string(lc_mpfi_sha256 *state, mpfr_srcptr value) +{ + mpfr_exp_t exponent; + char *digits = mpfr_get_str(NULL, &exponent, 16, 0, value, MPFR_RNDN); + uint8_t exponent_bytes[8]; + uint8_t length_bytes[8]; + size_t length; + + if (digits == NULL) { + return false; + } + length = strlen(digits); + lc_mpfi_write_u64_be(length_bytes, (uint64_t) length); + lc_mpfi_write_u64_be(exponent_bytes, (uint64_t) exponent); + lc_mpfi_sha256_update(state, exponent_bytes, sizeof(exponent_bytes)); + lc_mpfi_sha256_update(state, length_bytes, sizeof(length_bytes)); + lc_mpfi_sha256_update(state, (const uint8_t *) digits, length); + mpfr_free_str(digits); + return true; +} + +static bool +boundary_digest( + const lc_mpfi_job *job, + uint32_t ordinal, + uint32_t precision, + const lc_mpfi_region_result *result, + uint8_t digest[32] +) +{ + lc_mpfi_sha256 state; + uint8_t precision_bytes[4]; + uint8_t status = (uint8_t) result->formula_status; + uint8_t present = result->has_enclosure ? 1 : 0; + __mpfr_struct lower; + __mpfr_struct upper; + bool success = false; + + hash_common_prefix(&state, boundary_domain, sizeof(boundary_domain) - 1, job, ordinal); + lc_mpfi_write_u32_be(precision_bytes, precision); + lc_mpfi_sha256_update(&state, precision_bytes, sizeof(precision_bytes)); + lc_mpfi_sha256_update(&state, &status, sizeof(status)); + lc_mpfi_sha256_update(&state, &present, sizeof(present)); + if (result->has_enclosure) { + mpfr_init2(&lower, result->precision); + mpfr_init2(&upper, result->precision); + mpfi_get_left(&lower, &result->enclosure); + mpfi_get_right(&upper, &result->enclosure); + success = hash_mpfr_string(&state, &lower) + && hash_mpfr_string(&state, &upper); + mpfr_clear(&upper); + mpfr_clear(&lower); + } else { + success = true; + } + if (!success) { + return false; + } + lc_mpfi_sha256_finish(&state, digest); + return nonzero_digest(digest); +} + +static void +account_point( + lc_mpfi_sha256 *state, + uint32_t ordinal, + uint32_t precision, + uint64_t consumed, + lc_mpfi_region_outcome outcome +) +{ + uint8_t record[17]; + + lc_mpfi_write_u32_be(record, ordinal); + lc_mpfi_write_u32_be(record + 4, precision); + lc_mpfi_write_u64_be(record + 8, consumed); + record[16] = (uint8_t) outcome; + lc_mpfi_sha256_update(state, record, sizeof(record)); +} + +static bool +append_digest_witness( + mpfi_buffer *witnesses, + uint8_t kind, + uint32_t ordinal, + const uint8_t digest[32] +) +{ + return buffer_u8(witnesses, kind) + && buffer_u32(witnesses, ordinal) + && buffer_append(witnesses, digest, 32); +} + +static bool +append_resource_witness( + mpfi_buffer *witnesses, + uint32_t ordinal, + uint8_t scope, + uint64_t grant +) +{ + return buffer_u8(witnesses, 3) + && buffer_u32(witnesses, ordinal) + && buffer_u8(witnesses, scope) + && buffer_u64(witnesses, grant) + && buffer_u64(witnesses, grant); +} + +static uint64_t +smaller(uint64_t left, uint64_t right) +{ + return left < right ? left : right; +} + +static lc_mpfi_evaluation_status +evaluate_job( + const lc_mpfi_job *job, + const uint8_t comparator_identity[32], + mpfi_buffer *output +) +{ + mpfi_buffer decisions = {0}; + mpfi_buffer witnesses = {0}; + lc_mpfi_domain_iterator iterator; + lc_mpfi_region_result result; + lc_mpfi_sha256 accounting; + uint64_t counters[4] = {0, 0, 0, 0}; + uint64_t equality_count = 0; + uint64_t witness_count = 0; + uint64_t remaining_global = job->policy.global_pregrant; + uint8_t accounting_digest[32]; + lc_mpfi_evaluation_status status = LC_MPFI_EVALUATION_FAILED; + + decisions.maximum = (size_t) LC_MPFI_MAX_OUTPUT_BYTES_V1; + witnesses.maximum = (size_t) LC_MPFI_MAX_OUTPUT_BYTES_V1; + + if (job->domain.point_count == 0 + || job->policy.precision_count == 0 + || job->domain.point_count > SIZE_MAX - 3 + || !lc_mpfi_region_result_init(&result, job->maximum_precision)) { + return LC_MPFI_EVALUATION_FAILED; + } + size_t decision_length = ((size_t) job->domain.point_count + 3) / 4; + if (decision_length == 0 + || !buffer_reserve(&decisions, decision_length) + || decisions.bytes == NULL) { + goto cleanup_buffers; + } + memset(decisions.bytes, 0, decision_length); + decisions.length = decision_length; + lc_mpfi_sha256_init(&accounting); + lc_mpfi_sha256_update( + &accounting, + accounting_domain, + sizeof(accounting_domain) - 1 + ); + lc_mpfi_sha256_update(&accounting, job->job_identity, 32); + lc_mpfi_sha256_update(&accounting, job->domain.identity, 32); + lc_mpfi_sha256_update(&accounting, job->policy.identity, 32); + lc_mpfi_sha256_update(&accounting, comparator_identity, 32); + lc_mpfi_domain_iterator_init(&iterator, &job->domain); + for (uint64_t point_index = 0; point_index < job->domain.point_count; ++point_index) { + uint64_t point_grant = smaller( + job->policy.per_point_work, + remaining_global + ); + uint64_t point_remaining = point_grant; + uint64_t point_consumed = 0; + uint8_t scope = job->policy.per_point_work <= remaining_global ? 1 : 2; + uint32_t ordinal; + uint32_t final_precision = job->policy.precision_ladder[0]; + uint8_t rgb[3]; + + remaining_global -= point_grant; + if (!lc_mpfi_domain_iterator_next(&iterator, &ordinal)) { + goto cleanup_buffers; + } + lc_mpfi_ordinal_to_rgb(ordinal, rgb); + for (size_t rung = 0; rung < job->policy.precision_count; ++rung) { + uint64_t grant = point_remaining; + + final_precision = job->policy.precision_ladder[rung]; + lc_mpfi_region_evaluate_rgb( + &result, + rgb, + job->context, + job->surround, + &job->region, + final_precision, + grant + ); + if (result.consumed_branches > grant + || result.consumed_branches > point_remaining) { + goto cleanup_buffers; + } + point_remaining -= result.consumed_branches; + point_consumed += result.consumed_branches; + if (result.outcome != LC_MPFI_REGION_BOUNDARY_UNPROVEN) { + break; + } + } + if ((unsigned) result.outcome > LC_MPFI_REGION_RESOURCE_LIMIT_REACHED) { + goto cleanup_buffers; + } + decisions.bytes[point_index / 4] |= + (uint8_t) result.outcome << (6U - 2U * (unsigned) (point_index % 4)); + ++counters[result.outcome]; + account_point( + &accounting, + ordinal, + final_precision, + point_consumed, + result.outcome + ); + if (result.outcome == LC_MPFI_REGION_INSIDE && result.exact_boundary) { + uint8_t digest[32]; + + if (!exact_trace_digest(job, ordinal, result.exact_branch, digest) + || !append_digest_witness(&witnesses, 1, ordinal, digest)) { + goto cleanup_buffers; + } + ++equality_count; + ++witness_count; + } else if (result.outcome == LC_MPFI_REGION_BOUNDARY_UNPROVEN) { + uint8_t digest[32]; + + if (!boundary_digest(job, ordinal, final_precision, &result, digest) + || !append_digest_witness(&witnesses, 2, ordinal, digest)) { + goto cleanup_buffers; + } + ++witness_count; + } else if (result.outcome == LC_MPFI_REGION_RESOURCE_LIMIT_REACHED) { + if (point_consumed != point_grant + || !append_resource_witness(&witnesses, ordinal, scope, point_grant)) { + goto cleanup_buffers; + } + ++witness_count; + } + } + lc_mpfi_sha256_finish(&accounting, accounting_digest); + if (!nonzero_digest(accounting_digest) + || !buffer_append(output, transcript_magic, sizeof(transcript_magic)) + || !buffer_append(output, job->job_identity, 32) + || !buffer_append(output, job->domain.identity, 32) + || !buffer_append(output, comparator_identity, 32) + || !buffer_u64(output, job->domain.point_count) + || !buffer_u64(output, decisions.length) + || !buffer_append(output, decisions.bytes, decisions.length)) { + goto cleanup_buffers; + } + for (size_t index = 0; index < 4; ++index) { + if (!buffer_u64(output, counters[index])) { + goto cleanup_buffers; + } + } + if (!buffer_u64(output, equality_count) + || !buffer_append(output, accounting_digest, sizeof(accounting_digest)) + || !buffer_u64(output, witness_count) + || !buffer_append(output, witnesses.bytes, witnesses.length)) { + goto cleanup_buffers; + } + status = LC_MPFI_EVALUATION_OK; + +cleanup_buffers: + if (status != LC_MPFI_EVALUATION_OK + && (decisions.limit_exceeded + || witnesses.limit_exceeded + || output->limit_exceeded)) { + status = LC_MPFI_EVALUATION_RESOURCE_LIMIT; + } + buffer_clear(&witnesses); + buffer_clear(&decisions); + lc_mpfi_region_result_clear(&result); + return status; +} + +int +main(int argc, char **argv) +{ + mpfi_buffer input = {0}; + mpfi_buffer output = {0}; + lc_mpfi_job job; + lc_mpfi_wire_error error; + uint8_t comparator_identity[32]; + int status = 1; + lc_mpfi_read_status read_status; + + input.maximum = (size_t) LC_MPFI_MAX_JOB_BYTES_V1; + output.maximum = (size_t) LC_MPFI_MAX_OUTPUT_BYTES_V1; + + if (argc != 5 + || strcmp(argv[1], "--manifest-identity") != 0 + || !parse_identity(argv[2], comparator_identity) + || strcmp(argv[3], "--job") != 0 + || strcmp(argv[4], "/dev/stdin") != 0) { + fputs( + "usage: mpfi-evaluator --manifest-identity HEX64 --job /dev/stdin\n", + stderr + ); + return 64; + } + read_status = read_stdin(&input); + if (read_status != LC_MPFI_READ_OK) { + const char *reason = read_status == LC_MPFI_READ_TOO_LARGE + ? "input_limit" + : read_status == LC_MPFI_READ_EMPTY ? "empty_input" : "io"; + + fprintf(stderr, "job read failed: %s\n", reason); + goto cleanup_input; + } + if (!lc_mpfi_parse_job(&job, input.bytes, input.length, &error)) { + fprintf(stderr, "job rejected: %s\n", lc_mpfi_wire_error_name(error)); + goto cleanup_input; + } + lc_mpfi_evaluation_status evaluation = + evaluate_job(&job, comparator_identity, &output); + if (evaluation != LC_MPFI_EVALUATION_OK) { + fprintf( + stderr, + "evaluation failed: %s\n", + evaluation == LC_MPFI_EVALUATION_RESOURCE_LIMIT + ? "output_limit" + : "internal" + ); + goto cleanup_job; + } + if (!lc_mpfi_write_all(STDOUT_FILENO, output.bytes, output.length)) { + fputs("result write failed\n", stderr); + goto cleanup_job; + } + status = 0; + +cleanup_job: + buffer_clear(&output); + lc_mpfi_job_clear(&job); +cleanup_input: + buffer_clear(&input); + return status; +} diff --git a/proof/region/v1/mpfi/evaluator/region.c b/proof/region/v1/mpfi/evaluator/region.c new file mode 100644 index 00000000..d6bbd1fd --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/region.c @@ -0,0 +1,382 @@ +#include "region.h" + +#include + +#include "formula.h" + +static void +clear_knot(lc_mpfi_region_knot *knot) +{ + mpfi_clear(&knot->radius_squared); + mpfi_clear(&knot->center_b); + mpfi_clear(&knot->center_a); + mpfi_clear(&knot->tone); +} + +static void +init_knot(lc_mpfi_region_knot *knot, mpfr_prec_t precision) +{ + mpfi_init2(&knot->tone, precision); + mpfi_init2(&knot->center_a, precision); + mpfi_init2(&knot->center_b, precision); + mpfi_init2(&knot->radius_squared, precision); +} + +static void +reset_result(lc_mpfi_region_result *result) +{ + result->outcome = LC_MPFI_REGION_BOUNDARY_UNPROVEN; + result->formula_status = LC_MPFI_OK; + result->exact_boundary = false; + result->has_enclosure = false; + result->exact_branch = 0; + result->consumed_branches = 0; + mpfi_set_ui(&result->enclosure, 0); +} + +static bool +interval_strictly_below(mpfi_srcptr left, mpfi_srcptr right) +{ + mpfr_prec_t precision = mpfi_get_prec(left); + mpfr_t left_high; + mpfr_t right_low; + int result; + + mpfr_inits2(precision, left_high, right_low, (mpfr_ptr) 0); + mpfi_get_right(left_high, left); + mpfi_get_left(right_low, right); + result = mpfr_cmp(left_high, right_low) < 0; + mpfr_clears(left_high, right_low, (mpfr_ptr) 0); + return result != 0; +} + +static bool +interval_strictly_above(mpfi_srcptr left, mpfi_srcptr right) +{ + return interval_strictly_below(right, left); +} + +static bool +interval_at_least(mpfi_srcptr left, mpfi_srcptr right) +{ + mpfr_prec_t precision = mpfi_get_prec(left); + mpfr_t left_low; + mpfr_t right_high; + int result; + + mpfr_inits2(precision, left_low, right_high, (mpfr_ptr) 0); + mpfi_get_left(left_low, left); + mpfi_get_right(right_high, right); + result = mpfr_cmp(left_low, right_high) >= 0; + mpfr_clears(left_low, right_high, (mpfr_ptr) 0); + return result != 0; +} + +static bool +interval_at_most(mpfi_srcptr left, mpfi_srcptr right) +{ + mpfr_prec_t precision = mpfi_get_prec(left); + mpfr_t left_high; + mpfr_t right_low; + int result; + + mpfr_inits2(precision, left_high, right_low, (mpfr_ptr) 0); + mpfi_get_right(left_high, left); + mpfi_get_left(right_low, right); + result = mpfr_cmp(left_high, right_low) <= 0; + mpfr_clears(left_high, right_low, (mpfr_ptr) 0); + return result != 0; +} + +static bool +interval_equal(mpfi_srcptr left, mpfi_srcptr right) +{ + __mpfi_struct difference; + bool equal; + + mpfi_init2(&difference, mpfi_get_prec(left)); + mpfi_sub(&difference, left, right); + equal = mpfi_is_zero(&difference) != 0; + mpfi_clear(&difference); + return equal; +} + +static void +record_enclosure(lc_mpfi_region_result *result, mpfi_srcptr value) +{ + if (result->has_enclosure) { + mpfi_union(&result->enclosure, &result->enclosure, value); + } else { + mpfi_set(&result->enclosure, value); + result->has_enclosure = true; + } +} + +bool +lc_mpfi_region_init( + lc_mpfi_region *region, + size_t knot_count, + mpfr_prec_t precision +) +{ + region->knots = NULL; + region->knot_count = 0; + region->precision = precision; + mpfi_init2(®ion->metric_aa, precision); + mpfi_init2(®ion->metric_ab, precision); + mpfi_init2(®ion->metric_bb, precision); + if (knot_count == 0 || knot_count > SIZE_MAX / sizeof(*region->knots)) { + lc_mpfi_region_clear(region); + return false; + } + region->knots = calloc(knot_count, sizeof(*region->knots)); + if (region->knots == NULL) { + lc_mpfi_region_clear(region); + return false; + } + region->knot_count = knot_count; + for (size_t index = 0; index < knot_count; ++index) { + init_knot(region->knots + index, precision); + } + return true; +} + +void +lc_mpfi_region_clear(lc_mpfi_region *region) +{ + if (region->knots != NULL) { + for (size_t index = 0; index < region->knot_count; ++index) { + clear_knot(region->knots + index); + } + free(region->knots); + } + mpfi_clear(®ion->metric_bb); + mpfi_clear(®ion->metric_ab); + mpfi_clear(®ion->metric_aa); + region->knots = NULL; + region->knot_count = 0; + region->precision = 0; +} + +bool +lc_mpfi_region_result_init(lc_mpfi_region_result *result, mpfr_prec_t precision) +{ + if (precision == 0) { + return false; + } + result->precision = precision; + mpfi_init2(&result->enclosure, precision); + reset_result(result); + return true; +} + +void +lc_mpfi_region_result_clear(lc_mpfi_region_result *result) +{ + mpfi_clear(&result->enclosure); + result->precision = 0; +} + +static void +evaluate_singleton( + lc_mpfi_region_result *result, + mpfi_srcptr point, + const lc_mpfi_region *region, + mpfr_prec_t precision, + uint64_t branch_grant +) +{ + __mpfi_struct input[8]; + __mpfi_struct predicate; + + if (!interval_equal(point, ®ion->knots[0].tone)) { + __mpfi_struct overlap; + + mpfi_init2(&overlap, precision); + mpfi_intersect(&overlap, point, ®ion->knots[0].tone); + result->outcome = mpfi_is_empty(&overlap) + ? LC_MPFI_REGION_OUTSIDE + : LC_MPFI_REGION_BOUNDARY_UNPROVEN; + mpfi_clear(&overlap); + return; + } + if (branch_grant == 0) { + result->outcome = LC_MPFI_REGION_RESOURCE_LIMIT_REACHED; + return; + } + for (size_t index = 0; index < 8; ++index) { + mpfi_init2(input + index, precision); + } + mpfi_set(input + 0, point + 1); + mpfi_set(input + 1, point + 2); + mpfi_set(input + 2, ®ion->knots[0].center_a); + mpfi_set(input + 3, ®ion->knots[0].center_b); + mpfi_set(input + 4, ®ion->knots[0].radius_squared); + mpfi_set(input + 5, ®ion->metric_aa); + mpfi_set(input + 6, ®ion->metric_ab); + mpfi_set(input + 7, ®ion->metric_bb); + mpfi_init2(&predicate, precision); + result->formula_status = lc_mpfi_formula_singleton(&predicate, input); + result->consumed_branches = 1; + if (result->formula_status == LC_MPFI_OK) { + record_enclosure(result, &predicate); + if (mpfi_is_nonpos(&predicate)) { + result->outcome = LC_MPFI_REGION_INSIDE; + result->exact_boundary = mpfi_is_zero(&predicate) != 0; + result->exact_branch = 0; + } else if (mpfi_is_pos(&predicate)) { + result->outcome = LC_MPFI_REGION_OUTSIDE; + } + } + mpfi_clear(&predicate); + for (size_t index = 8; index-- != 0;) { + mpfi_clear(input + index); + } +} + +void +lc_mpfi_region_decide( + lc_mpfi_region_result *result, + mpfi_srcptr point, + const lc_mpfi_region *region, + mpfr_prec_t precision, + uint64_t branch_grant +) +{ + bool any_segment = false; + bool all_inside = true; + bool all_outside = true; + bool exact_zero = false; + bool outside_possible; + uint64_t exact_branch = 0; + __mpfi_struct segment_domain; + __mpfi_struct intersection; + + reset_result(result); + if (precision < 2) { + result->formula_status = LC_MPFI_DOMAIN_UNPROVEN; + return; + } + if (region->knot_count == 1) { + evaluate_singleton(result, point, region, precision, branch_grant); + return; + } + if (region->knot_count < 2) { + result->formula_status = LC_MPFI_DOMAIN_UNPROVEN; + return; + } + if (interval_strictly_below(point, ®ion->knots[0].tone) + || interval_strictly_above(point, ®ion->knots[region->knot_count - 1].tone)) { + result->outcome = LC_MPFI_REGION_OUTSIDE; + return; + } + outside_possible = !interval_at_least(point, ®ion->knots[0].tone) + || !interval_at_most(point, ®ion->knots[region->knot_count - 1].tone); + mpfi_init2(&segment_domain, precision); + mpfi_init2(&intersection, precision); + for (size_t index = 0; index + 1 < region->knot_count; ++index) { + const lc_mpfi_region_knot *left = region->knots + index; + const lc_mpfi_region_knot *right = region->knots + index + 1; + __mpfi_struct input[14]; + __mpfi_struct predicate; + + mpfi_union(&segment_domain, &left->tone, &right->tone); + mpfi_intersect(&intersection, point, &segment_domain); + if (mpfi_is_empty(&intersection)) { + continue; + } + any_segment = true; + if (result->consumed_branches == branch_grant) { + result->outcome = LC_MPFI_REGION_RESOURCE_LIMIT_REACHED; + break; + } + for (size_t input_index = 0; input_index < 14; ++input_index) { + mpfi_init2(input + input_index, precision); + } + mpfi_set(input + 0, &intersection); + mpfi_set(input + 1, point + 1); + mpfi_set(input + 2, point + 2); + mpfi_set(input + 3, &left->tone); + mpfi_set(input + 4, &right->tone); + mpfi_set(input + 5, &left->center_a); + mpfi_set(input + 6, &left->center_b); + mpfi_set(input + 7, &right->center_a); + mpfi_set(input + 8, &right->center_b); + mpfi_set(input + 9, &left->radius_squared); + mpfi_set(input + 10, &right->radius_squared); + mpfi_set(input + 11, ®ion->metric_aa); + mpfi_set(input + 12, ®ion->metric_ab); + mpfi_set(input + 13, ®ion->metric_bb); + mpfi_init2(&predicate, precision); + result->formula_status = lc_mpfi_formula_segment(&predicate, input); + ++result->consumed_branches; + if (result->formula_status == LC_MPFI_OK) { + bool inside = mpfi_is_nonpos(&predicate) != 0; + bool outside = mpfi_is_pos(&predicate) != 0; + bool branch_exact = mpfi_is_zero(&predicate) != 0; + + record_enclosure(result, &predicate); + all_inside = all_inside && inside; + all_outside = all_outside && outside; + if (branch_exact && !exact_zero) { + exact_branch = (uint64_t) index; + } + exact_zero = exact_zero || branch_exact; + } else { + all_inside = false; + all_outside = false; + } + mpfi_clear(&predicate); + for (size_t input_index = 14; input_index-- != 0;) { + mpfi_clear(input + input_index); + } + } + if (result->outcome == LC_MPFI_REGION_RESOURCE_LIMIT_REACHED) { + mpfi_clear(&intersection); + mpfi_clear(&segment_domain); + return; + } + if (!any_segment) { + result->outcome = LC_MPFI_REGION_BOUNDARY_UNPROVEN; + } else if (all_outside) { + result->outcome = LC_MPFI_REGION_OUTSIDE; + } else if (all_inside && !outside_possible) { + result->outcome = LC_MPFI_REGION_INSIDE; + result->exact_boundary = exact_zero; + result->exact_branch = exact_branch; + } else { + result->outcome = LC_MPFI_REGION_BOUNDARY_UNPROVEN; + } + mpfi_clear(&intersection); + mpfi_clear(&segment_domain); +} + +void +lc_mpfi_region_evaluate_rgb( + lc_mpfi_region_result *result, + const uint8_t rgb[3], + mpfi_srcptr context, + uint8_t surround, + const lc_mpfi_region *region, + mpfr_prec_t precision, + uint64_t branch_grant +) +{ + __mpfi_struct point[3]; + + reset_result(result); + if (precision < 2) { + result->formula_status = LC_MPFI_DOMAIN_UNPROVEN; + return; + } + for (size_t index = 0; index < 3; ++index) { + mpfi_init2(point + index, precision); + } + result->formula_status = lc_mpfi_formula_point(point, rgb, context, surround); + if (result->formula_status == LC_MPFI_OK) { + lc_mpfi_region_decide(result, point, region, precision, branch_grant); + } + for (size_t index = 3; index-- != 0;) { + mpfi_clear(point + index); + } +} diff --git a/proof/region/v1/mpfi/evaluator/region.h b/proof/region/v1/mpfi/evaluator/region.h new file mode 100644 index 00000000..d2d6e3a4 --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/region.h @@ -0,0 +1,72 @@ +#ifndef LABCOLOR_MPFI_REGION_H +#define LABCOLOR_MPFI_REGION_H + +#include +#include +#include + +#include "interval.h" + +typedef enum { + LC_MPFI_REGION_INSIDE = 0, + LC_MPFI_REGION_OUTSIDE = 1, + LC_MPFI_REGION_BOUNDARY_UNPROVEN = 2, + LC_MPFI_REGION_RESOURCE_LIMIT_REACHED = 3 +} lc_mpfi_region_outcome; + +typedef struct { + __mpfi_struct tone; + __mpfi_struct center_a; + __mpfi_struct center_b; + __mpfi_struct radius_squared; +} lc_mpfi_region_knot; + +typedef struct { + __mpfi_struct metric_aa; + __mpfi_struct metric_ab; + __mpfi_struct metric_bb; + lc_mpfi_region_knot *knots; + size_t knot_count; + mpfr_prec_t precision; +} lc_mpfi_region; + +typedef struct { + lc_mpfi_region_outcome outcome; + lc_mpfi_status formula_status; + bool exact_boundary; + bool has_enclosure; + uint64_t exact_branch; + uint64_t consumed_branches; + __mpfi_struct enclosure; + mpfr_prec_t precision; +} lc_mpfi_region_result; + +bool lc_mpfi_region_init( + lc_mpfi_region *region, + size_t knot_count, + mpfr_prec_t precision +); +void lc_mpfi_region_clear(lc_mpfi_region *region); +bool lc_mpfi_region_result_init( + lc_mpfi_region_result *result, + mpfr_prec_t precision +); +void lc_mpfi_region_result_clear(lc_mpfi_region_result *result); +void lc_mpfi_region_decide( + lc_mpfi_region_result *result, + mpfi_srcptr point, + const lc_mpfi_region *region, + mpfr_prec_t precision, + uint64_t branch_grant +); +void lc_mpfi_region_evaluate_rgb( + lc_mpfi_region_result *result, + const uint8_t rgb[3], + mpfi_srcptr context, + uint8_t surround, + const lc_mpfi_region *region, + mpfr_prec_t precision, + uint64_t branch_grant +); + +#endif diff --git a/proof/region/v1/mpfi/evaluator/wire.c b/proof/region/v1/mpfi/evaluator/wire.c new file mode 100644 index 00000000..a3f1ba04 --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/wire.c @@ -0,0 +1,822 @@ +#include "wire.h" + +#include +#include +#include +#include + +#include "hash.h" + +typedef struct { + const uint8_t *bytes; + size_t length; + size_t offset; + lc_mpfi_wire_error *error; +} mpfi_reader; + +static const uint8_t job_magic[8] = {'L', 'C', 'J', 'O', 'B', '1', 0, 0}; +static const uint8_t domain_magic[8] = {'L', 'C', 'D', 'O', 'M', '1', 0, 0}; +static const uint8_t policy_magic[8] = {'L', 'C', 'P', 'O', 'L', '1', 0, 0}; +static const uint8_t definition_domain[] = + "labcolors.contextual-region-family-provider.v1\0"; +static const uint8_t formula_domain[] = + "labcolors.nominal-exact-real-lift.ascii-ssa.v1\0"; +static const uint8_t domain_identity_label[] = + "labcolors.proof-region.domain.v1\0"; +static const uint8_t policy_identity_label[] = + "labcolors.proof-region.policy.v1\0"; +static const uint8_t job_identity_label[] = + "labcolors.proof-region.job.v1\0"; +static const size_t formula_spec_length = 24434; +static const uint8_t formula_release_v1[32] = { + 0x2c, 0x62, 0x6d, 0x8e, 0xe6, 0x0e, 0xeb, 0x62, + 0xae, 0x4d, 0xb5, 0x36, 0x60, 0xd6, 0x1b, 0xbc, + 0x25, 0xe0, 0xef, 0xd4, 0xe5, 0x57, 0xf0, 0xdc, + 0x1e, 0x77, 0x56, 0x5c, 0x13, 0x0b, 0x6e, 0x52, +}; + +static bool +reject(mpfi_reader *input, lc_mpfi_wire_error error) +{ + if (*input->error == LC_MPFI_WIRE_OK) { + *input->error = error; + } + return false; +} + +static size_t +available(const mpfi_reader *input) +{ + return input->length - input->offset; +} + +static bool +take(mpfi_reader *input, size_t length, lc_mpfi_slice *slice) +{ + if (length > available(input)) { + return reject(input, LC_MPFI_WIRE_TRUNCATED); + } + slice->bytes = input->bytes + input->offset; + slice->length = length; + input->offset += length; + return true; +} + +static bool +expect( + mpfi_reader *input, + const uint8_t *expected, + size_t length, + lc_mpfi_wire_error error +) +{ + lc_mpfi_slice actual; + + return take(input, length, &actual) + && (memcmp(actual.bytes, expected, length) == 0 || reject(input, error)); +} + +static bool +read_u8(mpfi_reader *input, uint8_t *value) +{ + lc_mpfi_slice byte; + + if (!take(input, 1, &byte)) { + return false; + } + *value = byte.bytes[0]; + return true; +} + +static bool +read_u32(mpfi_reader *input, uint32_t *value) +{ + lc_mpfi_slice bytes; + + if (!take(input, 4, &bytes)) { + return false; + } + *value = ((uint32_t) bytes.bytes[0] << 24) + | ((uint32_t) bytes.bytes[1] << 16) + | ((uint32_t) bytes.bytes[2] << 8) + | (uint32_t) bytes.bytes[3]; + return true; +} + +static bool +read_u64(mpfi_reader *input, uint64_t *value) +{ + lc_mpfi_slice bytes; + uint64_t result = 0; + + if (!take(input, 8, &bytes)) { + return false; + } + for (size_t index = 0; index < 8; ++index) { + result = (result << 8) | bytes.bytes[index]; + } + *value = result; + return true; +} + +static bool +read_blob(mpfi_reader *input, size_t exact_length, lc_mpfi_slice *value) +{ + uint64_t declared; + + if (!read_u64(input, &declared)) { + return false; + } + if (declared > SIZE_MAX + || (exact_length != SIZE_MAX && declared != exact_length)) { + return reject(input, LC_MPFI_WIRE_LENGTH_OUT_OF_BOUNDS); + } + if ((size_t) declared > available(input)) { + return reject(input, LC_MPFI_WIRE_LENGTH_OUT_OF_BOUNDS); + } + return take(input, (size_t) declared, value); +} + +static bool +finish(mpfi_reader *input) +{ + return available(input) == 0 + || reject(input, LC_MPFI_WIRE_TRAILING_BYTES); +} + +void +lc_mpfi_write_u32_be(uint8_t output[4], uint32_t value) +{ + output[0] = (uint8_t) (value >> 24); + output[1] = (uint8_t) (value >> 16); + output[2] = (uint8_t) (value >> 8); + output[3] = (uint8_t) value; +} + +void +lc_mpfi_write_u64_be(uint8_t output[8], uint64_t value) +{ + for (size_t index = 0; index < 8; ++index) { + output[7 - index] = (uint8_t) (value >> (index * 8)); + } +} + +static void +content_identity( + const uint8_t *label, + size_t label_length, + const uint8_t *bytes, + size_t length, + uint8_t digest[32] +) +{ + lc_mpfi_sha256 state; + uint8_t encoded_length[8]; + + lc_mpfi_write_u64_be(encoded_length, (uint64_t) length); + lc_mpfi_sha256_init(&state); + lc_mpfi_sha256_update(&state, label, label_length); + lc_mpfi_sha256_update(&state, encoded_length, sizeof(encoded_length)); + lc_mpfi_sha256_update(&state, bytes, length); + lc_mpfi_sha256_finish(&state, digest); +} + +static bool +decode_bits(lc_mpfi_slice field, uint64_t *bits) +{ + uint64_t value = 0; + + if (field.length != 8) { + return false; + } + for (size_t index = 0; index < 8; ++index) { + value = (value << 8) | field.bytes[index]; + } + if ((value >> 52 & UINT64_C(0x7ff)) == UINT64_C(0x7ff) + || value == UINT64_C(0x8000000000000000)) { + return false; + } + *bits = value; + return true; +} + +static bool +bits_to_rational(uint64_t bits, mpq_t result) +{ + uint64_t exponent_bits = (bits >> 52) & UINT64_C(0x7ff); + uint64_t significand = bits & UINT64_C(0x000fffffffffffff); + long exponent; + mpz_t numerator; + mpz_t denominator; + + if (exponent_bits == UINT64_C(0x7ff) + || bits == UINT64_C(0x8000000000000000)) { + return false; + } + if (exponent_bits == 0) { + exponent = -1074; + } else { + significand |= UINT64_C(0x0010000000000000); + exponent = (long) exponent_bits - 1075; + } + mpz_init_set_ui(numerator, significand); + mpz_init_set_ui(denominator, 1); + if ((bits >> 63) != 0 && significand != 0) { + mpz_neg(numerator, numerator); + } + if (exponent >= 0) { + mpz_mul_2exp(numerator, numerator, (unsigned long) exponent); + } else { + mpz_mul_2exp(denominator, denominator, (unsigned long) -exponent); + } + mpq_init(result); + mpq_set_num(result, numerator); + mpq_set_den(result, denominator); + mpq_canonicalize(result); + mpz_clear(denominator); + mpz_clear(numerator); + return true; +} + +static bool +field_rational(lc_mpfi_slice field, mpq_t result) +{ + uint64_t bits; + + return decode_bits(field, &bits) && bits_to_rational(bits, result); +} + +static bool +field_to_interval(lc_mpfi_slice field, mpfi_ptr output) +{ + uint64_t bits; + + if (!decode_bits(field, &bits)) { + return false; + } + return lc_mpfi_set_dyadic_bits(output, bits) == LC_MPFI_OK; +} + +static bool +fixed_one(lc_mpfi_slice field) +{ + return field.length == 1 && field.bytes[0] == 1; +} + +static bool +parse_definition( + lc_mpfi_job *job, + lc_mpfi_slice encoded, + mpfr_prec_t precision, + lc_mpfi_wire_error *error +) +{ + static const size_t prefix_lengths[22] = { + sizeof(definition_domain) - 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 4, + 8, 8, 1, 1, 1, 32, 1, 8, 8, 8, 8, + }; + mpfi_reader input = {encoded.bytes, encoded.length, 0, error}; + lc_mpfi_slice fields[22]; + uint64_t knot_count; + mpq_t adapting; + mpq_t background; + mpq_t metric_aa; + mpq_t metric_ab; + mpq_t metric_bb; + mpq_t determinant; + mpq_t product; + + for (size_t index = 0; index < 22; ++index) { + if (!read_blob(&input, prefix_lengths[index], fields + index)) { + return false; + } + } + knot_count = 0; + for (size_t index = 0; index < 8; ++index) { + knot_count = (knot_count << 8) | fields[21].bytes[index]; + } + if (knot_count == 0) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + if (knot_count > LC_MPFI_MAX_KNOTS_V1) { + return reject(&input, LC_MPFI_WIRE_RESOURCE_LIMIT); + } + if (knot_count > SIZE_MAX / 64 + || available(&input) != (size_t) knot_count * 64) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + if (memcmp(fields[0].bytes, definition_domain, sizeof(definition_domain) - 1) != 0) { + return reject(&input, LC_MPFI_WIRE_UNKNOWN_RELEASE); + } + for (size_t index = 1; index <= 17; ++index) { + bool required = index == 1 || index == 2 || index == 3 || index == 4 + || index == 5 || index == 6 || index == 8 || index == 9 + || index == 14 || index == 15 || index == 17; + + if (required && !fixed_one(fields[index])) { + return reject(&input, LC_MPFI_WIRE_UNKNOWN_RELEASE); + } + } + if (memcmp(fields[7].bytes, "\x01\x01\x01\x01", 4) != 0 + || memcmp(fields[10].bytes, "\x01\x01\x01\x01", 4) != 0 + || fields[13].bytes[0] < 1 || fields[13].bytes[0] > 3 + || memcmp(fields[16].bytes, formula_release_v1, 32) != 0) { + return reject(&input, LC_MPFI_WIRE_UNKNOWN_RELEASE); + } + bool adapting_ok = field_rational(fields[11], adapting); + bool background_ok = field_rational(fields[12], background); + + if (!adapting_ok || !background_ok + || mpq_sgn(adapting) <= 0 + || mpq_sgn(background) <= 0 + || mpq_cmp_ui(background, 1, 1) > 0) { + if (adapting_ok) { + mpq_clear(adapting); + } + if (background_ok) { + mpq_clear(background); + } + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + mpq_clear(adapting); + mpq_clear(background); + + bool metric_aa_ok = field_rational(fields[18], metric_aa); + bool metric_ab_ok = field_rational(fields[19], metric_ab); + bool metric_bb_ok = field_rational(fields[20], metric_bb); + + if (!metric_aa_ok || !metric_ab_ok || !metric_bb_ok) { + if (metric_aa_ok) { + mpq_clear(metric_aa); + } + if (metric_ab_ok) { + mpq_clear(metric_ab); + } + if (metric_bb_ok) { + mpq_clear(metric_bb); + } + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + mpq_init(determinant); + mpq_init(product); + mpq_mul(determinant, metric_aa, metric_bb); + mpq_mul(product, metric_ab, metric_ab); + mpq_sub(determinant, determinant, product); + if (mpq_sgn(metric_aa) <= 0 || mpq_sgn(determinant) <= 0) { + mpq_clear(product); + mpq_clear(determinant); + mpq_clear(metric_bb); + mpq_clear(metric_ab); + mpq_clear(metric_aa); + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + mpq_clear(product); + mpq_clear(determinant); + + mpfi_init2(&job->context[0], precision); + mpfi_init2(&job->context[1], precision); + job->context_ready = true; + if (!field_to_interval(fields[11], &job->context[0]) + || !field_to_interval(fields[12], &job->context[1])) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + job->surround = fields[13].bytes[0]; + memcpy(job->formula_release, fields[16].bytes, 32); + if (!lc_mpfi_region_init(&job->region, (size_t) knot_count, precision)) { + return reject(&input, LC_MPFI_WIRE_ALLOCATION_FAILED); + } + job->region_ready = true; + if (!field_to_interval(fields[18], &job->region.metric_aa) + || !field_to_interval(fields[19], &job->region.metric_ab) + || !field_to_interval(fields[20], &job->region.metric_bb)) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + bool have_previous_tone = false; + mpq_t previous_tone; + + for (size_t index = 0; index < (size_t) knot_count; ++index) { + lc_mpfi_slice knot_fields[4]; + lc_mpfi_region_knot *target = job->region.knots + index; + mpq_t tone; + mpq_t radius; + mpq_t center_a; + mpq_t center_b; + bool tone_ok; + bool radius_ok; + bool center_a_ok; + bool center_b_ok; + + for (size_t coordinate = 0; coordinate < 4; ++coordinate) { + if (!read_blob(&input, 8, knot_fields + coordinate)) { + if (have_previous_tone) { + mpq_clear(previous_tone); + } + return false; + } + } + tone_ok = field_rational(knot_fields[0], tone); + radius_ok = field_rational(knot_fields[3], radius); + center_a_ok = field_rational(knot_fields[1], center_a); + center_b_ok = field_rational(knot_fields[2], center_b); + if (!tone_ok || !radius_ok || !center_a_ok || !center_b_ok + || mpq_sgn(radius) < 0 + || (have_previous_tone && mpq_cmp(tone, previous_tone) <= 0)) { + if (tone_ok) { + mpq_clear(tone); + } + if (radius_ok) { + mpq_clear(radius); + } + if (center_a_ok) { + mpq_clear(center_a); + } + if (center_b_ok) { + mpq_clear(center_b); + } + if (have_previous_tone) { + mpq_clear(previous_tone); + } + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + if (have_previous_tone) { + mpq_clear(previous_tone); + } + mpq_init(previous_tone); + mpq_set(previous_tone, tone); + have_previous_tone = true; + mpq_clear(radius); + mpq_clear(tone); + mpq_clear(center_a); + mpq_clear(center_b); + if (!field_to_interval(knot_fields[0], &target->tone) + || !field_to_interval(knot_fields[1], &target->center_a) + || !field_to_interval(knot_fields[2], &target->center_b) + || !field_to_interval(knot_fields[3], &target->radius_squared)) { + if (have_previous_tone) { + mpq_clear(previous_tone); + } + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + } + if (have_previous_tone) { + mpq_clear(previous_tone); + } + return finish(&input); +} + +static bool +parse_domain( + lc_mpfi_domain *domain, + lc_mpfi_slice encoded, + const uint8_t expected[32], + lc_mpfi_wire_error *error +) +{ + mpfi_reader input = {encoded.bytes, encoded.length, 0, error}; + uint8_t release; + uint64_t range_count; + uint64_t maximum; + uint64_t total = 0; + + if (!expect(&input, domain_magic, sizeof(domain_magic), LC_MPFI_WIRE_BAD_MAGIC) + || !read_u8(&input, &release) + || release != 1 + || !read_u64(&input, &domain->point_count) + || domain->point_count == 0 + || domain->point_count > UINT64_C(0x1000000) + || !read_u64(&input, &range_count)) { + return *input.error != LC_MPFI_WIRE_OK + ? false + : reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + maximum = domain->point_count; + if (UINT64_C(0x1000001) - domain->point_count < maximum) { + maximum = UINT64_C(0x1000001) - domain->point_count; + } + if (range_count == 0 || range_count > maximum + || range_count > SIZE_MAX / sizeof(*domain->ranges) + || range_count > available(&input) / 8 + || (size_t) range_count * 8 != available(&input)) { + return reject(&input, LC_MPFI_WIRE_LENGTH_OUT_OF_BOUNDS); + } + domain->ranges = calloc((size_t) range_count, sizeof(*domain->ranges)); + if (domain->ranges == NULL) { + return reject(&input, LC_MPFI_WIRE_ALLOCATION_FAILED); + } + domain->range_count = (size_t) range_count; + for (size_t index = 0; index < domain->range_count; ++index) { + lc_mpfi_ordinal_range *range = domain->ranges + index; + + if (!read_u32(&input, &range->start) || !read_u32(&input, &range->end)) { + return false; + } + if (range->start >= range->end || range->end > UINT32_C(0x1000000) + || (index != 0 && range->start <= domain->ranges[index - 1].end)) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + total += (uint64_t) range->end - range->start; + } + if (total != domain->point_count || !finish(&input)) { + return *input.error != LC_MPFI_WIRE_OK + ? false + : reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + content_identity( + domain_identity_label, + sizeof(domain_identity_label) - 1, + encoded.bytes, + encoded.length, + domain->identity + ); + return memcmp(domain->identity, expected, 32) == 0 + || reject(&input, LC_MPFI_WIRE_DIGEST_MISMATCH); +} + +static bool +parse_policy( + lc_mpfi_policy *policy, + lc_mpfi_slice encoded, + const uint8_t expected[32], + lc_mpfi_wire_error *error +) +{ + mpfi_reader input = {encoded.bytes, encoded.length, 0, error}; + uint8_t equality_release; + uint8_t comparator_count; + + if (!expect(&input, policy_magic, sizeof(policy_magic), LC_MPFI_WIRE_BAD_MAGIC) + || !read_u8(&input, &equality_release) + || !read_u8(&input, &comparator_count)) { + return false; + } + if (equality_release != 1 || comparator_count != 2) { + return reject(&input, LC_MPFI_WIRE_UNKNOWN_RELEASE); + } + for (uint8_t expected_kind = 1; expected_kind <= 2; ++expected_kind) { + uint8_t kind; + uint32_t rung_count; + uint32_t previous = 0; + size_t minimum_tail = expected_kind == 1 ? 41 : 16; + uint32_t *ladder = NULL; + + if (!read_u8(&input, &kind) || !read_u32(&input, &rung_count)) { + return false; + } + if (kind != expected_kind || rung_count == 0 + || available(&input) < minimum_tail + || rung_count > (available(&input) - minimum_tail) / 4) { + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + if (rung_count > LC_MPFI_MAX_POLICY_RUNGS_V1) { + return reject(&input, LC_MPFI_WIRE_RESOURCE_LIMIT); + } + if (expected_kind == 2) { + if ((size_t) rung_count > SIZE_MAX / sizeof(*policy->precision_ladder)) { + return reject(&input, LC_MPFI_WIRE_LENGTH_OUT_OF_BOUNDS); + } + ladder = calloc(rung_count, sizeof(*ladder)); + if (ladder == NULL) { + return reject(&input, LC_MPFI_WIRE_ALLOCATION_FAILED); + } + } + for (size_t index = 0; index < rung_count; ++index) { + uint32_t precision; + + if (!read_u32(&input, &precision)) { + free(ladder); + return false; + } + if (precision == 0 || (index != 0 && precision <= previous) + || (uint64_t) precision > (uint64_t) MPFR_PREC_MAX) { + free(ladder); + return reject(&input, LC_MPFI_WIRE_NONCANONICAL); + } + if (precision > LC_MPFI_MAX_PRECISION_BITS_V1) { + free(ladder); + return reject(&input, LC_MPFI_WIRE_RESOURCE_LIMIT); + } + if (ladder != NULL) { + ladder[index] = precision; + } + previous = precision; + } + if (expected_kind == 1) { + uint64_t ignored; + + if (!read_u64(&input, &ignored) || !read_u64(&input, &ignored)) { + free(ladder); + return false; + } + } else { + if (!read_u64(&input, &policy->per_point_work) + || !read_u64(&input, &policy->global_pregrant)) { + free(ladder); + return false; + } + policy->precision_ladder = ladder; + policy->precision_count = rung_count; + } + } + if (!finish(&input)) { + return false; + } + content_identity( + policy_identity_label, + sizeof(policy_identity_label) - 1, + encoded.bytes, + encoded.length, + policy->identity + ); + return memcmp(policy->identity, expected, 32) == 0 + || reject(&input, LC_MPFI_WIRE_DIGEST_MISMATCH); +} + +static bool +formula_matches(lc_mpfi_slice formula) +{ + lc_mpfi_sha256 state; + uint8_t encoded_length[8]; + uint8_t digest[32]; + + if (formula.length != formula_spec_length) { + return false; + } + lc_mpfi_write_u64_be(encoded_length, (uint64_t) formula.length); + lc_mpfi_sha256_init(&state); + lc_mpfi_sha256_update(&state, formula_domain, sizeof(formula_domain) - 1); + lc_mpfi_sha256_update(&state, encoded_length, sizeof(encoded_length)); + lc_mpfi_sha256_update(&state, formula.bytes, formula.length); + lc_mpfi_sha256_finish(&state, digest); + return memcmp(digest, formula_release_v1, sizeof(formula_release_v1)) == 0; +} + +bool +lc_mpfi_parse_job( + lc_mpfi_job *job, + const uint8_t *bytes, + size_t length, + lc_mpfi_wire_error *error +) +{ + mpfi_reader input; + lc_mpfi_slice definition; + lc_mpfi_slice formula; + lc_mpfi_slice domain; + lc_mpfi_slice policy; + lc_mpfi_slice definition_digest; + lc_mpfi_slice declared_formula; + lc_mpfi_slice domain_identity; + lc_mpfi_slice policy_identity; + uint8_t actual[32]; + + memset(job, 0, sizeof(*job)); + *error = LC_MPFI_WIRE_OK; + if (length > (size_t) LC_MPFI_MAX_JOB_BYTES_V1) { + *error = LC_MPFI_WIRE_RESOURCE_LIMIT; + return false; + } + input = (mpfi_reader) {bytes, length, 0, error}; + if (!expect(&input, job_magic, sizeof(job_magic), LC_MPFI_WIRE_BAD_MAGIC) + || !take(&input, 32, &definition_digest) + || !read_blob(&input, SIZE_MAX, &definition) + || !take(&input, 32, &declared_formula) + || !read_blob(&input, formula_spec_length, &formula) + || !take(&input, 32, &domain_identity) + || !read_blob(&input, SIZE_MAX, &domain) + || !take(&input, 32, &policy_identity) + || !read_blob(&input, SIZE_MAX, &policy) + || !finish(&input)) { + lc_mpfi_job_clear(job); + return false; + } + lc_mpfi_sha256_bytes(definition.bytes, definition.length, actual); + if (memcmp(actual, definition_digest.bytes, 32) != 0) { + *error = LC_MPFI_WIRE_DIGEST_MISMATCH; + lc_mpfi_job_clear(job); + return false; + } + if (!parse_policy(&job->policy, policy, policy_identity.bytes, error) + || job->policy.precision_count == 0 + || !parse_domain(&job->domain, domain, domain_identity.bytes, error)) { + lc_mpfi_job_clear(job); + return false; + } + job->maximum_precision = job->policy.precision_ladder[ + job->policy.precision_count - 1 + ]; + if (!parse_definition(job, definition, job->maximum_precision, error) + || memcmp(declared_formula.bytes, job->formula_release, 32) != 0 + || !formula_matches(formula) + || memcmp(declared_formula.bytes, formula_release_v1, 32) != 0) { + if (*error == LC_MPFI_WIRE_OK) { + *error = LC_MPFI_WIRE_DIGEST_MISMATCH; + } + lc_mpfi_job_clear(job); + return false; + } + content_identity( + job_identity_label, + sizeof(job_identity_label) - 1, + bytes, + length, + job->job_identity + ); + return true; +} + +void +lc_mpfi_job_clear(lc_mpfi_job *job) +{ + free(job->policy.precision_ladder); + free(job->domain.ranges); + if (job->region_ready) { + lc_mpfi_region_clear(&job->region); + } + if (job->context_ready) { + mpfi_clear(&job->context[1]); + mpfi_clear(&job->context[0]); + } + memset(job, 0, sizeof(*job)); +} + +void +lc_mpfi_domain_iterator_init( + lc_mpfi_domain_iterator *iterator, + const lc_mpfi_domain *domain +) +{ + iterator->domain = domain; + iterator->range_index = 0; + iterator->ordinal = domain->ranges[0].start; + iterator->emitted = 0; +} + +bool +lc_mpfi_domain_iterator_next( + lc_mpfi_domain_iterator *iterator, + uint32_t *ordinal +) +{ + if (iterator->emitted == iterator->domain->point_count) { + return false; + } + *ordinal = iterator->ordinal; + ++iterator->emitted; + ++iterator->ordinal; + if (iterator->ordinal == iterator->domain->ranges[iterator->range_index].end + && iterator->emitted != iterator->domain->point_count) { + ++iterator->range_index; + iterator->ordinal = iterator->domain->ranges[iterator->range_index].start; + } + return true; +} + +void +lc_mpfi_ordinal_to_rgb(uint32_t ordinal, uint8_t rgb[3]) +{ + rgb[0] = (uint8_t) (ordinal >> 16); + rgb[1] = (uint8_t) (ordinal >> 8); + rgb[2] = (uint8_t) ordinal; +} + +const char * +lc_mpfi_wire_error_name(lc_mpfi_wire_error error) +{ + static const char *const names[] = { + "ok", + "truncated", + "trailing_bytes", + "length_out_of_bounds", + "bad_magic", + "unknown_release", + "noncanonical", + "digest_mismatch", + "allocation_failed", + "resource_limit", + }; + + return (unsigned) error < sizeof(names) / sizeof(names[0]) + ? names[error] + : "unknown_wire_error"; +} + +bool +lc_mpfi_write_all(int descriptor, const uint8_t *bytes, size_t length) +{ + while (length != 0) { + ssize_t written = write(descriptor, bytes, length); + + if (written < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (written == 0) { + return false; + } + bytes += (size_t) written; + length -= (size_t) written; + } + return true; +} diff --git a/proof/region/v1/mpfi/evaluator/wire.h b/proof/region/v1/mpfi/evaluator/wire.h new file mode 100644 index 00000000..18c35a74 --- /dev/null +++ b/proof/region/v1/mpfi/evaluator/wire.h @@ -0,0 +1,104 @@ +#ifndef LABCOLOR_MPFI_WIRE_H +#define LABCOLOR_MPFI_WIRE_H + +#include +#include +#include + +#include "region.h" + +/* + * M1.5's direct executable has an explicit resource profile. These are + * operational admission bounds, not mathematical restrictions on the + * contextual-region wire grammar; M2a must bind the same profile to its + * controller/executor limits before minting any observation. + */ +#define LC_MPFI_MAX_JOB_BYTES_V1 \ + (UINT64_C(16) * UINT64_C(1024) * UINT64_C(1024)) +#define LC_MPFI_MAX_OUTPUT_BYTES_V1 \ + (UINT64_C(16) * UINT64_C(1024) * UINT64_C(1024)) +#define LC_MPFI_MAX_PRECISION_BITS_V1 UINT32_C(4096) +#define LC_MPFI_MAX_POLICY_RUNGS_V1 UINT32_C(32) +#define LC_MPFI_MAX_KNOTS_V1 UINT64_C(1024) + +typedef enum { + LC_MPFI_WIRE_OK = 0, + LC_MPFI_WIRE_TRUNCATED = 1, + LC_MPFI_WIRE_TRAILING_BYTES = 2, + LC_MPFI_WIRE_LENGTH_OUT_OF_BOUNDS = 3, + LC_MPFI_WIRE_BAD_MAGIC = 4, + LC_MPFI_WIRE_UNKNOWN_RELEASE = 5, + LC_MPFI_WIRE_NONCANONICAL = 6, + LC_MPFI_WIRE_DIGEST_MISMATCH = 7, + LC_MPFI_WIRE_ALLOCATION_FAILED = 8, + LC_MPFI_WIRE_RESOURCE_LIMIT = 9 +} lc_mpfi_wire_error; + +typedef struct { + const uint8_t *bytes; + size_t length; +} lc_mpfi_slice; + +typedef struct { + uint32_t start; + uint32_t end; +} lc_mpfi_ordinal_range; + +typedef struct { + lc_mpfi_ordinal_range *ranges; + size_t range_count; + uint64_t point_count; + uint8_t identity[32]; +} lc_mpfi_domain; + +typedef struct { + uint32_t *precision_ladder; + size_t precision_count; + uint64_t per_point_work; + uint64_t global_pregrant; + uint8_t identity[32]; +} lc_mpfi_policy; + +typedef struct { + const lc_mpfi_domain *domain; + size_t range_index; + uint32_t ordinal; + uint64_t emitted; +} lc_mpfi_domain_iterator; + +typedef struct { + lc_mpfi_region region; + __mpfi_struct context[2]; + uint8_t surround; + lc_mpfi_domain domain; + lc_mpfi_policy policy; + uint8_t formula_release[32]; + uint8_t job_identity[32]; + mpfr_prec_t maximum_precision; + bool context_ready; + bool region_ready; +} lc_mpfi_job; + +bool lc_mpfi_parse_job( + lc_mpfi_job *job, + const uint8_t *bytes, + size_t length, + lc_mpfi_wire_error *error +); +void lc_mpfi_job_clear(lc_mpfi_job *job); +void lc_mpfi_domain_iterator_init( + lc_mpfi_domain_iterator *iterator, + const lc_mpfi_domain *domain +); +bool lc_mpfi_domain_iterator_next( + lc_mpfi_domain_iterator *iterator, + uint32_t *ordinal +); +void lc_mpfi_ordinal_to_rgb(uint32_t ordinal, uint8_t rgb[3]); +const char *lc_mpfi_wire_error_name(lc_mpfi_wire_error error); + +bool lc_mpfi_write_all(int descriptor, const uint8_t *bytes, size_t length); +void lc_mpfi_write_u32_be(uint8_t output[4], uint32_t value); +void lc_mpfi_write_u64_be(uint8_t output[8], uint64_t value); + +#endif diff --git a/proof/region/v1/mpfi/input.py b/proof/region/v1/mpfi/input.py new file mode 100644 index 00000000..c2623047 --- /dev/null +++ b/proof/region/v1/mpfi/input.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""MPFI-замыкание исходников, материализуемое в единый sealed input.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from enum import StrEnum +from typing import NoReturn + +import provenance +from build import input as build_input + + +_MPFI_SOURCE_INPUT_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-source-input.v1\0" +# Это release layout, не MPFI build recipe: source role — единственная +# инъективная namespace coordinate, гарантированная locked closure. +_MPFI_SOURCE_INPUT_LAYOUT_V1 = b"sources//" +_SOURCE_NAMESPACE_V1 = { + provenance.SourceRoleV1.GMP: "gmp", + provenance.SourceRoleV1.MPFR: "mpfr", + provenance.SourceRoleV1.MPFI: "mpfi", +} + + +class MpfiSourceInputReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + FOREIGN_SOURCE_CAPABILITY = "foreign_source_capability" + + +@dataclass(frozen=True) +class MpfiSourceInputErrorV1(ValueError): + reason: MpfiSourceInputReasonV1 + field: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.field}" + + +def _fail(reason: MpfiSourceInputReasonV1, field_name: str) -> NoReturn: + raise MpfiSourceInputErrorV1(reason, field_name) + + +def _canonical_lock_v1( + source_lock: provenance.MpfiSourceLockV1, +) -> provenance.MpfiSourceLockV1: + """Отбрасывает cached hostile state до именования input capability.""" + + if type(source_lock) is not provenance.MpfiSourceLockV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "source_lock") + try: + canonical = provenance.snapshot_source_closure_lock_v1(source_lock) + except ( + provenance.ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") + if type(canonical) is not provenance.MpfiSourceLockV1: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "source_lock") + return canonical + + +def _fresh_admitted_sources_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, +) -> provenance.ReplayedSourceClosureV1: + if type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "admitted_sources") + try: + source_lock_identity = admitted_sources.source_lock_identity + except Exception: + # Exact type не делает retained capability неуязвимой к post-admission + # подмене; ordinary hostile failure обязан остаться typed rejection. + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + bound = ( + type(source_lock_identity) is bytes + and len(source_lock_identity) == 32 + and source_lock_identity != bytes(32) + and source_lock_identity == source_lock.identity + ) + if not bound: + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + # Replay owns nested archive evidence; its typed provenance taxonomy must + # survive instead of being flattened into an MPFI declaration error. + try: + return provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + except provenance.ProvenanceErrorV1 as error: + if error.artifact == "source-closure-replay-v1": + _fail( + MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + raise + except TypeError: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources") + + +def _canonical_limits_v1( + limits: build_input.CanonicalInputLimitsV1, +) -> build_input.CanonicalInputLimitsV1: + if type(limits) is not build_input.CanonicalInputLimitsV1: + _fail(MpfiSourceInputReasonV1.WRONG_TYPE, "limits") + try: + canonical = build_input.CanonicalInputLimitsV1(*tuple(limits)) + except build_input.InputErrorV1: + raise + except (AttributeError, TypeError, ValueError, OverflowError): + raise build_input.InputErrorV1( + build_input.InputReasonV1.NONCANONICAL_SET, + "limits", + ) + if tuple(canonical) != tuple(limits): + raise build_input.InputErrorV1( + build_input.InputReasonV1.NONCANONICAL_SET, + "limits", + ) + return canonical + + +def _preflight_declared_resource_bounds_v1( + source_lock: provenance.MpfiSourceLockV1, + limits: build_input.CanonicalInputLimitsV1, +) -> None: + """Отклоняет declared totals до allocation file bodies во время replay.""" + + declared_file_count = sum( + item.regular_file_count for item in source_lock.sources + ) + declared_payload_bytes = sum( + item.regular_file_bytes for item in source_lock.sources + ) + if declared_file_count > limits.max_members: + raise build_input.InputErrorV1( + build_input.InputReasonV1.RESOURCE_LIMIT, + "max_members", + ) + if declared_payload_bytes > limits.max_payload_bytes: + raise build_input.InputErrorV1( + build_input.InputReasonV1.RESOURCE_LIMIT, + "max_payload_bytes", + ) + + +def _source_entries_v1( + snapshot: provenance.ReplayedSourceClosureV1, +) -> tuple[tuple[str, int, bytes], ...]: + entries = tuple( + ( + f"sources/{_SOURCE_NAMESPACE_V1[lock.role]}/{relative}", + mode, + contents, + ) + for lock, materialized in zip( + snapshot.source_lock.sources, + snapshot.sources, + strict=True, + ) + for relative, mode, contents in materialized.files + ) + if not entries: + _fail(MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, "admitted_sources") + return tuple(sorted(entries)) + + +def _binding_identity_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + contents: bytes, +) -> bytes: + if type(contents) is not bytes or not contents: + raise TypeError("MPFI source input contents must be exact nonempty bytes") + coordinates = ( + _MPFI_SOURCE_INPUT_LAYOUT_V1, + source_lock.identity, + admitted_sources.identity, + len(contents).to_bytes(8, "big"), + hashlib.sha256(contents).digest(), + ) + preimage = b"".join(len(value).to_bytes(8, "big") + value for value in coordinates) + return hashlib.sha256( + _MPFI_SOURCE_INPUT_ID_LABEL_V1 + + len(preimage).to_bytes(8, "big") + + preimage + ).digest() + + +def seal_mpfi_source_input_v1( + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + limits: build_input.CanonicalInputLimitsV1, +) -> build_input.SealedInputV1: + """Запечатывает MPFI source closure в caller-owned resource bounds. + + `MpfiSourceInputErrorV1` означает invalid public capability boundary, + `ProvenanceErrorV1` — failure exact archive replay, а `InputErrorV1` — + canonical USTAR или resource-bound rejection. + """ + + canonical_lock = _canonical_lock_v1(source_lock) + canonical_limits = _canonical_limits_v1(limits) + # Declared totals are authenticated by the canonical release lock. Check + # them before archive replay so an impossible caller budget cannot force + # archive-body allocation merely to learn it is impossible. + _preflight_declared_resource_bounds_v1(canonical_lock, canonical_limits) + snapshot = _fresh_admitted_sources_v1(canonical_lock, admitted_sources) + entries = _source_entries_v1(snapshot) + contents = build_input.canonical_ustar_v1(entries, canonical_limits) + return build_input.seal_input_v1( + _binding_identity_v1( + snapshot.source_lock, + snapshot.admitted_sources, + contents, + ), + contents, + ) + + +def mpfi_source_input_is_bound_v1( + source_lock: object, + admitted_sources: object, + limits: object, + value: object, +) -> bool: + """Независимо пересобирает MPFI input, а не доверяет одному seal.""" + + if ( + type(value) is not build_input.SealedInputV1 + or not build_input.sealed_input_is_intact_v1(value) + ): + return False + try: + expected = seal_mpfi_source_input_v1(source_lock, admitted_sources, limits) + except ( + MpfiSourceInputErrorV1, + provenance.ProvenanceErrorV1, + build_input.InputErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + ): + return False + return ( + value.binding_identity == expected.binding_identity + and value.contents == expected.contents + ) diff --git a/proof/region/v1/mpfi/operations.py b/proof/region/v1/mpfi/operations.py new file mode 100755 index 00000000..c99ca237 --- /dev/null +++ b/proof/region/v1/mpfi/operations.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Machine-readable operation boundary for the MPFI comparator. + +This file is intentionally small: it is the source-level gate for the +evaluator, not a claim about every symbol shipped by the MPFI distribution. +The dependency's broader API remains outside the comparator's authority. + +The gate is deliberately conservative. A forbidden dependency name is +rejected wherever it appears, not only when it is followed by ``(``: a macro, +function pointer, or assembler alias can otherwise hide the call from a +call-shaped regular expression. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +ENGINE = "mpfi" +COMPILER_FAMILY = "clang" +TARGET = "x86_64-pc-linux-gnu" +COMPILE_FLAGS = ( + "-std=c17", + "-O2", + "-fno-fast-math", + "-ffp-contract=off", + "-fno-lto", + "-march=x86-64", + "-mtune=generic", +) + +# These are the only MPFI calls the evaluator is allowed to make. GMP/MPFR +# calls used to construct exact inputs and to serialize witness bounds are a +# separate dependency boundary and are listed below rather than silently +# widened by a future source edit. +ALLOWED_MPFI_CALLS = frozenset( + { + "mpfi_abs", + "mpfi_add", + "mpfi_cbrt", + "mpfi_clear", + "mpfi_cos", + "mpfi_div", + "mpfi_exp", + "mpfi_get_left", + "mpfi_get_prec", + "mpfi_get_right", + "mpfi_has_zero", + "mpfi_init2", + "mpfi_intersect", + "mpfi_interv_fr", + "mpfi_interv_si", + "mpfi_is_empty", + "mpfi_is_nonneg", + "mpfi_is_nonpos", + "mpfi_is_pos", + "mpfi_is_strictly_neg", + "mpfi_is_strictly_pos", + "mpfi_is_zero", + "mpfi_log", + "mpfi_mul", + "mpfi_set", + "mpfi_set_q", + "mpfi_set_si", + "mpfi_set_ui", + "mpfi_sin", + "mpfi_sqrt", + "mpfi_sub", + "mpfi_union", + } +) + +FORBIDDEN_MPFI_CALLS = frozenset( + { + "mpfi_atan2", + "mpfi_div_ext", + "mpfi_exp10", + "mpfi_rec_sqrt", + "mpfi_set_ld", + } +) + +ALLOWED_MPFR_CALLS = frozenset( + { + "mpfr_clear", + "mpfr_clears", + "mpfr_cmp", + # MPFI's interval intersection implementation reaches this MPFR + # helper through its public operation; keep the linked ABI closed + # without mistaking the helper for a new evaluator operation. + "mpfr_cmp3", + "mpfr_free_str", + "mpfr_get_str", + "mpfr_init2", + "mpfr_inits2", + "mpfr_max", + "mpfr_min", + } +) + +ALLOWED_GMP_CALLS = frozenset( + { + "mpq_canonicalize", + "mpq_clear", + "mpq_cmp", + "mpq_cmp_ui", + "mpq_init", + "mpq_mul", + "mpq_set", + "mpq_set_den", + "mpq_set_num", + "mpq_sgn", + "mpq_sub", + "mpz_clear", + # GMP's rational comparison/canonicalization path may emit this + # transitive limb comparison even when source calls stay mpq-only. + "mpz_cmp", + "mpz_init_set_ui", + "mpz_mul_2exp", + "mpz_neg", + } +) + +_CALL = re.compile(r"\b((?:mpfi|mpfr|mpq|mpz)_[A-Za-z0-9_]+)\s*\(") +def called_symbols(source: str) -> frozenset[str]: + return frozenset(_CALL.findall(source)) + + +def undefined_symbols(nm_output: str) -> frozenset[str]: + """Extract dependency symbols from ``nm -u`` without trusting formatting.""" + + symbols: set[str] = set() + for line in nm_output.splitlines(): + fields = line.split() + if not fields: + continue + symbol = fields[-1].lstrip("_") + # ELF GMP exports commonly spell mpq/mpz calls as __gmpq/__gmpz; + # normalize that ABI spelling before comparing the closed call set. + if symbol.startswith("gmp") and len(symbol) > 4 and symbol[3] != "_": + symbol = "mp" + symbol[3:] + if symbol.startswith(("mpfi_", "mpfr_", "mpq_", "mpz_")): + symbols.add(symbol) + return frozenset(symbols) + + +def validate_undefined_symbols(nm_output: str) -> tuple[str, ...]: + """Check the symbols the compiler left unresolved in evaluator objects.""" + + seen = undefined_symbols(nm_output) + allowed = ALLOWED_MPFI_CALLS | ALLOWED_MPFR_CALLS | ALLOWED_GMP_CALLS + errors: list[str] = [] + errors.extend( + f"forbidden undefined external symbol {symbol}" + for symbol in sorted(seen & FORBIDDEN_MPFI_CALLS) + ) + errors.extend( + f"unexpected undefined external symbol {symbol}" + for symbol in sorted(seen - allowed - FORBIDDEN_MPFI_CALLS) + ) + return tuple(errors) + + +def validate_sources(directory: Path) -> tuple[str, ...]: + paths = sorted(directory.glob("*.c")) + sorted(directory.glob("*.h")) + if not paths: + return ("evaluator source directory is empty",) + errors: list[str] = [] + seen: set[str] = set() + for path in paths: + text = path.read_text(encoding="utf-8") + seen.update(called_symbols(text)) + # Token-pasting can construct an operation name that this source gate + # cannot enumerate. The linked undefined-symbol gate is a second + # defence, but admitting such source would make the static contract + # depend on the compiler rather than remain auditable from the tree. + if "##" in text: + errors.append(f"{path.name}: token-pasting is forbidden") + for forbidden in FORBIDDEN_MPFI_CALLS: + if re.search(rf"\b{re.escape(forbidden)}\b", text): + errors.append(f"{path.name}: forbidden operation {forbidden}") + for marker in ("arb", "flint", "long double", "strtod", "fallback"): + if marker in text.lower(): + errors.append(f"{path.name}: forbidden dependency marker {marker}") + allowed = ALLOWED_MPFI_CALLS | ALLOWED_MPFR_CALLS | ALLOWED_GMP_CALLS + errors.extend( + f"unexpected external call {symbol}" + for symbol in sorted(seen - allowed) + ) + errors.extend( + f"required MPFI call is absent {symbol}" + for symbol in sorted(ALLOWED_MPFI_CALLS - seen) + ) + return tuple(errors) + + +def main(argv: list[str]) -> int: + if len(argv) == 2: + errors = validate_sources(Path(argv[1])) + elif len(argv) == 3 and argv[1] == "--undefined-symbols": + errors = validate_undefined_symbols(Path(argv[2]).read_text(encoding="utf-8")) + else: + print( + "usage: operations.py EVALUATOR_DIRECTORY | " + "--undefined-symbols NM_OUTPUT", + file=sys.stderr, + ) + return 2 + if errors: + for error in errors: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/proof/region/v1/mpfi/receipt.py b/proof/region/v1/mpfi/receipt.py new file mode 100644 index 00000000..ad5d5b9c --- /dev/null +++ b/proof/region/v1/mpfi/receipt.py @@ -0,0 +1,1135 @@ +#!/usr/bin/env python3 +"""One-shot source → MPFI BUILD → RUN provenance boundary. + +The receipt is provenance-only. It does not certify interval semantics, +scientific correctness, or Arb/MPFI agreement; those remain separate protocol +admissions. All source, build, runtime and executor coordinates are replayed +before sealing so a self-consistent forged observation cannot pass. +""" + +from __future__ import annotations + +import hashlib +import os +import threading +from dataclasses import dataclass, fields +from enum import StrEnum +from pathlib import Path +from typing import TypeAlias + +from build import transport as build_transport +from build import input as build_input + +import executor +import provenance +import region_proof_protocol as protocol +from mpfi import build as mpfi_build +from mpfi import runtime as mpfi_runtime + + +_BUILD_OBSERVATION_TOKEN = object() +_EVIDENCE_TOKEN = object() +_RECEIPT_TOKEN = object() +_NATIVE_BUILD_BACKEND_TYPE = build_transport.NativeDockerBuildBackendV1 +_NATIVE_RUN_BACKEND_TYPE = executor.NativeLinuxBackendV1 + +_REQUEST_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-request.v1\0" +_BUILD_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-build-replay.v1\0" +_RUN_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-run-replay.v1\0" +_EVIDENCE_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-evaluator-replay.v1\0" +_POLICY_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-source-bound-policy.v1\0" +_PREIMAGE_LABEL = b"labcolors.proof-region.mpfi-comparator-preimage.v1\0" +_MPFI_FAILURE_DETAIL_LIMIT_V1 = 4096 +_MPFI_FAILURE_DETAIL_FALLBACK_V1 = "MPFI source-bound operation failed" + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join( + len(chunk).to_bytes(8, "big") + chunk + for chunk in chunks + ) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +def _preimage(role: str, chunks: tuple[bytes, ...]) -> bytes: + return _identity(_PREIMAGE_LABEL + role.encode("ascii") + b"\0", chunks) + + +def _digest(value: object, field_name: str) -> bytes: + if type(value) is not bytes or len(value) != 32 or value == bytes(32): + raise TypeError(f"invalid {field_name}") + return value + + +def _failure_detail_v1(value: object, *, diagnostic_repr: bool = False) -> str: + """Keep rejection diagnostics bounded even when an adapter raises badly.""" + + try: + detail = repr(value) if diagnostic_repr else str(value) + except Exception: + return _MPFI_FAILURE_DETAIL_FALLBACK_V1 + if not detail or len(detail) > _MPFI_FAILURE_DETAIL_LIMIT_V1: + return _MPFI_FAILURE_DETAIL_FALLBACK_V1 + return detail + + +class MpfiRequestErrorReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + FOREIGN_SOURCE_CAPABILITY = "foreign_source_capability" + FORMULA_MISMATCH = "formula_mismatch" + LIMIT_MISMATCH = "limit_mismatch" + GENERATED_FORMULA_DRIFT = "generated_formula_drift" + + +@dataclass(frozen=True) +class MpfiRequestErrorV1(ValueError): + reason: MpfiRequestErrorReasonV1 + field: str + + def __str__(self) -> str: + return f"{self.reason.value}: {self.field}" + + +class MpfiPipelineRequestV1(tuple): + """Exact detached inputs for one source-bound MPFI operation.""" + + __slots__ = () + + def __new__( + cls, + source_lock: provenance.MpfiSourceLockV1, + admitted_sources: provenance.AdmittedMpfiSourcesV1, + build_sources: mpfi_build.AdmittedMpfiBuildSourcesV1, + generated_formula: bytes, + build_limits: object, + job: protocol.ProofJobV1, + runtime_binding: mpfi_runtime.MpfiRuntimeBindingV1, + ) -> MpfiPipelineRequestV1: + if type(source_lock) is not provenance.MpfiSourceLockV1: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "source_lock", + ) + if type(admitted_sources) is not provenance.AdmittedMpfiSourcesV1: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "admitted_sources", + ) + if type(build_sources) is not mpfi_build.AdmittedMpfiBuildSourcesV1: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "build_sources", + ) + if type(generated_formula) is not bytes: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "generated_formula", + ) + if type(build_limits) is not build_input.CanonicalInputLimitsV1: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "build_limits", + ) + if type(job) is not protocol.ProofJobV1: + raise MpfiRequestErrorV1(MpfiRequestErrorReasonV1.WRONG_TYPE, "job") + if type(runtime_binding) is not mpfi_runtime.MpfiRuntimeBindingV1: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.WRONG_TYPE, + "runtime_binding", + ) + return tuple.__new__( + cls, + ( + source_lock, + admitted_sources, + build_sources, + generated_formula, + build_limits, + job, + runtime_binding, + ), + ) + + source_lock = property(lambda self: self[0]) + admitted_sources = property(lambda self: self[1]) + build_sources = property(lambda self: self[2]) + generated_formula = property(lambda self: self[3]) + build_limits = property(lambda self: self[4]) + job = property(lambda self: self[5]) + runtime_binding = property(lambda self: self[6]) + + +@dataclass(frozen=True) +class _MpfiOperationSnapshotV1: + request: MpfiPipelineRequestV1 + source_closure: provenance.ReplayedSourceClosureV1 + + +def _snapshot_request_v1( + request: object, +) -> _MpfiOperationSnapshotV1: + if type(request) is not MpfiPipelineRequestV1: + raise MpfiRequestErrorV1(MpfiRequestErrorReasonV1.WRONG_TYPE, "request") + try: + source_lock = provenance.snapshot_source_closure_lock_v1(request.source_lock) + admitted_sources = provenance.snapshot_admitted_source_closure_v1( + source_lock, + request.admitted_sources, + ) + source_closure = provenance.replay_admitted_source_closure_v1( + source_lock, + admitted_sources, + ) + if ( + type(source_closure.source_lock) is not provenance.MpfiSourceLockV1 + or type(source_closure.admitted_sources) + is not provenance.AdmittedMpfiSourcesV1 + ): + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.FOREIGN_SOURCE_CAPABILITY, + "admitted_sources", + ) + build_sources = mpfi_build.canonical_build_sources_v1(request.build_sources) + job = protocol.snapshot_proof_job_v1(request.job) + build_limits = build_input.CanonicalInputLimitsV1( + *tuple(request.build_limits) + ) + runtime_binding = mpfi_runtime.MpfiRuntimeBindingV1( + *tuple(request.runtime_binding) + ) + if ( + build_sources.contents(mpfi_build.MPFI_FORMULA_SPEC_PATH_V1) + != job.formula_spec + ): + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.FORMULA_MISMATCH, + "job", + ) + if ( + hashlib.sha256(request.generated_formula).hexdigest() + != mpfi_build.MPFI_GENERATED_FORMULA_SHA256_V1 + ): + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.GENERATED_FORMULA_DRIFT, + "generated_formula", + ) + if len(job.encode()) > runtime_binding.profile.max_job_bytes: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.LIMIT_MISMATCH, + "runtime_binding", + ) + canonical_request = MpfiPipelineRequestV1( + source_closure.source_lock, + source_closure.admitted_sources, + build_sources, + request.generated_formula, + build_limits, + job, + runtime_binding, + ) + return _MpfiOperationSnapshotV1(canonical_request, source_closure) + except MpfiRequestErrorV1: + raise + except ( + provenance.ProvenanceErrorV1, + mpfi_build.MpfiBuildSourceErrorV1, + protocol.ProtocolErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + ) as error: + raise MpfiRequestErrorV1( + MpfiRequestErrorReasonV1.FOREIGN_SOURCE_CAPABILITY, + "request", + ) from error + + +@dataclass(frozen=True) +class MpfiComparatorPreimagesV1: + engine_release: bytes + upstream_source: bytes + arithmetic_input_set: bytes + wrapper_source: bytes + evaluator_source: bytes + build_identity: bytes + operation_allowlist: bytes + test_observation: bytes + legal_file_set: bytes + exclusions: bytes + + def __post_init__(self) -> None: + values = tuple(getattr(self, item.name) for item in fields(self)) + if any(type(value) is not bytes or not value for value in values): + raise TypeError("MPFI comparator preimages must be nonempty bytes") + if len(set(values)) != len(values): + raise TypeError("MPFI comparator preimages must be distinct") + + +@dataclass(frozen=True) +class MpfiDiagnosticComparatorV1: + preimages: MpfiComparatorPreimagesV1 + manifest: protocol.ContentResolvedComparatorManifestV2 + source_identity: bytes + build_source_identity: bytes + runtime_binding_identity: bytes + binary_sha256: bytes + rebuild_sha256s: tuple[bytes, bytes] + + def __post_init__(self) -> None: + if ( + type(self.manifest) is not protocol.ContentResolvedComparatorManifestV2 + or self.manifest.manifest.kind is not protocol.ComparatorKindV1.MPFI + or tuple(item.name for item in fields(self.manifest.manifest) if item.name != "kind") + != tuple(item.name for item in fields(self.preimages)) + ): + raise TypeError("MPFI comparator manifest/preimages drift") + _digest(self.source_identity, "source_identity") + _digest(self.build_source_identity, "build_source_identity") + _digest(self.runtime_binding_identity, "runtime_binding_identity") + _digest(self.binary_sha256, "binary_sha256") + if self.rebuild_sha256s != (self.binary_sha256, self.binary_sha256): + raise TypeError("MPFI comparator rebuild binding drift") + + @property + def identity(self) -> bytes: + return self.manifest.identity + + +@dataclass(frozen=True) +class _MpfiBuildCoordinatesV1: + """Private BUILD coordinates captured before comparator derivation.""" + + source_identity: bytes + build_source_identity: bytes + generated_formula_sha256: bytes + runtime_binding_identity: bytes + docker_capability: build_transport.DockerSupportedV1 + input_bundle: build_input.SealedInputV1 + binary_sha256: bytes + rebuild_sha256s: tuple[bytes, bytes] + processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ] + binaries: tuple[bytes, bytes] + + +@dataclass(frozen=True, init=False) +class MpfiDiagnosticBuildObservationV1: + source_identity: bytes + build_source_identity: bytes + generated_formula_sha256: bytes + runtime_binding_identity: bytes + docker_capability: build_transport.DockerSupportedV1 + input_bundle: build_input.SealedInputV1 + binary_sha256: bytes + rebuild_sha256s: tuple[bytes, bytes] + processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ] + binaries: tuple[bytes, bytes] + comparator: MpfiDiagnosticComparatorV1 + + def __new__(cls, *args: object, **kwargs: object) -> "MpfiDiagnosticBuildObservationV1": + if kwargs.get("_token") is not _BUILD_OBSERVATION_TOKEN: + raise TypeError("MpfiDiagnosticBuildObservationV1 is controller-derived") + return object.__new__(cls) + + def __init__( + self, + source_identity: bytes, + build_source_identity: bytes, + generated_formula_sha256: bytes, + runtime_binding_identity: bytes, + docker_capability: build_transport.DockerSupportedV1, + input_bundle: build_input.SealedInputV1, + binary_sha256: bytes, + rebuild_sha256s: tuple[bytes, bytes], + processes: tuple[ + build_transport.DockerBuildExitedV1, + build_transport.DockerBuildExitedV1, + ], + binaries: tuple[bytes, bytes], + comparator: MpfiDiagnosticComparatorV1, + *, + _token: object, + ) -> None: + if _token is not _BUILD_OBSERVATION_TOKEN: + raise TypeError("MpfiDiagnosticBuildObservationV1 is controller-derived") + for name, value in ( + ("source_identity", source_identity), + ("build_source_identity", build_source_identity), + ("generated_formula_sha256", generated_formula_sha256), + ("runtime_binding_identity", runtime_binding_identity), + ("binary_sha256", binary_sha256), + ): + _digest(value, name) + if type(docker_capability) is not build_transport.DockerSupportedV1: + raise TypeError("invalid MPFI Docker capability") + canonical_capability = build_transport.DockerSupportedV1( + *tuple(docker_capability) + ) + if tuple(canonical_capability) != tuple(docker_capability): + raise TypeError("MPFI Docker capability did not replay") + if not build_input.sealed_input_is_intact_v1(input_bundle): + raise TypeError("invalid MPFI sealed build bundle") + if ( + type(rebuild_sha256s) is not tuple + or rebuild_sha256s != (binary_sha256, binary_sha256) + or type(processes) is not tuple + or len(processes) != 2 + or any( + type(item) is not build_transport.DockerBuildExitedV1 + or item.returncode != 0 + for item in processes + ) + or type(binaries) is not tuple + or len(binaries) != 2 + or binaries[0] != binaries[1] + or tuple(hashlib.sha256(item).digest() for item in binaries) + != rebuild_sha256s + or type(comparator) is not MpfiDiagnosticComparatorV1 + or comparator.source_identity != source_identity + or comparator.build_source_identity != build_source_identity + or comparator.runtime_binding_identity != runtime_binding_identity + or comparator.binary_sha256 != binary_sha256 + ): + raise TypeError("MPFI diagnostic BUILD observation drift") + for process in processes: + transfer = process.input_transfer + if ( + type(transfer) is not build_transport.BuildInputTransferV1 + or transfer.bundle_identity != input_bundle.binding_identity + or transfer.expected_length != input_bundle.length + or transfer.expected_sha256 != input_bundle.sha256 + or transfer.written_length != input_bundle.length + or transfer.written_sha256 != input_bundle.sha256 + ): + raise TypeError("MPFI BUILD transfer did not consume sealed input") + for name, value in ( + ("source_identity", source_identity), + ("build_source_identity", build_source_identity), + ("generated_formula_sha256", generated_formula_sha256), + ("runtime_binding_identity", runtime_binding_identity), + ("docker_capability", docker_capability), + ("input_bundle", input_bundle), + ("binary_sha256", binary_sha256), + ("rebuild_sha256s", rebuild_sha256s), + ("processes", processes), + ("binaries", binaries), + ("comparator", comparator), + ): + object.__setattr__(self, name, value) + + +def _source_identity_v1(snapshot: _MpfiOperationSnapshotV1) -> bytes: + return mpfi_build.source_identity_v1(snapshot.source_closure) + + +def _build_identity_v1( + snapshot: _MpfiOperationSnapshotV1, + build: _MpfiBuildCoordinatesV1 | MpfiDiagnosticBuildObservationV1, +) -> bytes: + policy_identity = build_transport.transport_policy_identity_v1( + build.docker_capability.policy + ) + process_bytes = tuple( + build_transport.build_process_bytes_v1(item) for item in build.processes + ) + runtime_identity = mpfi_runtime.runtime_binding_identity_v1( + snapshot.request.runtime_binding + ) + if type(runtime_identity) is not bytes: + raise TypeError("runtime binding identity did not replay") + return _identity( + _BUILD_ID_LABEL_V1, + ( + _source_identity_v1(snapshot), + snapshot.request.build_sources.identity, + hashlib.sha256(snapshot.request.generated_formula).digest(), + runtime_identity, + policy_identity, + build_transport.docker_capability_identity_v1(build.docker_capability), + build.input_bundle.binding_identity, + build.input_bundle.sha256, + build.input_bundle.length.to_bytes(8, "big"), + *process_bytes, + build.binary_sha256, + ), + ) + + +def _derive_comparator_v1( + snapshot: _MpfiOperationSnapshotV1, + build: _MpfiBuildCoordinatesV1 | MpfiDiagnosticBuildObservationV1, + build_identity: bytes, +) -> MpfiDiagnosticComparatorV1: + files = snapshot.request.build_sources.files + wrapper_paths = frozenset( + f"proof/region/v1/mpfi/evaluator/{name}" + for name in ("formula.h", "wire.h", "interval.h", "region.h", "hash.h") + ) + wrapper = tuple( + item + for item in files + if item.path in wrapper_paths + ) + evaluator = tuple( + item + for item in files + if item.path.startswith("proof/region/v1/mpfi/evaluator/") + and item.path not in wrapper_paths + ) + operation = snapshot.request.build_sources.contents( + "proof/region/v1/mpfi/operations.py" + ) + source_identity = _source_identity_v1(snapshot) + runtime_identity = mpfi_runtime.runtime_binding_identity_v1( + snapshot.request.runtime_binding + ) + if type(runtime_identity) is not bytes: + raise TypeError("runtime binding identity did not replay") + preimages = MpfiComparatorPreimagesV1( + _preimage("engine-release", (snapshot.request.source_lock.encode(),)), + _preimage("upstream-source", (source_identity,)), + _preimage( + "arithmetic-input-set", + ( + snapshot.request.job.formula_spec, + snapshot.request.generated_formula, + runtime_identity, + ), + ), + _preimage( + "wrapper-source", + tuple(item.contents for item in wrapper), + ), + _preimage( + "evaluator-source", + tuple(item.contents for item in evaluator), + ), + _preimage("build-identity", (build_identity,)), + _preimage("operation-allowlist", (operation,)), + _preimage( + "test-observation", + ( + snapshot.request.build_sources.contents(mpfi_build.MPFI_BUILD_RECIPE_PATH_V1), + snapshot.request.build_sources.contents(mpfi_build.MPFI_BUILD_INNER_RECIPE_PATH_V1), + *(build_transport.build_process_bytes_v1(item) for item in build.processes), + ), + ), + _preimage( + "legal-file-set", + tuple(lock.encode() for lock in snapshot.request.source_lock.sources), + ), + _preimage( + "exclusions", + ( + b"provenance-only", + b"no-semantic-verifier", + b"no-publisher-origin-claim", + ), + ), + ) + coordinates = tuple( + hashlib.sha256(getattr(preimages, field.name)).digest() + for field in fields(preimages) + ) + manifest = protocol.ContentResolvedComparatorManifestV2.admit( + protocol.ComparatorManifestV2(protocol.ComparatorKindV1.MPFI, *coordinates), + { + coordinate: getattr(preimages, field.name) + for coordinate, field in zip(coordinates, fields(preimages), strict=True) + }.get, + ) + return MpfiDiagnosticComparatorV1( + preimages, + manifest, + source_identity, + snapshot.request.build_sources.identity, + runtime_identity, + build.binary_sha256, + build.rebuild_sha256s, + ) + + +@dataclass(frozen=True, init=False) +class MpfiEvaluatorReplayV1: + request: MpfiPipelineRequestV1 + build: MpfiDiagnosticBuildObservationV1 + invocation: executor.ExecutionRequestV1 + platform: executor.SupportedV1 + process: executor.CompletedV1 + transcript: protocol.DecisionTranscriptV1 + run_claim: protocol.RunClaimV1 + source_identity: bytes + build_identity: bytes + run_identity: bytes + identity: bytes + + def __new__(cls, *args: object, **kwargs: object) -> "MpfiEvaluatorReplayV1": + if kwargs.get("_token") is not _EVIDENCE_TOKEN: + raise TypeError("MpfiEvaluatorReplayV1 is controller-derived") + return object.__new__(cls) + + def __init__( + self, + request: MpfiPipelineRequestV1, + build: MpfiDiagnosticBuildObservationV1, + invocation: executor.ExecutionRequestV1, + platform: executor.SupportedV1, + process: executor.CompletedV1, + transcript: protocol.DecisionTranscriptV1, + run_claim: protocol.RunClaimV1, + source_identity: bytes, + build_identity: bytes, + run_identity: bytes, + identity: bytes, + *, + _token: object, + ) -> None: + if _token is not _EVIDENCE_TOKEN: + raise TypeError("MpfiEvaluatorReplayV1 is controller-derived") + for name, value in ( + ("request", request), + ("build", build), + ("invocation", invocation), + ("platform", platform), + ("process", process), + ("transcript", transcript), + ("run_claim", run_claim), + ("source_identity", source_identity), + ("build_identity", build_identity), + ("run_identity", run_identity), + ("identity", identity), + ): + object.__setattr__(self, name, value) + + +def _run_identity_v1( + snapshot: _MpfiOperationSnapshotV1, + evidence: MpfiEvaluatorReplayV1, +) -> bytes: + invocation_identity = executor.invocation_identity_v1(evidence.invocation) + platform_identity = executor.platform_identity_v1(evidence.platform) + if type(invocation_identity) is not bytes or type(platform_identity) is not bytes: + raise TypeError("MPFI execution identity replay failed") + return _identity( + _RUN_ID_LABEL_V1, + ( + evidence.build_identity, + evidence.build.comparator.identity, + snapshot.request.job.identity, + invocation_identity, + platform_identity, + evidence.process.binary_sha256, + evidence.process.stdout, + evidence.process.stderr, + evidence.transcript.identity, + evidence.run_claim.identity, + ), + ) + + +def replay_mpfi_evidence_is_well_bound_v1(value: object) -> bool: + if type(value) is not MpfiEvaluatorReplayV1: + return False + try: + snapshot = _snapshot_request_v1(value.request) + if value.build.source_identity != _source_identity_v1(snapshot): + return False + runtime_identity = mpfi_runtime.runtime_binding_identity_v1( + snapshot.request.runtime_binding + ) + if ( + type(runtime_identity) is not bytes + or value.build.build_source_identity + != snapshot.request.build_sources.identity + or value.build.generated_formula_sha256 + != hashlib.sha256(snapshot.request.generated_formula).digest() + or value.build.runtime_binding_identity != runtime_identity + ): + return False + if not mpfi_build.mpfi_build_input_is_bound_from_snapshot_v1( + snapshot.source_closure, + snapshot.request.build_sources, + snapshot.request.generated_formula, + snapshot.request.build_limits, + value.build.input_bundle, + value.build.docker_capability.policy, + ): + return False + if ( + type(value.build.processes) is not tuple + or len(value.build.processes) != 2 + or any( + type(process) is not build_transport.DockerBuildExitedV1 + or process.returncode != 0 + or process.input_transfer.bundle_identity + != value.build.input_bundle.binding_identity + or process.input_transfer.expected_length + != value.build.input_bundle.length + or process.input_transfer.expected_sha256 + != value.build.input_bundle.sha256 + or process.input_transfer.written_length + != value.build.input_bundle.length + or process.input_transfer.written_sha256 + != value.build.input_bundle.sha256 + for process in value.build.processes + ) + or type(value.build.binaries) is not tuple + or len(value.build.binaries) != 2 + or value.build.binaries[0] != value.build.binaries[1] + or tuple(hashlib.sha256(item).digest() for item in value.build.binaries) + != value.build.rebuild_sha256s + or value.build.binary_sha256 != value.build.rebuild_sha256s[0] + ): + return False + expected_build_identity = _build_identity_v1(snapshot, value.build) + if _preimage("build-identity", (expected_build_identity,)) != ( + value.build.comparator.preimages.build_identity + ): + return False + expected_comparator = _derive_comparator_v1( + snapshot, + value.build, + expected_build_identity, + ) + if expected_comparator != value.build.comparator: + return False + if value.invocation.executable is not value.build.binaries[0]: + return False + if value.process.binary_sha256 != value.build.binary_sha256: + return False + if value.transcript.encode() != value.process.stdout: + return False + if ( + value.transcript.job_identity != snapshot.request.job.identity + or value.transcript.comparator_identity != value.build.comparator.identity + or value.process.stderr + or not executor.result_matches_request_v1(value.process, value.invocation) + ): + return False + invocation_identity = executor.invocation_identity_v1(value.invocation) + platform_identity = executor.platform_identity_v1(value.platform) + if type(invocation_identity) is not bytes or type(platform_identity) is not bytes: + return False + expected_claim = protocol.RunClaimV1.for_transcript( + snapshot.request.job, + value.build.comparator.manifest, + value.transcript, + value.build.binary_sha256, + invocation_identity, + platform_identity, + ) + if expected_claim != value.run_claim: + return False + expected_run = _run_identity_v1(snapshot, value) + expected_evidence = _identity( + _EVIDENCE_ID_LABEL_V1, + (_source_identity_v1(snapshot), expected_build_identity, expected_run), + ) + return ( + value.source_identity == _source_identity_v1(snapshot) + and value.build_identity == expected_build_identity + and value.run_identity == expected_run + and value.identity == expected_evidence + ) + except Exception: + return False + + +@dataclass(frozen=True, init=False) +class MpfiSourceBoundEvaluatorReceiptV1: + claim: protocol.EvaluatorProvenanceClaimV1 + evidence: MpfiEvaluatorReplayV1 + + def __new__(cls, *args: object, **kwargs: object) -> "MpfiSourceBoundEvaluatorReceiptV1": + if kwargs.get("_token") is not _RECEIPT_TOKEN: + raise TypeError("MpfiSourceBoundEvaluatorReceiptV1 is controller-sealed") + return object.__new__(cls) + + def __init__( + self, + claim: protocol.EvaluatorProvenanceClaimV1, + evidence: MpfiEvaluatorReplayV1, + *, + _token: object, + ) -> None: + if ( + _token is not _RECEIPT_TOKEN + or type(claim) is not protocol.EvaluatorProvenanceClaimV1 + or type(evidence) is not MpfiEvaluatorReplayV1 + or not replay_mpfi_evidence_is_well_bound_v1(evidence) + ): + raise TypeError("MPFI receipt evidence is not replayable") + expected_policy = mpfi_source_bound_policy_identity_v1( + evidence.build.docker_capability, + evidence.request.runtime_binding, + ) + if ( + claim.provenance_policy_identity != expected_policy + or claim.run_claim_identity != evidence.run_claim.identity + or claim.replay_evidence_identity != evidence.identity + ): + raise TypeError("MPFI provenance claim does not bind evidence") + object.__setattr__(self, "claim", claim) + object.__setattr__(self, "evidence", evidence) + + @property + def transcript(self) -> protocol.DecisionTranscriptV1: + return self.evidence.transcript + + @property + def comparator(self) -> MpfiDiagnosticComparatorV1: + return self.evidence.build.comparator + + @property + def executable(self) -> bytes: + return self.evidence.build.binaries[0] + + @property + def identity(self) -> bytes: + return _identity( + b"labcolors.proof-region.mpfi-receipt.v1\0", + (self.claim.provenance_policy_identity, self.evidence.identity), + ) + + +def mpfi_source_bound_policy_identity_v1( + capability: build_transport.DockerSupportedV1, + runtime_binding: mpfi_runtime.MpfiRuntimeBindingV1, +) -> bytes: + docker_identity = build_transport.docker_capability_identity_v1(capability) + binding_identity = mpfi_runtime.runtime_binding_identity_v1(runtime_binding) + if type(binding_identity) is not bytes: + raise TypeError("runtime binding did not replay") + return _identity( + _POLICY_ID_LABEL_V1, + ( + docker_identity, + binding_identity, + executor.SANDBOX_POLICY_RELEASE_V1.encode("ascii"), + b"authority=one-shot-native-mpfi-controller", + b"claim=provenance-only-no-semantic-verifier", + b"trust=unsealed-linux-x64-docker-host", + ), + ) + + +class MpfiSourceBoundFailureReasonV1(StrEnum): + CONTROLLER_CONSUMED = "controller_consumed" + CONTROLLER_PROCESS_CHANGED = "controller_process_changed" + REQUEST_REJECTED = "request_rejected" + OBSERVER_PLACEMENT_FAILED = "observer_placement_failed" + BUILD_FAILED = "build_failed" + RUN_FAILED = "run_failed" + REPLAY_BINDING_FAILED = "replay_binding_failed" + + +@dataclass(frozen=True) +class MpfiSourceBoundRejectedV1: + reason: MpfiSourceBoundFailureReasonV1 + detail: str + + def __post_init__(self) -> None: + if type(self.reason) is not MpfiSourceBoundFailureReasonV1: + raise TypeError("invalid MPFI source-bound failure reason") + if ( + type(self.detail) is not str + or not self.detail + or len(self.detail) > _MPFI_FAILURE_DETAIL_LIMIT_V1 + ): + raise TypeError("invalid MPFI source-bound failure detail") + + +MpfiSourceBoundResultV1: TypeAlias = ( + MpfiSourceBoundEvaluatorReceiptV1 + | MpfiSourceBoundRejectedV1 + | build_transport.BuildRejectedV1 + | build_transport.TwoBuildObservationV1 + | executor.ExecutionResultV1 +) + + +class MpfiSourceBoundControllerV1: + """One-shot authority for the MPFI source → BUILD → RUN chain.""" + + def __init__(self, docker_path: Path, cgroup_parent: Path) -> None: + if ( + not isinstance(docker_path, Path) + or not docker_path.is_absolute() + or not isinstance(cgroup_parent, Path) + or not cgroup_parent.is_absolute() + ): + raise TypeError("controller paths must be absolute Path values") + self._docker_path = docker_path + self._cgroup_parent = cgroup_parent + self._owner_pid = os.getpid() + self._consumed = False + self._lock = threading.Lock() + + def _consume(self) -> MpfiSourceBoundRejectedV1 | None: + if os.getpid() != self._owner_pid: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.CONTROLLER_PROCESS_CHANGED, + "controller authority cannot cross a process boundary", + ) + with self._lock: + if self._consumed: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.CONTROLLER_CONSUMED, + "controller authority is one-shot", + ) + self._consumed = True + return None + + def execute(self, request: MpfiPipelineRequestV1) -> MpfiSourceBoundResultV1: + consumed = self._consume() + if consumed is not None: + return consumed + try: + snapshot = _snapshot_request_v1(request) + bundle = mpfi_build.seal_mpfi_build_input_from_snapshot_v1( + snapshot.source_closure, + snapshot.request.build_sources, + snapshot.request.generated_formula, + snapshot.request.build_limits, + ) + except Exception as error: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.REQUEST_REJECTED, + _failure_detail_v1(error), + ) + backend = build_transport.NativeDockerBuildBackendV1( + self._docker_path, + mpfi_build.MPFI_BUILD_TRANSPORT_POLICY_V1, + ) + if type(backend) is not _NATIVE_BUILD_BACKEND_TYPE: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + "native MPFI build backend authority changed", + ) + transport = build_transport.ControlledBuildTransportV1( + policy=mpfi_build.MPFI_BUILD_TRANSPORT_POLICY_V1, + backend=backend, + ) + capability = transport.probe() + if type(capability) is not build_transport.DockerSupportedV1: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.BUILD_FAILED, + _failure_detail_v1(capability, diagnostic_repr=True), + ) + built = transport.build( + capability, + bundle, + snapshot.request.runtime_binding.limits.max_executable_bytes, + input_admission=lambda value: mpfi_build.mpfi_build_input_is_bound_from_snapshot_v1( + snapshot.source_closure, + snapshot.request.build_sources, + snapshot.request.generated_formula, + snapshot.request.build_limits, + value, + capability.policy, + ), + output_admission=_static_binary_is_admitted_v1, + ) + if type(built) is not build_transport.TwoBuildObservationV1: + return built + if built.relation is not build_transport.BuildByteRelationV1.IDENTICAL: + return built + binary = built.outputs[0] + try: + coordinates = _make_build_coordinates_v1( + snapshot, + capability, + bundle, + built, + binary, + ) + build_identity = _build_identity_v1(snapshot, coordinates) + comparator = _derive_comparator_v1(snapshot, coordinates, build_identity) + build = _make_build_observation_v1(coordinates, comparator) + except Exception as error: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + _failure_detail_v1(error), + ) + try: + executor.enter_observer_cgroup_v1(self._cgroup_parent) + except Exception as error: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.OBSERVER_PLACEMENT_FAILED, + _failure_detail_v1(error), + ) + run_backend = _NATIVE_RUN_BACKEND_TYPE(self._cgroup_parent) + if type(run_backend) is not _NATIVE_RUN_BACKEND_TYPE: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + "native MPFI run backend authority changed", + ) + observed = executor.ControlledExecutorV1(run_backend) + platform_value = observed.probe() + if type(platform_value) is not executor.SupportedV1: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.RUN_FAILED, + _failure_detail_v1(platform_value, diagnostic_repr=True), + ) + try: + invocation = executor.ExecutionRequestV1( + executable=binary, + argv=( + b"mpfi-evaluator", + b"--manifest-identity", + build.comparator.identity.hex().encode("ascii"), + b"--job", + b"/dev/stdin", + ), + environment=((b"LC_ALL", b"C"), (b"TZ", b"UTC")), + cwd=b"/", + stdin=snapshot.request.job.encode(), + umask=0o077, + limits=snapshot.request.runtime_binding.limits, + ) + except Exception as error: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.RUN_FAILED, + _failure_detail_v1(error), + ) + process = observed.execute(invocation, platform_value) + if ( + type(process) is not executor.CompletedV1 + or process.stderr + or process.binary_sha256 != build.binary_sha256 + or not executor.result_matches_request_v1(process, invocation) + ): + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.RUN_FAILED, + _failure_detail_v1(process, diagnostic_repr=True), + ) + try: + transcript = protocol.DecisionTranscriptV1.parse(process.stdout) + invocation_identity = executor.invocation_identity_v1(invocation) + platform_identity = executor.platform_identity_v1(platform_value) + if type(invocation_identity) is not bytes or type(platform_identity) is not bytes: + raise TypeError("execution identity rejected") + run_claim = protocol.RunClaimV1.for_transcript( + snapshot.request.job, + build.comparator.manifest, + transcript, + build.binary_sha256, + invocation_identity, + platform_identity, + ) + provisional = MpfiEvaluatorReplayV1( + snapshot.request, + build, + invocation, + platform_value, + process, + transcript, + run_claim, + _source_identity_v1(snapshot), + build_identity, + b"\0" * 32, + b"\0" * 32, + _token=_EVIDENCE_TOKEN, + ) + run_identity = _run_identity_v1(snapshot, provisional) + evidence_identity = _identity( + _EVIDENCE_ID_LABEL_V1, + (_source_identity_v1(snapshot), build_identity, run_identity), + ) + evidence = MpfiEvaluatorReplayV1( + snapshot.request, + build, + invocation, + platform_value, + process, + transcript, + run_claim, + _source_identity_v1(snapshot), + build_identity, + run_identity, + evidence_identity, + _token=_EVIDENCE_TOKEN, + ) + if not replay_mpfi_evidence_is_well_bound_v1(evidence): + raise TypeError("MPFI replay did not close") + claim = protocol.EvaluatorProvenanceClaimV1( + mpfi_source_bound_policy_identity_v1( + capability, + snapshot.request.runtime_binding, + ), + run_claim.identity, + evidence.identity, + ) + return MpfiSourceBoundEvaluatorReceiptV1( + claim, + evidence, + _token=_RECEIPT_TOKEN, + ) + except Exception as error: + return MpfiSourceBoundRejectedV1( + MpfiSourceBoundFailureReasonV1.REPLAY_BINDING_FAILED, + _failure_detail_v1(error), + ) + + +def _static_binary_is_admitted_v1(value: bytes) -> bool: + try: + executor.require_static_x86_64_elf_v1(value) + except executor.ExecutionRequestErrorV1: + return False + return True + + +def _make_build_coordinates_v1( + snapshot: _MpfiOperationSnapshotV1, + capability: build_transport.DockerSupportedV1, + bundle: build_input.SealedInputV1, + built: build_transport.TwoBuildObservationV1, + binary: bytes, +) -> _MpfiBuildCoordinatesV1: + binary_sha256 = hashlib.sha256(binary).digest() + runtime_identity = mpfi_runtime.runtime_binding_identity_v1( + snapshot.request.runtime_binding + ) + if type(runtime_identity) is not bytes: + raise TypeError("runtime binding identity did not replay") + return _MpfiBuildCoordinatesV1( + _source_identity_v1(snapshot), + snapshot.request.build_sources.identity, + hashlib.sha256(snapshot.request.generated_formula).digest(), + runtime_identity, + capability, + bundle, + binary_sha256, + (binary_sha256, binary_sha256), + built.processes, + built.outputs, + ) + + +def _make_build_observation_v1( + coordinates: _MpfiBuildCoordinatesV1, + comparator: MpfiDiagnosticComparatorV1, +) -> MpfiDiagnosticBuildObservationV1: + if type(coordinates) is not _MpfiBuildCoordinatesV1: + raise TypeError("coordinates must be _MpfiBuildCoordinatesV1") + return MpfiDiagnosticBuildObservationV1( + coordinates.source_identity, + coordinates.build_source_identity, + coordinates.generated_formula_sha256, + coordinates.runtime_binding_identity, + coordinates.docker_capability, + coordinates.input_bundle, + coordinates.binary_sha256, + coordinates.rebuild_sha256s, + coordinates.processes, + coordinates.binaries, + comparator, + _token=_BUILD_OBSERVATION_TOKEN, + ) diff --git a/proof/region/v1/mpfi/runtime.py b/proof/region/v1/mpfi/runtime.py new file mode 100644 index 00000000..badcf64a --- /dev/null +++ b/proof/region/v1/mpfi/runtime.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Каноническая связь MPFI runtime-профиля с executor limits. + +Профиль описывает границу MPFI wire/runtime. Executor остаётся общим leaf и +не знает о MPFI; эта lane-specific binding не даёт контроллеру случайно +запустить тот же бинарь с другими limits и назвать его тем же профилем. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from enum import StrEnum +from typing import TypeAlias + +import executor + + +MPFI_RUNTIME_PROFILE_ID_V1 = "LC-MPFI-RUNTIME-V1" + +# Эти значения уже являются опубликованной M1.5 operational boundary в +# PROTOCOL.md и wire.h. Здесь они собраны в одном типизированном источнике, +# чтобы build/run receipt связывал их с executor, а не копировал литералы. +MPFI_MAX_JOB_BYTES_V1 = 16 * 1024 * 1024 +MPFI_MAX_OUTPUT_BYTES_V1 = 16 * 1024 * 1024 +MPFI_MAX_PRECISION_BITS_V1 = 4096 +MPFI_MAX_POLICY_RUNGS_V1 = 32 +MPFI_MAX_KNOTS_V1 = 1024 + +_PROFILE_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-runtime-profile.v1\0" +_BINDING_ID_LABEL_V1 = b"labcolors.proof-region.mpfi-runtime-binding.v1\0" +_EXECUTION_LIMITS_ID_LABEL_V1 = b"labcolors.proof-region.execution-limits.v1\0" +_PROFILE_VALUES_V1 = ( + MPFI_MAX_JOB_BYTES_V1, + MPFI_MAX_OUTPUT_BYTES_V1, + MPFI_MAX_PRECISION_BITS_V1, + MPFI_MAX_POLICY_RUNGS_V1, + MPFI_MAX_KNOTS_V1, +) + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join( + len(chunk).to_bytes(8, "big") + chunk + for chunk in chunks + ) + return hashlib.sha256(label + len(payload).to_bytes(8, "big") + payload).digest() + + +class MpfiRuntimeProfileReasonV1(StrEnum): + WRONG_TYPE = "wrong_type" + NONCANONICAL = "noncanonical" + LIMIT_MISMATCH = "limit_mismatch" + + +@dataclass(frozen=True) +class MpfiRuntimeIdentityRejectedV1: + reason: MpfiRuntimeProfileReasonV1 + + def __post_init__(self) -> None: + if type(self.reason) is not MpfiRuntimeProfileReasonV1: + raise TypeError("reason must be MpfiRuntimeProfileReasonV1") + + +class MpfiRuntimeProfileV1(tuple): + """Один неизменяемый exact-профиль MPFI runtime V1.""" + + __slots__ = () + + def __new__( + cls, + max_job_bytes: int, + max_output_bytes: int, + max_precision_bits: int, + max_policy_rungs: int, + max_knots: int, + ) -> MpfiRuntimeProfileV1: + values = ( + max_job_bytes, + max_output_bytes, + max_precision_bits, + max_policy_rungs, + max_knots, + ) + if any(type(value) is not int for value in values): + raise TypeError("MPFI runtime profile coordinates must be exact ints") + if values != _PROFILE_VALUES_V1: + raise ValueError("unknown or noncanonical MPFI runtime profile") + return tuple.__new__(cls, values) + + max_job_bytes = property(lambda self: self[0]) + max_output_bytes = property(lambda self: self[1]) + max_precision_bits = property(lambda self: self[2]) + max_policy_rungs = property(lambda self: self[3]) + max_knots = property(lambda self: self[4]) + + +def mpfi_runtime_profile_v1() -> MpfiRuntimeProfileV1: + return MpfiRuntimeProfileV1(*_PROFILE_VALUES_V1) + + +class MpfiRuntimeBindingV1(tuple): + """Профиль и exact immutable executor limits одного RUN.""" + + __slots__ = () + + def __new__( + cls, + profile: MpfiRuntimeProfileV1, + limits: executor.ExecutionLimitsV1, + ) -> MpfiRuntimeBindingV1: + if type(profile) is not MpfiRuntimeProfileV1: + raise TypeError("profile must be MpfiRuntimeProfileV1") + if type(limits) is not executor.ExecutionLimitsV1: + raise TypeError("limits must be ExecutionLimitsV1") + canonical_profile = MpfiRuntimeProfileV1(*tuple(profile)) + canonical_limits = executor.ExecutionLimitsV1(*tuple(limits)) + if tuple(canonical_profile) != tuple(profile) or tuple(canonical_limits) != tuple(limits): + raise ValueError("runtime binding coordinates are not canonical") + # Пределы job и transcript — две доступные процессу координаты профиля. + # Остальные executor limits остаются явными координатами той же связи, + # а не молча выводятся из MPFI-семантики. + if ( + canonical_limits.max_stdin_bytes != canonical_profile.max_job_bytes + or canonical_limits.max_stdout_bytes != canonical_profile.max_output_bytes + ): + raise ValueError("executor limits do not implement MPFI profile") + return tuple.__new__(cls, (canonical_profile, canonical_limits)) + + profile = property(lambda self: self[0]) + limits = property(lambda self: self[1]) + + +MpfiRuntimeIdentityResultV1: TypeAlias = bytes | MpfiRuntimeIdentityRejectedV1 + + +def runtime_profile_identity_v1( + value: object, +) -> MpfiRuntimeIdentityResultV1: + if type(value) is not MpfiRuntimeProfileV1: + return MpfiRuntimeIdentityRejectedV1(MpfiRuntimeProfileReasonV1.WRONG_TYPE) + try: + profile = MpfiRuntimeProfileV1(*tuple(value)) + except (TypeError, ValueError, OverflowError): + return MpfiRuntimeIdentityRejectedV1(MpfiRuntimeProfileReasonV1.NONCANONICAL) + return _identity( + _PROFILE_ID_LABEL_V1, + ( + MPFI_RUNTIME_PROFILE_ID_V1.encode("ascii"), + *(item.to_bytes(8, "big") for item in profile), + ), + ) + + +def runtime_binding_identity_v1( + value: object, +) -> MpfiRuntimeIdentityResultV1: + if type(value) is not MpfiRuntimeBindingV1: + return MpfiRuntimeIdentityRejectedV1(MpfiRuntimeProfileReasonV1.WRONG_TYPE) + try: + binding = MpfiRuntimeBindingV1(*tuple(value)) + profile_identity = runtime_profile_identity_v1(binding.profile) + limits_identity = _identity( + _EXECUTION_LIMITS_ID_LABEL_V1, + tuple(item.to_bytes(8, "big") for item in binding.limits), + ) + except (TypeError, ValueError, OverflowError): + return MpfiRuntimeIdentityRejectedV1(MpfiRuntimeProfileReasonV1.LIMIT_MISMATCH) + if ( + type(profile_identity) is not bytes + or type(limits_identity) is not bytes + ): + return MpfiRuntimeIdentityRejectedV1(MpfiRuntimeProfileReasonV1.LIMIT_MISMATCH) + return _identity( + _BINDING_ID_LABEL_V1, + ( + profile_identity, + limits_identity, + executor.SANDBOX_POLICY_RELEASE_V1.encode("ascii"), + ), + ) diff --git a/proof/region/v1/mpfi/tests/gate.py b/proof/region/v1/mpfi/tests/gate.py new file mode 100644 index 00000000..ff319b81 --- /dev/null +++ b/proof/region/v1/mpfi/tests/gate.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Запускает обязательный MPFI source-contract gate с anti-vacuum inventory.""" + +from __future__ import annotations + +import hashlib +import sys +import unittest +from collections.abc import Iterator +from pathlib import Path + +TEST_DIRECTORY = Path(__file__).resolve().parent +from skip_contract import NATIVE_RECEIPT_SKIP_REASON_V1 + + +EXPECTED_TEST_COUNT = 29 +EXPECTED_TEST_INVENTORY_SHA256 = "8d14a07df84fd35968284422e5f5d15d97dc02fefc53d25b7f31fc9a1b175b88" +_RUNTIME_REASON = "set LABCOLORS_MPFI_EVALUATOR to the controlled C17 binary" +EXPECTED_SKIPS = frozenset( + { + ( + "test_evaluator_source.RuntimeTests.test_frozen_fixture_produces_a_canonical_transcript", + _RUNTIME_REASON, + ), + ( + "test_evaluator_source.RuntimeTests.test_black_exact_zero_emits_the_canonical_trace_witness", + _RUNTIME_REASON, + ), + ( + "test_evaluator_source.RuntimeTests.test_input_limit_is_enforced_before_wire_parse", + _RUNTIME_REASON, + ), + ( + ( + "test_receipt.NativeMpfiSourceBoundReceiptIntegrationTests." + "test_real_build_run_and_seal_are_one_source_bound_controller_execution" + ), + NATIVE_RECEIPT_SKIP_REASON_V1, + ), + } +) + + +def _iter_tests(suite: unittest.TestSuite) -> Iterator[unittest.TestCase]: + for item in suite: + if isinstance(item, unittest.TestSuite): + yield from _iter_tests(item) + elif isinstance(item, unittest.TestCase): + yield item + else: + raise TypeError("suite contains a non-test object") + + +def _inventory_digest(test_ids: tuple[str, ...]) -> str: + preimage = b"".join(test_id.encode("utf-8") + b"\n" for test_id in sorted(test_ids)) + return hashlib.sha256(preimage).hexdigest() + + +def run_gate() -> int: + suite = unittest.defaultTestLoader.discover( + str(TEST_DIRECTORY), + pattern="test_*.py", + ) + tests = tuple(_iter_tests(suite)) + test_ids = tuple(test.id() for test in tests) + actual_digest = _inventory_digest(test_ids) + if ( + not tests + or len(test_ids) != EXPECTED_TEST_COUNT + or len(set(test_ids)) != len(test_ids) + or actual_digest != EXPECTED_TEST_INVENTORY_SHA256 + ): + print( + "MPFI source gate inventory drift: " + f"count={len(test_ids)} sha256={actual_digest} " + f"expected_count={EXPECTED_TEST_COUNT} " + f"expected_sha256={EXPECTED_TEST_INVENTORY_SHA256}", + file=sys.stderr, + ) + return 1 + result = unittest.TextTestRunner(verbosity=2).run(suite) + actual_skips = frozenset((test.id(), reason) for test, reason in result.skipped) + if actual_skips != EXPECTED_SKIPS: + print(f"unexpected skips: {sorted(actual_skips - EXPECTED_SKIPS)!r}", file=sys.stderr) + print(f"missing skips: {sorted(EXPECTED_SKIPS - actual_skips)!r}", file=sys.stderr) + return 1 + return int(bool(result.failures or result.errors)) + + +if __name__ == "__main__": + raise SystemExit(run_gate()) diff --git a/proof/region/v1/mpfi/tests/native_gate.py b/proof/region/v1/mpfi/tests/native_gate.py new file mode 100644 index 00000000..00d97527 --- /dev/null +++ b/proof/region/v1/mpfi/tests/native_gate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Run the MPFI source-bound receipt integration without skip allowances.""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[5] +sys.path.insert(0, str(REPO)) +from proof.region.v1.arb.tests import gate +from proof.region.v1.mpfi.tests.test_receipt import ( + NativeMpfiSourceBoundReceiptIntegrationTests, +) + + +EXPECTED_INVENTORY_SHA256 = ( + "940e82f266c5b3d07962bcbb792c3e34d47f75b7f410c41896d062fa5b2f0f05" +) + + +def main() -> int: + if len(sys.argv) != 2 or sys.argv[1] != "receipt": + print("usage: native_gate.py receipt", file=sys.stderr) + return 64 + suite = unittest.defaultTestLoader.loadTestsFromTestCase( + NativeMpfiSourceBoundReceiptIntegrationTests, + ) + return gate.run_exact_suite_v1( + suite, + expected_inventory_sha256=EXPECTED_INVENTORY_SHA256, + expected_skips=frozenset(), + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/region/v1/mpfi/tests/skip_contract.py b/proof/region/v1/mpfi/tests/skip_contract.py new file mode 100644 index 00000000..179c3881 --- /dev/null +++ b/proof/region/v1/mpfi/tests/skip_contract.py @@ -0,0 +1,5 @@ +"""Shared exact skip text for the MPFI fast and native gates.""" + +NATIVE_RECEIPT_SKIP_REASON_V1 = ( + "requires Linux, Docker, a delegated cgroup, and all three exact MPFI source archives" +) diff --git a/proof/region/v1/mpfi/tests/test_evaluator_source.py b/proof/region/v1/mpfi/tests/test_evaluator_source.py new file mode 100644 index 00000000..23868ee7 --- /dev/null +++ b/proof/region/v1/mpfi/tests/test_evaluator_source.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 +"""Hostile source and runtime contract for the independent MPFI evaluator.""" + +from __future__ import annotations + +import hashlib +import ast +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +MPFI = Path(__file__).resolve().parents[1] +EVALUATOR = MPFI / "evaluator" +ENTRYPOINT = MPFI / "build.sh" +RECIPE = MPFI / "build-inner.sh" +REPO = MPFI.parents[3] +FORMULA = REPO / "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir" +GENERATOR = EVALUATOR / "formula.py" +sys.path.insert(0, str(MPFI.parent)) + +from mpfi import operations # noqa: E402 + + +def generate(source: bytes) -> subprocess.CompletedProcess[bytes]: + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "formula.lcir" + path.write_bytes(source) + return subprocess.run( + [sys.executable, str(GENERATOR), str(path)], + check=False, + capture_output=True, + stdin=subprocess.DEVNULL, + timeout=60, + env={ + "PATH": os.environ.get("PATH", ""), + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONHASHSEED": "0", + }, + ) + + +class FormulaSourceTests(unittest.TestCase): + def test_registered_formula_is_deterministic_and_has_no_binary64_path(self) -> None: + source = FORMULA.read_bytes() + first = generate(source) + second = generate(source) + + self.assertEqual(first.returncode, 0, first.stderr.decode()) + self.assertEqual(second.returncode, 0, second.stderr.decode()) + self.assertEqual(first.stdout, second.stdout) + self.assertEqual( + hashlib.sha256(first.stdout).hexdigest(), + "a8df7529261ba68e8fbf591cff283ec88a35cb98958b293bc7885d9fb4dd0fb6", + ) + self.assertIn(b"lc_mpfi_formula_point", first.stdout) + self.assertIn(b"lc_mpfi_formula_segment", first.stdout) + self.assertIn(b"lc_mpfi_formula_singleton", first.stdout) + self.assertNotIn(b"double", first.stdout) + + def test_formula_mutations_are_rejected_before_c_output(self) -> None: + source = FORMULA.read_bytes() + mutations = ( + (b"labcolors_exact_real_ssa 1", b"labcolors_exact_real_ssa 2"), + (b"operator add 2 real exact_x_plus_y", b"operator add 2 real exact_x_minus_y"), + (b"literal p1_7 3ffb333333333333", b"literal p1_7 3ffb333333333334"), + (b"rule boundary inclusive", b"rule boundary exclusive"), + (b"point_nodes 226", b"point_nodes 225"), + ) + for needle, replacement in mutations: + with self.subTest(replacement=replacement): + self.assertIn(needle, source) + result = generate(source.replace(needle, replacement, 1)) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + for mutant in (source + b"\n", source.replace(b"\n", b"\r\n", 1)): + result = generate(mutant) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + + def test_generator_does_not_import_the_protocol_or_the_other_engine(self) -> None: + source = GENERATOR.read_text(encoding="utf-8") + for forbidden in ( + "region_proof_protocol", + "controller", + "import arb", + "import flint", + "import numpy", + "import scipy", + ): + self.assertNotIn(forbidden, source) + + +class EvaluatorSourceTests(unittest.TestCase): + def test_public_build_entrypoint_cannot_be_bypassed_by_an_environment_sentinel(self) -> None: + entrypoint = ENTRYPOINT.read_text(encoding="utf-8") + recipe = RECIPE.read_text(encoding="utf-8") + + self.assertIn("/usr/bin/env -i", entrypoint) + self.assertIn("/usr/bin/readlink -f", entrypoint) + self.assertIn('inner="$script_dir/build-inner.sh"', entrypoint) + self.assertIn('/bin/sh "$inner"', entrypoint) + self.assertLess( + entrypoint.index("exec /usr/bin/env -i"), + entrypoint.index("script_path="), + ) + self.assertNotIn("LC_MPFI_BUILD_ENV_V1", entrypoint) + self.assertNotIn("LC_MPFI_BUILD_ENV_V1", recipe) + self.assertNotIn("/usr/bin/env -i", recipe) + + def test_source_owned_dispatch_cleans_child_environment(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + entrypoint = root / "build.sh" + inner = root / "build-inner.sh" + observed = root / "environment" + entrypoint.write_text(ENTRYPOINT.read_text(encoding="utf-8"), encoding="utf-8") + entrypoint.chmod(0o755) + inner.write_text( + "#!/bin/sh\n" + f"/usr/bin/env | /usr/bin/sort > '{observed}'\n", + encoding="utf-8", + ) + result = subprocess.run( + [str(entrypoint)], + check=False, + capture_output=True, + text=True, + env={ + "LC_MPFI_BUILD_ENV_V1": "1", + "MAKEFLAGS": "--jobserver-auth=spoof", + "PYTHONPATH": "/host-controlled", + "CONFIG_SITE": "/host-controlled/site", + "PATH": "/host-controlled/bin", + }, + ) + self.assertEqual(result.returncode, 0, result.stderr) + environment = observed.read_text(encoding="utf-8") + self.assertIn("PATH=/usr/bin:/bin\n", environment) + for forbidden in ( + "LC_MPFI_BUILD_ENV_V1=1", + "MAKEFLAGS=--jobserver-auth=spoof", + "PYTHONPATH=/host-controlled", + "CONFIG_SITE=/host-controlled/site", + "PATH=/host-controlled/bin", + ): + self.assertNotIn(forbidden, environment) + + def test_source_tree_is_complete_and_operation_closed(self) -> None: + required = ( + "main.c", + "wire.c", + "wire.h", + "hash.c", + "hash.h", + "interval.c", + "interval.h", + "region.c", + "region.h", + "formula.h", + "formula.py", + ) + for name in required: + with self.subTest(name=name): + self.assertTrue((EVALUATOR / name).is_file(), name) + self.assertEqual(operations.validate_sources(EVALUATOR), ()) + + def test_mutating_an_allowed_call_to_a_forbidden_operation_is_red(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + copy = Path(temporary) + for path in EVALUATOR.glob("*.c"): + (copy / path.name).write_text(path.read_text(encoding="utf-8"), encoding="utf-8") + for path in EVALUATOR.glob("*.h"): + (copy / path.name).write_text(path.read_text(encoding="utf-8"), encoding="utf-8") + interval = copy / "interval.c" + interval.write_text( + interval.read_text(encoding="utf-8").replace( + "mpfi_div(output, left, right)", + "mpfi_div_ext(output, left, right)", + 1, + ), + encoding="utf-8", + ) + errors = operations.validate_sources(copy) + self.assertTrue(any("forbidden operation mpfi_div_ext" in error for error in errors)) + + def test_forbidden_operation_aliases_and_asm_names_are_red(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + copy = Path(temporary) + for path in EVALUATOR.glob("*.c"): + (copy / path.name).write_text(path.read_text(encoding="utf-8"), encoding="utf-8") + for path in EVALUATOR.glob("*.h"): + (copy / path.name).write_text(path.read_text(encoding="utf-8"), encoding="utf-8") + interval = copy / "interval.c" + interval.write_text( + interval.read_text(encoding="utf-8") + + "\n#define hidden_division mpfi_div_ext\n" + + "static void hidden_call(void) { hidden_division; }\n" + + 'static const char *hidden_asm_name = "mpfi_div_ext";\n', + encoding="utf-8", + ) + errors = operations.validate_sources(copy) + self.assertTrue(any("forbidden operation mpfi_div_ext" in error for error in errors)) + + interval.write_text( + interval.read_text(encoding="utf-8") + + "\n#define LABCOLOR_MPFI_NAME(part) mpfi_ ## part\n" + + "static void hidden_token(mpfi_ptr output, mpfi_srcptr left, mpfi_srcptr right) {\n" + + " LABCOLOR_MPFI_NAME(div_ext)(output, left, right);\n" + + "}\n", + encoding="utf-8", + ) + errors = operations.validate_sources(copy) + self.assertTrue(any("token-pasting" in error for error in errors)) + + def test_linked_undefined_operation_symbols_are_closed(self) -> None: + errors = operations.validate_undefined_symbols( + " U _mpfi_div_ext\n" + " U mpfi_div\n" + " U __gmpz_init_set_ui\n" + ) + self.assertEqual(errors, ("forbidden undefined external symbol mpfi_div_ext",)) + self.assertEqual( + operations.validate_undefined_symbols(" U mpfi_formula_point\n"), + ("unexpected undefined external symbol mpfi_formula_point",), + ) + self.assertEqual( + operations.validate_undefined_symbols(" U __gmpz_not_allowed\n"), + ("unexpected undefined external symbol mpz_not_allowed",), + ) + self.assertEqual( + operations.validate_undefined_symbols( + " U mpfr_cmp3\n" + " U __gmpz_cmp\n" + ), + (), + ) + + def test_elf_absence_checks_are_fail_closed_on_inspection_error(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + start = recipe.index("require_absent_pattern()") + end = recipe.index("\nrequire_regular", start) + checker = recipe[start:end] + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "not-a-file" + directory.mkdir() + failed = subprocess.run( + ["/bin/sh", "-c", checker + "\nrequire_absent_pattern X \"$1\" message inspection\n", "sh", str(directory)], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(failed.returncode, 70) + + absent = Path(temporary) / "absent" + absent.write_text("nothing\n", encoding="utf-8") + passed = subprocess.run( + ["/bin/sh", "-c", checker + "\nrequire_absent_pattern X \"$1\" message inspection\n", "sh", str(absent)], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(passed.returncode, 0, passed.stderr) + + def test_build_recipe_is_closed_and_requires_clang_19(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + self.assertIn("/usr/bin/clang-19", recipe) + self.assertIn("clang version 19\\.", recipe) + self.assertIn("-fno-fast-math", recipe) + self.assertIn("-ffp-contract=off", recipe) + self.assertIn("-fno-lto", recipe) + self.assertIn( + "readonly mpfi_test_exclusions='^(tdiv_ext|texp10|trec_sqrt)$'", + recipe, + ) + self.assertIn( + '/usr/bin/make check -j1 TESTS="$mpfi_tests" ' + 'check_PROGRAMS="$mpfi_tests"', + recipe, + ) + self.assertIn("mpfi-evaluator-v1", recipe) + self.assertIn("readelf", recipe) + self.assertIn("--undefined-only", recipe) + self.assertIn("--undefined-symbols", recipe) + self.assertNotIn("gcc", recipe.lower()) + + def test_upstream_test_inventory_observation_is_fail_closed(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + self.assertIn( + 'if ! /usr/bin/make -pn > "$make_database"; then', + recipe, + ) + self.assertIn( + "' \"$make_database\"\n)", + recipe, + ) + self.assertNotIn( + "/usr/bin/make -pn \\\n | /usr/bin/awk", + recipe, + ) + + def test_compiler_admission_allows_a_stable_symlink_to_an_executable(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + start = recipe.index("require_executable()") + end = recipe.index("\nrequire_empty_directory", start) + checker = recipe[start:end] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + target = root / "clang-19" + target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + target.chmod(0o755) + link = root / "compiler" + link.symlink_to(target) + result = subprocess.run( + [ + "/bin/sh", + "-c", + checker + '\nrequire_executable "$1"\n', + "sh", + str(link), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_clang_admission_accepts_distribution_version_banner(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + start = recipe.index("require_clang_19()") + end = recipe.index("\nrequire_directory", start) + checker = recipe[start:end] + for banner in ("clang version 19.1.1", "Ubuntu clang version 19.1.1"): + with self.subTest(banner=banner), tempfile.TemporaryDirectory() as temporary: + compiler = Path(temporary) / "clang-19" + compiler.write_text( + f'#!/bin/sh\nprintf "%s\\n" "{banner}"\n', + encoding="utf-8", + ) + compiler.chmod(0o755) + result = subprocess.run( + [ + "/bin/sh", + "-c", + checker + '\nrequire_clang_19 "$1"\n', + "sh", + str(compiler), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_clang_admission_rejects_a_failed_version_probe(self) -> None: + recipe = RECIPE.read_text(encoding="utf-8") + start = recipe.index("require_clang_19()") + end = recipe.index("\nrequire_directory", start) + checker = recipe[start:end] + with tempfile.TemporaryDirectory() as temporary: + compiler = Path(temporary) / "clang-19" + compiler.write_text( + '#!/bin/sh\nprintf "%s\\n" "clang version 19.1.1"\nexit 1\n', + encoding="utf-8", + ) + compiler.chmod(0o755) + result = subprocess.run( + [ + "/bin/sh", + "-c", + checker + '\nrequire_clang_19 "$1"\n', + "sh", + str(compiler), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 67, result.stderr) + + with tempfile.TemporaryDirectory() as temporary: + compiler = Path(temporary) / "clang-18" + compiler.write_text( + '#!/bin/sh\nprintf "%s\\n" "clang version 18.1.8"\n', + encoding="utf-8", + ) + compiler.chmod(0o755) + result = subprocess.run( + [ + "/bin/sh", + "-c", + checker + '\nrequire_clang_19 "$1"\n', + "sh", + str(compiler), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 67) + + def test_runtime_profile_is_explicit_and_checked_before_allocation(self) -> None: + wire = (EVALUATOR / "wire.h").read_text(encoding="utf-8") + wire_source = (EVALUATOR / "wire.c").read_text(encoding="utf-8") + main = (EVALUATOR / "main.c").read_text(encoding="utf-8") + for name in ( + "LC_MPFI_MAX_JOB_BYTES_V1", + "LC_MPFI_MAX_OUTPUT_BYTES_V1", + "LC_MPFI_MAX_PRECISION_BITS_V1", + "LC_MPFI_MAX_POLICY_RUNGS_V1", + "LC_MPFI_MAX_KNOTS_V1", + ): + with self.subTest(name=name): + self.assertIn(name, wire) + self.assertIn("LC_MPFI_MAX_JOB_BYTES_V1", wire_source) + self.assertIn("LC_MPFI_MAX_PRECISION_BITS_V1", wire_source) + self.assertIn("LC_MPFI_MAX_KNOTS_V1", wire_source) + self.assertIn("LC_MPFI_MAX_JOB_BYTES_V1", main) + self.assertIn("LC_MPFI_MAX_OUTPUT_BYTES_V1", main) + self.assertIn("output_limit", main) + + def test_source_bound_receipt_is_outside_evaluator_and_no_arb_compatibility_layer_exists(self) -> None: + receipt = (MPFI / "receipt.py").read_text(encoding="utf-8") + self.assertTrue( + any( + isinstance(node, ast.ClassDef) + and node.name == "MpfiSourceBoundControllerV1" + for node in ast.parse(receipt).body + ) + ) + self.assertFalse((EVALUATOR / "receipt.py").exists()) + joined = "\n".join( + path.read_text(encoding="utf-8") + for path in EVALUATOR.glob("*.c") + ).lower() + for forbidden in ("arb", "flint", "fallback", "long double", "strtod"): + self.assertNotIn(forbidden, joined) + + +class RuntimeTests(unittest.TestCase): + @unittest.skipUnless( + os.environ.get("LABCOLORS_MPFI_EVALUATOR"), + "set LABCOLORS_MPFI_EVALUATOR to the controlled C17 binary", + ) + def test_frozen_fixture_produces_a_canonical_transcript(self) -> None: + executable = os.environ["LABCOLORS_MPFI_EVALUATOR"] + fixture = (REPO / "proof/region/v1/fixtures/proof-job-v1.bin").read_bytes() + manifest = bytes.fromhex("01" + "23" * 31) + result = subprocess.run( + [ + executable, + "--manifest-identity", + manifest.hex(), + "--job", + "/dev/stdin", + ], + input=fixture, + check=False, + capture_output=True, + timeout=300, + ) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(result.stderr, b"") + sys.path.insert(0, str(REPO / "proof/region/v1")) + from region_proof_protocol import DecisionTranscriptV1 # noqa: PLC0415 + + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(transcript.encode(), result.stdout) + self.assertEqual(transcript.comparator_identity, manifest) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_MPFI_EVALUATOR"), + "set LABCOLORS_MPFI_EVALUATOR to the controlled C17 binary", + ) + def test_black_exact_zero_emits_the_canonical_trace_witness(self) -> None: + sys.path.insert(0, str(REPO / "proof/region/v1")) + from region_proof_protocol import ( # noqa: PLC0415 + ComparatorBudgetV1, + ComparatorKindV1, + ContextualRegionDefinitionV1, + DecisionTranscriptV1, + ExactZeroSignalTraceV1, + ProofJobV1, + ProofPolicyV1, + ReducedDomainManifestV1, + ) + + registered = ContextualRegionDefinitionV1.parse( + (REPO / "proof/region/v1/fixtures/v5b2b-definition-0a8d1c3d.bin").read_bytes() + ) + zero = bytes(8) + definition = ContextualRegionDefinitionV1( + registered.fields[:21] + ((1).to_bytes(8, "big"),) + (zero,) * 4, + 1, + ) + policy = ProofPolicyV1( + 1, + ( + ComparatorBudgetV1(ComparatorKindV1.ARB, (128,), 1, 1), + ComparatorBudgetV1(ComparatorKindV1.MPFI, (192,), 1, 1), + ), + ) + job = ProofJobV1( + definition, + FORMULA.read_bytes(), + ReducedDomainManifestV1.from_ordinals((0,)), + policy, + ) + manifest = bytes.fromhex("ab" + "00" * 31) + result = subprocess.run( + [ + os.environ["LABCOLORS_MPFI_EVALUATOR"], + "--manifest-identity", + manifest.hex(), + "--job", + "/dev/stdin", + ], + input=job.encode(), + check=False, + capture_output=True, + timeout=300, + ) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + transcript = DecisionTranscriptV1.parse(result.stdout) + self.assertEqual(tuple(transcript.iter_decisions()), (0,)) + self.assertEqual(transcript.counters, (1, 0, 0, 0)) + self.assertEqual(transcript.exact_equality_count, 1) + witness = tuple(transcript.iter_witnesses())[0] + self.assertIs(type(witness), ExactZeroSignalTraceV1) + self.assertEqual( + witness.trace_digest, + hashlib.sha256( + b"labcolors.proof-region.exact-zero-signal-trace.v1\0" + + job.identity + + (0).to_bytes(4, "big") + + (0).to_bytes(8, "big") + ).digest(), + ) + + @unittest.skipUnless( + os.environ.get("LABCOLORS_MPFI_EVALUATOR"), + "set LABCOLORS_MPFI_EVALUATOR to the controlled C17 binary", + ) + def test_input_limit_is_enforced_before_wire_parse(self) -> None: + result = subprocess.run( + [ + os.environ["LABCOLORS_MPFI_EVALUATOR"], + "--manifest-identity", + ("01" + "23" * 31), + "--job", + "/dev/stdin", + ], + input=bytes(16 * 1024 * 1024 + 1), + check=False, + capture_output=True, + timeout=60, + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + self.assertEqual(result.stderr, b"job read failed: input_limit\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/proof/region/v1/mpfi/tests/test_receipt.py b/proof/region/v1/mpfi/tests/test_receipt.py new file mode 100644 index 00000000..f05be7e6 --- /dev/null +++ b/proof/region/v1/mpfi/tests/test_receipt.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +"""Hostile contract for the MPFI source-bound BUILD → RUN receipt.""" + +from __future__ import annotations + +import hashlib +import os +import sys +import unittest +from contextlib import ExitStack +from dataclasses import replace +from functools import cache +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[2] +TESTS = PROOF / "tests" +ARB_TESTS = PROOF / "arb/tests" +MPFI_TESTS = Path(__file__).resolve().parent +sys.path[:0] = [str(PROOF), str(TESTS), str(ARB_TESTS), str(MPFI_TESTS)] + +import executor # noqa: E402 +import region_proof_protocol as protocol # noqa: E402 +from build import transport as build_transport # noqa: E402 +import provenance # noqa: E402 +from mpfi import build as mpfi_build # noqa: E402 +from mpfi import receipt, runtime as mpfi_runtime # noqa: E402 +from test_mpfi_build import ( # noqa: E402 + _generated_formula, + _limits_for_bundle, + _workspace_sources, +) +from test_mpfi_input import _admitted_closure # noqa: E402 +from test_pipeline import ( # noqa: E402 + _BuildBackend, + _docker_capability, + _job, + _static_elf, +) +try: + from .skip_contract import NATIVE_RECEIPT_SKIP_REASON_V1 # noqa: E402 +except ImportError: # direct gate discovery imports this module as a top-level test + from skip_contract import NATIVE_RECEIPT_SKIP_REASON_V1 # noqa: E402 + + +def _digest(label: str) -> bytes: + return hashlib.sha256(label.encode("ascii")).digest() + + +@cache +def _request() -> receipt.MpfiPipelineRequestV1: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + build_limits = _limits_for_bundle(source_lock, admitted, sources, generated) + runtime_limits = executor.ExecutionLimitsV1( + 16 * 1024 * 1024, + 16 * 1024 * 1024, + 4096, + 16 * 1024 * 1024, + 64 * 1024, + 60_000_000_000, + 1024 * 1024 * 1024, + 1, + ) + runtime_binding = mpfi_runtime.MpfiRuntimeBindingV1( + mpfi_runtime.mpfi_runtime_profile_v1(), + runtime_limits, + ) + return receipt.MpfiPipelineRequestV1( + source_lock, + admitted, + sources, + generated, + build_limits, + _job(), + runtime_binding, + ) + + +@cache +def _comparator() -> protocol.ContentResolvedComparatorManifestV2: + contents = tuple(f"mpfi-fixture-coordinate-{index}".encode() for index in range(10)) + manifest = protocol.ComparatorManifestV2( + protocol.ComparatorKindV1.MPFI, + *(hashlib.sha256(content).digest() for content in contents), + ) + by_digest = { + hashlib.sha256(content).digest(): content for content in contents + } + return protocol.ContentResolvedComparatorManifestV2.admit(manifest, by_digest.get) + + +class _NativeRunBackend: + def __init__(self, result: executor.ExecutionResultV1 | None = None) -> None: + self.result = result + self.requests: list[executor.ExecutionRequestV1] = [] + + def probe(self, guard: object) -> executor.SupportedV1: + if not guard.is_current(): + raise AssertionError("controller supplied a stale probe guard") + return executor.SupportedV1( + executor.EXECUTION_PLATFORM_V1, + executor.SANDBOX_POLICY_RELEASE_V1, + ) + + def run( + self, + request: executor.ExecutionRequestV1, + _capability: executor.SupportedV1, + ) -> executor.ExecutionResultV1: + self.requests.append(request) + if self.result is not None: + return self.result + job = _job() + transcript = protocol.DecisionTranscriptV1.from_decisions( + job, + _comparator(), + tuple(protocol.DecisionV1.OUTSIDE for _ in range(job.domain.point_count)), + (), + _digest("accounting"), + ) + marker = b"--manifest-identity" + try: + marker_index = request.argv.index(marker) + except ValueError as error: + raise AssertionError("manifest identity marker is missing") from error + if marker_index + 1 >= len(request.argv): + raise AssertionError("manifest identity value is missing") + transcript = replace( + transcript, + comparator_identity=bytes.fromhex( + request.argv[marker_index + 1].decode("ascii") + ), + ) + return executor.CompletedV1( + hashlib.sha256(request.executable).digest(), + transcript.encode(), + b"", + ) + + +def _execute( + *, + binary: bytes | None = None, + binaries: tuple[bytes, bytes] | None = None, + second: bool = False, + result: executor.ExecutionResultV1 | None = None, +) -> tuple[receipt.MpfiSourceBoundResultV1, _NativeRunBackend]: + build_binaries = binaries or ( + binary or _static_elf(b"mpfi-source-bound"), + binary or _static_elf(b"mpfi-source-bound"), + ) + build_backend = _BuildBackend( + build_binaries, + probe=_docker_capability(mpfi_build.MPFI_BUILD_TRANSPORT_POLICY_V1), + ) + run_backend = _NativeRunBackend(result) + patches = ( + mock.patch.object( + build_transport.NativeDockerBuildBackendV1, + "probe", + autospec=True, + side_effect=lambda _self: build_backend.probe(), + ), + mock.patch.object( + build_transport.NativeDockerBuildBackendV1, + "run_build", + autospec=True, + side_effect=lambda _self, request: build_backend.run_build(request), + ), + mock.patch.object( + executor.NativeLinuxBackendV1, + "probe", + autospec=True, + side_effect=lambda _self, guard: run_backend.probe(guard), + ), + mock.patch.object( + executor.NativeLinuxBackendV1, + "run", + autospec=True, + side_effect=lambda _self, request, capability: run_backend.run( + request, + capability, + ), + ), + mock.patch.object(receipt.executor, "enter_observer_cgroup_v1"), + ) + controller = receipt.MpfiSourceBoundControllerV1( + Path("/usr/bin/docker"), + Path("/sys/fs/cgroup/labcolors/proof"), + ) + with ExitStack() as stack: + for patch in patches: + stack.enter_context(patch) + first = controller.execute(_request()) + if second: + if type(first) is not receipt.MpfiSourceBoundEvaluatorReceiptV1: + raise AssertionError(f"first execution failed: {first!r}") + return controller.execute(_request()), run_backend + return first, run_backend + + +def _tamper(value: object, field: str, replacement: object) -> object: + clone = object.__new__(type(value)) + for name, current in vars(value).items(): + object.__setattr__(clone, name, current) + object.__setattr__(clone, field, replacement) + return clone + + +class MpfiSourceBoundReceiptTests(unittest.TestCase): + def test_divergent_builds_return_an_explicit_nonidentical_observation(self) -> None: + result, _backend = _execute( + binaries=(_static_elf(b"first-build"), _static_elf(b"second-build")), + ) + self.assertIs(type(result), build_transport.TwoBuildObservationV1) + self.assertIs(result.relation, build_transport.BuildByteRelationV1.DIFFERENT) + + def test_controller_seals_one_source_bound_receipt_and_consumes_authority(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + self.assertIs(type(result.claim), protocol.EvaluatorProvenanceClaimV1) + self.assertEqual(result.comparator.manifest.manifest.kind, protocol.ComparatorKindV1.MPFI) + self.assertTrue(receipt.replay_mpfi_evidence_is_well_bound_v1(result.evidence)) + + consumed, _backend = _execute(second=True) + self.assertIs(type(consumed), receipt.MpfiSourceBoundRejectedV1) + self.assertEqual( + consumed.reason, + receipt.MpfiSourceBoundFailureReasonV1.CONTROLLER_CONSUMED, + ) + + def test_public_receipt_and_evidence_constructors_are_controller_only(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + with self.assertRaises(TypeError): + receipt.MpfiSourceBoundEvaluatorReceiptV1( + result.claim, + result.evidence, + ) + with self.assertRaises(TypeError): + receipt.MpfiEvaluatorReplayV1(*tuple(result.evidence)) + with self.assertRaises(TypeError): + build = result.evidence.build + receipt.MpfiDiagnosticBuildObservationV1( + build.source_identity, + build.build_source_identity, + build.generated_formula_sha256, + build.runtime_binding_identity, + build.docker_capability, + build.input_bundle, + build.binary_sha256, + build.rebuild_sha256s, + build.processes, + build.binaries, + build.comparator, + _token=object(), + ) + with self.assertRaises(TypeError): + receipt.MpfiSourceBoundRejectedV1( + "foreign-reason", + "bounded detail", + ) + with self.assertRaises(TypeError): + receipt.MpfiSourceBoundRejectedV1( + receipt.MpfiSourceBoundFailureReasonV1.REQUEST_REJECTED, + "", + ) + with self.assertRaises(TypeError): + receipt.MpfiSourceBoundRejectedV1( + receipt.MpfiSourceBoundFailureReasonV1.REQUEST_REJECTED, + "x" * 4097, + ) + + class BadRepr: + def __repr__(self) -> str: + raise RuntimeError("repr failed") + + self.assertEqual( + receipt._failure_detail_v1(BadRepr(), diagnostic_repr=True), + "MPFI source-bound operation failed", + ) + + def test_replay_rejects_a_changed_build_transfer(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + process = result.evidence.build.processes[0] + transfer = tuple.__new__( + type(process.input_transfer), + ( + process.input_transfer.bundle_identity, + process.input_transfer.expected_length, + process.input_transfer.expected_sha256, + process.input_transfer.written_length - 1, + process.input_transfer.written_sha256, + ), + ) + changed_process = tuple.__new__( + type(process), + (process.returncode, process.stdout, process.stderr, transfer), + ) + changed_processes = (changed_process, result.evidence.build.processes[1]) + changed_build = _tamper(result.evidence.build, "processes", changed_processes) + changed_evidence = _tamper(result.evidence, "build", changed_build) + self.assertFalse(receipt.replay_mpfi_evidence_is_well_bound_v1(changed_evidence)) + + def test_replay_rejects_a_runtime_limit_switch(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + current = result.evidence.request.runtime_binding + changed_limits = executor.ExecutionLimitsV1( + current.limits.max_executable_bytes, + current.limits.max_stdin_bytes, + current.limits.max_argument_bytes - 1, + current.limits.max_stdout_bytes, + current.limits.max_stderr_bytes, + current.limits.wall_timeout_ns, + current.limits.memory_max_bytes, + current.limits.pids_max, + ) + changed_binding = mpfi_runtime.MpfiRuntimeBindingV1( + current.profile, + changed_limits, + ) + changed_request = receipt.MpfiPipelineRequestV1( + result.evidence.request.source_lock, + result.evidence.request.admitted_sources, + result.evidence.request.build_sources, + result.evidence.request.generated_formula, + result.evidence.request.build_limits, + result.evidence.request.job, + changed_binding, + ) + changed_evidence = _tamper(result.evidence, "request", changed_request) + self.assertFalse(receipt.replay_mpfi_evidence_is_well_bound_v1(changed_evidence)) + + def test_replay_rejects_a_changed_comparator_preimage(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + preimages = result.evidence.build.comparator.preimages + changed_preimages = replace(preimages, exclusions=preimages.exclusions + b"!") + changed_comparator = _tamper( + result.evidence.build.comparator, + "preimages", + changed_preimages, + ) + changed_build = _tamper( + result.evidence.build, + "comparator", + changed_comparator, + ) + changed_evidence = _tamper(result.evidence, "build", changed_build) + self.assertFalse(receipt.replay_mpfi_evidence_is_well_bound_v1(changed_evidence)) + + def test_replay_rejects_a_raw_build_identity_preimage(self) -> None: + result, _backend = _execute() + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1) + preimages = result.evidence.build.comparator.preimages + changed_preimages = replace( + preimages, + build_identity=preimages.build_identity[:-1] + bytes(( + preimages.build_identity[-1] ^ 1, + )), + ) + changed_comparator = _tamper( + result.evidence.build.comparator, + "preimages", + changed_preimages, + ) + changed_build = _tamper( + result.evidence.build, + "comparator", + changed_comparator, + ) + changed_evidence = _tamper(result.evidence, "build", changed_build) + self.assertFalse(receipt.replay_mpfi_evidence_is_well_bound_v1(changed_evidence)) + + def test_malformed_generated_formula_is_rejected_before_transport(self) -> None: + request = _request() + malformed = receipt.MpfiPipelineRequestV1( + request.source_lock, + request.admitted_sources, + request.build_sources, + request.generated_formula[:-1] + + bytes((request.generated_formula[-1] ^ 1,)), + request.build_limits, + request.job, + request.runtime_binding, + ) + controller = receipt.MpfiSourceBoundControllerV1( + Path("/usr/bin/docker"), + Path("/sys/fs/cgroup/labcolors/proof"), + ) + with mock.patch.object(receipt.mpfi_build, "seal_mpfi_build_input_from_snapshot_v1") as seal: + result = controller.execute(malformed) + self.assertIs(type(result), receipt.MpfiSourceBoundRejectedV1) + self.assertEqual(result.reason, receipt.MpfiSourceBoundFailureReasonV1.REQUEST_REJECTED) + seal.assert_not_called() + + controller = receipt.MpfiSourceBoundControllerV1( + Path("/usr/bin/docker"), + Path("/sys/fs/cgroup/labcolors/proof"), + ) + with mock.patch.object( + receipt.mpfi_build, + "seal_mpfi_build_input_from_snapshot_v1", + side_effect=RuntimeError(), + ): + result = controller.execute(_request()) + self.assertIs(type(result), receipt.MpfiSourceBoundRejectedV1) + self.assertEqual(result.reason, receipt.MpfiSourceBoundFailureReasonV1.REQUEST_REJECTED) + self.assertEqual(result.detail, "MPFI source-bound operation failed") + + +@unittest.skipUnless( + sys.platform == "linux" + and all( + os.environ.get(name) + for name in ( + "LABCOLORS_MPFI_DOCKER", + "LABCOLORS_EXECUTOR_CGROUP_V1", + "LABCOLORS_GMP_ARCHIVE", + "LABCOLORS_MPFR_ARCHIVE", + "LABCOLORS_MPFI_ARCHIVE", + ) + ), + NATIVE_RECEIPT_SKIP_REASON_V1, +) +class NativeMpfiSourceBoundReceiptIntegrationTests(unittest.TestCase): + def test_real_build_run_and_seal_are_one_source_bound_controller_execution(self) -> None: + source_lock = provenance.mpfi_source_lock_v1() + archive_names = ( + "LABCOLORS_GMP_ARCHIVE", + "LABCOLORS_MPFR_ARCHIVE", + "LABCOLORS_MPFI_ARCHIVE", + ) + safe = tuple( + provenance.admit_source_archive(lock, Path(os.environ[name]).read_bytes()) + for lock, name in zip(source_lock.sources, archive_names, strict=True) + ) + admitted = provenance.admit_mpfi_sources(source_lock, safe) + request = _request() + build_limits = _limits_for_bundle( + source_lock, + admitted, + request.build_sources, + request.generated_formula, + ) + request = receipt.MpfiPipelineRequestV1( + source_lock, + admitted, + request.build_sources, + request.generated_formula, + build_limits, + request.job, + request.runtime_binding, + ) + result = receipt.MpfiSourceBoundControllerV1( + Path(os.environ["LABCOLORS_MPFI_DOCKER"]), + Path(os.environ["LABCOLORS_EXECUTOR_CGROUP_V1"]), + ).execute(request) + self.assertIs(type(result), receipt.MpfiSourceBoundEvaluatorReceiptV1, result) + self.assertTrue(receipt.replay_mpfi_evidence_is_well_bound_v1(result.evidence)) + self.assertIs(result.evidence.build.binaries[0], result.executable) + self.assertIs(result.evidence.invocation.executable, result.executable) + first, second = result.evidence.build.processes + self.assertEqual( + first.input_transfer.bundle_identity, + second.input_transfer.bundle_identity, + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/provenance.py b/proof/region/v1/provenance.py new file mode 100644 index 00000000..f5f2606f --- /dev/null +++ b/proof/region/v1/provenance.py @@ -0,0 +1,2236 @@ +#!/usr/bin/env python3 +"""Canonical source declarations and fail-closed archive admission for proof V1. + +This module deliberately stops before cryptographic origin verification, build +execution and evaluator replay. Observations remain claims; only archive bytes +that were hashed and structurally scanned become ``SafeSourceArchiveV1``. +""" + +from __future__ import annotations + +import hashlib +import io +import lzma +import tarfile +import zlib +from dataclasses import dataclass, field +from enum import IntEnum, StrEnum +from pathlib import PurePosixPath +from typing import NoReturn, TypeAlias +from urllib.parse import urlsplit + + +SOURCE_LOCK_MAGIC_V1 = b"LCSRC1\0\0" +SOURCE_LOCK_ID_LABEL_V1 = b"labcolors.proof-region.source-lock.v1\0" +SOURCE_TREE_ID_LABEL_V1 = b"labcolors.proof-region.safe-source-tree.v1\0" +ADMITTED_ARB_SOURCES_ID_LABEL_V1 = b"labcolors.proof-region.admitted-arb-sources.v1\0" +ADMITTED_MPFI_SOURCES_ID_LABEL_V1 = b"labcolors.proof-region.admitted-mpfi-sources.v1\0" +SOURCE_LOCK_RELEASE_V1 = 1 +SOURCE_CLOSURE_COUNT_V1 = 3 +SHA256_BYTES = 32 +SHA1_BYTES = 20 +OPENPGP_V4_FINGERPRINT_BYTES = 20 +TAR_BLOCK_BYTES = 512 +TAR_END_MARKER_BYTES = TAR_BLOCK_BYTES * 2 +READ_CHUNK_BYTES = 64 * 1024 +ALLOWED_REGULAR_MODES_V1 = frozenset((0o644, 0o700, 0o755)) +ALLOWED_DIRECTORY_MODE_V1 = 0o755 + +# Derived identities are capability coordinates, never cache state: a frozen +# dataclass still has a writable __dict__ through hostile object mutation. + + +class ProvenanceReasonV1(StrEnum): + BAD_MAGIC = "bad_magic" + TRUNCATED = "truncated" + TRAILING_BYTES = "trailing_bytes" + UNKNOWN_RELEASE = "unknown_release" + UNKNOWN_ENUM = "unknown_enum" + INVALID_FIELD = "invalid_field" + INVALID_DIGEST = "invalid_digest" + NONCANONICAL_ORDER = "noncanonical_order" + DUPLICATE_PATH = "duplicate_path" + CASE_COLLISION = "case_collision" + ABSOLUTE_PATH = "absolute_path" + UNSAFE_PATH = "unsafe_path" + UNSAFE_LINK = "unsafe_link" + UNSAFE_MEMBER_TYPE = "unsafe_member_type" + UNSAFE_MODE = "unsafe_mode" + ARCHIVE_LENGTH_MISMATCH = "archive_length_mismatch" + ARCHIVE_DIGEST_MISMATCH = "archive_digest_mismatch" + DECOMPRESSION_FAILED = "decompression_failed" + TAR_STREAM_LENGTH_MISMATCH = "tar_stream_length_mismatch" + TRAILING_COMPRESSED_DATA = "trailing_compressed_data" + NONCANONICAL_TAR = "noncanonical_tar" + ROOT_MISMATCH = "root_mismatch" + FILE_COUNT_MISMATCH = "file_count_mismatch" + FILE_BYTES_MISMATCH = "file_bytes_mismatch" + FILE_CONTENT_MISMATCH = "file_content_mismatch" + LEGAL_FILES_MISMATCH = "legal_files_mismatch" + CONTENT_RELATION_MISMATCH = "content_relation_mismatch" + FOREIGN_BINDING = "foreign_binding" + INTEGRITY_KIND_MISMATCH = "integrity_kind_mismatch" + + +@dataclass(frozen=True) +class ProvenanceErrorV1(ValueError): + artifact: str + reason: ProvenanceReasonV1 + detail: str + + def __str__(self) -> str: + return f"{self.artifact}: {self.reason}: {self.detail}" + + +def _fail(artifact: str, reason: ProvenanceReasonV1, detail: str) -> NoReturn: + raise ProvenanceErrorV1(artifact, reason, detail) + + +def _identity(label: bytes, encoded: bytes) -> bytes: + return hashlib.sha256(label + len(encoded).to_bytes(8, "big") + encoded).digest() + + +def _digest(value: bytes, artifact: str, field_name: str, length: int = SHA256_BYTES) -> bytes: + if type(value) is not bytes or len(value) != length or value == bytes(length): + _fail(artifact, ProvenanceReasonV1.INVALID_DIGEST, f"invalid {field_name}") + return value + + +def _positive(value: int, artifact: str, field_name: str) -> int: + if type(value) is not int or value <= 0 or value >= 1 << 64: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"invalid {field_name}") + return value + + +def _ascii(value: str, artifact: str, field_name: str, maximum: int) -> bytes: + if type(value) is not str or not value or "\0" in value: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"invalid {field_name}") + try: + encoded = value.encode("ascii") + except UnicodeEncodeError: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"non-ASCII {field_name}") + if any(byte < 0x20 or byte == 0x7F for byte in encoded): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"control byte in {field_name}") + if len(encoded) > maximum: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"oversized {field_name}") + return encoded + + +def _relative_path(value: str, artifact: str, field_name: str) -> bytes: + encoded = _ascii(value, artifact, field_name, 4096) + if value.startswith("/"): + _fail(artifact, ProvenanceReasonV1.ABSOLUTE_PATH, f"absolute {field_name}") + if "\\" in value: + _fail(artifact, ProvenanceReasonV1.UNSAFE_PATH, f"backslash in {field_name}") + parts = value.split("/") + if not parts or any(part in ("", ".", "..") for part in parts): + _fail(artifact, ProvenanceReasonV1.UNSAFE_PATH, f"unsafe {field_name}") + return encoded + + +def _root_prefix(value: str, artifact: str) -> bytes: + if type(value) is not str or not value.endswith("/") or value.count("/") != 1: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, "root prefix is one directory") + return _relative_path(value[:-1], artifact, "root_prefix") + b"/" + + +def _https_url(value: str, artifact: str, field_name: str) -> bytes: + encoded = _ascii(value, artifact, field_name, 2048) + try: + parsed = urlsplit(value) + hostname = parsed.hostname + _ = parsed.port + except ValueError: + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"malformed {field_name}") + if ( + parsed.scheme != "https" + or not hostname + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path in ("", "/") + ): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, f"noncanonical {field_name}") + return encoded + + +def _blob(value: bytes) -> bytes: + return len(value).to_bytes(4, "big") + value + + +class _Reader: + def __init__(self, data: bytes, artifact: str): + if type(data) is not bytes: + raise TypeError("canonical wire input must be bytes") + self.data = data + self.artifact = artifact + self.offset = 0 + + def exact(self, length: int) -> bytes: + if length < 0 or self.offset + length > len(self.data): + _fail(self.artifact, ProvenanceReasonV1.TRUNCATED, "wire is truncated") + start = self.offset + self.offset += length + return self.data[start : self.offset] + + def u8(self) -> int: + return self.exact(1)[0] + + def u16(self) -> int: + return int.from_bytes(self.exact(2), "big") + + def u32(self) -> int: + return int.from_bytes(self.exact(4), "big") + + def u64(self) -> int: + return int.from_bytes(self.exact(8), "big") + + def blob(self, maximum: int) -> bytes: + length = self.u32() + if length == 0 or length > maximum: + _fail(self.artifact, ProvenanceReasonV1.INVALID_FIELD, "invalid blob length") + return self.exact(length) + + def text(self, maximum: int, field_name: str) -> str: + raw = self.blob(maximum) + try: + return raw.decode("ascii") + except UnicodeDecodeError: + _fail(self.artifact, ProvenanceReasonV1.INVALID_FIELD, f"non-ASCII {field_name}") + + def finish(self) -> None: + if self.offset != len(self.data): + _fail(self.artifact, ProvenanceReasonV1.TRAILING_BYTES, "wire has trailing bytes") + + +class ArchiveFormatV1(IntEnum): + TAR_XZ = 1 + TAR_GZIP = 2 + + +class SourceRoleV1(IntEnum): + GMP = 1 + MPFR = 2 + FLINT_ARB = 3 + MPFI = 4 + + +class IntegrityKindV1(IntEnum): + DETACHED_SIGNATURE = 1 + GIT_CONTENT_RELATION = 2 + PROJECT_PINNED_ARCHIVE_DIGEST = 3 + + +@dataclass(frozen=True) +class LegalFileV1: + path: str + length: int + sha256: bytes + + def __post_init__(self) -> None: + _relative_path(self.path, "legal-file-v1", "path") + _positive(self.length, "legal-file-v1", "length") + _digest(self.sha256, "legal-file-v1", "sha256") + + def encode(self) -> bytes: + return _blob(self.path.encode("ascii")) + self.length.to_bytes(8, "big") + self.sha256 + + @classmethod + def parse_from(cls, reader: _Reader) -> "LegalFileV1": + return cls(reader.text(4096, "legal file path"), reader.u64(), reader.exact(SHA256_BYTES)) + + +@dataclass(frozen=True) +class ProjectPinnedReleaseOnlyFileV1: + path: str + mode: int + length: int + sha256: bytes + + def __post_init__(self) -> None: + _relative_path(self.path, "project-pinned-release-only-file-v1", "path") + if type(self.mode) is not int or self.mode not in ALLOWED_REGULAR_MODES_V1: + _fail( + "project-pinned-release-only-file-v1", + ProvenanceReasonV1.UNSAFE_MODE, + "invalid mode", + ) + _positive(self.length, "project-pinned-release-only-file-v1", "length") + _digest(self.sha256, "project-pinned-release-only-file-v1", "sha256") + + def encode(self) -> bytes: + return ( + _blob(self.path.encode("ascii")) + + self.mode.to_bytes(4, "big") + + self.length.to_bytes(8, "big") + + self.sha256 + ) + + @classmethod + def parse_from(cls, reader: _Reader) -> "ProjectPinnedReleaseOnlyFileV1": + return cls( + reader.text(4096, "project-pinned release-only path"), + reader.u32(), + reader.u64(), + reader.exact(SHA256_BYTES), + ) + + +@dataclass(frozen=True) +class DetachedSignaturePolicyV1: + signature_url: str + signature_length: int + signature_sha256: bytes + public_key_packets_sha256: bytes + signer_fingerprint: bytes + + kind: IntegrityKindV1 = field( + init=False, + default=IntegrityKindV1.DETACHED_SIGNATURE, + ) + + def __post_init__(self) -> None: + _https_url(self.signature_url, "signature-policy-v1", "signature_url") + _positive(self.signature_length, "signature-policy-v1", "signature_length") + _digest(self.signature_sha256, "signature-policy-v1", "signature_sha256") + _digest( + self.public_key_packets_sha256, + "signature-policy-v1", + "public_key_packets_sha256", + ) + _digest( + self.signer_fingerprint, + "signature-policy-v1", + "signer_fingerprint", + OPENPGP_V4_FINGERPRINT_BYTES, + ) + + def encode_payload(self) -> bytes: + return ( + _blob(self.signature_url.encode("ascii")) + + self.signature_length.to_bytes(8, "big") + + self.signature_sha256 + + self.public_key_packets_sha256 + + self.signer_fingerprint + ) + + @classmethod + def parse_from(cls, reader: _Reader) -> "DetachedSignaturePolicyV1": + return cls( + reader.text(2048, "signature URL"), + reader.u64(), + reader.exact(SHA256_BYTES), + reader.exact(SHA256_BYTES), + reader.exact(OPENPGP_V4_FINGERPRINT_BYTES), + ) + + +@dataclass(frozen=True) +class GitContentRelationPolicyV1: + repository_url: str + tag: str + commit: bytes + tree: bytes + common_file_count: int + omitted_paths: tuple[str, ...] + project_pinned_release_only_files: tuple[ProjectPinnedReleaseOnlyFileV1, ...] + + kind: IntegrityKindV1 = field( + init=False, + default=IntegrityKindV1.GIT_CONTENT_RELATION, + ) + + def __post_init__(self) -> None: + artifact = "git-content-relation-policy-v1" + _https_url(self.repository_url, artifact, "repository_url") + _ascii(self.tag, artifact, "tag", 128) + _digest(self.commit, artifact, "commit", SHA1_BYTES) + _digest(self.tree, artifact, "tree", SHA1_BYTES) + _positive(self.common_file_count, artifact, "common_file_count") + if ( + type(self.omitted_paths) is not tuple + or not self.omitted_paths + or len(self.omitted_paths) > 4096 + ): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, "omission count") + if ( + type(self.project_pinned_release_only_files) is not tuple + or not self.project_pinned_release_only_files + or len(self.project_pinned_release_only_files) > 4096 + ): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, "release-only file count") + for path in self.omitted_paths: + _relative_path(path, artifact, "omitted path") + if self.omitted_paths != tuple(sorted(set(self.omitted_paths))): + _fail(artifact, ProvenanceReasonV1.NONCANONICAL_ORDER, "omissions") + if any( + type(value) is not ProjectPinnedReleaseOnlyFileV1 + for value in self.project_pinned_release_only_files + ): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, "release-only file type") + release_only_paths = tuple( + value.path for value in self.project_pinned_release_only_files + ) + if release_only_paths != tuple(sorted(set(release_only_paths))): + _fail( + artifact, + ProvenanceReasonV1.NONCANONICAL_ORDER, + "project-pinned release-only files", + ) + if set(release_only_paths) & set(self.omitted_paths): + _fail(artifact, ProvenanceReasonV1.INVALID_FIELD, "relation overlap") + + def encode_payload(self) -> bytes: + chunks = [ + _blob(self.repository_url.encode("ascii")), + _blob(self.tag.encode("ascii")), + self.commit, + self.tree, + self.common_file_count.to_bytes(8, "big"), + len(self.omitted_paths).to_bytes(4, "big"), + ] + chunks.extend(_blob(path.encode("ascii")) for path in self.omitted_paths) + chunks.append(len(self.project_pinned_release_only_files).to_bytes(4, "big")) + chunks.extend(value.encode() for value in self.project_pinned_release_only_files) + return b"".join(chunks) + + @classmethod + def parse_from(cls, reader: _Reader) -> "GitContentRelationPolicyV1": + repository = reader.text(2048, "repository URL") + tag = reader.text(128, "tag") + commit = reader.exact(SHA1_BYTES) + tree = reader.exact(SHA1_BYTES) + common_file_count = reader.u64() + omitted_count = reader.u32() + if omitted_count == 0 or omitted_count > 4096: + _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "omission count") + omitted = tuple(reader.text(4096, "omitted path") for _ in range(omitted_count)) + release_only_count = reader.u32() + if release_only_count == 0 or release_only_count > 4096: + _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "release-only file count") + release_only = tuple( + ProjectPinnedReleaseOnlyFileV1.parse_from(reader) + for _ in range(release_only_count) + ) + return cls(repository, tag, commit, tree, common_file_count, omitted, release_only) + + +@dataclass(frozen=True) +class ProjectPinnedArchiveDigestPolicyV1: + """State that the project pins archive bytes without upstream authentication. + + The digest and exact archive coordinates live in ``SourceReleaseLockV1``. + This marker prevents an HTTPS download plus a project-chosen digest from + being misreported as a publisher signature or a verified Git relation. + """ + + kind: IntegrityKindV1 = field( + init=False, + default=IntegrityKindV1.PROJECT_PINNED_ARCHIVE_DIGEST, + ) + + def encode_payload(self) -> bytes: + return b"" + + @classmethod + def parse_from(cls, _reader: _Reader) -> ProjectPinnedArchiveDigestPolicyV1: + return cls() + + +SourceIntegrityPolicyV1: TypeAlias = ( + DetachedSignaturePolicyV1 + | GitContentRelationPolicyV1 + | ProjectPinnedArchiveDigestPolicyV1 +) + + +def _parse_integrity_policy(reader: _Reader) -> SourceIntegrityPolicyV1: + kind_value = reader.u8() + try: + kind = IntegrityKindV1(kind_value) + except ValueError: + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "integrity kind") + if kind is IntegrityKindV1.DETACHED_SIGNATURE: + return DetachedSignaturePolicyV1.parse_from(reader) + if kind is IntegrityKindV1.GIT_CONTENT_RELATION: + return GitContentRelationPolicyV1.parse_from(reader) + if kind is IntegrityKindV1.PROJECT_PINNED_ARCHIVE_DIGEST: + return ProjectPinnedArchiveDigestPolicyV1.parse_from(reader) + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "integrity kind") + + +@dataclass(frozen=True) +class SourceReleaseLockV1: + role: SourceRoleV1 + version: str + archive_url: str + archive_format: ArchiveFormatV1 + archive_length: int + archive_sha256: bytes + tar_stream_length: int + root_prefix: str + regular_file_count: int + regular_file_bytes: int + legal_files: tuple[LegalFileV1, ...] + integrity: SourceIntegrityPolicyV1 + + def __post_init__(self) -> None: + if type(self.role) is not SourceRoleV1 or type(self.archive_format) is not ArchiveFormatV1: + _fail("source-release-lock-v1", ProvenanceReasonV1.UNKNOWN_ENUM, "role or format") + _ascii(self.version, "source-release-lock-v1", "version", 128) + _https_url(self.archive_url, "source-release-lock-v1", "archive_url") + _positive(self.archive_length, "source-release-lock-v1", "archive_length") + _digest(self.archive_sha256, "source-release-lock-v1", "archive_sha256") + _positive(self.tar_stream_length, "source-release-lock-v1", "tar_stream_length") + if self.tar_stream_length % TAR_BLOCK_BYTES: + _fail("source-release-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "unaligned tar length") + _root_prefix(self.root_prefix, "source-release-lock-v1") + _positive(self.regular_file_count, "source-release-lock-v1", "regular_file_count") + _positive(self.regular_file_bytes, "source-release-lock-v1", "regular_file_bytes") + if ( + type(self.legal_files) is not tuple + or not self.legal_files + or len(self.legal_files) > 4096 + ): + _fail("source-release-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "legal file count") + if any(type(value) is not LegalFileV1 for value in self.legal_files): + _fail("source-release-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "legal file type") + paths = tuple(value.path for value in self.legal_files) + if paths != tuple(sorted(set(paths))): + _fail("source-release-lock-v1", ProvenanceReasonV1.NONCANONICAL_ORDER, "legal files") + if type(self.integrity) not in ( + DetachedSignaturePolicyV1, + GitContentRelationPolicyV1, + ProjectPinnedArchiveDigestPolicyV1, + ): + _fail( + "source-release-lock-v1", + ProvenanceReasonV1.UNKNOWN_ENUM, + "integrity policy", + ) + if isinstance(self.integrity, GitContentRelationPolicyV1): + if ( + self.integrity.common_file_count + + len(self.integrity.project_pinned_release_only_files) + != self.regular_file_count + ): + _fail( + "source-release-lock-v1", + ProvenanceReasonV1.CONTENT_RELATION_MISMATCH, + "common plus project-pinned release-only count does not cover archive", + ) + + def encode(self) -> bytes: + chunks = [ + bytes((self.role,)), + _blob(self.version.encode("ascii")), + _blob(self.archive_url.encode("ascii")), + bytes((self.archive_format,)), + self.archive_length.to_bytes(8, "big"), + self.archive_sha256, + self.tar_stream_length.to_bytes(8, "big"), + _blob(self.root_prefix.encode("ascii")), + self.regular_file_count.to_bytes(8, "big"), + self.regular_file_bytes.to_bytes(8, "big"), + len(self.legal_files).to_bytes(2, "big"), + ] + chunks.extend(value.encode() for value in self.legal_files) + chunks.append(bytes((self.integrity.kind,))) + chunks.append(self.integrity.encode_payload()) + return b"".join(chunks) + + @classmethod + def parse_from(cls, reader: _Reader) -> "SourceReleaseLockV1": + role_value = reader.u8() + try: + role = SourceRoleV1(role_value) + except ValueError: + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "source role") + version = reader.text(128, "version") + archive_url = reader.text(2048, "archive URL") + archive_format_value = reader.u8() + try: + archive_format = ArchiveFormatV1(archive_format_value) + except ValueError: + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_ENUM, "archive format") + archive_length = reader.u64() + archive_sha256 = reader.exact(SHA256_BYTES) + tar_stream_length = reader.u64() + root_prefix = reader.text(4096, "root prefix") + regular_file_count = reader.u64() + regular_file_bytes = reader.u64() + legal_file_count = reader.u16() + if legal_file_count == 0 or legal_file_count > 4096: + _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "legal file count") + legal_files = tuple( + LegalFileV1.parse_from(reader) for _ in range(legal_file_count) + ) + integrity = _parse_integrity_policy(reader) + return cls( + role, + version, + archive_url, + archive_format, + archive_length, + archive_sha256, + tar_stream_length, + root_prefix, + regular_file_count, + regular_file_bytes, + legal_files, + integrity, + ) + + @classmethod + def parse(cls, data: bytes) -> SourceReleaseLockV1: + """Rebuild one source declaration before it crosses a replay boundary.""" + + reader = _Reader(data, "source-release-lock-v1") + result = cls.parse_from(reader) + reader.finish() + if result.encode() != data: + _fail( + "source-release-lock-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "re-encode drift", + ) + return result + + @property + def identity(self) -> bytes: + return _identity(b"labcolors.proof-region.source-release-lock.v1\0", self.encode()) + + +_SourceClosureV1: TypeAlias = tuple[ + SourceReleaseLockV1, + SourceReleaseLockV1, + SourceReleaseLockV1, +] + + +def _encode_source_closure_v1(sources: _SourceClosureV1) -> bytes: + return ( + SOURCE_LOCK_MAGIC_V1 + + bytes((SOURCE_LOCK_RELEASE_V1, len(sources))) + + b"".join(source.encode() for source in sources) + ) + + +def _parse_source_closure_v1(data: bytes, artifact: str) -> _SourceClosureV1: + reader = _Reader(data, artifact) + if reader.exact(len(SOURCE_LOCK_MAGIC_V1)) != SOURCE_LOCK_MAGIC_V1: + _fail(reader.artifact, ProvenanceReasonV1.BAD_MAGIC, "source lock magic") + if reader.u8() != SOURCE_LOCK_RELEASE_V1: + _fail(reader.artifact, ProvenanceReasonV1.UNKNOWN_RELEASE, "source lock release") + if reader.u8() != SOURCE_CLOSURE_COUNT_V1: + _fail(reader.artifact, ProvenanceReasonV1.INVALID_FIELD, "source count") + result = ( + SourceReleaseLockV1.parse_from(reader), + SourceReleaseLockV1.parse_from(reader), + SourceReleaseLockV1.parse_from(reader), + ) + reader.finish() + return result + + +@dataclass(frozen=True) +class ArbSourceLockV1: + sources: _SourceClosureV1 + + def __post_init__(self) -> None: + if type(self.sources) is not tuple or len(self.sources) != SOURCE_CLOSURE_COUNT_V1: + _fail("arb-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source count") + if any(type(value) is not SourceReleaseLockV1 for value in self.sources): + _fail("arb-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source type") + if tuple(value.role for value in self.sources) != ( + SourceRoleV1.GMP, + SourceRoleV1.MPFR, + SourceRoleV1.FLINT_ARB, + ): + _fail("arb-source-lock-v1", ProvenanceReasonV1.NONCANONICAL_ORDER, "GMP, MPFR, FLINT") + if any( + not isinstance(value.integrity, DetachedSignaturePolicyV1) + for value in self.sources[:2] + ) or not isinstance( + self.sources[2].integrity, + GitContentRelationPolicyV1, + ): + _fail( + "arb-source-lock-v1", + ProvenanceReasonV1.INTEGRITY_KIND_MISMATCH, + "integrity policy", + ) + + def encode(self) -> bytes: + return _encode_source_closure_v1(self.sources) + + @classmethod + def parse(cls, data: bytes) -> ArbSourceLockV1: + result = cls(_parse_source_closure_v1(data, "arb-source-lock-v1")) + if result.encode() != data: + _fail("arb-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") + return result + + @property + def identity(self) -> bytes: + return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) + + +@dataclass(frozen=True) +class MpfiSourceLockV1: + sources: _SourceClosureV1 + + def __post_init__(self) -> None: + if type(self.sources) is not tuple or len(self.sources) != SOURCE_CLOSURE_COUNT_V1: + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source count") + if any(type(value) is not SourceReleaseLockV1 for value in self.sources): + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.INVALID_FIELD, "source type") + if tuple(value.role for value in self.sources) != ( + SourceRoleV1.GMP, + SourceRoleV1.MPFR, + SourceRoleV1.MPFI, + ): + _fail( + "mpfi-source-lock-v1", + ProvenanceReasonV1.NONCANONICAL_ORDER, + "GMP, MPFR, MPFI", + ) + if any( + not isinstance(value.integrity, DetachedSignaturePolicyV1) + for value in self.sources[:2] + ) or not isinstance( + self.sources[2].integrity, + ProjectPinnedArchiveDigestPolicyV1, + ): + _fail( + "mpfi-source-lock-v1", + ProvenanceReasonV1.INTEGRITY_KIND_MISMATCH, + "integrity policy", + ) + + def encode(self) -> bytes: + return _encode_source_closure_v1(self.sources) + + @classmethod + def parse(cls, data: bytes) -> MpfiSourceLockV1: + result = cls(_parse_source_closure_v1(data, "mpfi-source-lock-v1")) + if result.encode() != data: + _fail("mpfi-source-lock-v1", ProvenanceReasonV1.FOREIGN_BINDING, "re-encode drift") + return result + + @property + def identity(self) -> bytes: + return _identity(SOURCE_LOCK_ID_LABEL_V1, self.encode()) + + +def _rebuild_legal_file_v1(value: object) -> LegalFileV1: + if type(value) is not LegalFileV1: + raise TypeError("legal file must be LegalFileV1") + return LegalFileV1(value.path, value.length, value.sha256) + + +def _rebuild_project_pinned_release_only_file_v1( + value: object, +) -> ProjectPinnedReleaseOnlyFileV1: + if type(value) is not ProjectPinnedReleaseOnlyFileV1: + raise TypeError("release-only file must be ProjectPinnedReleaseOnlyFileV1") + return ProjectPinnedReleaseOnlyFileV1( + value.path, + value.mode, + value.length, + value.sha256, + ) + + +def _rebuild_integrity_policy_v1(value: object) -> SourceIntegrityPolicyV1: + """Copies only primitive policy coordinates; never dispatches caller methods.""" + + if type(value) is DetachedSignaturePolicyV1: + return DetachedSignaturePolicyV1( + value.signature_url, + value.signature_length, + value.signature_sha256, + value.public_key_packets_sha256, + value.signer_fingerprint, + ) + if type(value) is GitContentRelationPolicyV1: + omitted_paths = value.omitted_paths + release_only = value.project_pinned_release_only_files + if type(omitted_paths) is not tuple or type(release_only) is not tuple: + raise TypeError("git policy collections must be exact tuples") + return GitContentRelationPolicyV1( + value.repository_url, + value.tag, + value.commit, + value.tree, + value.common_file_count, + tuple(omitted_paths), + tuple( + _rebuild_project_pinned_release_only_file_v1(item) + for item in release_only + ), + ) + if type(value) is ProjectPinnedArchiveDigestPolicyV1: + return ProjectPinnedArchiveDigestPolicyV1() + raise TypeError("unknown source integrity policy") + + +def _rebuild_source_release_lock_v1(expected: object) -> SourceReleaseLockV1: + """Makes a fresh lock before a boundary can derive an authority identity.""" + + if type(expected) is not SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + legal_files = expected.legal_files + if type(legal_files) is not tuple: + raise TypeError("legal files must be an exact tuple") + return SourceReleaseLockV1( + expected.role, + expected.version, + expected.archive_url, + expected.archive_format, + expected.archive_length, + expected.archive_sha256, + expected.tar_stream_length, + expected.root_prefix, + expected.regular_file_count, + expected.regular_file_bytes, + tuple(_rebuild_legal_file_v1(item) for item in legal_files), + _rebuild_integrity_policy_v1(expected.integrity), + ) + + +def _rebuild_source_closure_lock_v1( + expected: object, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + if type(expected) not in (ArbSourceLockV1, MpfiSourceLockV1): + raise TypeError("expected must be an exact three-source lock") + sources = expected.sources + if type(sources) is not tuple or len(sources) != SOURCE_CLOSURE_COUNT_V1: + raise TypeError("source closure must be an exact three-source tuple") + first, second, third = sources + rebuilt = ( + _rebuild_source_release_lock_v1(first), + _rebuild_source_release_lock_v1(second), + _rebuild_source_release_lock_v1(third), + ) + if type(expected) is ArbSourceLockV1: + return ArbSourceLockV1(rebuilt) + return MpfiSourceLockV1(rebuilt) + + +@dataclass(frozen=True) +class ArchiveFileV1: + path: str + mode: int + length: int + sha256: bytes + + +_SAFE_ARCHIVE_TOKEN = object() +_ADMITTED_ARB_SOURCES_TOKEN = object() +_ADMITTED_MPFI_SOURCES_TOKEN = object() +_REPLAYED_SOURCE_MATERIALIZATION_TOKEN = object() +_REPLAYED_SOURCE_CLOSURE_TOKEN = object() + + +@dataclass(frozen=True, init=False) +class SafeSourceArchiveV1: + """Owned structural capability; it is neither origin nor build evidence. + + A materializer must consume archive_bytes from this value, never reopen a + pathname, and derive normalized directories from admitted regular files. + Empty archive directories intentionally carry no tree semantics. + """ + + source_lock_identity: bytes + archive_sha256: bytes + tree_identity: bytes + regular_file_count: int + regular_file_bytes: int + files: tuple[ArchiveFileV1, ...] + _archive_bytes: bytes = field(repr=False, compare=False) + + def __init__( + self, + source_lock_identity: bytes, + archive_sha256: bytes, + tree_identity: bytes, + regular_file_count: int, + regular_file_bytes: int, + files: tuple[ArchiveFileV1, ...], + archive_bytes: bytes, + *, + _token: object, + ) -> None: + if _token is not _SAFE_ARCHIVE_TOKEN: + raise TypeError("SafeSourceArchiveV1 is created only by archive admission") + object.__setattr__(self, "source_lock_identity", source_lock_identity) + object.__setattr__(self, "archive_sha256", archive_sha256) + object.__setattr__(self, "tree_identity", tree_identity) + object.__setattr__(self, "regular_file_count", regular_file_count) + object.__setattr__(self, "regular_file_bytes", regular_file_bytes) + object.__setattr__(self, "files", files) + object.__setattr__(self, "_archive_bytes", archive_bytes) + + @property + def archive_bytes(self) -> bytes: + """Return the immutable snapshot admitted by this capability.""" + + return self._archive_bytes + + +@dataclass(frozen=True, init=False) +class ReplayedSourceMaterializationV1: + """One private replay snapshot: lock, archive metadata and file bytes move together. + + Public callers may mutate a nominally frozen input after admission. This + value therefore owns a freshly parsed lock and re-admitted archive before + any downstream identity or USTAR layout is derived from it. + """ + + source_lock: SourceReleaseLockV1 + source: SafeSourceArchiveV1 + files: tuple[tuple[str, int, bytes], ...] + + def __init__( + self, + source_lock: SourceReleaseLockV1, + source: SafeSourceArchiveV1, + files: tuple[tuple[str, int, bytes], ...], + *, + _token: object, + ) -> None: + if _token is not _REPLAYED_SOURCE_MATERIALIZATION_TOKEN: + raise TypeError( + "ReplayedSourceMaterializationV1 is created only by source replay" + ) + if ( + type(source_lock) is not SourceReleaseLockV1 + or type(source) is not SafeSourceArchiveV1 + or type(files) is not tuple + or not files + or any( + type(path) is not str + or type(mode) is not int + or type(contents) is not bytes + for path, mode, contents in files + ) + ): + raise TypeError("invalid replayed source materialization") + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "source", source) + object.__setattr__(self, "files", files) + + +def archive_file_manifest_bytes_v1( + files_value: tuple[ArchiveFileV1, ...], +) -> bytes: + """Encode the one canonical retained-file manifest owned by provenance.""" + + if type(files_value) is not tuple or any( + type(item) is not ArchiveFileV1 for item in files_value + ): + raise TypeError("invalid archive file manifest") + paths = tuple(item.path for item in files_value) + if ( + any(type(path) is not str for path in paths) + or paths != tuple(sorted(paths)) + or len(paths) != len(set(paths)) + or len(paths) != len({path.lower() for path in paths}) + ): + raise TypeError("invalid archive file manifest") + chunks: list[bytes] = [len(files_value).to_bytes(8, "big")] + for item in files_value: + path = _relative_path(item.path, "archive-file-manifest-v1", "path") + if ( + type(item.mode) is not int + or item.mode not in ALLOWED_REGULAR_MODES_V1 + or type(item.length) is not int + or item.length < 0 + or item.length >= 1 << 64 + ): + raise TypeError("invalid archive file coordinate") + _digest(item.sha256, "archive-file-manifest-v1", "sha256") + chunks.extend( + ( + path, + item.mode.to_bytes(4, "big"), + item.length.to_bytes(8, "big"), + item.sha256, + ) + ) + return b"".join(_blob(chunk) for chunk in chunks) + + +def _source_archive_coordinates_from_replayed_v1( + source_lock: SourceReleaseLockV1, + replayed: SafeSourceArchiveV1, +) -> tuple[bytes, ...]: + """Encode the sole coordinate tuple shared by replay and owned snapshots. + + This is deliberately a leaf: callers establish whether their snapshot is + fresh or retained. Keeping only the wire projection here prevents those + two ownership paths from quietly acquiring different source identities. + """ + + if ( + type(source_lock) is not SourceReleaseLockV1 + or type(replayed) is not SafeSourceArchiveV1 + ): + raise TypeError("invalid replayed source snapshot") + archive = replayed.archive_bytes + if type(archive) is not bytes: + raise TypeError("invalid replayed source archive") + manifest = archive_file_manifest_bytes_v1(replayed.files) + return ( + bytes((int(source_lock.role),)), + source_lock.encode(), + replayed.source_lock_identity, + replayed.archive_sha256, + replayed.tree_identity, + replayed.regular_file_count.to_bytes(8, "big"), + replayed.regular_file_bytes.to_bytes(8, "big"), + manifest, + len(archive).to_bytes(8, "big"), + replayed.archive_sha256, + ) + + +def source_archive_replay_coordinates_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[bytes, ...]: + """Replay coordinates without creating separate extracted file-byte buffers.""" + + source_lock, replayed, _raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + return _source_archive_coordinates_from_replayed_v1(source_lock, replayed) + + +def _materialized_source_coordinates_v1( + value: ReplayedSourceMaterializationV1, +) -> tuple[bytes, ...]: + """Encode coordinates already owned by one operation without replaying it.""" + + if type(value) is not ReplayedSourceMaterializationV1: + raise TypeError("value must be ReplayedSourceMaterializationV1") + return _source_archive_coordinates_from_replayed_v1( + value.source_lock, + value.source, + ) + + +_SafeSourceClosureV1: TypeAlias = tuple[ + SafeSourceArchiveV1, + SafeSourceArchiveV1, + SafeSourceArchiveV1, +] + + +def _validate_admitted_source_closure_v1( + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, + artifact: str, +) -> None: + _digest(source_lock_identity, artifact, "source_lock_identity") + if ( + type(sources) is not tuple + or len(sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not SafeSourceArchiveV1 for source in sources) + ): + raise TypeError(f"invalid admitted source tuple for {artifact}") + + +def _admitted_source_closure_identity_v1( + label: bytes, + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, +) -> bytes: + chunks = [source_lock_identity] + for ordinal, source in enumerate(sources): + chunks.extend( + ( + bytes((ordinal,)), + source.source_lock_identity, + source.archive_sha256, + source.tree_identity, + ) + ) + return _identity(label, b"".join(chunks)) + + +@dataclass(frozen=True, init=False) +class AdmittedArbSourcesV1: + """One ordered capability for the complete locked Arb dependency closure.""" + + source_lock_identity: bytes + sources: _SafeSourceClosureV1 + + def __init__( + self, + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, + *, + _token: object, + ) -> None: + if _token is not _ADMITTED_ARB_SOURCES_TOKEN: + raise TypeError("AdmittedArbSourcesV1 is created only by source admission") + _validate_admitted_source_closure_v1( + source_lock_identity, + sources, + "admitted-arb-sources-v1", + ) + object.__setattr__(self, "source_lock_identity", source_lock_identity) + object.__setattr__(self, "sources", sources) + + @property + def identity(self) -> bytes: + return _admitted_source_closure_identity_v1( + ADMITTED_ARB_SOURCES_ID_LABEL_V1, + self.source_lock_identity, + self.sources, + ) + + +@dataclass(frozen=True, init=False) +class AdmittedMpfiSourcesV1: + """One ordered capability for the complete locked MPFI dependency closure.""" + + source_lock_identity: bytes + sources: _SafeSourceClosureV1 + + def __init__( + self, + source_lock_identity: bytes, + sources: _SafeSourceClosureV1, + *, + _token: object, + ) -> None: + if _token is not _ADMITTED_MPFI_SOURCES_TOKEN: + raise TypeError("AdmittedMpfiSourcesV1 is created only by source admission") + _validate_admitted_source_closure_v1( + source_lock_identity, + sources, + "admitted-mpfi-sources-v1", + ) + object.__setattr__(self, "source_lock_identity", source_lock_identity) + object.__setattr__(self, "sources", sources) + + @property + def identity(self) -> bytes: + return _admitted_source_closure_identity_v1( + ADMITTED_MPFI_SOURCES_ID_LABEL_V1, + self.source_lock_identity, + self.sources, + ) + + +@dataclass(frozen=True, init=False) +class ReplayedSourceClosureV1: + """One operation-owned three-source snapshot without caller-held refs.""" + + source_lock: ArbSourceLockV1 | MpfiSourceLockV1 + admitted_sources: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1 + sources: tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ] + + def __init__( + self, + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + admitted_sources: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, + sources: tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ], + *, + _token: object, + ) -> None: + if _token is not _REPLAYED_SOURCE_CLOSURE_TOKEN: + raise TypeError("ReplayedSourceClosureV1 is created only by closure replay") + if ( + type(source_lock) not in (ArbSourceLockV1, MpfiSourceLockV1) + or type(admitted_sources) + not in (AdmittedArbSourcesV1, AdmittedMpfiSourcesV1) + or type(sources) is not tuple + or len(sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not ReplayedSourceMaterializationV1 for source in sources) + or tuple(source.source for source in sources) != admitted_sources.sources + ): + raise TypeError("invalid replayed source closure") + object.__setattr__(self, "source_lock", source_lock) + object.__setattr__(self, "admitted_sources", admitted_sources) + object.__setattr__(self, "sources", sources) + + +def _decompress_exact( + archive: bytes, + archive_format: ArchiveFormatV1, + expected_length: int, +) -> bytes: + try: + if archive_format is ArchiveFormatV1.TAR_GZIP: + decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS) + output = decompressor.decompress(archive, expected_length + 1) + if len(output) > expected_length: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + while not decompressor.eof and decompressor.unconsumed_tail: + remaining = expected_length + 1 - len(output) + if remaining <= 0: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + output += decompressor.decompress( + decompressor.unconsumed_tail, + remaining, + ) + if len(output) > expected_length: + break + if not decompressor.eof: + if len(output) > expected_length: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + _fail( + "source-archive-v1", + ProvenanceReasonV1.DECOMPRESSION_FAILED, + "truncated gzip stream", + ) + if decompressor.unused_data: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TRAILING_COMPRESSED_DATA, + "concatenated or trailing gzip data", + ) + else: + decompressor_xz = lzma.LZMADecompressor(format=lzma.FORMAT_XZ) + output = decompressor_xz.decompress(archive, max_length=expected_length + 1) + if len(output) > expected_length: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + while not decompressor_xz.eof and not decompressor_xz.needs_input: + remaining = expected_length + 1 - len(output) + if remaining <= 0: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + output += decompressor_xz.decompress( + b"", max_length=remaining + ) + if len(output) > expected_length: + break + if not decompressor_xz.eof: + if len(output) > expected_length: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "expanded beyond lock", + ) + _fail( + "source-archive-v1", + ProvenanceReasonV1.DECOMPRESSION_FAILED, + "truncated xz stream", + ) + if decompressor_xz.unused_data: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TRAILING_COMPRESSED_DATA, + "concatenated or trailing xz data", + ) + except (zlib.error, lzma.LZMAError, EOFError): + _fail("source-archive-v1", ProvenanceReasonV1.DECOMPRESSION_FAILED, "invalid compressed stream") + if len(output) != expected_length: + _fail( + "source-archive-v1", + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + "tar stream length", + ) + return output + + +def _tree_identity(files: tuple[ArchiveFileV1, ...]) -> bytes: + chunks = [len(files).to_bytes(8, "big")] + for item in files: + chunks.extend( + ( + _blob(item.path.encode("ascii")), + item.mode.to_bytes(4, "big"), + item.length.to_bytes(8, "big"), + item.sha256, + ) + ) + encoded = b"".join(chunks) + return _identity(SOURCE_TREE_ID_LABEL_V1, encoded) + + +def _scan_tar(expected: SourceReleaseLockV1, raw_tar: bytes) -> tuple[ArchiveFileV1, ...]: + files: list[ArchiveFileV1] = [] + seen: set[str] = set() + folded: set[str] = set() + directories: set[str] = set() + root = expected.root_prefix[:-1] + last_payload_end = 0 + admitted_file_bytes = 0 + try: + with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive: + if archive.pax_headers: + _fail("source-archive-v1", ProvenanceReasonV1.NONCANONICAL_TAR, "global pax headers") + for member in archive: + if member.pax_headers: + _fail("source-archive-v1", ProvenanceReasonV1.NONCANONICAL_TAR, "member pax headers") + name = member.name + try: + name.encode("ascii") + except UnicodeEncodeError: + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_PATH, "non-ASCII member") + if name.startswith("/"): + _fail("source-archive-v1", ProvenanceReasonV1.ABSOLUTE_PATH, name) + if "\\" in name or any(part in ("", ".", "..") for part in name.split("/")): + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_PATH, name) + if name in seen: + _fail("source-archive-v1", ProvenanceReasonV1.DUPLICATE_PATH, name) + casefolded = name.lower() + if casefolded in folded: + _fail("source-archive-v1", ProvenanceReasonV1.CASE_COLLISION, name) + seen.add(name) + folded.add(casefolded) + last_payload_end = max( + last_payload_end, + member.offset_data + ((member.size + TAR_BLOCK_BYTES - 1) // TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES, + ) + if member.issym() or member.islnk(): + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_LINK, name) + if not (member.isdir() or member.isreg()): + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_MEMBER_TYPE, name) + if member.isdir(): + if member.mode != ALLOWED_DIRECTORY_MODE_V1: + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_MODE, name) + if name != root and not name.startswith(expected.root_prefix): + _fail("source-archive-v1", ProvenanceReasonV1.ROOT_MISMATCH, name) + parent = str(PurePosixPath(name).parent) + if name != root and parent not in directories: + _fail( + "source-archive-v1", + ProvenanceReasonV1.UNSAFE_PATH, + f"undeclared parent of {name}", + ) + directories.add(name) + continue + if not name.startswith(expected.root_prefix): + _fail("source-archive-v1", ProvenanceReasonV1.ROOT_MISMATCH, name) + relative = name[len(expected.root_prefix) :] + _relative_path(relative, "source-archive-v1", "member path") + parent = str(PurePosixPath(name).parent) + if parent not in directories: + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_PATH, f"undeclared parent of {name}") + if member.mode not in ALLOWED_REGULAR_MODES_V1: + _fail("source-archive-v1", ProvenanceReasonV1.UNSAFE_MODE, name) + if len(files) >= expected.regular_file_count: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_COUNT_MISMATCH, "too many files") + if member.size > expected.regular_file_bytes - admitted_file_bytes: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_BYTES_MISMATCH, "declared bytes exceed lock") + stream = archive.extractfile(member) + if stream is None: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_CONTENT_MISMATCH, name) + hasher = hashlib.sha256() + length = 0 + while True: + chunk = stream.read(READ_CHUNK_BYTES) + if not chunk: + break + length += len(chunk) + if length > member.size: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_CONTENT_MISMATCH, name) + hasher.update(chunk) + if length != member.size: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_CONTENT_MISMATCH, name) + files.append(ArchiveFileV1(relative, member.mode, length, hasher.digest())) + admitted_file_bytes += length + except tarfile.TarError: + _fail("source-archive-v1", ProvenanceReasonV1.NONCANONICAL_TAR, "invalid tar stream") + if root not in directories: + _fail("source-archive-v1", ProvenanceReasonV1.ROOT_MISMATCH, "missing root directory") + trailing = raw_tar[last_payload_end:] + if len(trailing) < TAR_END_MARKER_BYTES or any(trailing): + _fail("source-archive-v1", ProvenanceReasonV1.NONCANONICAL_TAR, "nonzero or missing tar terminator") + return tuple(sorted(files, key=lambda item: item.path)) + + +def _admit_source_archive_once( + expected: SourceReleaseLockV1, + archive: bytes, +) -> tuple[SafeSourceArchiveV1, bytes]: + + if type(expected) is not SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + if type(archive) is not bytes: + raise TypeError("archive must be owned bytes") + if len(archive) != expected.archive_length: + _fail("source-archive-v1", ProvenanceReasonV1.ARCHIVE_LENGTH_MISMATCH, "archive length") + archive_sha256 = hashlib.sha256(archive).digest() + if archive_sha256 != expected.archive_sha256: + _fail("source-archive-v1", ProvenanceReasonV1.ARCHIVE_DIGEST_MISMATCH, "archive digest") + raw_tar = _decompress_exact(archive, expected.archive_format, expected.tar_stream_length) + files = _scan_tar(expected, raw_tar) + if len(files) != expected.regular_file_count: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_COUNT_MISMATCH, "regular file count") + total_bytes = sum(item.length for item in files) + if total_bytes != expected.regular_file_bytes: + _fail("source-archive-v1", ProvenanceReasonV1.FILE_BYTES_MISMATCH, "regular file bytes") + by_path = {item.path: item for item in files} + for legal_file in expected.legal_files: + actual = by_path.get(legal_file.path) + if ( + actual is None + or actual.length != legal_file.length + or actual.sha256 != legal_file.sha256 + ): + _fail( + "source-archive-v1", + ProvenanceReasonV1.LEGAL_FILES_MISMATCH, + legal_file.path, + ) + if isinstance(expected.integrity, GitContentRelationPolicyV1): + for path in expected.integrity.omitted_paths: + if path in by_path: + _fail( + "source-archive-v1", + ProvenanceReasonV1.CONTENT_RELATION_MISMATCH, + f"omitted path present: {path}", + ) + for release_only in expected.integrity.project_pinned_release_only_files: + actual = by_path.get(release_only.path) + if ( + actual is None + or actual.mode != release_only.mode + or actual.length != release_only.length + or actual.sha256 != release_only.sha256 + ): + _fail( + "source-archive-v1", + ProvenanceReasonV1.CONTENT_RELATION_MISMATCH, + release_only.path, + ) + tree_identity = _tree_identity(files) + admitted = SafeSourceArchiveV1( + expected.identity, + archive_sha256, + tree_identity, + len(files), + total_bytes, + files, + archive, + _token=_SAFE_ARCHIVE_TOKEN, + ) + return admitted, raw_tar + + +def admit_source_archive(expected: SourceReleaseLockV1, archive: bytes) -> SafeSourceArchiveV1: + """Hash then scan one locked archive; this establishes no origin trust.""" + + source_lock = _canonical_source_lock_for_replay_v1(expected) + admitted, _raw_tar = _admit_source_archive_once(source_lock, archive) + return admitted + + +def _canonical_source_lock_for_replay_v1( + expected: SourceReleaseLockV1, +) -> SourceReleaseLockV1: + if type(expected) is not SourceReleaseLockV1: + raise TypeError("expected must be SourceReleaseLockV1") + try: + return _rebuild_source_release_lock_v1(expected) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source lock", + ) + + +_RetainedSourceArchiveSnapshotV1: TypeAlias = tuple[ + bytes, + bytes, + bytes, + int, + int, + bytes, + bytes, +] + + +def _retained_source_archive_snapshot_v1( + admitted: SafeSourceArchiveV1, +) -> _RetainedSourceArchiveSnapshotV1: + """Copies only exact primitives before a replay can re-enter caller code.""" + + if type(admitted) is not SafeSourceArchiveV1: + raise TypeError("admitted must be SafeSourceArchiveV1") + try: + source_lock_identity = admitted.source_lock_identity + archive_sha256 = admitted.archive_sha256 + tree_identity = admitted.tree_identity + regular_file_count = admitted.regular_file_count + regular_file_bytes = admitted.regular_file_bytes + files = admitted.files + archive = admitted.archive_bytes + _digest( + source_lock_identity, + "source-archive-replay-v1", + "source_lock_identity", + ) + _digest( + archive_sha256, + "source-archive-replay-v1", + "archive_sha256", + ) + _digest(tree_identity, "source-archive-replay-v1", "tree_identity") + _positive( + regular_file_count, + "source-archive-replay-v1", + "regular_file_count", + ) + _positive( + regular_file_bytes, + "source-archive-replay-v1", + "regular_file_bytes", + ) + if type(archive) is not bytes: + raise TypeError("archive must be exact bytes") + manifest = archive_file_manifest_bytes_v1(files) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source capability", + ) + return ( + source_lock_identity, + archive_sha256, + tree_identity, + regular_file_count, + regular_file_bytes, + manifest, + archive, + ) + + +def _replay_source_archive_from_retained_v1( + source_lock: SourceReleaseLockV1, + retained: _RetainedSourceArchiveSnapshotV1, +) -> tuple[SafeSourceArchiveV1, bytes]: + """Re-admit one copied archive before extracting individual file-byte buffers.""" + + ( + retained_source_lock_identity, + retained_archive_sha256, + retained_tree_identity, + retained_file_count, + retained_file_bytes, + retained_manifest, + archive, + ) = retained + + try: + replayed, raw_tar = _admit_source_archive_once(source_lock, archive) + replayed_manifest = archive_file_manifest_bytes_v1(replayed.files) + except ProvenanceErrorV1: + raise + except (AttributeError, TypeError, ValueError, OverflowError): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "archive replay failed", + ) + if ( + retained_source_lock_identity != replayed.source_lock_identity + or retained_archive_sha256 != replayed.archive_sha256 + or retained_tree_identity != replayed.tree_identity + or retained_file_count != replayed.regular_file_count + or retained_file_bytes != replayed.regular_file_bytes + or retained_manifest != replayed_manifest + ): + _fail( + "source-archive-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "retained source coordinates changed", + ) + return replayed, raw_tar + + +def _replay_admitted_source_archive_snapshot_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[SourceReleaseLockV1, SafeSourceArchiveV1, bytes]: + """Makes the source-owned replay needed by metadata and body consumers.""" + + source_lock = _canonical_source_lock_for_replay_v1(expected) + replayed, raw_tar = _replay_source_archive_from_retained_v1( + source_lock, + _retained_source_archive_snapshot_v1(admitted), + ) + return source_lock, replayed, raw_tar + + +def _materialize_replayed_source_files_v1( + source_lock: SourceReleaseLockV1, + replayed: SafeSourceArchiveV1, + raw_tar: bytes, +) -> tuple[tuple[str, int, bytes], ...]: + """Reads only one locally replayed archive and its canonical lock snapshot.""" + + expected_by_path = {item.path: item for item in replayed.files} + values: list[tuple[str, int, bytes]] = [] + seen: set[str] = set() + try: + with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive: + for member in archive: + if member.isdir(): + continue + if not member.isreg() or not member.name.startswith(source_lock.root_prefix): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "unexpected archive member", + ) + relative = member.name[len(source_lock.root_prefix) :] + coordinate = expected_by_path.get(relative) + if ( + coordinate is None + or relative in seen + or member.mode != coordinate.mode + or member.size != coordinate.length + ): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "archive file set changed", + ) + stream = archive.extractfile(member) + if stream is None: + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + chunks: list[bytes] = [] + length = 0 + hasher = hashlib.sha256() + while True: + chunk = stream.read(READ_CHUNK_BYTES) + if not chunk: + break + length += len(chunk) + if length > coordinate.length: + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + chunks.append(chunk) + hasher.update(chunk) + if ( + length != coordinate.length + or hasher.digest() != coordinate.sha256 + ): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + relative, + ) + values.append((relative, coordinate.mode, b"".join(chunks))) + seen.add(relative) + except ProvenanceErrorV1: + raise + except (OSError, tarfile.TarError, ValueError): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.NONCANONICAL_TAR, + "archive replay failed", + ) + if seen != set(expected_by_path): + _fail( + "source-archive-materialization-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "archive file set is incomplete", + ) + return tuple(sorted(values)) + + +def replay_materialize_admitted_source_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> ReplayedSourceMaterializationV1: + """Builds one owned replay snapshot for all source-derived consumers. + + The snapshot is deliberately below engine and recipe layers. Its lock, + archive identity and materialized bytes originate from the same fresh + replay, so a caller-held capability cannot relabel already-read bytes. + """ + + source_lock, replayed, raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + files = _materialize_replayed_source_files_v1(source_lock, replayed, raw_tar) + return ReplayedSourceMaterializationV1( + source_lock, + replayed, + files, + _token=_REPLAYED_SOURCE_MATERIALIZATION_TOKEN, + ) + + +def replay_admitted_source_archive_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[SafeSourceArchiveV1, bytes]: + """Return a bounded-decompressed replay without extracted file-byte buffers.""" + + _source_lock, replayed, raw_tar = _replay_admitted_source_archive_snapshot_v1( + expected, + admitted, + ) + return replayed, raw_tar + + +def materialize_admitted_source_files_v1( + expected: SourceReleaseLockV1, + admitted: SafeSourceArchiveV1, +) -> tuple[tuple[str, int, bytes], ...]: + """Return exact relative files from one owned replay snapshot.""" + + return replay_materialize_admitted_source_v1(expected, admitted).files + + +def _source_closure_lock_snapshot_v1( + expected: ArbSourceLockV1 | MpfiSourceLockV1, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + try: + return _rebuild_source_closure_lock_v1(expected) + except ( + ProvenanceErrorV1, + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained source closure lock", + ) + + +def snapshot_source_closure_lock_v1( + expected: object, +) -> ArbSourceLockV1 | MpfiSourceLockV1: + """Return a detached structural lock snapshot before a public replay.""" + + return _source_closure_lock_snapshot_v1(expected) + + +def _source_capability_matches_lock_v1( + lock: SourceReleaseLockV1, + source: SafeSourceArchiveV1, +) -> _RetainedSourceArchiveSnapshotV1: + retained = _retained_source_archive_snapshot_v1(source) + ( + retained_lock_identity, + retained_archive_sha256, + _retained_tree_identity, + retained_file_count, + retained_file_bytes, + _retained_manifest, + _archive, + ) = retained + if ( + retained_lock_identity != lock.identity + or retained_archive_sha256 != lock.archive_sha256 + or retained_file_count != lock.regular_file_count + or retained_file_bytes != lock.regular_file_bytes + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "source capability does not match ordered lock", + ) + return retained + + +def snapshot_admitted_source_closure_v1( + expected: object, + admitted: object, +) -> AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1: + """Return a detached source-closure declaration without extracted file buffers. + + The archive is re-admitted so the retained manifest, tree and compressed + bytes agree. Re-admission bounded-decompresses and scans the tar, but + unlike an operation replay it does not retain separate file-byte buffers; + consumers that need those buffers must still call + ``replay_admitted_source_closure_v1``. + """ + + canonical_lock = _source_closure_lock_snapshot_v1(expected) + if ( + ( + type(canonical_lock) is ArbSourceLockV1 + and type(admitted) is not AdmittedArbSourcesV1 + ) + or ( + type(canonical_lock) is MpfiSourceLockV1 + and type(admitted) is not AdmittedMpfiSourcesV1 + ) + ): + raise TypeError("admitted sources do not match the source lock kind") + try: + retained_lock_identity = admitted.source_lock_identity + retained_sources = admitted.sources + _digest( + retained_lock_identity, + "source-closure-snapshot-v1", + "source_lock_identity", + ) + if retained_lock_identity != canonical_lock.identity: + _fail( + "source-closure-snapshot-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "admitted closure lock identity changed", + ) + sources = _validate_source_replay_arguments_v1( + canonical_lock.sources, + retained_sources, + ) + snapshots = _replay_source_archives_v1( + canonical_lock.sources, + sources, + ) + except ProvenanceErrorV1: + raise + except ( + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-snapshot-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained admitted closure", + ) + return _fresh_admitted_source_closure_v1(canonical_lock, snapshots) + + +def _validate_source_replay_arguments_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, +) -> tuple[SafeSourceArchiveV1, SafeSourceArchiveV1, SafeSourceArchiveV1]: + if ( + type(expected_sources) is not tuple + or len(expected_sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(lock) is not SourceReleaseLockV1 for lock in expected_sources) + ): + raise TypeError("expected sources must be three SourceReleaseLockV1 values") + if ( + type(sources) is not tuple + or len(sources) != SOURCE_CLOSURE_COUNT_V1 + or any(type(source) is not SafeSourceArchiveV1 for source in sources) + ): + raise TypeError("sources must be three SafeSourceArchiveV1 values") + first, second, third = sources + return first, second, third + + +def _replay_source_archives_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, +) -> _SafeSourceClosureV1: + """Re-admit a closure for metadata without retaining file-byte buffers.""" + + admitted_sources = _validate_source_replay_arguments_v1(expected_sources, sources) + replayed: list[SafeSourceArchiveV1] = [] + for lock, source in zip(expected_sources, admitted_sources, strict=True): + retained = _source_capability_matches_lock_v1(lock, source) + fresh, _raw_tar = _replay_source_archive_from_retained_v1(lock, retained) + replayed.append(fresh) + first, second, third = replayed + return first, second, third + + +def _replay_source_materializations_v1( + expected_sources: _SourceClosureV1, + sources: _SafeSourceClosureV1, +) -> tuple[ + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, + ReplayedSourceMaterializationV1, +]: + """Materialize only the operation path that actually needs source bytes.""" + + admitted_sources = _validate_source_replay_arguments_v1(expected_sources, sources) + materializations: list[ReplayedSourceMaterializationV1] = [] + for lock, source in zip(expected_sources, admitted_sources, strict=True): + retained = _source_capability_matches_lock_v1(lock, source) + replayed, raw_tar = _replay_source_archive_from_retained_v1(lock, retained) + materializations.append( + ReplayedSourceMaterializationV1( + lock, + replayed, + _materialize_replayed_source_files_v1(lock, replayed, raw_tar), + _token=_REPLAYED_SOURCE_MATERIALIZATION_TOKEN, + ) + ) + first, second, third = materializations + return first, second, third + + +def _fresh_admitted_source_closure_v1( + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1: + if type(source_lock) is ArbSourceLockV1: + return AdmittedArbSourcesV1( + source_lock.identity, + sources, + _token=_ADMITTED_ARB_SOURCES_TOKEN, + ) + if type(source_lock) is MpfiSourceLockV1: + return AdmittedMpfiSourcesV1( + source_lock.identity, + sources, + _token=_ADMITTED_MPFI_SOURCES_TOKEN, + ) + raise TypeError("source lock is not a supported closure") + + +def _admitted_closure_snapshot_v1( + source_lock: ArbSourceLockV1 | MpfiSourceLockV1, + admitted: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, +) -> ReplayedSourceClosureV1: + canonical_lock = _source_closure_lock_snapshot_v1(source_lock) + if ( + ( + type(canonical_lock) is ArbSourceLockV1 + and type(admitted) is not AdmittedArbSourcesV1 + ) + or ( + type(canonical_lock) is MpfiSourceLockV1 + and type(admitted) is not AdmittedMpfiSourcesV1 + ) + ): + raise TypeError("admitted sources do not match the source lock kind") + try: + retained_lock_identity = admitted.source_lock_identity + retained_sources = admitted.sources + _digest( + retained_lock_identity, + "source-closure-replay-v1", + "source_lock_identity", + ) + if retained_lock_identity != canonical_lock.identity: + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "admitted closure lock identity changed", + ) + _validate_source_replay_arguments_v1( + canonical_lock.sources, + retained_sources, + ) + except ProvenanceErrorV1: + raise + except ( + AttributeError, + TypeError, + ValueError, + OverflowError, + UnicodeError, + ): + _fail( + "source-closure-replay-v1", + ProvenanceReasonV1.FOREIGN_BINDING, + "invalid retained admitted closure", + ) + snapshots = _replay_source_materializations_v1( + canonical_lock.sources, + retained_sources, + ) + fresh = _fresh_admitted_source_closure_v1( + canonical_lock, + tuple(snapshot.source for snapshot in snapshots), + ) + return ReplayedSourceClosureV1( + canonical_lock, + fresh, + snapshots, + _token=_REPLAYED_SOURCE_CLOSURE_TOKEN, + ) + + +def admit_arb_sources( + expected: ArbSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedArbSourcesV1: + """Replay and own three archives before minting one Arb closure capability.""" + + if type(expected) is not ArbSourceLockV1: + raise TypeError("expected must be ArbSourceLockV1") + canonical_lock = _source_closure_lock_snapshot_v1(expected) + replayed_sources = _replay_source_archives_v1( + canonical_lock.sources, + sources, + ) + fresh = _fresh_admitted_source_closure_v1(canonical_lock, replayed_sources) + if type(fresh) is not AdmittedArbSourcesV1: + raise AssertionError("Arb closure kind changed during admission") + return fresh + + +def admit_mpfi_sources( + expected: MpfiSourceLockV1, + sources: _SafeSourceClosureV1, +) -> AdmittedMpfiSourcesV1: + """Replay and own three archives before minting one MPFI closure capability.""" + + if type(expected) is not MpfiSourceLockV1: + raise TypeError("expected must be MpfiSourceLockV1") + canonical_lock = _source_closure_lock_snapshot_v1(expected) + replayed_sources = _replay_source_archives_v1( + canonical_lock.sources, + sources, + ) + fresh = _fresh_admitted_source_closure_v1(canonical_lock, replayed_sources) + if type(fresh) is not AdmittedMpfiSourcesV1: + raise AssertionError("MPFI closure kind changed during admission") + return fresh + + +def replay_admitted_source_closure_v1( + expected: ArbSourceLockV1 | MpfiSourceLockV1, + admitted: AdmittedArbSourcesV1 | AdmittedMpfiSourcesV1, +) -> ReplayedSourceClosureV1: + """Take one local source-closure snapshot for a build-like operation.""" + + return _admitted_closure_snapshot_v1(expected, admitted) + + +def _legal_file(path: str, length: int, digest_hex: str) -> LegalFileV1: + return LegalFileV1(path, length, bytes.fromhex(digest_hex)) + + +def _gmp_source_release_v1() -> SourceReleaseLockV1: + return SourceReleaseLockV1( + SourceRoleV1.GMP, + "6.3.0", + "https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz", + ArchiveFormatV1.TAR_XZ, + 2_094_196, + bytes.fromhex("a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898"), + 18_759_680, + "gmp-6.3.0/", + 2_156, + 16_998_222, + ( + _legal_file("COPYING", 35_147, "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903"), + _legal_file("COPYING.LESSERv3", 7_639, "a853c2ffec17057872340eee242ae4d96cbf2b520ae27d903e1b2fef1a5f9d1c"), + _legal_file("COPYINGv2", 18_092, "8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643"), + _legal_file("COPYINGv3", 35_150, "e6037104443f9a7829b2aa7c5370d0789a7bda3ca65a0b904cdc0c2e285d9195"), + _legal_file("README", 4_051, "5e9f9325fd702bc4bcda27d7a78fea88a2a09fa39b4b15ac7b9b205e0863dc7e"), + ), + DetachedSignaturePolicyV1( + "https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz.sig", + 374, + bytes.fromhex("94def8c1a731854de684689126046ec93589147abd4cd0025f12d741d323aa82"), + bytes.fromhex("928ac84aa0e2134bbb335cd439110dc3f9b967eb04caff4a44dd5d04a3f13474"), + bytes.fromhex("343c2ff0fbee5ec2edbef399f3599ff828c67298"), + ), + ) + + +def _mpfr_source_release_v1() -> SourceReleaseLockV1: + return SourceReleaseLockV1( + SourceRoleV1.MPFR, + "4.2.2", + "https://www.mpfr.org/mpfr-4.2.2/mpfr-4.2.2.tar.xz", + ArchiveFormatV1.TAR_XZ, + 1_505_596, + bytes.fromhex("b67ba0383ef7e8a8563734e2e889ef5ec3c3b898a01d00fa0a6869ad81c6ce01"), + 10_045_440, + "mpfr-4.2.2/", + 572, + 9_590_620, + ( + _legal_file("COPYING", 35_149, "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"), + _legal_file("COPYING.LESSER", 7_652, "e3a994d82e644b03a792a930f574002658412f62407f5fee083f2555c5f23118"), + _legal_file("README", 3_333, "74e733d2cfa1a6f4e6530326ed460f13ac9e4a5d79bb0f682ab67db2c9dc4d5b"), + ), + DetachedSignaturePolicyV1( + "https://www.mpfr.org/mpfr-4.2.2/mpfr-4.2.2.tar.xz.asc", + 228, + bytes.fromhex("c6264c9a3652bc40775205ce90e7c96cea5058629e2e68f9eede5d8213f23ee6"), + bytes.fromhex("3fe00f68bbf3888ae185b950d4db0f708dd01b6159cb03dec77296f9045b6372"), + bytes.fromhex("a534be3f83e241d918280aeb5831d11a0d4db02a"), + ), + ) + + +def arb_source_lock_v1() -> ArbSourceLockV1: + """Return the exact published source declarations for the first Arb lane.""" + + gmp = _gmp_source_release_v1() + mpfr = _mpfr_source_release_v1() + omitted = ( + ".gitattributes", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md", + ".github/codecov.yml", + ".github/workflows/CI.yml", + ".github/workflows/docs.yml", + ".github/workflows/push_CI.yml", + ".github/workflows/release.yml", + ".gitignore", + "dev/bench.py", + "dev/check_examples.sh", + "dev/check_prototypes", + "dev/conway/convert_cp_to_new_form.jl", + "dev/conway/notes.c", + "dev/find_gmp_mpfr.jl", + "dev/gen_mul_basecase.jl", + "dev/gen_mul_basecase.py", + "dev/gen_mulhigh_basecase.jl", + "dev/make_dist.sh", + ) + project_pinned_release_only_files = ( + ProjectPinnedReleaseOnlyFileV1( + "config/install-sh", + 0o700, + 15_358, + bytes.fromhex("3d7488bebd0cfc9b5c440c55d5b44f1c6e2e3d3e19894821bae4a27f9307f1d2"), + ), + ProjectPinnedReleaseOnlyFileV1( + "config/ltmain.sh", + 0o755, + 333_053, + bytes.fromhex("579a1445e6a9a8b0809a44aa9f908387d4a43a2a440c9b84ea979f2b4f17816c"), + ), + ProjectPinnedReleaseOnlyFileV1( + "configure", + 0o755, + 731_646, + bytes.fromhex("43192d2f63812610726d943ada13bfc25864c39a8555314395d2d459d1502f45"), + ), + ProjectPinnedReleaseOnlyFileV1( + "src/config.h.in", + 0o644, + 6_645, + bytes.fromhex("af5b88c82a1549585b43a5dc856f3325d3513f423da0880f5459d913a25f9455"), + ), + ) + flint = SourceReleaseLockV1( + SourceRoleV1.FLINT_ARB, + "3.6.0", + "https://github.com/flintlib/flint/releases/download/v3.6.0/flint-3.6.0.tar.gz", + ArchiveFormatV1.TAR_GZIP, + 9_313_139, + bytes.fromhex("b95e2c7792f5eea4a1c8d2d42c4098434756832e57a094b295eb5dfdc9b4c36b"), + 56_811_520, + "flint-3.6.0/", + 10_112, + 48_758_775, + ( + _legal_file("COPYING", 35_149, "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"), + _legal_file("COPYING.LESSER", 7_652, "e3a994d82e644b03a792a930f574002658412f62407f5fee083f2555c5f23118"), + _legal_file("README.md", 3_008, "1a1c629fe32957b0bdf197c6048a83a987e8d28793234aff6feec5e1dcf7633f"), + ), + GitContentRelationPolicyV1( + "https://github.com/flintlib/flint.git", + "v3.6.0", + bytes.fromhex("8d5454b96761fafe4d5a9da76a369a602f500f49"), + bytes.fromhex("18d57417a96227b27dd5336881403dee6fdc851b"), + 10_108, + omitted, + project_pinned_release_only_files, + ), + ) + return ArbSourceLockV1((gmp, mpfr, flint)) + + +def mpfi_source_lock_v1() -> MpfiSourceLockV1: + """Return the exact source declarations for the first MPFI lane. + + MPFI 1.5.4 has no verified detached signature or archive-to-Git content + relation. Its archive is therefore named honestly as a project-pinned + byte digest while GMP and MPFR retain their independently signed locks. + """ + + mpfi = SourceReleaseLockV1( + SourceRoleV1.MPFI, + "1.5.4", + "https://perso.ens-lyon.fr/nathalie.revol/softwares/mpfi-1.5.4.tar.xz", + ArchiveFormatV1.TAR_XZ, + 370_932, + bytes.fromhex( + "819e98bc7dad7cf7e67c9ddb592f44545c300de143fe30bc29ca1b422b55306a" + ), + 3_502_080, + "mpfi-1.5.4/", + 495, + 3_117_639, + ( + _legal_file( + "COPYING", + 35_147, + "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903", + ), + _legal_file( + "COPYING.LESSER", + 7_651, + "da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768", + ), + _legal_file( + "README", + 1_336, + "dab7a52115f111ff3771dc4311a837919d45ffaa654e64c110af78bd2a003e20", + ), + ), + ProjectPinnedArchiveDigestPolicyV1(), + ) + return MpfiSourceLockV1( + (_gmp_source_release_v1(), _mpfr_source_release_v1(), mpfi) + ) diff --git a/proof/region/v1/region_proof_protocol.py b/proof/region/v1/region_proof_protocol.py index f11cc2ff..3778d6a3 100644 --- a/proof/region/v1/region_proof_protocol.py +++ b/proof/region/v1/region_proof_protocol.py @@ -47,7 +47,7 @@ DOMAIN_MAGIC_V1 = b"LCDOM1\0\0" POLICY_MAGIC_V1 = b"LCPOL1\0\0" JOB_MAGIC_V1 = b"LCJOB1\0\0" -MANIFEST_MAGIC_V1 = b"LCMAN1\0\0" +MANIFEST_MAGIC_V2 = b"LCMAN2\0\0" TRANSCRIPT_MAGIC_V1 = b"LCTRN1\0\0" RUN_CLAIM_MAGIC_V1 = b"LCRUN1\0\0" PROVENANCE_CLAIM_MAGIC_V1 = b"LCPRV1\0\0" @@ -56,7 +56,7 @@ DOMAIN_ID_LABEL_V1 = b"labcolors.proof-region.domain.v1\0" POLICY_ID_LABEL_V1 = b"labcolors.proof-region.policy.v1\0" JOB_ID_LABEL_V1 = b"labcolors.proof-region.job.v1\0" -MANIFEST_ID_LABEL_V1 = b"labcolors.proof-region.comparator-manifest.v1\0" +MANIFEST_ID_LABEL_V2 = b"labcolors.proof-region.comparator-manifest.v2\0" TRANSCRIPT_ID_LABEL_V1 = b"labcolors.proof-region.transcript.v1\0" RUN_CLAIM_ID_LABEL_V1 = b"labcolors.proof-region.run-claim.v1\0" PROVENANCE_CLAIM_ID_LABEL_V1 = b"labcolors.proof-region.evaluator-provenance-claim.v1\0" @@ -713,31 +713,100 @@ def identity(self) -> bytes: return _identity(JOB_ID_LABEL_V1, self.encode()) +def _snapshot_contextual_region_definition_v1( + value: object, +) -> ContextualRegionDefinitionV1: + if type(value) is not ContextualRegionDefinitionV1: + raise TypeError("definition must be ContextualRegionDefinitionV1") + fields_value = value.fields + if type(fields_value) is not tuple: + raise TypeError("definition fields must be an exact tuple") + return ContextualRegionDefinitionV1(tuple(fields_value), value.knot_count) + + +def _snapshot_reduced_domain_manifest_v1( + value: object, +) -> ReducedDomainManifestV1: + if type(value) is not ReducedDomainManifestV1: + raise TypeError("domain must be ReducedDomainManifestV1") + ranges = value.ranges + if type(ranges) is not tuple: + raise TypeError("domain ranges must be an exact tuple") + return ReducedDomainManifestV1(tuple(ranges), value.point_count) + + +def _snapshot_comparator_budget_v1(value: object) -> ComparatorBudgetV1: + if type(value) is not ComparatorBudgetV1: + raise TypeError("comparator budget must be ComparatorBudgetV1") + ladder = value.precision_ladder + if type(ladder) is not tuple: + raise TypeError("precision ladder must be an exact tuple") + return ComparatorBudgetV1( + value.kind, + tuple(ladder), + value.per_point_work, + value.global_pregrant, + ) + + +def _snapshot_proof_policy_v1(value: object) -> ProofPolicyV1: + if type(value) is not ProofPolicyV1: + raise TypeError("policy must be ProofPolicyV1") + comparators = value.comparators + if type(comparators) is not tuple or len(comparators) != 2: + raise TypeError("policy comparators must be an exact pair") + arb, mpfi = comparators + return ProofPolicyV1( + value.equality_release, + ( + _snapshot_comparator_budget_v1(arb), + _snapshot_comparator_budget_v1(mpfi), + ), + ) + + +def snapshot_proof_job_v1(value: object) -> ProofJobV1: + """Return a detached job from raw coordinates, never caller-dispatched wire methods.""" + + if type(value) is not ProofJobV1: + raise TypeError("job must be ProofJobV1") + return ProofJobV1( + _snapshot_contextual_region_definition_v1(value.definition), + value.formula_spec, + _snapshot_reduced_domain_manifest_v1(value.domain), + _snapshot_proof_policy_v1(value.policy), + ) + + @dataclass(frozen=True) -class ComparatorManifestV1: +class ComparatorManifestV2: kind: ComparatorKindV1 engine_release: bytes upstream_source: bytes - arithmetic_closure: bytes + arithmetic_input_set: bytes wrapper_source: bytes evaluator_source: bytes build_identity: bytes operation_allowlist: bytes - test_receipt: bytes - license_closure: bytes + test_observation: bytes + legal_file_set: bytes exclusions: bytes def __post_init__(self) -> None: if type(self.kind) is not ComparatorKindV1: - _fail("comparator-manifest-v1", 0, ProtocolReasonV1.UNKNOWN_RELEASE, "unknown comparator kind") - for field in fields(self): - if field.name != "kind": - _require_digest(getattr(self, field.name), "comparator-manifest-v1", field.name) + _fail("comparator-manifest-v2", 0, ProtocolReasonV1.UNKNOWN_RELEASE, "unknown comparator kind") + for manifest_field in fields(self): + if manifest_field.name != "kind": + _require_digest( + getattr(self, manifest_field.name), + "comparator-manifest-v2", + manifest_field.name, + ) @classmethod - def parse(cls, data: bytes) -> "ComparatorManifestV1": - reader = _Reader(data, "comparator-manifest-v1") - reader.magic(MANIFEST_MAGIC_V1) + def parse(cls, data: bytes) -> "ComparatorManifestV2": + reader = _Reader(data, "comparator-manifest-v2") + reader.magic(MANIFEST_MAGIC_V2) kind_offset = reader.offset try: kind = ComparatorKindV1(reader.u8()) @@ -751,33 +820,35 @@ def parse(cls, data: bytes) -> "ComparatorManifestV1": return result def encode(self) -> bytes: - return MANIFEST_MAGIC_V1 + bytes((int(self.kind),)) + b"".join( - getattr(self, field.name) for field in fields(self) if field.name != "kind" + return MANIFEST_MAGIC_V2 + bytes((int(self.kind),)) + b"".join( + getattr(self, manifest_field.name) + for manifest_field in fields(self) + if manifest_field.name != "kind" ) @cached_property def identity(self) -> bytes: - return _identity(MANIFEST_ID_LABEL_V1, self.encode()) + return _identity(MANIFEST_ID_LABEL_V2, self.encode()) @dataclass(frozen=True, init=False) -class ContentResolvedComparatorManifestV1: - manifest: ComparatorManifestV1 +class ContentResolvedComparatorManifestV2: + manifest: ComparatorManifestV2 def __new__(cls): - raise TypeError("use ContentResolvedComparatorManifestV1.admit") + raise TypeError("use ContentResolvedComparatorManifestV2.admit") @classmethod def admit( cls, - manifest: ComparatorManifestV1, + manifest: ComparatorManifestV2, resolve_content_address: Callable[[bytes], bytes | Iterable[bytes] | None], - ) -> "ContentResolvedComparatorManifestV1": + ) -> "ContentResolvedComparatorManifestV2": # A digest declaration alone is not source binding. This structural # transition only re-hashes caller-provided bytes; a future controlled # replay must establish where those bytes came from. - if type(manifest) is not ComparatorManifestV1: + if type(manifest) is not ComparatorManifestV2: _fail( - "comparator-manifest-v1", + "comparator-manifest-v2", 0, ProtocolReasonV1.INVALID_MANIFEST, "content resolution requires a canonical manifest", @@ -789,7 +860,7 @@ def admit( content = resolve_content_address(coordinate) if content is None: _fail( - "comparator-manifest-v1", + "comparator-manifest-v2", 0, ProtocolReasonV1.INVALID_MANIFEST, f"unresolved content address: {field.name}", @@ -801,7 +872,7 @@ def admit( chunks = iter(content) except TypeError: _fail( - "comparator-manifest-v1", + "comparator-manifest-v2", 0, ProtocolReasonV1.INVALID_MANIFEST, f"content resolver did not return bytes: {field.name}", @@ -810,7 +881,7 @@ def admit( for chunk in chunks: if type(chunk) is not bytes: _fail( - "comparator-manifest-v1", + "comparator-manifest-v2", 0, ProtocolReasonV1.INVALID_MANIFEST, f"non-byte content chunk: {field.name}", @@ -818,7 +889,7 @@ def admit( replay.update(chunk) if replay.digest() != coordinate: _fail( - "comparator-manifest-v1", + "comparator-manifest-v2", 0, ProtocolReasonV1.DIGEST_MISMATCH, f"content digest mismatch: {field.name}", @@ -1249,7 +1320,7 @@ def iter_witnesses(self) -> Iterator[WitnessV1]: cursor = end -def _validate_witness_alignment( +def validate_witness_alignment_v1( domain: ReducedDomainManifestV1, decision_bits: bytes, point_count: int, @@ -1333,7 +1404,7 @@ def __post_init__(self) -> None: def from_decisions( cls, job: ProofJobV1, - comparator: ContentResolvedComparatorManifestV1, + comparator: ContentResolvedComparatorManifestV2, decisions: Iterable[DecisionV1], witnesses: Iterable[WitnessV1], accounting_digest: bytes, @@ -1357,7 +1428,7 @@ def from_decisions( accounting_digest, witness_store, ) - _validate_witness_alignment( + validate_witness_alignment_v1( job.domain, result.decision_bits, result.point_count, @@ -1496,7 +1567,7 @@ def __post_init__(self) -> None: def for_transcript( cls, job: ProofJobV1, - comparator: ContentResolvedComparatorManifestV1, + comparator: ContentResolvedComparatorManifestV2, transcript: DecisionTranscriptV1, binary_identity: bytes, invocation_identity: bytes, @@ -1667,7 +1738,7 @@ def identity(self) -> bytes: def _admit_transcript( job: ProofJobV1, - comparator: ContentResolvedComparatorManifestV1, + comparator: ContentResolvedComparatorManifestV2, transcript: DecisionTranscriptV1, run: RunClaimV1, *, @@ -1686,7 +1757,7 @@ def _admit_transcript( or run.transcript_identity != transcript_identity ): _fail("dual-admission-v1", 0, ProtocolReasonV1.FOREIGN_BINDING, "foreign transcript/run coordinate") - _validate_witness_alignment( + validate_witness_alignment_v1( job.domain, transcript.decision_bits, transcript.point_count, @@ -1697,10 +1768,10 @@ def _admit_transcript( def compare_dual_transcripts( job: ProofJobV1, - first_manifest: ContentResolvedComparatorManifestV1, + first_manifest: ContentResolvedComparatorManifestV2, first_transcript: DecisionTranscriptV1, first_run: RunClaimV1, - second_manifest: ContentResolvedComparatorManifestV1, + second_manifest: ContentResolvedComparatorManifestV2, second_transcript: DecisionTranscriptV1, second_run: RunClaimV1, ) -> DualComparisonCandidateV1: @@ -1718,10 +1789,10 @@ def compare_dual_transcripts( "dual admission requires canonical job, transcripts and runs", ) if ( - type(first_manifest) is not ContentResolvedComparatorManifestV1 - or type(second_manifest) is not ContentResolvedComparatorManifestV1 - or type(first_manifest.manifest) is not ComparatorManifestV1 - or type(second_manifest.manifest) is not ComparatorManifestV1 + type(first_manifest) is not ContentResolvedComparatorManifestV2 + or type(second_manifest) is not ContentResolvedComparatorManifestV2 + or type(first_manifest.manifest) is not ComparatorManifestV2 + or type(second_manifest.manifest) is not ComparatorManifestV2 ): _fail( "dual-admission-v1", diff --git a/proof/region/v1/tests/test_build.py b/proof/region/v1/tests/test_build.py new file mode 100644 index 00000000..e0c93d73 --- /dev/null +++ b/proof/region/v1/tests/test_build.py @@ -0,0 +1,2461 @@ +#!/usr/bin/env python3 +"""Контракт нейтрального к движку BUILD-слоя с сохранением идентичности.""" + +from __future__ import annotations + +import ast +import contextlib +import dis +import errno +import gc +import hashlib +import importlib +import json +import os +import select +import subprocess +import sys +import tempfile +import threading +import unittest +from collections.abc import Iterator +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[1] +ARB = PROOF / "arb" +ARB_TESTS = ARB / "tests" +REPO = PROOF.parents[2] +sys.path[:0] = (str(REPO), str(PROOF), str(ARB_TESTS)) + +from arb import pipeline # noqa: E402 +from proof.region.v1.arb.tests import gate as arb_gate # noqa: E402 +from test_pipeline import ( # noqa: E402 + _docker_capability, + _probe_native_backend, + _request, +) +from test_receipt import _execute # noqa: E402 + + +@contextlib.contextmanager +def _temporary_mode(path: Path, mode: int) -> Iterator[None]: + """Временно сменить mode и восстановить точное исходное значение.""" + + original_mode = path.stat().st_mode & 0o7777 + path.chmod(mode) + try: + yield + finally: + path.chmod(original_mode) + + +# Keep an independent outer oracle: importing the gate's expected hash here +# would let a coordinated gate edit hide inventory drift. +ARB_INVENTORY_SHA256_V1 = ( + "030cd7d43490c3aea5e10ba7d29baa2ab7de61639f05b9e9a98d0007cd990c05" +) +ARB_ORDER_SHA256_V1 = ( + "d7210149257cb51bd3df3397f8a69323977db8b83425ee28baa42d441685bcbf" +) +ARB_TEST_COUNT_V1 = 267 + +MOVED_INPUT_SURFACE_V1 = ( + "CanonicalInputLimitsV1", + "SealedInputV1", + "seal_input_v1", + "sealed_input_is_intact_v1", + "canonical_ustar_v1", +) + +MOVED_TRANSPORT_SURFACE_V1 = ( + "DockerBuildPolicyV1", + "DockerUserModeV1", + "DockerBlockerReasonV1", + "DockerUnsupportedV1", + "DockerSupportedV1", + "DockerDaemonObservationV1", + "NativeCommandCoordinateV1", + "DockerBuildRequestV1", + "transport_policy_identity_v1", + "native_command_contract_identity_v1", + "native_command_coordinate_v1", + "docker_capability_identity_v1", + "BuildInputTransferProgressV1", + "BuildInputTransferV1", + "DockerBuildExitedV1", + "DockerBuildTimedOutV1", + "DockerOutputStreamV1", + "DockerBuildOutputLimitV1", + "DockerBuildObserverFailureV1", + "DockerBuildInputRejectedV1", + "DockerCleanupTriggerV1", + "CleanupResourceV1", + "CleanupFailureRecordV1", + "DockerBuildCleanupFailureV1", + "DockerBuildBackendV1", + "NativeDockerBuildBackendV1", + "ControlledBuildTransportV1", + "BuildFailureReasonV1", + "BuildRejectedV1", + "BuildByteRelationV1", + "TwoBuildObservationV1", + "build_process_bytes_v1", +) + +REMOVED_TRANSPORT_SURFACE_V1 = ( + "NonReproducibleBuildV1", + "ReproducibleBuildV1", + "docker_report_matches_policy_v1", +) + +FORBIDDEN_INPUT_IMPORTS_V1 = ( + "arb", + "mpfi", + "pipeline", + "formula", + "comparator", + "receipt", + "region_proof_protocol", + "provenance", +) + +# Both shared leaves must remain unaware of engine semantics; separate names +# keep the two contracts legible without making their import policy diverge. +FORBIDDEN_TRANSPORT_IMPORTS_V1 = FORBIDDEN_INPUT_IMPORTS_V1 + + +def _imported_modules(source: str) -> tuple[str, ...]: + modules: list[str] = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + modules.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom): + modules.append(node.module or "") + return tuple(modules) + + +def _digest(label: str) -> bytes: + return hashlib.sha256(label.encode("ascii")).digest() + + +def _sealed_input() -> object: + build_input = importlib.import_module("build.input") + return build_input.seal_input_v1(_digest("generic-build-binding"), b"input") + + +def _docker_capability_fixture(policy: object) -> object: + return _docker_capability(policy) + + +def _completed_process( + transport: object, + input_value: object, + stdout: bytes, + *, + returncode: int = 0, + stderr: bytes = b"", +) -> object: + transfer = transport._completed_build_input_transfer_v1( + input_value, + input_value.length, + input_value.sha256, + ) + return transport._docker_build_exited_v1( + returncode, + stdout, + stderr, + transfer, + ) + + +def _initial_progress(transport: object, input_value: object) -> object: + return transport._build_input_progress_v1( + input_value, + 0, + hashlib.sha256(b"").digest(), + ) + + +def _forged_exact_type(value_type: type[object]) -> object: + if issubclass(value_type, tuple): + return tuple.__new__(value_type, ()) + return object.__new__(value_type) + + +class _ScriptedBuildBackend: + def __init__(self, report: object, observations: tuple[object, ...]) -> None: + self._report = report + self._observations = list(observations) + self.requests: list[object] = [] + + def probe(self) -> object: + return self._report + + def run_build(self, request: object) -> object: + self.requests.append(request) + return self._observations.pop(0) + + +def _racing_build_transport( + transport: object, + *, + policy: object, + backend: object, +) -> object: + """Force the former unlocked check→consume race without scheduler guesses.""" + + class TrackingLock: + def __init__(self) -> None: + self._lock = threading.Lock() + self._owner: int | None = None + + def __enter__(self) -> TrackingLock: + self._lock.acquire() + self._owner = threading.get_ident() + return self + + def __exit__( + self, + _exception_type: object, + _exception: object, + _traceback: object, + ) -> None: + self._owner = None + self._lock.release() + + def held_by_current_thread(self) -> bool: + return self._owner == threading.get_ident() + + class RacingController(transport.ControlledBuildTransportV1): + def __init__(self) -> None: + self._consume_barrier = threading.Barrier(2) + self._race_armed = False + super().__init__(policy=policy, backend=backend) + self._lease_lock = TrackingLock() + + def arm_consume_race(self) -> None: + self._race_armed = True + + def __getattribute__(self, name: str) -> object: + if ( + name == "_consumed" + and object.__getattribute__(self, "_race_armed") + and not object.__getattribute__( + self, + "_lease_lock", + ).held_by_current_thread() + ): + object.__getattribute__(self, "_consume_barrier").wait(timeout=2) + return super().__getattribute__(name) + + return RacingController() + + +def _controlled_build( + transport: object, + policy: object, + observations: tuple[object, ...], + *, + max_output_bytes: int = 64, + input_value: object | None = None, +) -> tuple[object, _ScriptedBuildBackend, object, object]: + if input_value is None: + input_value = _sealed_input() + capability = _docker_capability_fixture(policy) + backend = _ScriptedBuildBackend(capability, observations) + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + result = controller.build( + owned_capability, + input_value, + max_output_bytes, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + return result, backend, owned_capability, input_value + + +class ExistingArbGateTests(unittest.TestCase): + def test_existing_arb_suite_keeps_exact_count_order_and_inventory(self) -> None: + tests = tuple(arb_gate.iter_tests_v1(arb_gate.full_suite_v1())) + identifiers = tuple(test.id() for test in tests) + ordered_preimage = b"".join( + identifier.encode("utf-8") + b"\n" for identifier in identifiers + ) + + self.assertEqual(len(identifiers), ARB_TEST_COUNT_V1) + self.assertEqual(len(set(identifiers)), ARB_TEST_COUNT_V1) + self.assertEqual( + arb_gate.test_inventory_sha256_v1(arb_gate.full_suite_v1()), + ARB_INVENTORY_SHA256_V1, + ) + self.assertEqual( + hashlib.sha256(ordered_preimage).hexdigest(), + ARB_ORDER_SHA256_V1, + ) + + +class ArbBuildIdentityCharacterizationTests(unittest.TestCase): + def test_arb_runtime_binding_propagates_to_downstream_identities(self) -> None: + transport = importlib.import_module("build.transport") + request = _request() + result, _backend = _execute() + observed = result.evidence.build + process_bytes = transport.build_process_bytes_v1( + observed.build_processes[0] + ) + + self.assertEqual(observed.input_bundle_length, 174_080) + self.assertEqual( + observed.input_bundle_sha256.hex(), + "19b32598d41b021a792e54b807f0143940108055591ca5ef6ecb6a826dec576d", + ) + self.assertEqual( + observed.input_bundle_identity.hex(), + "a6580242b448c88a8f24e61819de47d512ab8fe78cbfe850e7447540e83360e6", + ) + self.assertEqual( + pipeline.pipeline_policy_identity_v2( + request.host_trust, + observed.docker_capability.policy, + ).hex(), + "5ff9cac8af5fee7ffb05d18da33721842150dafe43edd6f0e356566c7be12144", + ) + self.assertEqual(len(process_bytes), 196) + self.assertEqual( + hashlib.sha256(process_bytes).hexdigest(), + "d33e0ed28f88e957fbec83679732d325db5f6a893e7ee6058f2d5376a94466cb", + ) + self.assertEqual( + result.comparator.identity.hex(), + "1a17002c015a938f7464d23e4cdc6f567c9aa93ee83201fe2bd33bb8fb3c7a4f", + ) + self.assertEqual( + result.evidence.source_identity.hex(), + "c34c7c787f23e2f35edc6bdc31b936eb73ffa7a4d5a7ef9c86e9735b7a442cb1", + ) + self.assertEqual( + result.evidence.build_identity.hex(), + "b58d3d95bd73f511a45b23cb3567b4a8fce67f90f929ba2d0ca4359d132b524e", + ) + self.assertEqual( + result.evidence.run_identity.hex(), + "11258597b3ada79f48faf484340c9726699fb02006083a114b25a760ceffa308", + ) + self.assertEqual( + result.evidence.identity.hex(), + "ac1e6c7b99a21b8b419dcdee21b3e24a8580bcf7e5b0793804e0e0d48b6e6e48", + ) + self.assertEqual( + result.claim.identity.hex(), + "546fe704c3e5ad04a23f5d5fe9815ff3560c1cde020f352bd786543fd1ebeb68", + ) + + +class SharedBuildExtractionTests(unittest.TestCase): + def test_build_namespace_has_two_focused_shared_leaves(self) -> None: + package = importlib.import_module("build") + build_input = importlib.import_module("build.input") + transport = importlib.import_module("build.transport") + namespace = PROOF / "build" + + self.assertEqual( + Path(package.__file__).resolve(), + (namespace / "__init__.py").resolve(), + ) + self.assertEqual( + tuple(Path(item).resolve() for item in package.__path__), + (namespace.resolve(),), + ) + self.assertFalse((PROOF / "build.py").exists()) + self.assertEqual( + Path(build_input.__file__).resolve(), + (namespace / "input.py").resolve(), + ) + self.assertEqual( + Path(transport.__file__).resolve(), + (namespace / "transport.py").resolve(), + ) + self.assertFalse((ARB / "build").exists()) + self.assertFalse((PROOF / "mpfi/build").exists()) + self.assertFalse(hasattr(transport, "input")) + self.assertFalse(hasattr(transport, "build_input")) + self.assertFalse(hasattr(build_input, "normalized_source_entries_v1")) + for name in MOVED_INPUT_SURFACE_V1: + with self.subTest(name=name): + self.assertTrue(hasattr(build_input, name)) + for name in MOVED_TRANSPORT_SURFACE_V1: + with self.subTest(name=name): + self.assertTrue(hasattr(transport, name)) + for name in REMOVED_TRANSPORT_SURFACE_V1: + with self.subTest(removed=name): + self.assertFalse(hasattr(transport, name)) + + def test_shared_leaves_import_no_engine_or_proof_semantics(self) -> None: + surfaces = ( + ( + importlib.import_module("build.input"), + FORBIDDEN_INPUT_IMPORTS_V1, + ), + ( + importlib.import_module("build.transport"), + FORBIDDEN_TRANSPORT_IMPORTS_V1, + ), + ) + for surface, forbidden_imports in surfaces: + source = Path(surface.__file__).read_text(encoding="utf-8") + for module in _imported_modules(source): + with self.subTest(surface=surface.__name__, module=module): + top_level = module.lstrip(".").split(".", 1)[0].lower() + self.assertNotIn(top_level, forbidden_imports, module) + + def test_observation_contract_uses_current_non_claiming_language(self) -> None: + transport_source = (PROOF / "build" / "transport.py").read_text( + encoding="utf-8" + ) + pipeline_source = (ARB / "pipeline.py").read_text(encoding="utf-8") + protocol_source = (PROOF / "PROTOCOL.md").read_text(encoding="utf-8") + self.assertNotIn( + "backend-contract rejection cannot retain authority", + transport_source, + ) + self.assertNotIn("invalid reproducible-build digests", pipeline_source) + self.assertIn("fresh one-job VM workflow Arb", protocol_source) + self.assertIn("same-UID writer", protocol_source) + self.assertIn("Popen construction", protocol_source) + self.assertNotIn("cleanup выполняет только по его точному имени", protocol_source) + self.assertIn("### Профили исполнения V1", protocol_source) + self.assertIn("Грамматика формата передачи", protocol_source) + self.assertIn("символическая ссылка", protocol_source) + self.assertIn("незапечатанной границей доверия", protocol_source) + for english_prose in ( + "Runtime profiles V1", + "Wire grammar", + "operational coordinates", + "public evaluator API", + "filesystem resolution", + "symbolic link", + "unsealed trust boundary", + ): + with self.subTest(english_prose=english_prose): + self.assertNotIn(english_prose, protocol_source) + + def test_arb_consumers_move_atomically_without_compatibility_reexports(self) -> None: + build_input = importlib.import_module("build.input") + transport = importlib.import_module("build.transport") + provenance = importlib.import_module("provenance") + arb_pipeline = pipeline + arb_receipt = importlib.import_module("arb.receipt") + request = _request() + bundle = arb_pipeline._seal_build_input_bundle_v1( + request, + arb_pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + ) + + self.assertIs(arb_pipeline.build_input, build_input) + self.assertIs(arb_pipeline.build_transport, transport) + self.assertIs(arb_receipt.build_transport, transport) + self.assertTrue( + hasattr(provenance, "materialize_admitted_source_files_v1") + ) + self.assertFalse(hasattr(arb_pipeline, "_normalized_source_entries_v1")) + self.assertFalse(hasattr(arb_receipt, "build_input")) + self.assertIs(type(bundle), build_input.SealedInputV1) + for name in MOVED_INPUT_SURFACE_V1 + MOVED_TRANSPORT_SURFACE_V1: + with self.subTest(consumer=arb_pipeline.__name__, name=name): + self.assertFalse(hasattr(arb_pipeline, name)) + for name in MOVED_TRANSPORT_SURFACE_V1: + with self.subTest(consumer=arb_receipt.__name__, name=name): + self.assertFalse(hasattr(arb_receipt, name)) + for name in REMOVED_TRANSPORT_SURFACE_V1: + with self.subTest(consumer=arb_pipeline.__name__, removed=name): + self.assertFalse(hasattr(arb_pipeline, name)) + with self.subTest(consumer=arb_receipt.__name__, removed=name): + self.assertFalse(hasattr(arb_receipt, name)) + + def test_arb_policy_reuses_generic_observer_ceiling_ssot(self) -> None: + transport = importlib.import_module("build.transport") + pipeline_source = (ARB / "pipeline.py").read_text(encoding="utf-8") + for name in ( + "BUILD_STDOUT_LIMIT_V1", + "BUILD_STDERR_LIMIT_V1", + "BUILD_TIMEOUT_NS_V1", + "DOCKER_PROBE_OUTPUT_LIMIT_V1", + "DOCKER_PROBE_TIMEOUT_NS_V1", + ): + with self.subTest(name=name): + self.assertIs(getattr(pipeline, name), getattr(transport, name)) + self.assertIn(f"build_transport.{name}", pipeline_source) + + def test_shared_input_and_policy_are_deeply_immutable_coordinates(self) -> None: + build_input = importlib.import_module("build.input") + transport = importlib.import_module("build.transport") + sealed = build_input.seal_input_v1( + hashlib.sha256(b"binding").digest(), + b"exact bytes", + ) + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + + for value in (sealed, policy): + with self.subTest(value=type(value).__name__): + self.assertFalse(hasattr(value, "__dict__")) + with self.assertRaises((AttributeError, TypeError)): + value[0] = value[0] + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(value, "foreign", object()) + self.assertIs(type(sealed), build_input.SealedInputV1) + self.assertIs(type(policy), transport.DockerBuildPolicyV1) + + def test_forged_source_authorities_fail_in_shared_provenance_taxonomy(self) -> None: + provenance = importlib.import_module("provenance") + request = _request() + lock = request.source_lock.sources[0] + admitted = request.admitted_sources.sources[0] + + for hostile_lock, hostile_admitted in ( + (object.__new__(provenance.SourceReleaseLockV1), admitted), + (lock, object.__new__(provenance.SafeSourceArchiveV1)), + ): + with self.subTest(authority=type(hostile_lock).__name__): + with self.assertRaises(provenance.ProvenanceErrorV1) as raised: + provenance.materialize_admitted_source_files_v1( + hostile_lock, + hostile_admitted, + ) + self.assertEqual( + raised.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) + + +class SharedBuildTransportTargetTests(unittest.TestCase): + def test_stream_close_fallback_requires_current_stream_ownership(self) -> None: + transport = importlib.import_module("build.transport") + real_close = os.close + + descriptor = os.open(os.devnull, os.O_RDONLY) + + class ClosesThenRaises: + closed = False + replacement: int | None = None + close_calls = 0 + + def close(self) -> None: + self.close_calls += 1 + real_close(descriptor) + self.closed = True + self.replacement = os.open(os.devnull, os.O_RDONLY) + raise OSError("stream close released its descriptor") + + stream = ClosesThenRaises() + try: + with mock.patch.object( + transport.os, + "close", + side_effect=AssertionError("helper closed a numeric descriptor"), + ) as direct_close: + failed, interruption = ( + transport.NativeDockerBuildBackendV1._close_owned_stream( + stream, + ) + ) + + self.assertTrue(failed) + self.assertIsNone(interruption) + self.assertEqual(stream.replacement, descriptor) + self.assertEqual(stream.close_calls, 1) + direct_close.assert_not_called() + os.fstat(descriptor) + finally: + if stream.replacement is not None: + try: + real_close(stream.replacement) + except OSError: + pass + + descriptor = os.open(os.devnull, os.O_RDONLY) + + class RaisesBeforeClose: + closed = False + close_calls = 0 + + def close(self) -> None: + self.close_calls += 1 + if self.close_calls == 1: + raise OSError("stream close kept its descriptor") + real_close(descriptor) + self.closed = True + + stream = RaisesBeforeClose() + try: + with mock.patch.object( + transport.os, + "close", + side_effect=AssertionError("helper closed a numeric descriptor"), + ) as direct_close: + failed, interruption = ( + transport.NativeDockerBuildBackendV1._close_owned_stream( + stream, + ) + ) + + self.assertTrue(failed) + self.assertIsNone(interruption) + self.assertEqual(stream.close_calls, 2) + direct_close.assert_not_called() + with self.assertRaises(OSError): + os.fstat(descriptor) + finally: + try: + real_close(descriptor) + except OSError: + pass + + def test_session_property_is_pure_while_boundary_validator_rejects_forgery(self) -> None: + transport = importlib.import_module("build.transport") + build_input = importlib.import_module("build.input") + forged_input = tuple.__new__(build_input.SealedInputV1, ()) + session = tuple.__new__( + transport.BuildSessionV1, + ( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1), + forged_input, + 64, + ), + ) + + self.assertIs(session.input_value, forged_input) + self.assertFalse(transport._build_session_is_valid_v1(session)) + + def test_overlapping_probe_is_rejected_without_a_second_backend_probe(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + capability = _docker_capability_fixture(policy) + + class BlockingBackend: + def __init__(self) -> None: + self.entered = threading.Event() + self.release = threading.Event() + self.calls = 0 + + def probe(self) -> object: + self.calls += 1 + self.entered.set() + self.release.wait(timeout=2) + return capability + + def run_build(self, _request: object) -> object: + raise AssertionError("probe-only test reached build") + + backend = BlockingBackend() + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + first_results: list[object] = [] + second_results: list[object] = [] + second_done = threading.Event() + first = threading.Thread(target=lambda: first_results.append(controller.probe())) + + def second_probe() -> None: + try: + second_results.append(controller.probe()) + finally: + second_done.set() + + second = threading.Thread(target=second_probe) + first.start() + self.assertTrue(backend.entered.wait(timeout=1)) + second.start() + try: + self.assertTrue(second_done.wait(timeout=1)) + finally: + backend.release.set() + first.join(timeout=2) + second.join(timeout=2) + + self.assertFalse(first.is_alive()) + self.assertFalse(second.is_alive()) + self.assertEqual(backend.calls, 1) + self.assertEqual(first_results, [capability]) + self.assertEqual(len(second_results), 1) + self.assertIs(type(second_results[0]), transport.DockerUnsupportedV1) + self.assertEqual( + second_results[0].reason, + transport.DockerBlockerReasonV1.BACKEND_CONTRACT, + ) + + def test_one_probe_lease_cannot_start_two_concurrent_two_build_sessions(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + capability = _docker_capability_fixture(policy) + backend = _ScriptedBuildBackend( + capability, + tuple( + _completed_process(transport, input_value, b"same executable") + for _ in range(4) + ), + ) + controller = _racing_build_transport( + transport, + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + controller.arm_consume_race() + start = threading.Barrier(3) + results: list[object] = [] + failures: list[BaseException] = [] + + def build() -> None: + try: + start.wait(timeout=2) + results.append( + controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + ) + except BaseException as error: + failures.append(error) + + workers = tuple(threading.Thread(target=build) for _ in range(2)) + for worker in workers: + worker.start() + start.wait(timeout=2) + for worker in workers: + worker.join(timeout=3) + self.assertFalse(worker.is_alive()) + + self.assertEqual(failures, []) + self.assertEqual(len(results), 2) + self.assertEqual( + sum(type(result) is transport.TwoBuildObservationV1 for result in results), + 1, + ) + rejections = tuple( + result + for result in results + if type(result) is transport.BuildRejectedV1 + ) + self.assertEqual(len(rejections), 1) + self.assertEqual( + rejections[0].reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(len(backend.requests), 2) + + def test_rejected_preflight_preserves_the_unconsumed_build_lease(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + capability = _docker_capability_fixture(policy) + backend = _ScriptedBuildBackend( + capability, + ( + _completed_process(transport, input_value, b"same executable"), + _completed_process(transport, input_value, b"same executable"), + ), + ) + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + + rejected = controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: False, + output_admission=lambda _value: True, + ) + self.assertIs(type(rejected), transport.BuildRejectedV1) + self.assertEqual( + rejected.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + admitted = controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + self.assertIs(type(admitted), transport.TwoBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + + @unittest.skipUnless(hasattr(os, "fork"), "requires POSIX fork") + def test_forked_child_cannot_wait_on_or_duplicate_a_build_lease(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + capability = _docker_capability_fixture(policy) + backend = _ScriptedBuildBackend( + capability, + ( + _completed_process(transport, input_value, b"same executable"), + _completed_process(transport, input_value, b"same executable"), + ), + ) + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + read_fd, write_fd = os.pipe() + controller._lease_lock.acquire() + child_pid: int | None = None + child_reaped = False + try: + child_pid = os.fork() + if child_pid == 0: + os.close(read_fd) + try: + child_result = controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + os.write( + write_fd, + ( + f"{type(child_result).__name__}:" + f"{len(backend.requests)}" + ).encode("ascii"), + ) + finally: + os.close(write_fd) + os._exit(0) + os.close(write_fd) + ready, _write_ready, _errors = select.select([read_fd], [], [], 1) + self.assertEqual(ready, [read_fd]) + child_message = os.read(read_fd, 128).decode("ascii") + _waited_pid, status = os.waitpid(child_pid, 0) + child_reaped = True + finally: + controller._lease_lock.release() + if child_pid is not None and not child_reaped: + try: + os.kill(child_pid, 9) + except ProcessLookupError: + pass + try: + os.waitpid(child_pid, 0) + except ChildProcessError: + pass + try: + os.close(read_fd) + except OSError: + pass + + self.assertTrue(os.WIFEXITED(status)) + self.assertEqual(child_message, "BuildRejectedV1:0") + parent_result = controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + self.assertIs(type(parent_result), transport.TwoBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + + def test_public_build_contract_violations_are_typed_before_backend(self) -> None: + build_input = importlib.import_module("build.input") + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + cases = ( + ("wrong type", None, lambda _value: True), + ( + "forged sealed input", + tuple.__new__(build_input.SealedInputV1, ()), + lambda _value: True, + ), + ("lane admission", _sealed_input(), lambda _value: False), + ) + for name, hostile, admission in cases: + with self.subTest(case=name): + capability = _docker_capability_fixture(policy) + backend = _ScriptedBuildBackend(capability, ()) + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + result = controller.build( + owned_capability, + hostile, + 64, + input_admission=admission, + output_admission=lambda _value: True, + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual( + result.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + def test_capability_owns_injected_host_user_and_rejects_surrogate_coordinates(self) -> None: + transport = importlib.import_module("build.transport") + self.assertTrue(hasattr(transport, "DockerUserModeV1")) + user_mode = transport.DockerUserModeV1.HOST_EFFECTIVE_IDS + shipped = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + coordinates = { + "image_reference": shipped.image_reference, + "platform": shipped.platform, + "hostname": shipped.hostname, + "bootstrap": shipped.bootstrap, + "bootstrap_argv0": shipped.bootstrap_argv0, + "tmpfs_specs": shipped.tmpfs_specs, + "user_mode": user_mode, + "stdout_limit": shipped.stdout_limit, + "stderr_limit": shipped.stderr_limit, + "build_timeout_ns": shipped.build_timeout_ns, + "probe_output_limit": shipped.probe_output_limit, + "probe_timeout_ns": shipped.probe_timeout_ns, + } + policy = transport.DockerBuildPolicyV1(**coordinates) + input_value = _sealed_input() + backends = tuple( + transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + for _ in range(2) + ) + capabilities = tuple( + _probe_native_backend(backend, policy) for backend in backends + ) + requests = tuple( + transport.DockerBuildRequestV1( + 1, + capability, + input_value, + 64, + ) + for capability in capabilities + ) + commands: list[tuple[str, ...]] = [] + + def observe( + command: tuple[str, ...], + **_kwargs: object, + ) -> object: + commands.append(command) + return _completed_process(transport, input_value, b"") + + with mock.patch.object( + transport.os, + "geteuid", + side_effect=AssertionError("command_for performed ambient uid IO"), + ), mock.patch.object( + transport.os, + "getegid", + side_effect=AssertionError("command_for performed ambient gid IO"), + ): + for backend, request in zip(backends, requests, strict=True): + with mock.patch.object( + backend, + "_observe_command", + side_effect=observe, + ): + self.assertIs( + type(backend.run_build(request)), + transport.DockerBuildExitedV1, + ) + self.assertEqual(len(commands), 2) + + def without_native_cid_path(command: tuple[str, ...]) -> tuple[str, ...]: + index = command.index("--cidfile") + return command[: index + 1] + command[index + 2 :] + + self.assertEqual( + without_native_cid_path(commands[0]), + without_native_cid_path(commands[1]), + ) + user_index = commands[0].index("--user") + self.assertEqual(commands[0][user_index + 1], "501:20") + + for field_name, value in ( + ("bootstrap", "\ud800"), + ("tmpfs_specs", ("/tmp/\ud800:rw",)), + ): + with self.subTest(field=field_name): + hostile = dict(coordinates) + hostile[field_name] = value + with self.assertRaises(TypeError): + transport.DockerBuildPolicyV1(**hostile) + with self.assertRaises(TypeError): + transport.NativeDockerBuildBackendV1( + Path("/tmp/\ud800"), + policy, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + self.assertEqual( + transport.docker_command_coordinate_v1(Path("/usr/bin/true")).path, + Path("/usr/bin/true"), + ) + for path in ( + object(), + Path("relative"), + Path("/tmp/\ud800"), + Path("/tmp/docker\0"), + Path("/tmp/docker\n"), + Path("/tmp/docker,comma"), + ): + with self.subTest(path=type(path).__name__): + with self.assertRaises(TypeError): + transport.docker_command_coordinate_v1(path) + for exact_path in ( + Path("/usr/bin/../bin/true"), + Path("//usr/bin/true"), + ): + with self.subTest(exact_path=str(exact_path)): + self.assertEqual( + transport.docker_command_coordinate_v1(exact_path).path, + exact_path, + ) + + def test_native_probe_never_follows_docker_path_aliases(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + real = root / "реальный" + real.mkdir() + docker = real / "docker" + docker.write_bytes(b"fixture") + docker.chmod(0o755) + # Searching a command coordinate needs execute permission, not + # directory-read permission, on every intermediate component. + execute_only = root / "docker-execute-only" + execute_only.write_bytes(b"fixture") + # The native preflight observes metadata; requiring read permission + # here would reject a valid executable-only Docker CLI before it can + # ever reach the command observer. + real_mode = real.stat().st_mode & 0o7777 + execute_only_mode = execute_only.stat().st_mode & 0o7777 + with _temporary_mode(real, 0o111), _temporary_mode( + execute_only, + 0o111, + ): + alias = root / "alias" + alias.symlink_to(real, target_is_directory=True) + final_alias = root / "docker-alias" + final_alias.symlink_to(docker) + + image_observation = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + separators=(",", ":"), + ).encode("ascii") + + for path, expected_type, expected_calls in ( + (docker, transport.DockerSupportedV1, 2), + (execute_only, transport.DockerSupportedV1, 2), + (alias / "docker", transport.DockerUnsupportedV1, 0), + (final_alias, transport.DockerUnsupportedV1, 0), + ): + backend = transport.NativeDockerBuildBackendV1( + path, + policy, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + with self.subTest(path=str(path)), mock.patch.object( + backend, + "_observe_command", + side_effect=( + transport._docker_command_exited_v1( + 0, + b'{"Version":"fixture"}', + b"", + ), + transport._docker_command_exited_v1( + 0, + image_observation, + b"", + ), + ), + ) as observe: + report = backend.probe() + self.assertIs(type(report), expected_type) + self.assertEqual(observe.call_count, expected_calls) + if type(report) is transport.DockerUnsupportedV1: + self.assertEqual( + report.reason, + transport.DockerBlockerReasonV1.DOCKER_UNAVAILABLE, + ) + self.assertEqual(real.stat().st_mode & 0o7777, real_mode) + self.assertEqual( + execute_only.stat().st_mode & 0o7777, + execute_only_mode, + ) + + def test_docker_metadata_mode_fails_closed_without_positive_linux_o_path(self) -> None: + transport = importlib.import_module("build.transport") + for unavailable in (None, 0): + with self.subTest(o_path=unavailable), mock.patch.object( + transport.sys, + "platform", + "linux", + ), mock.patch.object( + transport.os, + "O_PATH", + unavailable, + create=True, + ): + with self.assertRaises(OSError) as caught: + transport._docker_coordinate_metadata_flags_v1() + self.assertEqual(caught.exception.errno, errno.ENOTSUP) + + def test_docker_coordinate_open_propagates_the_selected_metadata_mode(self) -> None: + transport = importlib.import_module("build.transport") + marker = 1 << 50 + opened: list[tuple[object, int, int | None]] = [] + descriptors = iter((31, 32, 33, 34)) + + def open_coordinate( + path: object, + flags: int, + mode: int = 0o777, + *, + dir_fd: int | None = None, + ) -> int: + del mode + opened.append((path, flags, dir_fd)) + return next(descriptors) + + directory_flags = marker | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW + command_flags = marker | os.O_NONBLOCK | os.O_CLOEXEC | os.O_NOFOLLOW + coordinate = transport.docker_command_coordinate_v1(Path("/docker/root/cli")) + self.assertEqual( + marker + & (os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW | os.O_NONBLOCK), + 0, + ) + with mock.patch.object( + transport.sys, + "platform", + "linux", + ), mock.patch.object( + transport.os, + "O_PATH", + marker, + create=True, + ), mock.patch.object(transport.os, "open", side_effect=open_coordinate), mock.patch.object( + transport.os, + "close", + ) as close: + descriptor = transport._open_docker_command_v1(coordinate) + + self.assertEqual(descriptor, 34) + self.assertEqual( + opened, + [ + (b"/", directory_flags, None), + (b"docker", directory_flags, 31), + (b"root", directory_flags, 32), + (b"cli", command_flags, 33), + ], + ) + self.assertEqual(close.call_args_list, [mock.call(31), mock.call(32), mock.call(33)]) + + def test_native_backend_defers_host_user_observation_to_supported_probe(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + + with mock.patch.object( + transport.os, + "geteuid", + side_effect=AttributeError("not available on this host"), + ), mock.patch.object( + transport.os, + "getegid", + side_effect=AttributeError("not available on this host"), + ): + unsupported_backend = transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="windows", + machine_name="amd64", + ) + unsupported = unsupported_backend.probe() + + self.assertIs(type(unsupported), transport.DockerUnsupportedV1) + self.assertEqual( + unsupported.reason, + transport.DockerBlockerReasonV1.HOST_NOT_LINUX_AMD64, + ) + + supported_backend = transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="linux", + machine_name="x86_64", + ) + with mock.patch.object( + transport.os, + "geteuid", + side_effect=AttributeError("not available on this host"), + ): + unavailable = supported_backend.probe() + + self.assertIs(type(unavailable), transport.DockerUnsupportedV1) + self.assertEqual( + unavailable.reason, + transport.DockerBlockerReasonV1.HOST_USER_UNAVAILABLE, + ) + + def test_native_probe_observes_unconfigured_host_user_each_time(self) -> None: + """Ambient uid/gid belong to a capability observation, never backend cache.""" + + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + backend = transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="linux", + machine_name="x86_64", + ) + + with mock.patch.object( + transport.os, + "geteuid", + side_effect=(501, 502), + ), mock.patch.object( + transport.os, + "getegid", + side_effect=(20, 21), + ): + first = _probe_native_backend(backend, policy) + second = _probe_native_backend(backend, policy) + + self.assertEqual(first.host_user, (501, 20)) + self.assertEqual(second.host_user, (502, 21)) + + def test_native_host_coordinates_are_exact_strings_and_oci_ports_are_ascii(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + + class StringSubclass(str): + pass + + for field_name, value in ( + ("platform_name", StringSubclass("linux")), + ("machine_name", StringSubclass("x86_64")), + ("platform_name", 7), + ("machine_name", object()), + ): + with self.subTest(field=field_name, value_type=type(value).__name__): + coordinates = { + "platform_name": "linux", + "machine_name": "x86_64", + } + coordinates[field_name] = value + with self.assertRaises(TypeError): + transport.NativeDockerBuildBackendV1( + Path("/usr/bin/docker"), + policy, + host_user=(501, 20), + **coordinates, + ) + + hostile_policy = { + "image_reference": ( + "registry.example:\u0661/toolchain@sha256:" + "a" * 64 + ), + "platform": policy.platform, + "hostname": policy.hostname, + "bootstrap": policy.bootstrap, + "bootstrap_argv0": policy.bootstrap_argv0, + "tmpfs_specs": policy.tmpfs_specs, + "user_mode": policy.user_mode, + "stdout_limit": policy.stdout_limit, + "stderr_limit": policy.stderr_limit, + "build_timeout_ns": policy.build_timeout_ns, + "probe_output_limit": policy.probe_output_limit, + "probe_timeout_ns": policy.probe_timeout_ns, + } + with self.assertRaises(TypeError): + transport.DockerBuildPolicyV1(**hostile_policy) + + def test_stream_close_failure_retries_owner_and_retains_evidence(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + wrapped_streams: list[object] = [] + + class CloseRaises: + def __init__(self, wrapped: object) -> None: + self.wrapped = wrapped + self.descriptor = wrapped.fileno() + self.close_calls = 0 + + @property + def closed(self) -> bool: + return self.wrapped.closed + + def fileno(self) -> int: + return self.descriptor + + def close(self) -> None: + self.close_calls += 1 + if self.close_calls == 1: + raise OSError("forced close failure") + self.wrapped.close() + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + wrapped = CloseRaises(process.stdout) + wrapped_streams.append(wrapped) + process.stdout = wrapped + return process + + input_value = _sealed_input() + command = ( + sys.executable, + "-c", + ( + "import sys; sys.stdin.buffer.read(); " + "sys.stdout.buffer.write(b'evidence')" + ), + ) + try: + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ): + result = backend._observe_command( + command, + stdout_limit=64, + stderr_limit=64, + timeout_ns=1_000_000_000, + input_bundle=input_value, + ) + finally: + for process in spawned: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stderr): + if stream is not None and not stream.closed: + stream.close() + original = wrapped_streams[0].wrapped + if not original.closed: + original.close() + + self.assertIs(type(result), transport.DockerBuildObserverFailureV1) + self.assertEqual(result.stdout, b"evidence") + self.assertEqual(result.stderr, b"") + self.assertIsNotNone(result.input_progress) + self.assertEqual(result.input_progress.written_length, input_value.length) + self.assertEqual(result.input_progress.written_sha256, input_value.sha256) + self.assertTrue(wrapped_streams[0].wrapped.closed) + self.assertEqual(wrapped_streams[0].close_calls, 2) + + def test_post_popen_failures_always_close_streams_and_cleanup_once(self) -> None: + transport = importlib.import_module("build.transport") + real_popen = subprocess.Popen + + def exercise( + *, + selector_failure: bool, + ) -> tuple[object, bool, int]: + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + spawned: list[subprocess.Popen[bytes]] = [] + cleanup_calls: list[object] = [] + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + def cleanup(lease: object, **_kwargs: object) -> None: + cleanup_calls.append(lease) + return None + + def stop_raises(process: subprocess.Popen[bytes]) -> None: + process.kill() + process.wait(timeout=5) + raise RuntimeError("forced stop failure") + + selector_patch = ( + mock.patch.object( + transport.selectors, + "DefaultSelector", + side_effect=RuntimeError("forced selector failure"), + ) + if selector_failure + else mock.patch.object( + backend, + "_stop_process", + side_effect=stop_raises, + ) + ) + command = ( + sys.executable, + "-c", + "pass" if selector_failure else "import time; time.sleep(5)", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_cleanup_container", + side_effect=cleanup, + ), selector_patch: + try: + result: object = backend._observe_command( + command, + stdout_limit=64, + stderr_limit=64, + timeout_ns=1 if not selector_failure else 1_000_000_000, + lease=lease, + ) + except Exception as error: + result = error + self.assertEqual(len(spawned), 1) + process = spawned[0] + streams_closed = bool( + process.stdout is not None + and process.stdout.closed + and process.stderr is not None + and process.stderr.closed + ) + if process.poll() is None: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stdout, process.stderr): + if stream is not None and not stream.closed: + stream.close() + return result, streams_closed, len(cleanup_calls) + + selector_result, selector_closed, selector_cleanups = exercise( + selector_failure=True + ) + self.assertIs(type(selector_result), transport.DockerBuildObserverFailureV1) + self.assertTrue(selector_closed) + self.assertEqual(selector_cleanups, 1) + + stop_result, stop_closed, stop_cleanups = exercise(selector_failure=False) + self.assertIn( + type(stop_result), + ( + transport.DockerBuildObserverFailureV1, + transport.DockerBuildCleanupFailureV1, + ), + ) + self.assertTrue(stop_closed) + self.assertEqual(stop_cleanups, 1) + + def test_base_exception_during_stop_still_reaps_streams_and_container(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + cleanup_calls: list[object] = [] + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + def cleanup(lease: object, **_kwargs: object) -> None: + cleanup_calls.append(lease) + + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_stop_process", + side_effect=KeyboardInterrupt("interrupt during stop"), + ), mock.patch.object( + backend, + "_cleanup_container", + side_effect=cleanup, + ): + with self.assertRaises(KeyboardInterrupt): + backend._observe_command( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + process = spawned[0] + running_before_test_cleanup = process.poll() is None + streams_closed = bool( + process.stdout is not None + and process.stdout.closed + and process.stderr is not None + and process.stderr.closed + ) + if running_before_test_cleanup: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stdout, process.stderr): + if stream is not None and not stream.closed: + stream.close() + + self.assertFalse(running_before_test_cleanup) + self.assertTrue(streams_closed) + self.assertEqual(len(cleanup_calls), 1) + + def test_interrupt_after_spawn_still_reaps_and_attempts_cid_cleanup(self) -> None: + """A post-spawn interruption cannot bypass the native finalizer.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_mark_run_lease_launched_v1", + side_effect=KeyboardInterrupt("interrupt after spawn"), + ), mock.patch.object( + backend, + "_cleanup_container", + return_value=None, + ) as cleanup: + with self.assertRaisesRegex(KeyboardInterrupt, "after spawn"): + backend._observe_command( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + + process = spawned[0] + self.assertIsNotNone(process.poll()) + cleanup.assert_called_once_with(lease, spawn_may_have_started=True) + + def test_interrupt_during_post_spawn_state_initialization_reaps_and_cleans(self) -> None: + """No allocation between Popen and the finalizer may leak a child.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + transport, + "bytearray", + side_effect=KeyboardInterrupt("interrupt during post-spawn allocation"), + create=True, + ), mock.patch.object( + backend, + "_cleanup_container", + return_value=None, + ) as cleanup: + with self.assertRaisesRegex( + KeyboardInterrupt, + "post-spawn allocation", + ): + backend._observe_command( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + + process = spawned[0] + try: + self.assertIsNotNone(process.poll()) + cleanup.assert_called_once_with(lease, spawn_may_have_started=True) + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stdout, process.stderr): + if stream is not None and not stream.closed: + stream.close() + + def test_post_popen_handler_gap_cannot_bypass_finalizer(self) -> None: + """An interrupt at the first bytecode after Popen still owns its child.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + observe = backend._observe_command + instructions = tuple(dis.Bytecode(observe)) + process_store = next( + ( + index + for index, instruction in enumerate(instructions) + if ( + instruction.opname == "STORE_FAST" + and instruction.argval == "process" + and index > 0 + and instructions[index - 1].opname == "CALL_FUNCTION_EX" + ) + ), + None, + ) + if process_store is None: + self.fail( + "CPython bytecode no longer exposes CALL_FUNCTION_EX before " + f"STORE_FAST process (Python {sys.version})" + ) + interruption_offset = instructions[process_store + 1].offset + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + injected = False + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + def tracer(frame: object, event: str, _arg: object) -> object: + nonlocal injected + if getattr(frame, "f_code", None) is observe.__code__: + frame.f_trace_opcodes = True + if ( + not injected + and event == "opcode" + and frame.f_lasti == interruption_offset + ): + injected = True + raise KeyboardInterrupt("interrupt in post-Popen handler gap") + return tracer + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_cleanup_container", + return_value=None, + ) as cleanup: + previous = sys.gettrace() + sys.settrace(tracer) + try: + with self.assertRaisesRegex(KeyboardInterrupt, "handler gap"): + observe( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + finally: + sys.settrace(previous) + + self.assertTrue(injected) + process = spawned[0] + try: + self.assertIsNotNone(process.poll()) + cleanup.assert_called_once_with(lease, spawn_may_have_started=True) + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stdout, process.stderr): + if stream is not None and not stream.closed: + stream.close() + + def test_popen_construction_interrupt_attempts_cid_cleanup_without_a_handle(self) -> None: + """The pre-handle boundary retains the interruption and tries CID cleanup.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=KeyboardInterrupt("interrupt during Popen construction"), + ), mock.patch.object( + backend, + "_cleanup_container", + return_value=None, + ) as cleanup: + with self.assertRaisesRegex(KeyboardInterrupt, "Popen construction"): + backend._observe_command( + (sys.executable, "-c", "pass"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + + cleanup.assert_called_once_with(lease, spawn_may_have_started=True) + + @unittest.skipUnless(hasattr(os, "fork"), "requires POSIX fork") + def test_forked_child_gc_cannot_delete_parent_cid_root(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + root = lease.cid_file.parent + try: + child = os.fork() + if child == 0: + del lease + gc.collect() + os._exit(0) + _pid, status = os.waitpid(child, 0) + self.assertEqual(os.waitstatus_to_exitcode(status), 0) + self.assertTrue(root.is_dir()) + finally: + backend._release_run_lease_v1(lease) + + def test_interrupted_cid_root_release_remains_retryable(self) -> None: + """A failed root release must not permanently consume its cleanup lease.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + root = lease.cid_file.parent + real_rmtree = transport.shutil.rmtree + try: + with mock.patch.object( + transport.shutil, + "rmtree", + side_effect=KeyboardInterrupt("interrupt during CID-root release"), + ): + with self.assertRaisesRegex(KeyboardInterrupt, "CID-root release"): + backend._release_run_lease_v1(lease) + + self.assertTrue(root.is_dir()) + self.assertFalse(lease._released) + self.assertIsNone(backend._release_run_lease_v1(lease)) + self.assertFalse(root.exists()) + finally: + if root.exists(): + real_rmtree(root) + + def test_stop_interrupt_survives_container_cleanup_failure(self) -> None: + """A later cleanup error cannot replace the caller's interruption.""" + + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + return process + + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_stop_process", + side_effect=KeyboardInterrupt("interrupt during stop"), + ), mock.patch.object( + backend, + "_cleanup_container", + side_effect=OSError("cleanup failed after interruption"), + ) as cleanup: + with self.assertRaisesRegex(KeyboardInterrupt, "interrupt during stop"): + backend._observe_command( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + ) + process = spawned[0] + running_before_test_cleanup = process.poll() is None + streams_closed = bool( + process.stdout is not None + and process.stdout.closed + and process.stderr is not None + and process.stderr.closed + ) + if running_before_test_cleanup: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stdout, process.stderr): + if stream is not None and not stream.closed: + stream.close() + + self.assertFalse(running_before_test_cleanup) + self.assertTrue(streams_closed) + self.assertEqual(cleanup.call_count, 1) + + def test_stream_close_interrupt_still_closes_siblings_and_cleans_container(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + wrapped_stdout: list[object] = [] + cleanup_calls: list[object] = [] + + class CloseInterrupts: + def __init__(self, wrapped: object) -> None: + self.wrapped = wrapped + self.descriptor = wrapped.fileno() + self.close_calls = 0 + + @property + def closed(self) -> bool: + return self.wrapped.closed + + def fileno(self) -> int: + return self.descriptor + + def close(self) -> None: + self.close_calls += 1 + if self.close_calls == 1: + raise KeyboardInterrupt("interrupt during stdout close") + self.wrapped.close() + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + spawned.append(process) + wrapper = CloseInterrupts(process.stdout) + wrapped_stdout.append(wrapper) + process.stdout = wrapper + return process + + def cleanup(lease: object, **_kwargs: object) -> None: + cleanup_calls.append(lease) + + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_cleanup_container", + side_effect=cleanup, + ): + with self.assertRaisesRegex(KeyboardInterrupt, "stdout close"): + backend._observe_command( + (sys.executable, "-c", "pass"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1_000_000_000, + lease=lease, + ) + process = spawned[0] + stderr_closed = process.stderr is not None and process.stderr.closed + try: + os.fstat(wrapped_stdout[0].descriptor) + except OSError: + stdout_descriptor_closed = True + else: + stdout_descriptor_closed = False + if process.poll() is None: + process.kill() + process.wait(timeout=5) + for stream in (process.stdin, process.stderr): + if stream is not None and not stream.closed: + stream.close() + try: + wrapped_stdout[0].wrapped.close() + except OSError: + pass + + self.assertTrue(stderr_closed) + self.assertTrue(stdout_descriptor_closed) + self.assertEqual(wrapped_stdout[0].close_calls, 2) + self.assertEqual(cleanup_calls, [lease]) + + def test_persistent_stream_close_interrupt_keeps_release_failure_honest(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + spawned: list[subprocess.Popen[bytes]] = [] + wrapped_stdout: list[object] = [] + cleanup_calls: list[object] = [] + + class CloseAlwaysInterrupts: + def __init__(self, wrapped: object) -> None: + self.wrapped = wrapped + self.descriptor = wrapped.fileno() + self.close_calls = 0 + + @property + def closed(self) -> bool: + return False + + def fileno(self) -> int: + return self.descriptor + + def close(self) -> None: + self.close_calls += 1 + raise KeyboardInterrupt("persistent stdout close interrupt") + + def cleanup_spawned( + process: subprocess.Popen[bytes], + original_stdout: object, + ) -> None: + if process.poll() is None: + try: + process.kill() + except ProcessLookupError: + pass + process.wait(timeout=5) + for stream in (process.stdin, process.stderr, original_stdout): + if stream is not None and not stream.closed: + stream.close() + + def spawn(*args: object, **kwargs: object) -> subprocess.Popen[bytes]: + process = real_popen(*args, **kwargs) + # Fixture setup may fail after Popen; ownership starts with its + # original stream, before the hostile wrapper exists. + self.addCleanup(cleanup_spawned, process, process.stdout) + spawned.append(process) + wrapper = CloseAlwaysInterrupts(process.stdout) + wrapped_stdout.append(wrapper) + process.stdout = wrapper + return process + + def cleanup(lease: object, **_kwargs: object) -> None: + cleanup_calls.append(lease) + + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=spawn, + ), mock.patch.object( + backend, + "_cleanup_container", + side_effect=cleanup, + ): + with self.assertRaisesRegex(KeyboardInterrupt, "persistent stdout"): + backend._observe_command( + (sys.executable, "-c", "pass"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1_000_000_000, + lease=lease, + ) + process = spawned[0] + stderr_closed = process.stderr is not None and process.stderr.closed + os.fstat(wrapped_stdout[0].descriptor) + + self.assertTrue(stderr_closed) + self.assertEqual(wrapped_stdout[0].close_calls, 2) + self.assertEqual(cleanup_calls, [lease]) + + def test_process_and_container_cleanup_failures_are_both_retained_in_order(self) -> None: + transport = importlib.import_module("build.transport") + backend = transport.NativeDockerBuildBackendV1( + Path("/bin/true"), + pipeline.ARB_BUILD_TRANSPORT_POLICY_V1, + host_user=(501, 20), + platform_name="linux", + machine_name="x86_64", + ) + real_popen = subprocess.Popen + + def stop(process: subprocess.Popen[bytes]) -> str: + process.kill() + process.wait(timeout=5) + return "process stop failed" + + lease = backend._next_run_lease_v1( + _docker_capability_fixture(pipeline.ARB_BUILD_TRANSPORT_POLICY_V1) + ) + self.addCleanup(backend._release_run_lease_v1, lease) + + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=real_popen, + ), mock.patch.object( + backend, + "_stop_process", + side_effect=stop, + ), mock.patch.object( + backend, + "_cleanup_container", + return_value="container cleanup failed", + ): + result = backend._observe_command( + (sys.executable, "-c", "import time; time.sleep(5)"), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1, + lease=lease, + input_bundle=_sealed_input(), + ) + + self.assertIs(type(result), transport.DockerBuildCleanupFailureV1) + self.assertEqual( + tuple(record.resource for record in result.failures), + ( + transport.CleanupResourceV1.DOCKER_CLI_PROCESS, + transport.CleanupResourceV1.DOCKER_CONTAINER, + ), + ) + self.assertEqual( + tuple(record.detail for record in result.failures), + ("process stop failed", "container cleanup failed"), + ) + self.assertTrue( + all( + type(record) is transport.CleanupFailureRecordV1 + for record in result.failures + ) + ) + + def test_impossible_build_failures_without_input_progress_are_contract_violations(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + cleanup = transport.CleanupFailureRecordV1( + transport.CleanupResourceV1.DOCKER_CONTAINER, + "container cleanup failed", + ) + impossible = ( + transport.DockerBuildTimedOutV1(b"", b""), + transport.DockerBuildOutputLimitV1( + transport.DockerOutputStreamV1.STDOUT, + b"x" * 64, + b"", + ), + transport.DockerBuildCleanupFailureV1( + transport.DockerCleanupTriggerV1.TIMEOUT, + (cleanup,), + b"", + b"", + ), + ) + for observation in impossible: + with self.subTest(observation=type(observation).__name__): + result, _backend, _report, _input = _controlled_build( + transport, + policy, + (observation,), + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual( + result.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertIsNone(result.process) + + def test_controller_passes_only_semantic_build_request_to_its_backend(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + observations = ( + _completed_process(transport, input_value, b"first"), + _completed_process(transport, input_value, b"second"), + ) + result, backend, _capability, _input = _controlled_build( + transport, + policy, + observations, + input_value=input_value, + ) + + self.assertIs(type(result), transport.TwoBuildObservationV1) + self.assertEqual(len(backend.requests), 2) + for request in backend.requests: + self.assertEqual(len(tuple(request)), 4) + self.assertFalse(hasattr(request, "cid_file")) + self.assertFalse(hasattr(request, "container_name")) + + def test_backend_interrupt_propagates_without_controller_cleanup_authority(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + capability = _docker_capability_fixture(policy) + + class InterruptingBackend: + def __init__(self) -> None: + self.requests: list[object] = [] + + def probe(self) -> object: + return capability + + def run_build(self, request: object) -> object: + self.requests.append(request) + raise KeyboardInterrupt("interrupt during build observation") + + backend = InterruptingBackend() + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + owned_capability = controller.probe() + with self.assertRaisesRegex(KeyboardInterrupt, "interrupt during build observation"): + controller.build( + owned_capability, + input_value, + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + + self.assertEqual(len(backend.requests), 1) + self.assertEqual(len(tuple(backend.requests[0])), 4) + + def test_forged_backend_failures_canonicalize_to_contract_violation(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + for failure_type in ( + transport.DockerBuildTimedOutV1, + transport.DockerBuildOutputLimitV1, + transport.DockerBuildObserverFailureV1, + transport.DockerBuildInputRejectedV1, + transport.DockerBuildCleanupFailureV1, + ): + with self.subTest(failure=failure_type.__name__): + forged = _forged_exact_type(failure_type) + if hasattr(forged, "__dict__"): + object.__setattr__(forged, "foreign", object()) + result, _backend, _report, _input = _controlled_build( + transport, + policy, + (forged,), + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual( + result.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertIsNone(result.process) + self.assertEqual(result.completed_processes, ()) + + def test_observer_failure_evidence_is_bounded_and_progress_is_exact(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + foreign_input = importlib.import_module("build.input").seal_input_v1( + _digest("foreign-build-binding"), + input_value.contents, + ) + cases = ( + transport.DockerBuildObserverFailureV1( + "oversized stdout", + b"x" * 65, + b"", + _initial_progress(transport, input_value), + ), + transport.DockerBuildObserverFailureV1( + "foreign progress", + b"", + b"", + _initial_progress(transport, foreign_input), + ), + ) + for observation in cases: + with self.subTest(detail=observation.detail): + result, _backend, _report, _input = _controlled_build( + transport, + policy, + (observation,), + input_value=input_value, + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual( + result.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertIsNone(result.process) + + def test_top_level_failure_reason_preserves_observer_outcome(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + expected_reasons = ( + transport.BuildFailureReasonV1.TIMEOUT, + transport.BuildFailureReasonV1.OUTPUT_LIMIT, + transport.BuildFailureReasonV1.OBSERVER_FAILURE, + transport.BuildFailureReasonV1.PROCESS_FAILED, + ) + input_value = _sealed_input() + progress = _initial_progress(transport, input_value) + observations = ( + transport.DockerBuildTimedOutV1(b"", b"", progress), + transport.DockerBuildOutputLimitV1( + transport.DockerOutputStreamV1.STDOUT, + b"x" * 64, + b"", + progress, + ), + transport.DockerBuildObserverFailureV1( + "observer failed", + b"observer stdout", + b"observer stderr", + progress, + ), + _completed_process( + transport, + input_value, + b"", + returncode=7, + ), + ) + for observation, reason in zip( + observations, + expected_reasons, + strict=True, + ): + with self.subTest(reason=reason): + result, _backend, _report, _input = _controlled_build( + transport, + policy, + (observation,), + input_value=input_value, + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual(result.reason, reason) + + def test_second_attempt_failure_retains_first_completed_process(self) -> None: + transport = importlib.import_module("build.transport") + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + input_value = _sealed_input() + first = _completed_process(transport, input_value, b"first") + progress = _initial_progress(transport, input_value) + result, _backend, _report, _input = _controlled_build( + transport, + policy, + (first, transport.DockerBuildTimedOutV1(b"", b"", progress)), + input_value=input_value, + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual(result.attempt, 2) + self.assertTrue(hasattr(result, "completed_processes")) + self.assertIs(type(result.completed_processes), tuple) + self.assertEqual(result.completed_processes, (first,)) + self.assertIs(result.completed_processes[0], first) + self.assertFalse(hasattr(result, "__dict__")) + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(result, "completed_processes", ()) + + def test_two_build_observation_derives_byte_relation_and_binds_session(self) -> None: + transport = importlib.import_module("build.transport") + self.assertTrue(hasattr(transport, "BuildByteRelationV1")) + self.assertTrue(hasattr(transport, "TwoBuildObservationV1")) + self.assertFalse(hasattr(transport, "ReproducibleBuildV1")) + self.assertFalse(hasattr(transport, "NonReproducibleBuildV1")) + policy = pipeline.ARB_BUILD_TRANSPORT_POLICY_V1 + for outputs, relation in ( + ((b"same", b"same"), transport.BuildByteRelationV1.IDENTICAL), + ((b"first", b"second"), transport.BuildByteRelationV1.DIFFERENT), + ): + with self.subTest(relation=relation): + input_value = _sealed_input() + processes = tuple( + _completed_process(transport, input_value, output) + for output in outputs + ) + result, _backend, capability, _input = _controlled_build( + transport, + policy, + processes, + max_output_bytes=64, + input_value=input_value, + ) + self.assertIs(type(result), transport.TwoBuildObservationV1) + self.assertEqual(result.relation, relation) + self.assertEqual(result.policy, policy) + self.assertEqual(result.capability, capability) + self.assertIs(result.input_value, input_value) + self.assertEqual(result.max_output_bytes, 64) + self.assertEqual(result.processes, processes) + self.assertEqual( + result.relation, + ( + transport.BuildByteRelationV1.IDENTICAL + if processes[0].stdout == processes[1].stdout + else transport.BuildByteRelationV1.DIFFERENT + ), + ) + + def test_build_process_encoding_is_total_and_keeps_exact_golden(self) -> None: + transport = importlib.import_module("build.transport") + result, _backend = _execute() + process = result.evidence.build.build_processes[0] + encoded = transport.build_process_bytes_v1(process) + self.assertEqual(len(encoded), 196) + self.assertEqual( + hashlib.sha256(encoded).hexdigest(), + "d33e0ed28f88e957fbec83679732d325db5f6a893e7ee6058f2d5376a94466cb", + ) + + forged = tuple.__new__(transport.DockerBuildExitedV1, ()) + with self.assertRaises(TypeError): + transport.build_process_bytes_v1(forged) + overflow = tuple.__new__( + transport.DockerBuildExitedV1, + ( + 1 << 40, + process.stdout, + process.stderr, + process.input_transfer, + ), + ) + with self.assertRaises(TypeError): + transport.build_process_bytes_v1(overflow) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_build_identity.py b/proof/region/v1/tests/test_build_identity.py new file mode 100644 index 00000000..19032312 --- /dev/null +++ b/proof/region/v1/tests/test_build_identity.py @@ -0,0 +1,914 @@ +#!/usr/bin/env python3 +"""RED contract for orthogonal Docker BUILD capability identities.""" + +from __future__ import annotations + +import hashlib +import inspect +import json +import os +import subprocess +import sys +import unittest +from pathlib import Path +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROOF)) + +from build import input as build_input # noqa: E402 +from build import transport # noqa: E402 + + +_POLICY_FIELDS_V1 = ( + "image_reference", + "platform", + "hostname", + "bootstrap", + "bootstrap_argv0", + "tmpfs_specs", + "user_mode", + "stdout_limit", + "stderr_limit", + "build_timeout_ns", + "probe_output_limit", + "probe_timeout_ns", +) + +def _literal(value: str) -> tuple[str, str]: + return "literal", value + + +def _slot(value: str) -> tuple[str, str]: + return "slot", value + + +_NATIVE_COMMAND_TEMPLATES_V1 = ( + ( + "version_probe", + ( + _slot("cli_path"), + _literal("version"), + _literal("--format"), + _literal("{{json .Server}}"), + ), + ), + ( + "image_inspect", + ( + _slot("cli_path"), + _literal("image"), + _literal("inspect"), + _slot("image_reference"), + ), + ), + ( + "build", + ( + _slot("cli_path"), + _literal("run"), + _literal("--rm"), + _literal("--interactive"), + _literal("--pull"), + _literal("never"), + _literal("--platform"), + _slot("platform"), + _literal("--network"), + _literal("none"), + _literal("--read-only"), + _literal("--tmpfs"), + _slot("ordered_tmpfs_specs"), + _literal("--cap-drop"), + _literal("ALL"), + _literal("--security-opt"), + _literal("no-new-privileges:true"), + _literal("--hostname"), + _slot("hostname"), + _literal("--user"), + _slot("host_user"), + _literal("--workdir"), + _literal("/"), + _literal("--cidfile"), + _slot("cid_file"), + _literal("--entrypoint"), + _literal("/usr/bin/env"), + _slot("image_reference"), + _literal("-i"), + _literal("PATH=/usr/local/bin:/usr/bin:/bin"), + _literal("LC_ALL=C"), + _literal("LANG=C"), + _literal("TZ=UTC"), + _literal("HOME=/nonexistent"), + _literal("/bin/sh"), + _literal("-c"), + _slot("bootstrap"), + _slot("bootstrap_argv0"), + _slot("input_length"), + _slot("input_sha256"), + ), + ), + ( + "cleanup_rm", + ( + _slot("cli_path"), + _literal("container"), + _literal("rm"), + _literal("--force"), + _slot("container_coordinate"), + ), + ), + ( + "cleanup_inspect", + ( + _slot("cli_path"), + _literal("container"), + _literal("inspect"), + _literal("--format"), + _literal("{{.Id}}"), + _slot("container_coordinate"), + ), + ), + ( + "cleanup_ls", + ( + _slot("cli_path"), + _literal("container"), + _literal("ls"), + _literal("--all"), + _literal("--quiet"), + _literal("--no-trunc"), + _literal("--filter"), + _slot("container_filter"), + ), + ), +) + +_NATIVE_PROCESS_ENVIRONMENT_V1 = ( + ("DOCKER_CONFIG", "/nonexistent"), + ("HOME", "/nonexistent"), + ("LANG", "C"), + ("LC_ALL", "C"), + ("PATH", "/usr/bin:/bin"), + ("TZ", "UTC"), +) +_NATIVE_PROCESS_CWD_V1 = "/" +_NATIVE_PROCESS_UMASK_V1 = 0o077 +_NATIVE_PROCESS_CLOSE_FDS_V1 = True +_NATIVE_PROCESS_RESTORE_SIGNALS_V1 = True +_NATIVE_PROCESS_START_NEW_SESSION_V1 = True +_NATIVE_PROCESS_STDIN_WITH_INPUT_V1 = "pipe" +_NATIVE_PROCESS_STDIN_WITHOUT_INPUT_V1 = "devnull" +_NATIVE_PROCESS_STDOUT_V1 = "pipe" +_NATIVE_PROCESS_STDERR_V1 = "pipe" + + +# This literal oracle intentionally does not call production encoders: changing +# a production preimage silently must turn a test failure, not rewrite its proof. +def _blob(value: bytes) -> bytes: + return len(value).to_bytes(8, "big") + value + + +def _identity(label: bytes, chunks: tuple[bytes, ...]) -> bytes: + payload = b"".join(_blob(chunk) for chunk in chunks) + return hashlib.sha256( + label + len(payload).to_bytes(8, "big") + payload + ).digest() + + +def _policy_coordinates(policy: object) -> dict[str, object]: + return {name: getattr(policy, name) for name in _POLICY_FIELDS_V1} + + +def _policy_chunks(coordinates: dict[str, object]) -> tuple[bytes, ...]: + tmpfs_specs = coordinates["tmpfs_specs"] + user_mode = coordinates["user_mode"] + if type(tmpfs_specs) is not tuple: + raise TypeError("tmpfs_specs must be an exact tuple") + return ( + coordinates["image_reference"].encode("utf-8"), + coordinates["platform"].encode("utf-8"), + coordinates["hostname"].encode("utf-8"), + coordinates["bootstrap"].encode("utf-8"), + coordinates["bootstrap_argv0"].encode("utf-8"), + len(tmpfs_specs).to_bytes(4, "big"), + *(item.encode("utf-8") for item in tmpfs_specs), + user_mode.value.encode("ascii"), + coordinates["stdout_limit"].to_bytes(8, "big"), + coordinates["stderr_limit"].to_bytes(8, "big"), + coordinates["build_timeout_ns"].to_bytes(8, "big"), + coordinates["probe_output_limit"].to_bytes(8, "big"), + coordinates["probe_timeout_ns"].to_bytes(8, "big"), + ) + + +def _expected_policy_identity(coordinates: dict[str, object]) -> bytes: + return _identity( + b"labcolors.proof-region.docker-transport-policy.v1\0", + _policy_chunks(coordinates), + ) + + +def _expected_command_contract_identity() -> bytes: + chunks: list[bytes] = [len(_NATIVE_COMMAND_TEMPLATES_V1).to_bytes(4, "big")] + for name, tokens in _NATIVE_COMMAND_TEMPLATES_V1: + chunks.extend((name.encode("ascii"), len(tokens).to_bytes(4, "big"))) + for tag, value in tokens: + chunks.extend((tag.encode("ascii"), value.encode("utf-8"))) + chunks.extend( + ( + b"native-process-context.v1", + len(_NATIVE_PROCESS_ENVIRONMENT_V1).to_bytes(4, "big"), + *( + item + for key, value in _NATIVE_PROCESS_ENVIRONMENT_V1 + for item in (key.encode("ascii"), value.encode("utf-8")) + ), + _NATIVE_PROCESS_CWD_V1.encode("ascii"), + _NATIVE_PROCESS_UMASK_V1.to_bytes(4, "big"), + bytes((_NATIVE_PROCESS_CLOSE_FDS_V1,)), + bytes((_NATIVE_PROCESS_RESTORE_SIGNALS_V1,)), + bytes((_NATIVE_PROCESS_START_NEW_SESSION_V1,)), + b"native-stdio-topology.v1", + b"stdin-with-input", + _NATIVE_PROCESS_STDIN_WITH_INPUT_V1.encode("ascii"), + b"stdin-without-input", + _NATIVE_PROCESS_STDIN_WITHOUT_INPUT_V1.encode("ascii"), + b"stdout", + _NATIVE_PROCESS_STDOUT_V1.encode("ascii"), + b"stderr", + _NATIVE_PROCESS_STDERR_V1.encode("ascii"), + ) + ) + return _identity( + b"labcolors.proof-region.native-command-contract.v1\0", + tuple(chunks), + ) + + +def _expected_command_coordinate(docker_path: Path) -> bytes: + return _identity( + b"labcolors.proof-region.native-command-coordinate.v1\0", + (_expected_command_contract_identity(), os.fsencode(docker_path)), + ) + + +def _expected_daemon_identity( + server_stdout: bytes, + image_inspect_stdout: bytes, +) -> bytes: + return _identity( + b"labcolors.proof-region.docker-daemon-observation.v1\0", + (server_stdout, image_inspect_stdout), + ) + + +def _expected_host_user_identity(host_user: tuple[int, int]) -> bytes: + return _identity( + b"labcolors.proof-region.host-user.v1\0", + ( + host_user[0].to_bytes(4, "big"), + host_user[1].to_bytes(4, "big"), + ), + ) + + +def _expected_capability_identity( + policy_identity: bytes, + daemon_identity: bytes, + command_coordinate: bytes, + host_user: tuple[int, int], +) -> bytes: + return _identity( + b"labcolors.proof-region.docker-capability.v1\0", + ( + policy_identity, + command_coordinate, + daemon_identity, + host_user[0].to_bytes(4, "big"), + host_user[1].to_bytes(4, "big"), + ), + ) + + +def _policy(**changes: object) -> object: + coordinates: dict[str, object] = { + "image_reference": ( + "registry.example/toolchain@sha256:" + "1" * 64 + ), + "platform": "linux/amd64", + "hostname": "lc-build", + "bootstrap": "set -eu\ncat", + "bootstrap_argv0": "labcolors-build-v1", + "tmpfs_specs": ( + "/work:rw,nosuid,nodev,noexec,size=1048576", + "/tmp:rw,nosuid,nodev,noexec,size=2097152", + ), + "user_mode": transport.DockerUserModeV1.HOST_EFFECTIVE_IDS, + "stdout_limit": 4096, + "stderr_limit": 2048, + "build_timeout_ns": 5_000_000_000, + "probe_output_limit": 1024, + "probe_timeout_ns": 1_000_000_000, + } + coordinates.update(changes) + return transport.DockerBuildPolicyV1(**coordinates) + + +def _daemon( + *, + server_stdout: bytes = b'{"Version":"identity-test"}\n', + image_inspect_stdout: bytes = b'[{"Id":"sha256:identity-test"}]\n', +) -> object: + return transport.DockerDaemonObservationV1( + server_stdout, + image_inspect_stdout, + ) + + +def _capability( + *, + policy: object | None = None, + daemon: object | None = None, + docker_path: Path = Path("/usr/bin/true"), + host_user: tuple[int, int] = (501, 20), +) -> object: + owned_policy = _policy() if policy is None else policy + owned_daemon = _daemon() if daemon is None else daemon + return transport.DockerSupportedV1( + owned_policy, + owned_daemon, + transport.native_command_coordinate_v1(docker_path), + host_user, + ) + + +def _sealed_input() -> object: + return build_input.seal_input_v1( + hashlib.sha256(b"build-identity-test-binding").digest(), + b"identity-test-input", + ) + + +def _assert_deeply_immutable( + case: unittest.TestCase, + value: object, +) -> None: + case.assertFalse(hasattr(value, "__dict__"), type(value).__name__) + with case.assertRaises((AttributeError, TypeError)): + value[0] = value[0] + with case.assertRaises((AttributeError, TypeError)): + object.__setattr__(value, "foreign", object()) + + +class _AlternateUserMode: + """Test-only value with the encoder surface of the closed production enum.""" + + def __init__(self, value: str) -> None: + self.value = value + + +class BuildIdentitySurfaceTests(unittest.TestCase): + def test_identity_surface_is_exact_and_has_no_legacy_report_aliases(self) -> None: + for name in ( + "DockerDaemonObservationV1", + "NativeCommandCoordinateV1", + "transport_policy_identity_v1", + "native_command_contract_identity_v1", + "native_command_coordinate_v1", + "docker_capability_identity_v1", + ): + with self.subTest(required=name): + self.assertTrue(hasattr(transport, name), name) + + self.assertEqual( + tuple(inspect.signature(transport.DockerDaemonObservationV1).parameters), + ("server_stdout", "image_inspect_stdout"), + ) + self.assertEqual( + tuple(inspect.signature(transport.DockerSupportedV1).parameters), + ( + "policy", + "daemon_observation", + "command_coordinate", + "host_user", + ), + ) + self.assertEqual( + tuple(inspect.signature(transport.DockerBuildRequestV1).parameters), + ( + "attempt", + "capability", + "input_bundle", + "max_output_bytes", + ), + ) + self.assertFalse(hasattr(transport, "docker_report_matches_policy_v1")) + self.assertNotIn( + "docker_report", + Path(transport.__file__).read_text(encoding="utf-8"), + ) + + capability = _capability() + request = transport.DockerBuildRequestV1( + 1, + capability, + _sealed_input(), + 64, + ) + for legacy in ( + "image_reference", + "platform", + "daemon_observation_sha256", + ): + with self.subTest(legacy_capability_property=legacy): + self.assertFalse(hasattr(capability, legacy)) + self.assertFalse(hasattr(request, "policy")) + self.assertFalse(hasattr(request, "cid_file")) + self.assertFalse(hasattr(request, "container_name")) + self.assertIs(request.capability, capability) + + with self.assertRaises(TypeError): + transport.DockerSupportedV1( + capability.policy.image_reference, + capability.policy.platform, + capability.daemon_observation.identity, + capability.host_user, + ) + + def test_policy_identity_binds_all_twelve_coordinates(self) -> None: + policy = _policy() + coordinates = _policy_coordinates(policy) + identity = transport.transport_policy_identity_v1(policy) + + self.assertEqual(identity, _expected_policy_identity(coordinates)) + self.assertIs(type(identity), bytes) + self.assertEqual(len(identity), 32) + + # V1 has one admitted platform and one admitted user mode. Their + # mutation cannot be represented as a valid policy, so the independent + # literal preimage and source guard prove that neither closed-domain + # coordinate silently disappears from the versioned identity. + source = inspect.getsource(transport.transport_policy_identity_v1) + for field_name in _POLICY_FIELDS_V1: + with self.subTest(source_coordinate=field_name): + self.assertIn(f".{field_name}", source) + + raw_mutations: dict[str, object] = { + "image_reference": ( + "registry.example/toolchain@sha256:" + "2" * 64 + ), + "platform": "linux/arm64", + "hostname": "lc-build-alt", + "bootstrap": "set -eu\nprintf changed", + "bootstrap_argv0": "labcolors-build-v1-alt", + "tmpfs_specs": coordinates["tmpfs_specs"] + ("/run:rw,size=4096",), + "user_mode": _AlternateUserMode("explicit_ids"), + "stdout_limit": coordinates["stdout_limit"] + 1, + "stderr_limit": coordinates["stderr_limit"] + 1, + "build_timeout_ns": coordinates["build_timeout_ns"] + 1, + "probe_output_limit": coordinates["probe_output_limit"] + 1, + "probe_timeout_ns": coordinates["probe_timeout_ns"] + 1, + } + for field_name, changed_value in raw_mutations.items(): + with self.subTest(preimage_coordinate=field_name): + changed = dict(coordinates) + changed[field_name] = changed_value + self.assertNotEqual( + _expected_policy_identity(changed), + _expected_policy_identity(coordinates), + ) + + valid_mutations = { + key: value + for key, value in raw_mutations.items() + if key not in ("platform", "user_mode") + } + for field_name, changed_value in valid_mutations.items(): + with self.subTest(runtime_coordinate=field_name): + changed_policy = _policy(**{field_name: changed_value}) + self.assertNotEqual( + transport.transport_policy_identity_v1(changed_policy), + identity, + ) + + def test_literal_oracle_rejects_non_tuple_without_asserts(self) -> None: + coordinates = _policy_coordinates(_policy()) + coordinates["tmpfs_specs"] = object() + + with self.assertRaises(TypeError): + _policy_chunks(coordinates) + + +class BuildCapabilityIdentityTests(unittest.TestCase): + def test_daemon_identity_owns_only_the_two_raw_probe_outputs(self) -> None: + server_stdout = b'{"Version":"26.1.4","Os":"linux"}\n' + image_stdout = b'[{"Os":"linux","Architecture":"amd64"}]\n' + daemon = transport.DockerDaemonObservationV1( + server_stdout, + image_stdout, + ) + + self.assertEqual(daemon.server_stdout, server_stdout) + self.assertEqual(daemon.image_inspect_stdout, image_stdout) + self.assertEqual( + daemon.identity, + _expected_daemon_identity(server_stdout, image_stdout), + ) + self.assertEqual( + transport.DockerDaemonObservationV1( + server_stdout, + image_stdout, + ).identity, + daemon.identity, + ) + self.assertNotEqual( + transport.DockerDaemonObservationV1( + server_stdout + b" ", + image_stdout, + ).identity, + daemon.identity, + ) + self.assertNotEqual( + transport.DockerDaemonObservationV1( + server_stdout, + image_stdout + b" ", + ).identity, + daemon.identity, + ) + _assert_deeply_immutable(self, daemon) + + def test_native_command_coordinate_binds_path_and_literal_template(self) -> None: + first_path = Path("/usr/bin/true") + second_path = Path("/usr/bin/false") + expected_contract = _expected_command_contract_identity() + first = transport.native_command_coordinate_v1(first_path) + second = transport.native_command_coordinate_v1(second_path) + + self.assertEqual( + transport.native_command_contract_identity_v1(), + expected_contract, + ) + self.assertEqual( + transport.native_command_contract_identity_v1(), + transport.native_command_contract_identity_v1(), + ) + self.assertIs(type(first), transport.NativeCommandCoordinateV1) + self.assertEqual(first.path, first_path) + self.assertEqual(first.path_bytes, os.fsencode(first_path)) + self.assertEqual(first.command_contract_identity, expected_contract) + self.assertEqual(first.identity, _expected_command_coordinate(first_path)) + self.assertEqual(second.path, second_path) + self.assertEqual(second.identity, _expected_command_coordinate(second_path)) + self.assertNotEqual(first.identity, second.identity) + _assert_deeply_immutable(self, first) + + def test_capability_identity_keeps_policy_daemon_path_and_user_orthogonal(self) -> None: + policy = _policy() + daemon = _daemon() + command_coordinate = transport.native_command_coordinate_v1( + Path("/usr/bin/true") + ) + host_user = (501, 20) + capability = transport.DockerSupportedV1( + policy, + daemon, + command_coordinate, + host_user, + ) + expected_policy_identity = transport.transport_policy_identity_v1(policy) + expected = _expected_capability_identity( + expected_policy_identity, + daemon.identity, + command_coordinate.identity, + host_user, + ) + + self.assertIs(capability.policy, policy) + self.assertIs(capability.daemon_observation, daemon) + self.assertIs(capability.command_coordinate, command_coordinate) + self.assertEqual(capability.host_user, host_user) + self.assertEqual(capability.policy_identity, expected_policy_identity) + self.assertEqual( + capability.daemon_observation_identity, + daemon.identity, + ) + self.assertEqual( + capability.command_coordinate_identity, + command_coordinate.identity, + ) + self.assertEqual( + capability.host_user_identity, + _expected_host_user_identity(host_user), + ) + self.assertEqual(capability.identity, expected) + self.assertEqual( + transport.docker_capability_identity_v1(capability), + expected, + ) + + variants = ( + _capability(policy=_policy(hostname="lc-build-other"), daemon=daemon), + _capability( + policy=policy, + daemon=_daemon(server_stdout=b'{"Version":"other"}\n'), + ), + _capability(policy=policy, daemon=daemon, docker_path=Path("/bin/sh")), + _capability(policy=policy, daemon=daemon, host_user=(502, 20)), + _capability(policy=policy, daemon=daemon, host_user=(501, 21)), + ) + for variant in variants: + with self.subTest(component=variant): + self.assertNotEqual(variant.identity, capability.identity) + + # Changing outer capability coordinates never contaminates the raw + # daemon-observation identity. + self.assertTrue( + all( + variant.daemon_observation_identity + == variant.daemon_observation.identity + for variant in variants + ) + ) + self.assertEqual( + variants[0].daemon_observation_identity, + daemon.identity, + ) + self.assertEqual( + variants[2].daemon_observation_identity, + daemon.identity, + ) + self.assertEqual( + variants[3].daemon_observation_identity, + daemon.identity, + ) + self.assertEqual( + variants[4].daemon_observation_identity, + daemon.identity, + ) + + for value in (policy, capability): + with self.subTest(immutable=type(value).__name__): + _assert_deeply_immutable(self, value) + + +class NativeCommandAndRequestTests(unittest.TestCase): + def test_native_process_context_is_one_identity_bound_launch_renderer(self) -> None: + expected_base = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "cwd": _NATIVE_PROCESS_CWD_V1, + "env": dict(_NATIVE_PROCESS_ENVIRONMENT_V1), + "close_fds": _NATIVE_PROCESS_CLOSE_FDS_V1, + "restore_signals": _NATIVE_PROCESS_RESTORE_SIGNALS_V1, + "start_new_session": _NATIVE_PROCESS_START_NEW_SESSION_V1, + "umask": _NATIVE_PROCESS_UMASK_V1, + } + context = transport._NATIVE_PROCESS_CONTEXT_V1 + for receives_stdin, stdin in ( + (False, subprocess.DEVNULL), + (True, subprocess.PIPE), + ): + with self.subTest(receives_stdin=receives_stdin): + expected = {"stdin": stdin, **expected_base} + first = context.popen_kwargs_v1(receives_stdin) + second = context.popen_kwargs_v1(receives_stdin) + self.assertEqual(first, expected) + self.assertEqual(second, expected) + self.assertIsNot(first, second) + self.assertIsNot(first["env"], second["env"]) + + backend = transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + _policy(), + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + with mock.patch.object( + transport.subprocess, + "Popen", + side_effect=OSError("do not launch in identity test"), + ) as spawn: + for receives_stdin, stdin in ( + (False, subprocess.DEVNULL), + (True, subprocess.PIPE), + ): + with self.subTest(receives_stdin=receives_stdin): + result = backend._observe_command( + ("/usr/bin/true",), + stdout_limit=64, + stderr_limit=64, + timeout_ns=1_000_000_000, + input_bundle=_sealed_input() if receives_stdin else None, + ) + self.assertIs( + type(result), + transport.DockerBuildObserverFailureV1, + ) + expected = {"stdin": stdin, **expected_base} + self.assertEqual(spawn.call_args.kwargs, expected) + + def test_native_backend_requires_its_exact_probe_lease(self) -> None: + policy = _policy() + backend = transport.NativeDockerBuildBackendV1( + Path("/usr/bin/true"), + policy, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + unobserved = _capability(policy=policy) + request = transport.DockerBuildRequestV1( + 1, + unobserved, + _sealed_input(), + 64, + ) + with self.assertRaises(TypeError): + backend._bound_request_capability_v1(request) + + server_stdout = b'{"Version":"identity-test"}\n' + image_stdout = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + separators=(",", ":"), + ).encode("ascii") + observed = ( + transport._docker_command_exited_v1(0, server_stdout, b""), + transport._docker_command_exited_v1(0, image_stdout, b""), + ) + with mock.patch.object(backend, "_observe_command", side_effect=observed): + capability = backend.probe() + self.assertIs(type(capability), transport.DockerSupportedV1) + + equal_but_foreign = transport.DockerSupportedV1(*tuple(capability)) + self.assertEqual(equal_but_foreign, capability) + self.assertIsNot(equal_but_foreign, capability) + cloned_request = transport.DockerBuildRequestV1( + 1, + equal_but_foreign, + _sealed_input(), + 64, + ) + with self.assertRaises(TypeError): + backend._bound_request_capability_v1(cloned_request) + + def test_native_adapter_expands_the_versioned_template_to_exact_argv(self) -> None: + policy = _policy() + docker_path = Path("/usr/bin/true") + backend = transport.NativeDockerBuildBackendV1( + docker_path, + policy, + platform_name="linux", + machine_name="x86_64", + host_user=(501, 20), + ) + server_stdout = b'{"Version":"identity-test"}\n' + image_stdout = json.dumps( + [ + { + "Os": "linux", + "Architecture": "amd64", + "RepoDigests": [policy.image_reference], + } + ], + separators=(",", ":"), + ).encode("ascii") + observed = ( + transport._docker_command_exited_v1(0, server_stdout, b""), + transport._docker_command_exited_v1(0, image_stdout, b""), + ) + with mock.patch.object( + backend, + "_observe_command", + side_effect=observed, + ): + capability = backend.probe() + self.assertIs(type(capability), transport.DockerSupportedV1) + + input_bundle = _sealed_input() + request = transport.DockerBuildRequestV1( + 1, + capability, + input_bundle, + 64, + ) + lease = backend._next_run_lease_v1(capability) + try: + command = backend._command_for_v1(request, lease) + expected = ( + str(docker_path), + "run", + "--rm", + "--interactive", + "--pull", + "never", + "--platform", + policy.platform, + "--network", + "none", + "--read-only", + "--tmpfs", + policy.tmpfs_specs[0], + "--tmpfs", + policy.tmpfs_specs[1], + "--cap-drop", + "ALL", + "--security-opt", + "no-new-privileges:true", + "--hostname", + policy.hostname, + "--user", + "501:20", + "--workdir", + "/", + "--cidfile", + str(lease.cid_file), + "--entrypoint", + "/usr/bin/env", + policy.image_reference, + "-i", + "PATH=/usr/local/bin:/usr/bin:/bin", + "LC_ALL=C", + "LANG=C", + "TZ=UTC", + "HOME=/nonexistent", + "/bin/sh", + "-c", + policy.bootstrap, + policy.bootstrap_argv0, + str(input_bundle.length), + input_bundle.sha256.hex(), + ) + self.assertEqual(command, expected) + self.assertEqual(command.count("--tmpfs"), len(policy.tmpfs_specs)) + self.assertLess( + command.index(policy.tmpfs_specs[0]), + command.index(policy.tmpfs_specs[1]), + ) + self.assertEqual( + transport.native_command_contract_identity_v1(), + _expected_command_contract_identity(), + ) + finally: + backend._release_run_lease_v1(lease) + + def test_foreign_capability_is_rejected_before_backend_run(self) -> None: + policy = _policy() + owned = _capability(policy=policy) + foreign = _capability(policy=policy, docker_path=Path("/bin/sh")) + + class Backend: + def __init__(self) -> None: + self.requests: list[object] = [] + + def probe(self) -> object: + return owned + + def run_build(self, request: object) -> object: + self.requests.append(request) + raise AssertionError("foreign capability reached backend") + + backend = Backend() + controller = transport.ControlledBuildTransportV1( + policy=policy, + backend=backend, + ) + self.assertIs(controller.probe(), owned) + result = controller.build( + foreign, + _sealed_input(), + 64, + input_admission=lambda _value: True, + output_admission=lambda _value: True, + ) + self.assertIs(type(result), transport.BuildRejectedV1) + self.assertEqual( + result.reason, + transport.BuildFailureReasonV1.CONTRACT_VIOLATION, + ) + self.assertEqual(backend.requests, []) + + def test_request_is_deeply_immutable_and_owns_capability_not_policy(self) -> None: + capability = _capability() + request = transport.DockerBuildRequestV1( + 1, + capability, + _sealed_input(), + 64, + ) + + self.assertIs(request.capability, capability) + self.assertFalse(hasattr(request, "policy")) + _assert_deeply_immutable(self, request) + _assert_deeply_immutable(self, request.capability) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_executor.py b/proof/region/v1/tests/test_executor.py new file mode 100644 index 00000000..2e24fc58 --- /dev/null +++ b/proof/region/v1/tests/test_executor.py @@ -0,0 +1,2226 @@ +#!/usr/bin/env python3 +"""Hostile tests for the shared Linux-only proof process boundary.""" + +from __future__ import annotations + +import errno +import fcntl +import hashlib +import os +import signal +import stat +import struct +import sys +import tempfile +import threading +import unittest +from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + + +PROOF = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROOF)) + +import executor # noqa: E402 + + +def _static_elf(*, interpreter: bool = False, needed: bool = False) -> bytes: + """Return a parseable ELF64/x86-64 shape; it is not intended to run.""" + + program_headers: list[bytes] = [] + body = bytearray(64) + program_headers.append( + struct.pack(" tuple[tuple[int, ...], ...]: + count = struct.unpack_from(" bytes: + """Create a literal static ELF64 fixture with one RX load segment.""" + + code_offset = 64 + 56 + file_size = code_offset + len(code) + ident = b"\x7fELF\x02\x01\x01" + bytes(9) + header = ident + struct.pack( + " executor.ExecutionLimitsV1: + values = { + "max_executable_bytes": 4096, + "max_stdin_bytes": 4096, + "max_argument_bytes": 4096, + "max_stdout_bytes": 16, + "max_stderr_bytes": 16, + "wall_timeout_ns": 1_000_000_000, + "memory_max_bytes": 64 * 1024 * 1024, + "pids_max": 1, + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +def _replace_limits( + value: executor.ExecutionLimitsV1, + **changes: int, +) -> executor.ExecutionLimitsV1: + values = { + name: getattr(value, name) + for name in ( + "max_executable_bytes", + "max_stdin_bytes", + "max_argument_bytes", + "max_stdout_bytes", + "max_stderr_bytes", + "wall_timeout_ns", + "memory_max_bytes", + "pids_max", + ) + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +def _request(**changes: object) -> executor.ExecutionRequestV1: + values: dict[str, object] = { + "executable": _static_elf(), + "argv": ( + b"proof-evaluator", + b"--manifest-identity", + b"1" * 64, + b"--job", + b"/dev/stdin", + ), + "environment": ((b"LC_ALL", b"C"), (b"TZ", b"UTC")), + "cwd": b"/work", + "stdin": b"LCJOB1\0\0", + "umask": 0o077, + "limits": _limits(), + } + values.update(changes) + return executor.ExecutionRequestV1(**values) + + +class _Backend: + def __init__( + self, + probe_result: executor.CapabilityReportV1, + run_result: executor.ExecutionResultV1 | None = None, + ) -> None: + self.probe_result = probe_result + self.run_result = run_result + self.probe_calls = 0 + self.received: list[ + tuple[executor.ExecutionRequestV1, executor.SupportedV1] + ] = [] + + def probe(self, guard: object) -> executor.CapabilityReportV1: + self.probe_calls += 1 + if not guard.is_current(): # type: ignore[attr-defined] + raise AssertionError("controller supplied a stale probe guard") + return self.probe_result + + def run( + self, + request: executor.ExecutionRequestV1, + capability: executor.SupportedV1, + ) -> executor.ExecutionResultV1: + self.received.append((request, capability)) + if self.run_result is None: + raise AssertionError("unsupported backend must not be run") + return self.run_result + + +class _MemfdOperations: + def __init__(self) -> None: + self.fd = 41 + self.bytes_by_fd: dict[int, bytes] = {} + self.seals_by_fd: dict[int, int] = {} + self.exec_calls: list[tuple[int, tuple[bytes, ...], tuple[tuple[bytes, bytes], ...]]] = [] + self.events: list[str] = [] + + def create_executable_memfd(self) -> int: + self.events.append("create") + return self.fd + + def pipe_cloexec(self) -> tuple[int, int]: + read_fd, write_fd = os.pipe() + for descriptor in (read_fd, write_fd): + flags = fcntl.fcntl(descriptor, fcntl.F_GETFD) + fcntl.fcntl(descriptor, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + return read_fd, write_fd + + def write_all(self, fd: int, data: bytes) -> None: + self.events.append("write") + self.bytes_by_fd[fd] = data + + def make_executable(self, fd: int) -> None: + self.events.append("chmod") + self.assert_known(fd) + + def add_seals(self, fd: int, seals: int) -> None: + self.events.append("seal") + self.assert_known(fd) + self.seals_by_fd[fd] = seals + + def get_seals(self, fd: int) -> int: + self.events.append("get_seals") + self.assert_known(fd) + return self.seals_by_fd[fd] + + def pread(self, fd: int, size: int, offset: int) -> bytes: + self.events.append("pread") + self.assert_known(fd) + return self.bytes_by_fd[fd][offset : offset + size] + + def execveat( + self, + fd: int, + argv: tuple[bytes, ...], + environment: tuple[tuple[bytes, bytes], ...], + ) -> None: + self.events.append("execveat") + self.assert_known(fd) + self.exec_calls.append((fd, argv, environment)) + + def close(self, fd: int) -> None: + self.assert_known(fd) + + def assert_known(self, fd: int) -> None: + if fd != self.fd: + raise AssertionError(f"unexpected file descriptor: {fd}") + + +class _ProbeOperations(_MemfdOperations): + def __init__(self) -> None: + super().__init__() + self.probes: list[str] = [] + + def probe_execveat(self) -> None: + self.probes.append("execveat") + + def probe_standard_fds(self) -> None: + self.probes.append("standard_fds") + + def probe_single_threaded(self) -> None: + self.probes.append("single_threaded") + + def probe_close_range(self) -> None: + self.probes.append("close_range") + + def probe_namespaces(self) -> None: + self.probes.append("namespaces") + + def probe_seccomp(self) -> None: + self.probes.append("seccomp") + + +class _LateThreadOperations(_ProbeOperations): + def probe_single_threaded(self) -> None: + self.probes.append("single_threaded") + raise OSError(errno.EBUSY, "late thread") + + +class _OverlapAfterSingleThreadOperations(_ProbeOperations): + def __init__(self) -> None: + super().__init__() + self.single_thread_passed = threading.Event() + self.release_outer_probe = threading.Event() + self.blocked_once = False + + def probe_single_threaded(self) -> None: + super().probe_single_threaded() + if not self.blocked_once: + self.blocked_once = True + self.single_thread_passed.set() + if not self.release_outer_probe.wait(timeout=1): + raise AssertionError("overlap test did not release the outer probe") + + +class _SecondSingleThreadFailureOperations(_ProbeOperations): + def __init__(self) -> None: + super().__init__() + self.single_thread_probes = 0 + + def probe_single_threaded(self) -> None: + super().probe_single_threaded() + self.single_thread_probes += 1 + if self.single_thread_probes == 2: + raise OSError(errno.EBUSY, "thread appeared before fork") + + +class _CgroupFactory: + def __init__(self) -> None: + self.observer_budgets: list[Path] = [] + self.probed: list[Path] = [] + + def probe_observer_task_budget(self, parent: Path) -> None: + self.observer_budgets.append(parent) + + def probe(self, parent: Path) -> None: + self.probed.append(parent) + + +class _ObserverCgroup: + def __init__(self, pid: int) -> None: + self.pid = pid + + def kill_all(self) -> None: + try: + os.kill(self.pid, signal.SIGKILL) + except ProcessLookupError: + pass + + def oom_kill_count(self) -> int: + return 0 + + def populated(self) -> bool: + return False + + +class _ReadbackCgroup(executor._CgroupV2V1): + def __init__(self, values: dict[bytes, bytes]) -> None: + self.values = values + + def _read_required(self, name: bytes) -> bytes: + return self.values[name] + + +class SharedExecutorBoundaryTests(unittest.TestCase): + def test_executor_is_one_shared_leaf_outside_engine_packages(self) -> None: + shared = PROOF / "executor.py" + + self.assertTrue(shared.is_file()) + self.assertFalse((PROOF / "arb/executor.py").exists()) + self.assertEqual(Path(executor.__file__).resolve(), shared.resolve()) + source = shared.read_text(encoding="utf-8") + self.assertNotIn("Arb", source) + self.assertNotIn("labcolors-arb", source.lower()) + self.assertNotIn("mpfi", source.lower()) + for engine_import in ( + "import arb", + "from arb", + ".arb", + "import mpfi", + "from mpfi", + ".mpfi", + ): + with self.subTest(engine_import=engine_import): + self.assertNotIn(engine_import, source.lower()) + + def test_execution_identities_match_independent_literal_goldens(self) -> None: + request = _request() + platform_value = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + + self.assertEqual( + executor.invocation_identity_v1(request).hex(), + "4c5bf676852b086fe7909a572bdbcc497ea52cec8d5affbe162fcd776605c384", + ) + self.assertEqual( + executor.platform_identity_v1(platform_value).hex(), + "0e37fa87cadce6814466528b2ea419e964554339bcbcb8d27c2da66c95dabc51", + ) + + platform_value = tuple.__new__( + executor.SupportedV1, + (executor.EXECUTION_PLATFORM_V1, "foreign"), + ) + self.assertEqual( + executor.platform_identity_v1(platform_value), + executor.ExecutionIdentityRejectedV1( + executor.ExecutionIdentityReasonV1.FOREIGN_PLATFORM, + ), + ) + + def test_invocation_identity_binds_every_representable_coordinate(self) -> None: + request = _request() + baseline = executor.invocation_identity_v1(request) + limit_mutations = ( + {"max_executable_bytes": request.limits.max_executable_bytes + 1}, + {"max_stdin_bytes": request.limits.max_stdin_bytes + 1}, + {"max_argument_bytes": request.limits.max_argument_bytes + 1}, + {"max_stdout_bytes": request.limits.max_stdout_bytes + 1}, + {"max_stderr_bytes": request.limits.max_stderr_bytes + 1}, + {"wall_timeout_ns": request.limits.wall_timeout_ns + 1}, + {"memory_max_bytes": request.limits.memory_max_bytes + 1}, + ) + mutants = ( + _request(executable=request.executable + b"x"), + _request(argv=request.argv + (b"--strict",)), + _request( + environment=((b"LC_ALL", b"POSIX"), (b"TZ", b"UTC")), + ), + _request(cwd=b"/"), + _request(stdin=request.stdin + b"x"), + _request(umask=0o022), + *( + _request(limits=_replace_limits(request.limits, **changes)) + for changes in limit_mutations + ), + ) + + self.assertEqual(len(mutants), 13) + identities = {executor.invocation_identity_v1(item) for item in mutants} + self.assertEqual(len(identities), 13) + self.assertTrue(all(type(identity) is bytes for identity in identities)) + self.assertTrue( + all(executor.invocation_identity_v1(item) != baseline for item in mutants) + ) + + def test_identity_api_returns_typed_rejections_for_foreign_inputs(self) -> None: + cases = ( + ( + object(), + executor.ExecutionIdentityReasonV1.WRONG_REQUEST_TYPE, + ), + ( + tuple.__new__(executor.ExecutionRequestV1, ()), + executor.ExecutionIdentityReasonV1.REQUEST_NOT_ADMITTED, + ), + ) + for value, reason in cases: + with self.subTest(reason=reason): + self.assertEqual( + executor.invocation_identity_v1(value), + executor.ExecutionIdentityRejectedV1(reason), + ) + self.assertEqual( + executor.platform_identity_v1(object()), + executor.ExecutionIdentityRejectedV1( + executor.ExecutionIdentityReasonV1.FOREIGN_PLATFORM, + ), + ) + + def test_post_admission_request_mutation_cannot_receive_an_identity(self) -> None: + request = _request() + capability = executor.SupportedV1( + executor.EXECUTION_PLATFORM_V1, + executor.SANDBOX_POLICY_RELEASE_V1, + ) + invocation_identity = executor.invocation_identity_v1(request) + platform_identity = executor.platform_identity_v1(capability) + + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(request, "stdin", request.stdin + b"x") + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(request.limits, "wall_timeout_ns", 1) + with self.assertRaises((AttributeError, TypeError)): + object.__setattr__(capability, "sandbox_policy_release", "foreign") + self.assertEqual(executor.invocation_identity_v1(request), invocation_identity) + self.assertEqual(executor.platform_identity_v1(capability), platform_identity) + + def test_supported_platform_rejects_hostile_string_subclasses(self) -> None: + class HostileString(str): + def encode(self, *_args: object, **_kwargs: object) -> bytes: + raise RuntimeError("hostile encoding") + + with self.assertRaises(TypeError): + executor.SupportedV1( + HostileString(executor.EXECUTION_PLATFORM_V1), + HostileString(executor.SANDBOX_POLICY_RELEASE_V1), + ) + + +class RequestAdmissionTests(unittest.TestCase): + def test_combined_dynamic_fixture_points_after_its_full_header_table(self) -> None: + elf = _static_elf(interpreter=True, needed=True) + headers = _program_headers(elf) + dynamic = next(header for header in headers if header[0] == 2) + + self.assertEqual(dynamic[2], 64 + 56 * len(headers)) + self.assertEqual( + elf[dynamic[2] : dynamic[2] + dynamic[5]], + struct.pack(" None: + with self.assertRaises(executor.ExecutionRequestErrorV1) as caught: + _request(**changes) + self.assertEqual(caught.exception.reason, reason) + + def test_request_preserves_exact_invocation_without_mapping_or_inheritance(self) -> None: + request = _request() + + self.assertEqual( + request.argv, + ( + b"proof-evaluator", + b"--manifest-identity", + b"1" * 64, + b"--job", + b"/dev/stdin", + ), + ) + self.assertEqual(request.environment, ((b"LC_ALL", b"C"), (b"TZ", b"UTC"))) + self.assertEqual(request.cwd, b"/work") + self.assertEqual(request.stdin, b"LCJOB1\0\0") + self.assertFalse(hasattr(request, "network_isolated")) + self.assertFalse(hasattr(request, "cgroup_isolated")) + + def test_request_rejects_forged_exact_limit_values(self) -> None: + forged = tuple.__new__( + executor.ExecutionLimitsV1, + (4096, 4096, 4096, 16, 16, 1_000_000_000, 64 * 1024 * 1024, 2), + ) + + with self.assertRaises(executor.ExecutionRequestErrorV1) as caught: + _request(limits=forged) + + self.assertEqual(caught.exception.reason, executor.RequestReasonV1.INVALID_LIMIT) + + def test_argv_environment_cwd_and_stdin_are_strict_bytes(self) -> None: + cases = ( + ({"argv": [b"proof-evaluator"]}, executor.RequestReasonV1.WRONG_TYPE), + ({"argv": (b"",)}, executor.RequestReasonV1.EMPTY_ARGV_ZERO), + ({"argv": (b"arb\0evil",)}, executor.RequestReasonV1.NUL_BYTE), + ({"environment": {b"LC_ALL": b"C"}}, executor.RequestReasonV1.WRONG_TYPE), + ( + {"environment": ((b"TZ", b"UTC"), (b"LC_ALL", b"C"))}, + executor.RequestReasonV1.NONCANONICAL_ENVIRONMENT, + ), + ( + {"environment": ((b"LC_ALL", b"C"), (b"LC_ALL", b"POSIX"))}, + executor.RequestReasonV1.DUPLICATE_ENVIRONMENT, + ), + ({"environment": ((b"A=B", b"C"),)}, executor.RequestReasonV1.INVALID_ENVIRONMENT_KEY), + ({"cwd": b"relative"}, executor.RequestReasonV1.RELATIVE_CWD), + ({"cwd": b"/work/../tmp"}, executor.RequestReasonV1.NONCANONICAL_CWD), + ({"stdin": "not-bytes"}, executor.RequestReasonV1.WRONG_TYPE), + ) + for changes, reason in cases: + with self.subTest(changes=changes): + self.assert_rejected(reason, **changes) + + def test_explicit_limits_reject_oversized_inputs_and_bool_numbers(self) -> None: + self.assert_rejected( + executor.RequestReasonV1.LIMIT_EXCEEDED, + stdin=b"12345", + limits=_limits(max_stdin_bytes=4), + ) + self.assert_rejected( + executor.RequestReasonV1.LIMIT_EXCEEDED, + executable=_static_elf() + b"x" * 4096, + ) + with self.assertRaises(executor.ExecutionRequestErrorV1) as caught: + _replace_limits(_limits(), pids_max=True) # type: ignore[arg-type] + self.assertEqual(caught.exception.reason, executor.RequestReasonV1.INVALID_LIMIT) + with self.assertRaises(executor.ExecutionRequestErrorV1) as caught: + _replace_limits(_limits(), pids_max=2) + self.assertEqual(caught.exception.reason, executor.RequestReasonV1.INVALID_LIMIT) + for field_name in ( + "max_executable_bytes", + "max_stdin_bytes", + "max_argument_bytes", + "max_stdout_bytes", + "max_stderr_bytes", + "wall_timeout_ns", + "memory_max_bytes", + "pids_max", + ): + with self.subTest(u64_field=field_name): + with self.assertRaises(executor.ExecutionRequestErrorV1) as caught: + _replace_limits(_limits(), **{field_name: 1 << 64}) + self.assertEqual( + caught.exception.reason, + executor.RequestReasonV1.INVALID_LIMIT, + ) + cardinalities = ( + ("argv", (b"proof-evaluator",)), + ("environment", ((b"LC_ALL", b"C"),)), + ) + for field_name, target in cardinalities: + with self.subTest(u32_cardinality=field_name): + with mock.patch.object( + executor, + "_sequence_count_v1", + side_effect=lambda value, target=target: ( + 1 << 32 if value is target else len(value) + ), + ): + with self.assertRaises( + executor.ExecutionRequestErrorV1 + ) as caught: + _request(**{field_name: target}) + self.assertEqual( + caught.exception.reason, + executor.RequestReasonV1.LIMIT_EXCEEDED, + ) + + def test_only_static_x86_64_elf_is_admitted(self) -> None: + self.assert_rejected(executor.RequestReasonV1.INVALID_ELF, executable=b"#!/bin/sh\n") + self.assert_rejected( + executor.RequestReasonV1.DYNAMIC_EXECUTABLE, + executable=_static_elf(interpreter=True), + ) + self.assert_rejected( + executor.RequestReasonV1.DYNAMIC_EXECUTABLE, + executable=_static_elf(needed=True), + ) + + def test_cross_module_verifiers_are_explicit_versioned_api(self) -> None: + self.assertTrue(callable(executor.require_static_x86_64_elf_v1)) + self.assertTrue(callable(executor.result_matches_request_v1)) + self.assertTrue(callable(executor.canonical_cgroup_parent_v1)) + self.assertTrue(callable(executor.enter_observer_cgroup_v1)) + self.assertFalse(hasattr(executor, "_require_static_x86_64_elf")) + self.assertFalse(hasattr(executor, "_result_matches_request")) + self.assertFalse(hasattr(executor, "_canonical_cgroup_parent_v1")) + self.assertFalse(hasattr(executor, "_enter_observer_cgroup_v1")) + + +class CapabilityAndExecutionTests(unittest.TestCase): + def test_non_linux_host_fails_closed_before_any_run(self) -> None: + native = executor.NativeLinuxBackendV1( + cgroup_parent=None, + platform_name="darwin", + machine_name="arm64", + ) + controller = executor.ControlledExecutorV1(native) + report = controller.probe() + + self.assertIs(type(report), executor.UnsupportedV1) + self.assertEqual( + report.failures, + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.HOST_NOT_LINUX, + None, + ), + ), + ) + result = controller.execute(_request()) + self.assertEqual(result, report) + + def test_linux_without_an_explicit_delegated_cgroup_is_unsupported(self) -> None: + native = executor.NativeLinuxBackendV1( + cgroup_parent=None, + platform_name="linux", + machine_name="x86_64", + ) + report = executor.ControlledExecutorV1(native).probe() + + self.assertIs(type(report), executor.UnsupportedV1) + self.assertIn( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.CGROUP_PARENT_NOT_DECLARED, + None, + ), + report.failures, + ) + + def test_native_backend_rejects_noncanonical_cgroup_configuration(self) -> None: + class ExplodingPathLike: + def __fspath__(self) -> str: + raise RuntimeError("hostile cgroup path") + + for invalid in ( + object(), + b"/delegated-proof-cgroup", + "relative", + "/delegated-proof-cgroup\0", + "/delegated-proof-cgroup\ud800", + "/delegated/./proof-cgroup", + "/delegated/../proof-cgroup", + "//delegated/proof-cgroup", + "/delegated/proof-cgroup/", + ExplodingPathLike(), + ): + with self.subTest(invalid=type(invalid).__name__): + with self.assertRaises(TypeError): + executor.NativeLinuxBackendV1(cgroup_parent=invalid) # type: ignore[arg-type] + + def test_supported_probe_executes_every_required_mechanism(self) -> None: + operations = _ProbeOperations() + cgroups = _CgroupFactory() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=cgroups, + ) + + report = executor.ControlledExecutorV1(native).probe() + + self.assertEqual( + report, + executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ), + ) + self.assertEqual( + operations.probes, + [ + "standard_fds", + "single_threaded", + "execveat", + "close_range", + "single_threaded", + "namespaces", + "single_threaded", + "seccomp", + ], + ) + self.assertEqual( + cgroups.observer_budgets, + [Path("/delegated-proof-cgroup")], + ) + self.assertEqual(cgroups.probed, [Path("/delegated-proof-cgroup")]) + + def test_final_probe_cannot_outlive_its_controller_lease(self) -> None: + current = True + + class InvalidatingCgroupFactory(_CgroupFactory): + def probe(self, parent: Path) -> None: + nonlocal current + super().probe(parent) + current = False + + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=_ProbeOperations(), + cgroup_factory=InvalidatingCgroupFactory(), + ) + + report = native._probe_capability_v1( + executor._ProbeGuardV1(lambda: current) + ) + + self.assertEqual(report, executor._invalidated_capability_report_v1()) + + def test_overlap_after_single_thread_gate_cancels_before_fork_and_revokes_authority(self) -> None: + operations = _OverlapAfterSingleThreadOperations() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=_CgroupFactory(), + ) + controller = executor.ControlledExecutorV1(native) + reports: list[executor.CapabilityReportV1] = [] + worker = threading.Thread( + target=lambda: reports.append(controller.probe()), + daemon=True, + ) + + worker.start() + self.assertTrue( + operations.single_thread_passed.wait(timeout=1), + "outer probe did not reach the single-thread gate", + ) + overlap = controller.probe() + operations.release_outer_probe.set() + worker.join(timeout=1) + + self.assertFalse(worker.is_alive(), "overlapping probe deadlocked") + self.assertEqual(len(reports), 1) + invalidated = executor.UnsupportedV1( + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.OBSERVATION_INVALIDATED, + errno.EBUSY, + ), + ) + ) + self.assertEqual(overlap, invalidated) + self.assertEqual(reports[0], invalidated) + self.assertEqual(operations.probes, ["standard_fds", "single_threaded"]) + self.assertIsNone(controller._issued_capability) + self.assertEqual( + controller.probe(), + executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ), + ) + + def test_failed_single_thread_gate_suppresses_every_forking_probe(self) -> None: + operations = _LateThreadOperations() + cgroups = _CgroupFactory() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=cgroups, + ) + + report = executor.ControlledExecutorV1(native).probe() + + self.assertEqual( + report, + executor.UnsupportedV1( + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.OBSERVER_NOT_SINGLE_THREADED, + errno.EBUSY, + ), + ) + ), + ) + self.assertEqual(operations.probes, ["standard_fds", "single_threaded"]) + self.assertEqual(cgroups.observer_budgets, []) + self.assertEqual(cgroups.probed, []) + + def test_second_single_thread_gate_suppresses_forking_probe(self) -> None: + operations = _SecondSingleThreadFailureOperations() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=_CgroupFactory(), + ) + + report = executor.ControlledExecutorV1(native).probe() + + self.assertEqual( + report, + executor.UnsupportedV1( + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.OBSERVER_NOT_SINGLE_THREADED, + errno.EBUSY, + ), + ) + ), + ) + self.assertEqual( + operations.probes, + ["standard_fds", "single_threaded", "execveat", "close_range", "single_threaded"], + ) + + def test_one_probe_capability_is_forwarded_to_exactly_one_run(self) -> None: + capability = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + expected = executor.CompletedV1( + binary_sha256=hashlib.sha256(_static_elf()).digest(), + stdout=b"answer", + stderr=b"", + ) + backend = _Backend(capability, expected) + request = _request() + + actual = executor.ControlledExecutorV1(backend).execute(request) + + self.assertEqual(actual, expected) + self.assertEqual(backend.probe_calls, 1) + self.assertEqual(len(backend.received), 1) + received_request, received_capability = backend.received[0] + self.assertIs(received_request, request) + self.assertEqual(received_capability, capability) + self.assertIsNot(received_capability, capability) + + def test_forged_exact_capability_is_rejected_without_exception(self) -> None: + forged = tuple.__new__(executor.SupportedV1, ()) + backend = _Backend(forged) + + report = executor.ControlledExecutorV1(backend).probe() + + self.assertEqual( + report, + executor.UnsupportedV1( + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.KERNEL_API_UNAVAILABLE, + None, + ), + ) + ), + ) + + def test_forged_exact_unsupported_report_is_rejected_without_exception(self) -> None: + forged = object.__new__(executor.UnsupportedV1) + backend = _Backend(forged) + + report = executor.ControlledExecutorV1(backend).probe() + + self.assertEqual( + report, + executor.UnsupportedV1( + ( + executor.CapabilityFailureV1( + executor.CapabilityReasonV1.KERNEL_API_UNAVAILABLE, + None, + ), + ) + ), + ) + + def test_unadmitted_exact_request_never_reaches_the_backend(self) -> None: + admitted = _request() + forged_limits = tuple.__new__( + executor.ExecutionLimitsV1, + (*admitted.limits[:-1], 2), + ) + forged = tuple.__new__( + executor.ExecutionRequestV1, + (*admitted[:-1], forged_limits), + ) + capability = executor.SupportedV1( + executor.EXECUTION_PLATFORM_V1, + executor.SANDBOX_POLICY_RELEASE_V1, + ) + backend = _Backend( + capability, + executor.CompletedV1( + hashlib.sha256(admitted.executable).digest(), + b"answer", + b"", + ), + ) + + result = executor.ControlledExecutorV1(backend).execute(forged) + + self.assertEqual( + result, + executor.ObserverFailureV1( + executor.ObserverReasonV1.REQUEST_NOT_ADMITTED + ), + ) + self.assertEqual(backend.probe_calls, 0) + self.assertEqual(backend.received, []) + + def test_preprobed_capability_is_consumed_without_a_second_probe(self) -> None: + capability = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + request = _request() + expected = executor.CompletedV1( + binary_sha256=hashlib.sha256(request.executable).digest(), + stdout=b"answer", + stderr=b"", + ) + backend = _Backend(capability, expected) + controller = executor.ControlledExecutorV1(backend) + issued = controller.probe() + self.assertEqual(issued, capability) + self.assertIsNot(issued, capability) + + with mock.patch.object( + backend, + "probe", + side_effect=AssertionError("execute must consume the supplied observation"), + ): + actual = controller.execute(request, issued) + + self.assertEqual(actual, expected) + self.assertEqual(backend.probe_calls, 1) + self.assertEqual( + controller.execute(request, issued), + executor.ObserverFailureV1(executor.ObserverReasonV1.PROBE_FAILED), + ) + + def test_backend_reused_report_cannot_renew_a_stale_controller_lease(self) -> None: + backend_report = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + request = _request() + expected = executor.CompletedV1( + binary_sha256=hashlib.sha256(request.executable).digest(), + stdout=b"answer", + stderr=b"", + ) + backend = _Backend(backend_report, expected) + controller = executor.ControlledExecutorV1(backend) + + stale = controller.probe() + fresh = controller.probe() + + self.assertIs(type(stale), executor.SupportedV1) + self.assertIs(type(fresh), executor.SupportedV1) + self.assertIsNot(stale, fresh) + self.assertEqual( + controller.execute(request, stale), + executor.ObserverFailureV1(executor.ObserverReasonV1.PROBE_FAILED), + ) + self.assertEqual(backend.received, []) + self.assertEqual(controller.execute(request, fresh), expected) + self.assertEqual(len(backend.received), 1) + + def test_controller_capability_cannot_be_duplicated_across_fork(self) -> None: + backend_report = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + request = _request() + expected = executor.CompletedV1( + binary_sha256=hashlib.sha256(request.executable).digest(), + stdout=b"answer", + stderr=b"", + ) + backend = _Backend(backend_report, expected) + controller = executor.ControlledExecutorV1(backend) + capability = controller.probe() + read_descriptor, write_descriptor = os.pipe() + + child = os.fork() + if child == 0: + os.close(read_descriptor) + try: + result = controller.execute(request, capability) + payload = ( + b"blocked" + if result + == executor.ObserverFailureV1( + executor.ObserverReasonV1.PROBE_FAILED + ) + else b"executed" + ) + os.write(write_descriptor, payload) + status = 0 + except BaseException: + status = 1 + finally: + os.close(write_descriptor) + os._exit(status) + + os.close(write_descriptor) + try: + payload = os.read(read_descriptor, 32) + finally: + os.close(read_descriptor) + waited, status = os.waitpid(child, 0) + + self.assertEqual(waited, child) + self.assertTrue(os.WIFEXITED(status)) + self.assertEqual(os.WEXITSTATUS(status), 0) + self.assertEqual(payload, b"blocked") + self.assertEqual(controller.execute(request, capability), expected) + self.assertEqual(len(backend.received), 1) + + def test_capability_cannot_cross_a_backend_replacement(self) -> None: + request = _request() + capability = executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ) + expected = executor.CompletedV1( + binary_sha256=hashlib.sha256(request.executable).digest(), + stdout=b"answer", + stderr=b"", + ) + original = _Backend(capability, expected) + replacement = _Backend(capability, expected) + controller = executor.ControlledExecutorV1(original) + issued = controller.probe() + self.assertEqual(issued, capability) + self.assertIsNot(issued, capability) + controller._backend = replacement + + result = controller.execute(request, issued) + + self.assertEqual( + result, + executor.ObserverFailureV1(executor.ObserverReasonV1.PROBE_FAILED), + ) + self.assertEqual(original.received, []) + self.assertEqual(replacement.received, []) + + def test_native_run_consumes_capability_without_reprobe_and_rejects_foreign(self) -> None: + operations = _ProbeOperations() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=_CgroupFactory(), + ) + controller = executor.ControlledExecutorV1(native) + capability = controller.probe() + self.assertIs(type(capability), executor.SupportedV1) + creates_after_probe = operations.events.count("create") + request = _request(cwd=b"/definitely-missing-labcolors-cwd") + + with mock.patch.object( + native, + "probe", + side_effect=AssertionError("run must consume, not repeat, capability probe"), + ): + result = controller.execute(request, capability) + + self.assertIs(type(result), executor.SandboxSetupFailedV1) + self.assertEqual(result.stage, executor.SetupStageV1.CWD) + self.assertEqual(operations.events.count("create"), creates_after_probe + 1) + + equal_but_foreign = executor.SupportedV1( + capability.platform, + capability.sandbox_policy_release, + ) + self.assertEqual(equal_but_foreign, capability) + self.assertIsNot(equal_but_foreign, capability) + for invalid in (capability, equal_but_foreign, object()): + with self.subTest(invalid=invalid): + with mock.patch.object( + executor, + "_seal_executable_v1", + side_effect=AssertionError("foreign capability must not execute"), + ): + rejected = controller.execute( # type: ignore[arg-type] + request, + invalid, + ) + self.assertEqual( + rejected, + executor.ObserverFailureV1( + executor.ObserverReasonV1.PROBE_FAILED, + ), + ) + + def test_native_capability_has_one_atomic_consumer(self) -> None: + operations = _ProbeOperations() + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=operations, + cgroup_factory=_CgroupFactory(), + ) + controller = executor.ControlledExecutorV1(native) + capability = controller.probe() + self.assertIs(type(capability), executor.SupportedV1) + creates_after_probe = operations.events.count("create") + request = _request(cwd=b"/definitely-missing-labcolors-cwd") + + with ThreadPoolExecutor(max_workers=2) as pool: + results = tuple( + pool.map( + lambda _index: controller.execute(request, capability), + range(2), + ) + ) + + self.assertEqual(operations.events.count("create"), creates_after_probe + 1) + self.assertEqual( + sum(type(result) is executor.SandboxSetupFailedV1 for result in results), + 1, + ) + self.assertEqual( + sum( + result + == executor.ObserverFailureV1(executor.ObserverReasonV1.PROBE_FAILED) + for result in results + ), + 1, + ) + + def test_failed_native_probe_revokes_earlier_capability(self) -> None: + native = executor.NativeLinuxBackendV1( + cgroup_parent="/delegated-proof-cgroup", + platform_name="linux", + machine_name="x86_64", + operations=_ProbeOperations(), + cgroup_factory=_CgroupFactory(), + ) + controller = executor.ControlledExecutorV1(native) + stale = controller.probe() + self.assertIs(type(stale), executor.SupportedV1) + native._platform_name = "darwin" + + failed = controller.probe() + + self.assertIs(type(failed), executor.UnsupportedV1) + with mock.patch.object( + executor, + "_seal_executable_v1", + side_effect=AssertionError("failed probe must revoke earlier capability"), + ): + rejected = controller.execute(_request(), stale) + self.assertEqual( + rejected, + executor.ObserverFailureV1(executor.ObserverReasonV1.PROBE_FAILED), + ) + + def test_untrusted_backend_cannot_return_unbounded_output(self) -> None: + class ExplosiveEquality: + def __eq__(self, _other: object) -> bool: + raise RuntimeError("comparison escaped") + + invalid_results = ( + executor.CompletedV1( + binary_sha256=b"x" * 32, + stdout=b"17 bytes overflow", + stderr=b"", + ), + executor.CompletedV1(ExplosiveEquality(), b"", b""), + executor.ObserverFailureV1(ExplosiveEquality()), + ) + for invalid in invalid_results: + with self.subTest(invalid=type(invalid).__name__): + self.assertFalse(executor.result_matches_request_v1(invalid, _request())) + backend = _Backend( + executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ), + invalid, + ) + + result = executor.ControlledExecutorV1(backend).execute(_request()) + + self.assertIs(type(result), executor.ObserverFailureV1) + self.assertEqual( + result.reason, + executor.ObserverReasonV1.BACKEND_CONTRACT, + ) + self.assertFalse(hasattr(result, "stdout")) + + def test_process_failures_remain_distinct_from_evaluator_resource_outcome(self) -> None: + digest = hashlib.sha256(_static_elf()).digest() + results: tuple[executor.ExecutionResultV1, ...] = ( + executor.ExitNonZeroV1(digest, b"", b"bad", 17), + executor.SignaledV1(digest, b"", b"", 11, True), + executor.TimedOutV1(digest, b"", b"", 1_000_000_000), + executor.OomKilledV1(digest, b"", b"", 1), + executor.OutputLimitExceededV1( + digest, + b"x" * 16, + b"", + executor.OutputStreamV1.STDOUT, + 16, + ), + ) + for expected in results: + with self.subTest(result_type=type(expected).__name__): + backend = _Backend( + executor.SupportedV1( + "linux-x86_64", executor.SANDBOX_POLICY_RELEASE_V1 + ), + expected, + ) + actual = executor.ControlledExecutorV1(backend).execute(_request()) + self.assertEqual(actual, expected) + self.assertNotIn("ResourceLimit", type(actual).__name__) + + def test_executor_exports_observations_but_no_receipt_mint(self) -> None: + self.assertFalse(any("Receipt" in name for name in dir(executor))) + self.assertFalse(hasattr(executor.ControlledExecutorV1, "mint")) + self.assertFalse(hasattr(executor.ControlledExecutorV1, "admit")) + + +class SameObjectAndObserverProtocolTests(unittest.TestCase): + @staticmethod + def _seccomp_verdict( + program: list[object], + syscall_number: int, + *, + architecture: int = 0xC000003E, + arguments: tuple[int, ...] = (0, 0, 0, 0, 0, 0), + ) -> int: + words = {0: syscall_number, 4: architecture} + for index, argument in enumerate(arguments): + words[16 + index * 8] = argument & 0xFFFFFFFF + words[20 + index * 8] = (argument >> 32) & 0xFFFFFFFF + accumulator = 0 + pc = 0 + for _ in range(1024): + instruction = program[pc] + if instruction.code == 0x20: # BPF_LD | BPF_W | BPF_ABS + accumulator = words.get(instruction.k, 0) + pc += 1 + elif instruction.code == 0x15: # BPF_JMP | BPF_JEQ | BPF_K + pc += 1 + (instruction.jt if accumulator == instruction.k else instruction.jf) + elif instruction.code == 0x06: # BPF_RET | BPF_K + return instruction.k + else: + raise AssertionError(f"unknown BPF opcode {instruction.code:#x}") + raise AssertionError("seccomp program did not terminate") + + def test_seccomp_filter_denies_files_network_processes_and_exec_path_swaps(self) -> None: + operations = executor._NativeLinuxOperationsV1() + program = operations._seccomp_program(exec_fd=3, setup_error_fd=4) + killed = 0x80000000 + allowed = 0x7FFF0000 + + self.assertEqual(self._seccomp_verdict(program, 1), allowed) # write + for syscall_number in (2, 41, 56, 257, 319): + with self.subTest(syscall_number=syscall_number): + self.assertEqual(self._seccomp_verdict(program, syscall_number), killed) + # Static glibc issues exactly these two extra syscalls before main: + # it resolves /proc/self/exe once and seeds the stack protector canary + # from the kernel RNG. Denying either kills the sealed evaluator with + # signal 31 before its first output byte (readlink resolves a link + # target and opens nothing; getrandom only reads kernel entropy). + for startup_syscall in (89, 318): + with self.subTest(startup_syscall=startup_syscall): + self.assertEqual( + self._seccomp_verdict(program, startup_syscall), + allowed, + ) + self.assertEqual( + self._seccomp_verdict(program, 302, arguments=(0, 0, 0, 0, 0, 0)), + allowed, + ) + for foreign_pid in (1, 42, 0xFFFFFFFFFFFFFFFF): + with self.subTest(prlimit_pid=foreign_pid): + self.assertEqual( + self._seccomp_verdict( + program, + 302, + arguments=(foreign_pid, 0, 0, 0, 0, 0), + ), + killed, + ) + self.assertEqual( + self._seccomp_verdict( + program, + 322, + arguments=(3, 0, 0, 0, 0x1000, 0), + ), + allowed, + ) + self.assertEqual( + self._seccomp_verdict( + program, + 322, + arguments=(5, 0, 0, 0, 0x1000, 0), + ), + killed, + ) + self.assertEqual( + self._seccomp_verdict( + program, + 322, + arguments=(3, 0, 0, 0, 0, 0), + ), + killed, + ) + self.assertEqual( + self._seccomp_verdict(program, 1, architecture=0x40000003), + killed, + ) + + def test_hash_and_exec_use_the_same_sealed_memfd(self) -> None: + operations = _MemfdOperations() + executable = _static_elf() + + sealed = executor._seal_executable_v1(executable, operations) + sealed.execveat( + (b"proof-evaluator",), + ((b"LC_ALL", b"C"),), + operations, + ) + + self.assertEqual(sealed.fd, operations.fd) + self.assertEqual(sealed.sha256, hashlib.sha256(executable).digest()) + self.assertEqual( + operations.seals_by_fd[sealed.fd] & executor.REQUIRED_FILE_SEALS_V1, + executor.REQUIRED_FILE_SEALS_V1, + ) + self.assertEqual(operations.exec_calls[0][0], sealed.fd) + self.assertEqual( + operations.events, + ["create", "write", "chmod", "seal", "get_seals", "pread", "execveat"], + ) + + def test_child_error_packet_rejects_unknown_trailing_and_truncated_bytes(self) -> None: + valid = executor._encode_child_error_packet_v1( + executor.SetupStageV1.EXECVEAT, + 8, + ) + parsed = executor._parse_child_error_packet_v1(valid) + self.assertEqual(parsed.stage, executor.SetupStageV1.EXECVEAT) + self.assertEqual(parsed.errno, 8) + + cases = ( + b"BAD!" + valid[4:], + valid[:-1], + valid + b"\0", + valid[:5] + b"\xff" + valid[6:], + ) + for packet in cases: + with self.subTest(packet=packet): + with self.assertRaises(executor.ObserverProtocolErrorV1): + executor._parse_child_error_packet_v1(packet) + + def test_capture_keeps_exact_cap_and_detects_only_cap_plus_one(self) -> None: + captured = bytearray() + + self.assertFalse(executor._append_bounded_v1(captured, b"1234", 4)) + self.assertEqual(bytes(captured), b"1234") + self.assertTrue(executor._append_bounded_v1(captured, b"5", 4)) + self.assertEqual(bytes(captured), b"1234") + + def test_observer_does_not_infer_oom_from_sigkill(self) -> None: + digest = hashlib.sha256(_static_elf()).digest() + + signal_only = executor._classify_process_v1( + digest=digest, + stdout=b"", + stderr=b"", + child_status=9, + oom_kill_delta=0, + residual=False, + setup_packet=b"", + terminal=None, + limits=_limits(), + ) + actual_oom = executor._classify_process_v1( + digest=digest, + stdout=b"", + stderr=b"", + child_status=9, + oom_kill_delta=2, + residual=False, + setup_packet=b"", + terminal=None, + limits=_limits(), + ) + + self.assertEqual(signal_only, executor.SignaledV1(digest, b"", b"", 9, False)) + self.assertEqual(actual_oom, executor.OomKilledV1(digest, b"", b"", 2)) + + def test_cgroup_limits_are_read_back_before_execution(self) -> None: + expected = { + b"memory.max": b"67108864\n", + b"memory.swap.max": b"0\n", + b"memory.oom.group": b"1\n", + b"pids.max": b"1\n", + } + _ReadbackCgroup(expected)._require_applied_limits( + memory_max=64 * 1024 * 1024, + pids_max=1, + ) + + for name in expected: + hostile = dict(expected) + hostile[name] = b"max\n" if name != b"memory.max" else b"67112960\n" + with self.subTest(name=name): + with self.assertRaises(OSError) as caught: + _ReadbackCgroup(hostile)._require_applied_limits( + memory_max=64 * 1024 * 1024, + pids_max=1, + ) + self.assertEqual(caught.exception.errno, errno.EPROTO) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + (parent / "pids.max").write_bytes(b"2\n") + (parent / "pids.current").write_bytes(b"1\n") + (observer / "pids.current").write_bytes(b"1\n") + with mock.patch.object( + executor, + "_current_unified_cgroup_v1", + return_value=observer, + ): + executor._CgroupV2V1.probe_observer_task_budget(parent) + for path, hostile in ( + (parent / "pids.max", b"3\n"), + (parent / "pids.current", b"2\n"), + (observer / "pids.current", b"2\n"), + ): + original = path.read_bytes() + path.write_bytes(hostile) + with self.subTest(path=path.name, hostile=hostile): + with self.assertRaises(OSError): + executor._CgroupV2V1.probe_observer_task_budget( + parent + ) + path.write_bytes(original) + + def test_cgroup_directory_coordinates_never_follow_aliases(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + target = root / "target" + parent = target / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + alias = root / "alias" + alias.symlink_to(target, target_is_directory=True) + declared_parent = alias / "proof" + + with mock.patch.object( + executor, + "_current_unified_cgroup_v1", + ) as current: + with self.assertRaises(OSError): + executor._CgroupV2V1.probe_observer_task_budget(declared_parent) + current.assert_not_called() + + before = tuple(parent.iterdir()) + with mock.patch.object( + executor, + "_read_cgroup_file", + side_effect=AssertionError("alias must fail before cgroup IO"), + ) as read_cgroup_file: + with self.assertRaises(OSError): + executor._CgroupV2V1.create( + declared_parent, + memory_max=None, + pids_max=1, + ) + read_cgroup_file.assert_not_called() + self.assertEqual(tuple(parent.iterdir()), before) + + current_target = root / "current-target" + actual_parent = current_target / "proof" + current_observer = actual_parent / "observer" + current_observer.mkdir(parents=True) + (actual_parent / "pids.max").write_bytes(b"2\n") + (actual_parent / "pids.current").write_bytes(b"1\n") + (current_observer / "pids.current").write_bytes(b"1\n") + current_alias = root / "current-alias" + current_alias.symlink_to(current_target, target_is_directory=True) + with mock.patch.object( + executor, + "_current_unified_cgroup_v1", + return_value=current_alias / "proof" / "observer", + ): + with self.assertRaises(OSError): + executor._CgroupV2V1.probe_observer_task_budget(actual_parent) + + def test_cgroup_directory_coordinates_allow_search_only_components(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + search_only = root / "search-only" + parent = search_only / "proof" + parent.mkdir(parents=True) + + for name, search_mode, parent_mode in ( + ("intermediate", 0o111, 0o755), + ("final", 0o755, 0o111), + ): + descriptor = -1 + try: + search_only.chmod(search_mode) + parent.chmod(parent_mode) + with self.subTest(component=name): + descriptor = executor._open_cgroup_directory_v1(parent) + self.assertTrue(stat.S_ISDIR(os.fstat(descriptor).st_mode)) + finally: + if descriptor >= 0: + os.close(descriptor) + parent.chmod(0o755) + search_only.chmod(0o755) + + def test_cgroup_metadata_mode_fails_closed_without_positive_linux_o_path(self) -> None: + for unavailable in (None, 0): + with self.subTest(o_path=unavailable), mock.patch.object( + executor.sys, + "platform", + "linux", + ), mock.patch.object( + executor.os, + "O_PATH", + unavailable, + create=True, + ): + with self.assertRaises(OSError) as caught: + executor._cgroup_coordinate_metadata_flags_v1() + self.assertEqual(caught.exception.errno, errno.ENOTSUP) + + def test_cgroup_coordinate_open_propagates_the_selected_metadata_mode(self) -> None: + marker = 1 << 50 + opened: list[tuple[object, int, int | None]] = [] + descriptors = iter((41, 42, 43)) + + def open_coordinate( + path: object, + flags: int, + mode: int = 0o777, + *, + dir_fd: int | None = None, + ) -> int: + del mode + opened.append((path, flags, dir_fd)) + return next(descriptors) + + directory_flags = marker | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW + self.assertEqual(marker & (os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW), 0) + with mock.patch.object( + executor.sys, + "platform", + "linux", + ), mock.patch.object( + executor.os, + "O_PATH", + marker, + create=True, + ), mock.patch.object(executor.os, "open", side_effect=open_coordinate), mock.patch.object( + executor.os, + "close", + ) as close: + descriptor = executor._open_cgroup_directory_v1(Path("/proof/parent")) + + self.assertEqual(descriptor, 43) + self.assertEqual( + opened, + [ + (b"/", directory_flags, None), + (b"proof", directory_flags, 41), + (b"parent", directory_flags, 42), + ], + ) + self.assertEqual(close.call_args_list, [mock.call(41), mock.call(42)]) + + def test_cgroup_parent_parser_is_total_and_preserves_root(self) -> None: + class ExplodingPathLike: + def __fspath__(self) -> str: + raise RuntimeError("hostile cgroup path") + + self.assertEqual( + executor.canonical_cgroup_parent_v1("/delegated/proof"), + Path("/delegated/proof"), + ) + self.assertEqual(executor.canonical_cgroup_parent_v1(Path("/")), Path("/")) + root_fd = executor._open_cgroup_directory_v1(Path("/")) + try: + self.assertTrue(stat.S_ISDIR(os.fstat(root_fd).st_mode)) + finally: + os.close(root_fd) + + for invalid in ( + object(), + b"/delegated/proof", + "relative", + "/delegated/proof\0", + "/delegated/proof\ud800", + "/delegated/./proof", + "/delegated/../proof", + "//delegated/proof", + "/delegated/proof/", + ExplodingPathLike(), + ): + with self.subTest(invalid=type(invalid).__name__): + with self.assertRaises(TypeError): + executor.canonical_cgroup_parent_v1(invalid) + + def test_cgroup_directory_walk_never_recloses_a_released_descriptor(self) -> None: + """A late close interruption must not target a reused descriptor number.""" + + with tempfile.TemporaryDirectory() as temporary: + parent = Path(temporary).resolve() / "delegated" / "proof" + parent.mkdir(parents=True) + real_open = os.open + real_close = os.close + released_descriptor: int | None = None + successor_descriptor: int | None = None + close_calls: list[int] = [] + opened_descriptors: list[int] = [] + + def record_open(*args: object, **kwargs: object) -> int: + descriptor = real_open(*args, **kwargs) + opened_descriptors.append(descriptor) + return descriptor + + def close_after_release(descriptor: int) -> None: + nonlocal released_descriptor, successor_descriptor + close_calls.append(descriptor) + if released_descriptor is None: + successor_descriptor = opened_descriptors[-1] + real_close(descriptor) + released_descriptor = real_open( + os.devnull, + os.O_RDONLY | os.O_CLOEXEC, + ) + raise KeyboardInterrupt("interrupted after descriptor release") + real_close(descriptor) + + try: + with mock.patch.object(executor.os, "open", side_effect=record_open), mock.patch.object( + executor.os, "close", side_effect=close_after_release + ): + with self.assertRaisesRegex( + KeyboardInterrupt, + "interrupted after descriptor release", + ): + executor._open_cgroup_directory_v1(parent) + self.assertIsNotNone(released_descriptor) + self.assertEqual(close_calls.count(released_descriptor), 1) + os.fstat(released_descriptor) + self.assertIsNotNone(successor_descriptor) + with self.assertRaises(OSError): + os.fstat(successor_descriptor) + finally: + if released_descriptor is not None: + try: + real_close(released_descriptor) + except OSError: + pass + + def test_cgroup_directory_walk_preserves_primary_close_interruption(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + parent = Path(temporary).resolve() / "delegated" / "proof" + parent.mkdir(parents=True) + real_close = os.close + replacement: int | None = None + close_stage = 0 + + def close_with_two_interruptions(descriptor: int) -> None: + nonlocal close_stage, replacement + if close_stage == 0: + close_stage += 1 + real_close(descriptor) + replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC) + raise KeyboardInterrupt("primary close interruption") + if close_stage == 1: + close_stage += 1 + real_close(descriptor) + raise KeyboardInterrupt("cleanup close interruption") + real_close(descriptor) + + try: + with mock.patch.object( + executor.os, + "close", + side_effect=close_with_two_interruptions, + ): + with self.assertRaisesRegex( + KeyboardInterrupt, + "primary close interruption", + ) as caught: + executor._open_cgroup_directory_v1(parent) + self.assertIsInstance(caught.exception.__cause__, KeyboardInterrupt) + self.assertEqual(str(caught.exception.__cause__), "cleanup close interruption") + self.assertIsNotNone(replacement) + os.fstat(replacement) + finally: + if replacement is not None: + try: + real_close(replacement) + except OSError: + pass + + def test_observer_cgroup_placement_supports_search_only_directories(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + procs = observer / "cgroup.procs" + procs.write_bytes(b"") + + parent.chmod(0o111) + observer.chmod(0o111) + # Проверяем owner-write/search-only contract без лишнего доступа + # для других пользователей, не моделируя world-writable файл. + procs.chmod(0o200) + try: + executor.enter_observer_cgroup_v1(parent) + finally: + procs.chmod(0o644) + observer.chmod(0o755) + parent.chmod(0o755) + self.assertEqual(procs.read_bytes(), str(os.getpid()).encode("ascii")) + + def test_observer_cgroup_placement_rejects_invalid_parent_values(self) -> None: + for invalid in ( + object(), + Path("relative"), + Path("/absolute\0"), + Path("/absolute\ud800"), + "/absolute", + ): + with self.subTest(invalid=invalid): + with self.assertRaises(TypeError): + executor.enter_observer_cgroup_v1(invalid) # type: ignore[arg-type] + + def test_observer_cgroup_placement_ignores_hostile_path_operator(self) -> None: + class ExplodingPath(type(Path())): + def __truediv__(self, _other: object) -> Path: + raise RuntimeError("hostile path operator") + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + procs = observer / "cgroup.procs" + procs.write_bytes(b"") + + executor.enter_observer_cgroup_v1(ExplodingPath(str(parent))) + self.assertEqual(procs.read_bytes(), str(os.getpid()).encode("ascii")) + + def test_observer_cgroup_placement_rejects_parent_symlink(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + target = root / "target" + observer = target / "observer" + observer.mkdir(parents=True) + procs = observer / "cgroup.procs" + procs.write_bytes(b"") + parent.symlink_to(target, target_is_directory=True) + + with self.assertRaises(OSError): + executor.enter_observer_cgroup_v1(parent) + self.assertEqual(procs.read_bytes(), b"") + + def test_observer_cgroup_placement_rejects_path_component_symlink(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + target = root / "target" + parent = target / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + procs = observer / "cgroup.procs" + procs.write_bytes(b"") + alias = root / "alias" + alias.symlink_to(target, target_is_directory=True) + + with self.assertRaises(OSError): + executor.enter_observer_cgroup_v1(alias / "proof") + self.assertEqual(procs.read_bytes(), b"") + + def test_observer_cgroup_placement_rejects_observer_symlink(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + target = root / "target" + parent.mkdir() + target.mkdir() + (target / "cgroup.procs").write_bytes(b"") + (parent / "observer").symlink_to(target, target_is_directory=True) + + with self.assertRaises(OSError): + executor.enter_observer_cgroup_v1(parent) + + def test_observer_cgroup_placement_rejects_procs_symlink(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + target = root / "foreign-procs" + target.write_bytes(b"") + (observer / "cgroup.procs").symlink_to(target) + + with self.assertRaises(OSError): + executor.enter_observer_cgroup_v1(parent) + + def test_observer_cgroup_short_write_closes_every_descriptor(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + parent = root / "proof" + observer = parent / "observer" + observer.mkdir(parents=True) + (observer / "cgroup.procs").write_bytes(b"") + opened: list[int] = [] + real_open = os.open + + def track_open(*args: object, **kwargs: object) -> int: + descriptor = real_open(*args, **kwargs) + opened.append(descriptor) + return descriptor + + with mock.patch.object( + executor.os, + "open", + side_effect=track_open, + ), mock.patch.object(executor.os, "write", return_value=0): + with self.assertRaises(OSError) as caught: + executor.enter_observer_cgroup_v1(parent) + self.assertEqual(caught.exception.errno, errno.EIO) + + self.assertEqual(len(opened), len(parent.parts) + 2) + for descriptor in opened: + with self.subTest(descriptor=descriptor): + with self.assertRaises(OSError) as caught: + os.fstat(descriptor) + self.assertEqual(caught.exception.errno, errno.EBADF) + + def test_observer_initialization_failure_closes_fds_and_reaps_child(self) -> None: + stdin_read, stdin_write = os.pipe() + stdout_read, stdout_write = os.pipe() + stderr_read, stderr_write = os.pipe() + setup_read, setup_write = os.pipe() + pid = os.fork() + if pid == 0: + os.close(stdin_write) + os.close(stdout_read) + os.close(stderr_read) + os.close(setup_read) + while True: + signal.pause() + + os.close(stdin_read) + os.close(stdout_write) + os.close(stderr_write) + os.close(setup_write) + observed_fds = (stdin_write, stdout_read, stderr_read, setup_read) + backend = executor.NativeLinuxBackendV1() + try: + with mock.patch.object( + executor.selectors, + "DefaultSelector", + side_effect=OSError(errno.EMFILE, "selector unavailable"), + ): + result = backend._observe( + _request(), + hashlib.sha256(_static_elf()).digest(), + pid, + _ObserverCgroup(pid), + 0, + *observed_fds, + ) + self.assertEqual( + result, + executor.ObserverFailureV1(executor.ObserverReasonV1.BACKEND_EXCEPTION), + ) + for descriptor in observed_fds: + with self.subTest(descriptor=descriptor): + with self.assertRaises(OSError) as caught: + os.fstat(descriptor) + self.assertEqual(caught.exception.errno, errno.EBADF) + with self.assertRaises(ChildProcessError): + os.waitpid(pid, os.WNOHANG) + finally: + for descriptor in observed_fds: + try: + os.close(descriptor) + except OSError: + pass + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + try: + os.waitpid(pid, 0) + except ChildProcessError: + pass + + def test_fork_rechecks_single_thread_precondition_after_all_setup(self) -> None: + backend = executor.NativeLinuxBackendV1() + cwd_fd = os.open("/", os.O_RDONLY) + try: + with mock.patch.object( + executor.os, + "fork", + side_effect=AssertionError("fork must not run after failed final gate"), + ): + result = backend._fork_and_observe( + _request(cwd=b"/"), + executor._SealedExecutableV1( + 41, + len(_static_elf()), + hashlib.sha256(_static_elf()).digest(), + ), + _LateThreadOperations(), + cwd_fd, + object(), + ) + finally: + os.close(cwd_fd) + + self.assertEqual( + result, + executor.SandboxSetupFailedV1( + hashlib.sha256(_static_elf()).digest(), + b"", + b"", + executor.SetupStageV1.OBSERVER_PRECONDITION, + errno.EBUSY, + ), + ) + + def test_controller_timeout_and_output_limit_are_not_child_outcomes(self) -> None: + digest = hashlib.sha256(_static_elf()).digest() + limits = _limits() + + timeout = executor._classify_process_v1( + digest=digest, + stdout=b"", + stderr=b"", + child_status=9, + oom_kill_delta=1, + residual=False, + setup_packet=b"", + terminal=("timeout", None), + limits=limits, + ) + output = executor._classify_process_v1( + digest=digest, + stdout=b"x" * limits.max_stdout_bytes, + stderr=b"", + child_status=9, + oom_kill_delta=1, + residual=False, + setup_packet=b"", + terminal=("output", executor.OutputStreamV1.STDOUT), + limits=limits, + ) + + self.assertEqual( + timeout, + executor.TimedOutV1(digest, b"", b"", limits.wall_timeout_ns), + ) + self.assertEqual( + output, + executor.OutputLimitExceededV1( + digest, + b"x" * limits.max_stdout_bytes, + b"", + executor.OutputStreamV1.STDOUT, + limits.max_stdout_bytes, + ), + ) + + +@unittest.skipUnless( + sys.platform == "linux" and os.environ.get("LABCOLORS_EXECUTOR_CGROUP_V1"), + "requires Linux and an explicit delegated cgroup v2 parent", +) +class NativeLinuxIntegrationTests(unittest.TestCase): + def setUp(self) -> None: + raw_parent = os.environ["LABCOLORS_EXECUTOR_CGROUP_V1"] + self.cgroup_parent = Path(raw_parent) + self.backend = executor.NativeLinuxBackendV1(self.cgroup_parent) + self.controller = executor.ControlledExecutorV1(self.backend) + report = self.controller.probe() + self.assertEqual( + report, + executor.SupportedV1( + "linux-x86_64", + executor.SANDBOX_POLICY_RELEASE_V1, + ), + ) + + def _native_request( + self, + code: bytes, + *, + stdin: bytes = b"", + stdout_limit: int = 4, + timeout_ns: int = 500_000_000, + memory_max: int = 64 * 1024 * 1024, + ) -> executor.ExecutionRequestV1: + return executor.ExecutionRequestV1( + executable=_linux_executable_elf(code), + argv=(b"native-executor-fixture",), + environment=(), + cwd=b"/", + stdin=stdin, + umask=0o077, + limits=executor.ExecutionLimitsV1( + max_executable_bytes=4096, + max_stdin_bytes=16, + max_argument_bytes=512, + max_stdout_bytes=stdout_limit, + max_stderr_bytes=4, + wall_timeout_ns=timeout_ns, + memory_max_bytes=memory_max, + pids_max=1, + ), + ) + + def _owned_cgroups(self) -> set[str]: + prefix = f"labcolors-executor-{os.getpid()}-" + return { + child.name + for child in self.cgroup_parent.iterdir() + if child.name.startswith(prefix) + } + + def test_real_kernel_success_output_timeout_signal_oom_and_cleanup(self) -> None: + before = self._owned_cgroups() + controlled = self.controller + + exit_request = self._native_request(_LINUX_EXIT_ZERO) + exit_result = controlled.execute(exit_request) + self.assertEqual( + exit_result, + executor.CompletedV1( + hashlib.sha256(exit_request.executable).digest(), + b"", + b"", + ), + ) + + echo_request = self._native_request(_LINUX_ECHO_FOUR, stdin=b"PING") + echo_result = controlled.execute(echo_request) + self.assertEqual( + echo_result, + executor.CompletedV1( + hashlib.sha256(echo_request.executable).digest(), + b"PING", + b"", + ), + ) + + output_request = self._native_request(_LINUX_WRITE_FIVE_AND_LOOP) + output_result = controlled.execute(output_request) + self.assertEqual( + output_result, + executor.OutputLimitExceededV1( + hashlib.sha256(output_request.executable).digest(), + b"1234", + b"", + executor.OutputStreamV1.STDOUT, + 4, + ), + ) + + timeout_request = self._native_request( + _LINUX_BUSY_LOOP, + timeout_ns=50_000_000, + ) + timeout_result = controlled.execute(timeout_request) + self.assertEqual( + timeout_result, + executor.TimedOutV1( + hashlib.sha256(timeout_request.executable).digest(), + b"", + b"", + timeout_request.limits.wall_timeout_ns, + ), + ) + + signal_request = self._native_request(_LINUX_SIGILL) + signal_result = controlled.execute(signal_request) + self.assertEqual( + signal_result, + executor.SignaledV1( + hashlib.sha256(signal_request.executable).digest(), + b"", + b"", + signal.SIGILL, + False, + ), + ) + + foreign_prlimit_request = self._native_request(_LINUX_FOREIGN_PRLIMIT) + foreign_prlimit_result = controlled.execute(foreign_prlimit_request) + self.assertIs(type(foreign_prlimit_result), executor.SignaledV1) + self.assertEqual(foreign_prlimit_result.signal_number, signal.SIGSYS) + + oom_request = self._native_request( + _LINUX_ALLOCATE_UNTIL_OOM, + timeout_ns=5_000_000_000, + memory_max=16 * 1024 * 1024, + ) + oom_result = controlled.execute(oom_request) + self.assertIs(type(oom_result), executor.OomKilledV1) + self.assertGreater(oom_result.oom_kill_delta, 0) + + self.assertEqual(self._owned_cgroups(), before) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_mpfi_build.py b/proof/region/v1/tests/test_mpfi_build.py new file mode 100644 index 00000000..130cd3d8 --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_build.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""Contract tests for the MPFI source-owned sealed BUILD input.""" + +from __future__ import annotations + +import hashlib +import io +import sys +import tarfile +import unittest +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "tests")) + +import provenance # noqa: E402 +from build import input as build_input # noqa: E402 +from mpfi import build as mpfi_build # noqa: E402 +from mpfi.evaluator import formula # noqa: E402 +from test_mpfi_input import _admitted_closure # noqa: E402 + + +def _generated_formula() -> bytes: + source = (ROOT.parents[2] / "crates/labcolors-core/contracts/contextual-region-formula-v1.lcir").read_bytes() + return formula.emit(formula.parse(source)) + + +def _workspace_sources() -> mpfi_build.AdmittedMpfiBuildSourcesV1: + files = tuple( + mpfi_build.MpfiBuildSourceFileV1( + path, + mode, + (ROOT.parents[2] / path).read_bytes(), + ) + for path, mode in mpfi_build.REQUIRED_WORKSPACE_MODES_V1 + ) + return mpfi_build.admit_mpfi_build_sources_v1(files) + + +def _limits_for_bundle( + source_lock: provenance.MpfiSourceLockV1, + admitted: provenance.AdmittedMpfiSourcesV1, + sources: mpfi_build.AdmittedMpfiBuildSourcesV1, + generated: bytes, +) -> build_input.CanonicalInputLimitsV1: + replayed = provenance.replay_admitted_source_closure_v1(source_lock, admitted) + entries = list( + mpfi_build.canonical_input_entries_v1(replayed, sources, generated) + ) + directories = { + "/".join(path.split("/")[:length]) + for path, _mode, _contents in entries + for length in range(1, len(path.split("/"))) + } + return build_input.CanonicalInputLimitsV1( + len(entries) + len(directories), + max(len(contents) for _path, _mode, contents in entries), + sum(len(contents) for _path, _mode, contents in entries), + ) + + +def _members(value: build_input.SealedInputV1) -> tuple[str, ...]: + with tarfile.open(fileobj=io.BytesIO(value.contents), mode="r:") as archive: + return tuple(member.name for member in archive.getmembers()) + + +class MpfiBuildInputTests(unittest.TestCase): + def test_sealed_bundle_contains_source_input_generated_formula_and_workspace(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + limits = _limits_for_bundle(source_lock, admitted, sources, generated) + + first = mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated, + limits, + ) + second = mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated, + limits, + ) + self.assertEqual(first, second) + self.assertTrue(build_input.sealed_input_is_intact_v1(first)) + self.assertTrue( + mpfi_build.mpfi_build_input_is_bound_v1( + source_lock, + admitted, + sources, + generated, + limits, + first, + ) + ) + members = _members(first) + self.assertIn("inputs/formula.generated.c", members) + self.assertIn("workspace/proof/region/v1/mpfi/build.sh", members) + self.assertIn("workspace/proof/region/v1/mpfi/evaluator/wire.c", members) + self.assertIn("inputs/sources/gmp/value", members) + + def test_formula_and_workspace_mutations_are_red_before_transport(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + limits = _limits_for_bundle(source_lock, admitted, sources, generated) + sealed = mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated, + limits, + ) + + with self.assertRaises(ValueError): + mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated[:-1] + bytes((generated[-1] ^ 1,)), + limits, + ) + changed = list(sources.files) + item = changed[0] + changed[0] = mpfi_build.MpfiBuildSourceFileV1( + item.path, + item.mode, + item.contents + b"\n", + ) + foreign = mpfi_build.AdmittedMpfiBuildSourcesV1( + tuple(changed), + hashlib.sha256(b"foreign").digest(), + ) + self.assertFalse( + mpfi_build.mpfi_build_input_is_bound_v1( + source_lock, + admitted, + foreign, + generated, + limits, + sealed, + ) + ) + + def test_retained_workspace_identity_is_replayed_not_trusted(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + limits = _limits_for_bundle(source_lock, admitted, sources, generated) + poisoned = mpfi_build.AdmittedMpfiBuildSourcesV1( + sources.files, + hashlib.sha256(b"poison").digest(), + ) + with self.assertRaises(ValueError): + mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + poisoned, + generated, + limits, + ) + + def test_policy_is_pinned_to_the_clang_19_linux_amd64_manifest(self) -> None: + policy = mpfi_build.MPFI_BUILD_TRANSPORT_POLICY_V1 + self.assertEqual(policy.platform, "linux/amd64") + self.assertEqual( + policy.image_reference, + mpfi_build.MPFI_BUILD_IMAGE_REFERENCE_V1, + ) + self.assertEqual( + policy.image_reference, + "silkeh/clang@sha256:" + "f1d693e7af5ee954370e1f3605830d8cabc05f9731226fc99aa5e26127797c11", + ) + self.assertIn("/build/work/mpfi-evaluator-v1", policy.bootstrap) + self.assertIn("proof/region/v1/mpfi/build.sh", policy.bootstrap) + + def test_canonical_input_limits_reject_an_undersized_member_bound(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + limits = _limits_for_bundle(source_lock, admitted, sources, generated) + undersized = build_input.CanonicalInputLimitsV1( + limits.max_members - 1, + limits.max_file_bytes, + limits.max_payload_bytes, + ) + with self.assertRaises(ValueError): + mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated, + undersized, + ) + + def test_invalid_limits_are_rejected_before_source_replay(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + sources = _workspace_sources() + generated = _generated_formula() + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=AssertionError("source replay happened before limit admission"), + ): + with self.assertRaises(TypeError): + mpfi_build.seal_mpfi_build_input_v1( + source_lock, + admitted, + sources, + generated, + object(), + ) + + def test_snapshot_bound_check_does_not_replay_an_owned_closure(self) -> None: + source_lock, admitted, _entries = _admitted_closure() + snapshot = provenance.replay_admitted_source_closure_v1(source_lock, admitted) + sources = _workspace_sources() + generated = _generated_formula() + limits = _limits_for_bundle(source_lock, admitted, sources, generated) + sealed = mpfi_build.seal_mpfi_build_input_from_snapshot_v1( + snapshot, + sources, + generated, + limits, + ) + with mock.patch.object( + provenance, + "replay_admitted_source_closure_v1", + side_effect=AssertionError("snapshot path replayed the closure"), + ): + self.assertTrue( + mpfi_build.mpfi_build_input_is_bound_from_snapshot_v1( + snapshot, + sources, + generated, + limits, + sealed, + ) + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_mpfi_input.py b/proof/region/v1/tests/test_mpfi_input.py new file mode 100644 index 00000000..cac171f6 --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_input.py @@ -0,0 +1,773 @@ +#!/usr/bin/env python3 +"""Контракт границы MPFI admitted-source → sealed input.""" + +from __future__ import annotations + +import ast +import hashlib +import io +import lzma +import sys +import tarfile +import unittest +from dataclasses import replace +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import provenance # noqa: E402 +from build import input as build_input # noqa: E402 +from mpfi import input as mpfi_input # noqa: E402 + + +# Characterization-pins маленького three-source closure. Versioned layout +# меняет их только явным решением, а не silent drift. +SYNTHETIC_MPFI_INPUT_LENGTH_V1 = 10_240 +SYNTHETIC_MPFI_INPUT_SHA256_V1 = ( + "fac4761a9018ca55f467328dae238ccea0a280c08277e533a7bbef696eea567f" +) +SYNTHETIC_MPFI_INPUT_BINDING_V1 = ( + "d0adc51b30e68b672efcf7f3a4be4a6ec4171b9a1f053ef52787adeb713b6653" +) + + +def _sha256(value: bytes) -> bytes: + return hashlib.sha256(value).digest() + + +def _detached_policy() -> provenance.DetachedSignaturePolicyV1: + return provenance.DetachedSignaturePolicyV1( + "https://example.invalid/source.tar.xz.sig", + 3, + _sha256(b"signature"), + _sha256(b"packets"), + bytes.fromhex("00112233445566778899aabbccddeeff00112233"), + ) + + +def _fixture_archive( + *, + license_body: bytes, + value_body: bytes, + value_mode: int = 0o644, +) -> bytes: + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("shared/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + root.uid = 0 + root.gid = 0 + root.mtime = 0 + archive.addfile(root) + for name, body, mode in ( + ("LICENSE", license_body, 0o644), + ("value", value_body, value_mode), + ): + member = tarfile.TarInfo(f"shared/{name}") + member.mode = mode + member.uid = 0 + member.gid = 0 + member.mtime = 0 + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return lzma.compress(raw.getvalue(), format=lzma.FORMAT_XZ) + + +def _fixture_release( + role: provenance.SourceRoleV1, + archive: bytes, + *, + license_body: bytes, + value_body: bytes, +) -> provenance.SourceReleaseLockV1: + raw_tar = lzma.decompress(archive) + integrity: ( + provenance.DetachedSignaturePolicyV1 + | provenance.ProjectPinnedArchiveDigestPolicyV1 + ) + if role is provenance.SourceRoleV1.MPFI: + integrity = provenance.ProjectPinnedArchiveDigestPolicyV1() + else: + integrity = _detached_policy() + return provenance.SourceReleaseLockV1( + role, + "1", + f"https://example.invalid/{role.name.lower()}.tar.xz", + provenance.ArchiveFormatV1.TAR_XZ, + len(archive), + _sha256(archive), + len(raw_tar), + "shared/", + 2, + len(license_body) + len(value_body), + (provenance.LegalFileV1("LICENSE", len(license_body), _sha256(license_body)),), + integrity, + ) + + +def _admitted_closure( + *, + mpfr_value_mode: int = 0o755, +) -> tuple[ + provenance.MpfiSourceLockV1, + provenance.AdmittedMpfiSourcesV1, + tuple[tuple[str, int, bytes], ...], +]: + specifications = ( + (provenance.SourceRoleV1.GMP, b"gmp-license", b"gmp-source", 0o644), + (provenance.SourceRoleV1.MPFR, b"mpfr-license", b"mpfr-source", mpfr_value_mode), + (provenance.SourceRoleV1.MPFI, b"mpfi-license", b"mpfi-source", 0o644), + ) + releases: list[provenance.SourceReleaseLockV1] = [] + archives: list[bytes] = [] + expected_entries: list[tuple[str, int, bytes]] = [] + for role, license_body, value_body, value_mode in specifications: + archive = _fixture_archive( + license_body=license_body, + value_body=value_body, + value_mode=value_mode, + ) + archives.append(archive) + releases.append( + _fixture_release( + role, + archive, + license_body=license_body, + value_body=value_body, + ) + ) + namespace = role.name.lower() + expected_entries.extend( + ( + (f"sources/{namespace}/LICENSE", 0o644, license_body), + (f"sources/{namespace}/value", value_mode, value_body), + ) + ) + lock = provenance.MpfiSourceLockV1(tuple(releases)) + sources = tuple( + provenance.admit_source_archive(release, archive) + for release, archive in zip(lock.sources, archives, strict=True) + ) + return ( + lock, + provenance.admit_mpfi_sources(lock, sources), + tuple(sorted(expected_entries)), + ) + + +def _limits_for_entries( + entries: tuple[tuple[str, int, bytes], ...], +) -> build_input.CanonicalInputLimitsV1: + directories = { + "/".join(path.split("/")[:length]) + for path, _mode, _contents in entries + for length in range(1, len(path.split("/"))) + } + return build_input.CanonicalInputLimitsV1( + len(entries) + len(directories), + max(len(contents) for _path, _mode, contents in entries), + sum(len(contents) for _path, _mode, contents in entries), + ) + + +def _regular_ustar_entries(value: build_input.SealedInputV1) -> tuple[tuple[str, int, bytes], ...]: + with tarfile.open(fileobj=io.BytesIO(value.contents), mode="r:") as archive: + return tuple( + (member.name, member.mode, archive.extractfile(member).read()) + for member in archive.getmembers() + if member.isreg() + ) + + +def _imported_module_names(source: str) -> tuple[str, ...]: + modules: list[str] = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + modules.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom): + prefix = node.module or "" + modules.extend( + ".".join(part for part in (prefix, alias.name) if part) + for alias in node.names + ) + return tuple(modules) + + +class MpfiSourceInputTests(unittest.TestCase): + def test_three_same_root_archives_become_one_deterministic_lane_input(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + + first = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + second = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + + self.assertIs(type(first), build_input.SealedInputV1) + self.assertTrue(build_input.sealed_input_is_intact_v1(first)) + self.assertEqual(first, second) + self.assertEqual(first.length, SYNTHETIC_MPFI_INPUT_LENGTH_V1) + self.assertEqual(first.sha256.hex(), SYNTHETIC_MPFI_INPUT_SHA256_V1) + self.assertEqual( + first.binding_identity.hex(), + SYNTHETIC_MPFI_INPUT_BINDING_V1, + ) + self.assertEqual(_regular_ustar_entries(first), expected_entries) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, first) + ) + relaxed_limits = build_input.CanonicalInputLimitsV1( + limits.max_members + 1, + limits.max_file_bytes + 1, + limits.max_payload_bytes + 1, + ) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + relaxed_limits, + first, + ) + ) + with tarfile.open(fileobj=io.BytesIO(first.contents), mode="r:") as archive: + members = archive.getmembers() + self.assertEqual( + tuple(sorted(member.name for member in members if member.isdir())), + ("sources", "sources/gmp", "sources/mpfi", "sources/mpfr"), + ) + self.assertTrue(all(member.uid == 0 and member.gid == 0 for member in members)) + self.assertTrue(all(member.mtime == 0 for member in members)) + + def test_binding_rechecks_the_closure_and_exact_ustar_bytes(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + + foreign_binding = build_input.seal_input_v1(_sha256(b"foreign"), sealed.contents) + changed_entries = list(expected_entries) + path, mode, contents = changed_entries[0] + changed_entries[0] = (path, mode, contents + b"!") + changed_contents = build_input.canonical_ustar_v1( + tuple(changed_entries), + _limits_for_entries(tuple(changed_entries)), + ) + stale_binding = build_input.seal_input_v1(sealed.binding_identity, changed_contents) + + self.assertTrue(build_input.sealed_input_is_intact_v1(foreign_binding)) + self.assertTrue(build_input.sealed_input_is_intact_v1(stale_binding)) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + foreign_binding, + ) + ) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + stale_binding, + ) + ) + + def test_reordered_or_foreign_closure_is_rejected_before_ustar_encoding( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + _ = admitted.identity + original_sources = admitted.sources + object.__setattr__( + admitted, + "sources", + (original_sources[1], original_sources[0], original_sources[2]), + ) + try: + with mock.patch.object(mpfi_input.build_input, "canonical_ustar_v1") as encoder: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + encoder.assert_not_called() + finally: + object.__setattr__(admitted, "sources", original_sources) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + def test_lock_identity_cannot_hide_source_or_capability_drift(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + original_version = lock.sources[0].version + object.__setattr__(lock.sources[0], "version", "2") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, sealed) + ) + finally: + object.__setattr__(lock.sources[0], "version", original_version) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + source = admitted.sources[2] + original_tree_identity = source.tree_identity + object.__setattr__(source, "tree_identity", _sha256(b"foreign tree")) + try: + with self.assertRaises(provenance.ProvenanceErrorV1): + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1(lock, admitted, limits, sealed) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + + def test_wrong_public_capability_types_are_typed_rejections(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + for hostile_lock, hostile_admitted, hostile_limits, field_name in ( + (object(), admitted, limits, "source_lock"), + (lock, object(), limits, "admitted_sources"), + (lock, admitted, object(), "limits"), + ): + with self.subTest(field=field_name): + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + hostile_lock, + hostile_admitted, + hostile_limits, + ) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.WRONG_TYPE, + ) + self.assertEqual(caught.exception.field, field_name) + + def test_locked_mode_is_preserved_without_silent_normalization(self) -> None: + lock, admitted, expected_entries = _admitted_closure(mpfr_value_mode=0o700) + + sealed = mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + _limits_for_entries(expected_entries), + ) + + self.assertEqual(_regular_ustar_entries(sealed), expected_entries) + + def test_limits_reject_declared_closure_before_archive_materialization(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + total_files = len(expected_entries) + total_payload = sum(len(contents) for _path, _mode, contents in expected_entries) + max_file = max(len(contents) for _path, _mode, contents in expected_entries) + cases = ( + ( + build_input.CanonicalInputLimitsV1(total_files - 1, max_file, total_payload), + "max_members", + ), + ( + build_input.CanonicalInputLimitsV1(total_files + 4, max_file, total_payload - 1), + "max_payload_bytes", + ), + ) + for limits, field in cases: + with self.subTest(limit=field): + with mock.patch.object( + mpfi_input.provenance, + "replay_admitted_source_closure_v1", + ) as replay_closure: + with self.assertRaises(build_input.InputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + replay_closure.assert_not_called() + self.assertEqual(caught.exception.reason, build_input.InputReasonV1.RESOURCE_LIMIT) + self.assertEqual(caught.exception.field, field) + + def test_final_ustar_limits_remain_typed_rejections(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + cases = ( + ( + build_input.CanonicalInputLimitsV1( + limits.max_members, + limits.max_file_bytes - 1, + limits.max_payload_bytes, + ), + "max_file_bytes", + ), + ( + build_input.CanonicalInputLimitsV1( + limits.max_members, + limits.max_file_bytes, + limits.max_payload_bytes, + sealed.length - 1, + ), + "max_encoded_bytes", + ), + ) + for constrained, field in cases: + with self.subTest(limit=field): + with self.assertRaises(build_input.InputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, constrained) + self.assertEqual(caught.exception.reason, build_input.InputReasonV1.RESOURCE_LIMIT) + self.assertEqual(caught.exception.field, field) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + constrained, + sealed, + ) + ) + + def test_missing_capability_field_is_a_typed_rejection(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + original = admitted.source_lock_identity + object.__delattr__(admitted, "source_lock_identity") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + limits, + ) + finally: + object.__setattr__(admitted, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + class ExplodesOnComparison: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(admitted, "source_lock_identity", ExplodesOnComparison()) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + finally: + object.__setattr__(admitted, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "admitted_sources") + + def test_hostile_exact_source_capability_stays_a_typed_replay_failure( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + source = admitted.sources[0] + original = source.source_lock_identity + + class ExplodesOnComparison: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(source, "source_lock_identity", ExplodesOnComparison()) + try: + with self.assertRaises(provenance.ProvenanceErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "source_lock_identity", original) + self.assertEqual( + caught.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) + + def test_source_input_keeps_one_snapshot_across_reentrant_source_mutation( + self, + ) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + source = admitted.sources[0] + original_tree_identity = source.tree_identity + real_encoder = build_input.canonical_ustar_v1 + + def encode_then_mutate( + entries: tuple[tuple[str, int, bytes], ...], + encoder_limits: build_input.CanonicalInputLimitsV1, + ) -> bytes: + encoded = real_encoder(entries, encoder_limits) + object.__setattr__(source, "tree_identity", _sha256(b"reentrant-tree")) + return encoded + + try: + with mock.patch.object( + mpfi_input.build_input, + "canonical_ustar_v1", + side_effect=encode_then_mutate, + ): + sealed = mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + limits, + ) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "tree_identity", original_tree_identity) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + + def test_hostile_replayed_source_stays_a_typed_provenance_failure(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + source = admitted.sources[0] + original = source.files + + class ExplodesOnComparison: + def __eq__(self, _other: object) -> bool: + raise RuntimeError("comparison ran") + + object.__setattr__(source, "files", ExplodesOnComparison()) + try: + with self.assertRaises(provenance.ProvenanceErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(source, "files", original) + self.assertEqual( + caught.exception.reason, + provenance.ProvenanceReasonV1.FOREIGN_BINDING, + ) + + def test_noncanonical_exact_type_lock_is_a_typed_rejection(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + original_version = lock.sources[0].version + object.__setattr__(lock.sources[0], "version", "\0") + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + admitted, + _limits_for_entries(expected_entries), + ) + finally: + object.__setattr__(lock.sources[0], "version", original_version) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + def test_hostile_exact_lock_cannot_escape_public_boundary(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + original_sources = lock.sources + + class ExplodesOnEncode: + def encode(self) -> bytes: + raise RuntimeError("encode ran") + + object.__setattr__(lock, "sources", (ExplodesOnEncode(),) * 3) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertFalse( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + object.__setattr__(lock, "sources", original_sources) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + self.assertEqual(caught.exception.field, "source_lock") + + def test_source_lock_encoder_shadow_cannot_select_foreign_closure(self) -> None: + lock, admitted, expected_entries = _admitted_closure() + limits = _limits_for_entries(expected_entries) + foreign_releases = list(lock.sources) + foreign_releases[0] = replace(foreign_releases[0], version="shadowed") + foreign_lock = provenance.MpfiSourceLockV1(tuple(foreign_releases)) + foreign_sources = tuple( + provenance.admit_source_archive(release, source.archive_bytes) + for release, source in zip( + foreign_lock.sources, + admitted.sources, + strict=True, + ) + ) + foreign_admitted = provenance.admit_mpfi_sources( + foreign_lock, + foreign_sources, + ) + lock.__dict__["encode"] = lambda: provenance.MpfiSourceLockV1.encode( + foreign_lock + ) + try: + with self.assertRaises(mpfi_input.MpfiSourceInputErrorV1) as caught: + mpfi_input.seal_mpfi_source_input_v1( + lock, + foreign_admitted, + limits, + ) + self.assertEqual( + caught.exception.reason, + mpfi_input.MpfiSourceInputReasonV1.FOREIGN_SOURCE_CAPABILITY, + ) + + sealed = mpfi_input.seal_mpfi_source_input_v1(lock, admitted, limits) + self.assertTrue( + mpfi_input.mpfi_source_input_is_bound_v1( + lock, + admitted, + limits, + sealed, + ) + ) + finally: + del lock.__dict__["encode"] + + def test_source_input_owner_has_no_engine_dependency(self) -> None: + source_path = ROOT / "mpfi" / "input.py" + self.assertTrue(source_path.is_file()) + source = source_path.read_text(encoding="utf-8") + tree = ast.parse(source) + imported_modules = _imported_module_names(source) + forbidden = ( + "arb", + "pipeline", + "receipt", + "executor", + "transport", + "formula", + "controller", + "region_proof_protocol", + ) + self.assertFalse( + any( + name in module.split(".") + for module in imported_modules + for name in forbidden + ), + ) + self.assertFalse( + any( + isinstance(node, ast.Call) + and ( + ( + isinstance(node.func, ast.Name) + and node.func.id == "__import__" + ) + or ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "import_module" + ) + ) + for node in ast.walk(tree) + ), + ) + self.assertFalse((ROOT / "mpfi" / "build").exists()) + + def test_import_guard_resolves_from_import_targets(self) -> None: + self.assertEqual( + _imported_module_names( + "from build import transport\n" + "from proof.region.v1.arb import pipeline\n" + ), + ("build.transport", "proof.region.v1.arb.pipeline"), + ) + + def test_protocol_keeps_the_source_input_boundary_below_build_authority(self) -> None: + protocol = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") + source_input_start = protocol.index("`mpfi/input.py`") + transport_start = protocol.index( + "`proof/region/v1/build/transport.py`", + source_input_start, + ) + arb_replay_start = protocol.index("## Воспроизведение Arb", transport_start) + reference = " ".join(protocol[source_input_start:transport_start].split()) + transport_reference = " ".join(protocol[transport_start:arb_replay_start].split()) + source_path = ROOT / "mpfi" / "input.py" + source_text = source_path.read_text(encoding="utf-8") + # Тест намеренно запускается с ``-OO``. Явно выключаем оптимизацию + # parser-а, чтобы контракт документации наблюдался по исходнику, а не + # случайно зависел от сохранения runtime ``__doc__``. + source_tree = compile( + source_text, + str(source_path), + "exec", + flags=ast.PyCF_ONLY_AST, + optimize=0, + ) + seal_function = next( + node + for node in source_tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "seal_mpfi_source_input_v1" + ) + seal_reference = ast.get_docstring(seal_function) + + self.assertIn("`sources//`", reference) + self.assertIn("не требует уникальности root", reference) + self.assertIn( + "Вызывающая сторона передаёт канонический " + "`CanonicalInputLimitsV1`", + reference, + ) + self.assertIn("`MpfiSourceInputErrorV1`", reference) + self.assertIn("`ProvenanceErrorV1`", reference) + self.assertIn("`InputErrorV1`", reference) + self.assertIn("MPFI source-input binding", reference) + self.assertIn("materialization archive", reference) + self.assertIn( + "не вводит recipe, Docker policy, BUILD/RUN authority", + reference, + ) + self.assertIn( + "MPFI sealed source input сам по себе не является MPFI build policy", + transport_reference, + ) + self.assertIn("engine-owned input binding", transport_reference) + self.assertIsNotNone(seal_reference) + self.assertIn("failure exact archive replay", seal_reference) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_mpfi_runtime.py b/proof/region/v1/tests/test_mpfi_runtime.py new file mode 100644 index 00000000..ab414735 --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_runtime.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""RED/green contract for the MPFI runtime profile binding.""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import executor # noqa: E402 +from mpfi import runtime # noqa: E402 + + +def _limits(**changes: int) -> executor.ExecutionLimitsV1: + values = { + "max_executable_bytes": 8 * 1024 * 1024, + "max_stdin_bytes": runtime.MPFI_MAX_JOB_BYTES_V1, + "max_argument_bytes": 64 * 1024, + "max_stdout_bytes": runtime.MPFI_MAX_OUTPUT_BYTES_V1, + "max_stderr_bytes": 64 * 1024, + "wall_timeout_ns": 300_000_000_000, + "memory_max_bytes": 2 * 1024 * 1024 * 1024, + "pids_max": 1, + } + values.update(changes) + return executor.ExecutionLimitsV1(**values) + + +class MpfiRuntimeProfileTests(unittest.TestCase): + def test_profile_rejects_int_subclasses_and_identity_totalizes_forgery(self) -> None: + class EqualInt(int): + def to_bytes(self, *_args: object, **_kwargs: object) -> bytes: + raise RuntimeError("foreign scalar executed") + + values = tuple(runtime.mpfi_runtime_profile_v1()) + hostile_values = (EqualInt(values[0]), *values[1:]) + with self.assertRaises(TypeError): + runtime.MpfiRuntimeProfileV1(*hostile_values) + + forged = tuple.__new__(runtime.MpfiRuntimeProfileV1, hostile_values) + result = runtime.runtime_profile_identity_v1(forged) + self.assertIs(type(result), runtime.MpfiRuntimeIdentityRejectedV1) + self.assertEqual( + result.reason, + runtime.MpfiRuntimeProfileReasonV1.NONCANONICAL, + ) + + def test_profile_is_one_exact_wire_v1_value(self) -> None: + profile = runtime.mpfi_runtime_profile_v1() + self.assertEqual( + tuple(profile), + ( + 16 * 1024 * 1024, + 16 * 1024 * 1024, + 4096, + 32, + 1024, + ), + ) + self.assertEqual( + runtime.MPFI_RUNTIME_PROFILE_ID_V1, + "LC-MPFI-RUNTIME-V1", + ) + self.assertEqual( + runtime.MpfiRuntimeProfileV1(*tuple(profile)), + profile, + ) + with self.assertRaises(ValueError): + runtime.MpfiRuntimeProfileV1( + profile.max_job_bytes - 1, + profile.max_output_bytes, + profile.max_precision_bits, + profile.max_policy_rungs, + profile.max_knots, + ) + + def test_profile_identity_is_typed_and_not_accepted_from_a_plain_tuple(self) -> None: + profile = runtime.mpfi_runtime_profile_v1() + identity = runtime.runtime_profile_identity_v1(profile) + self.assertIs(type(identity), bytes) + self.assertEqual(identity, runtime.runtime_profile_identity_v1(profile)) + rejected = runtime.runtime_profile_identity_v1(tuple(profile)) + self.assertIs(type(rejected), runtime.MpfiRuntimeIdentityRejectedV1) + self.assertEqual( + rejected.reason, + runtime.MpfiRuntimeProfileReasonV1.WRONG_TYPE, + ) + + def test_binding_requires_profile_job_and_output_limits_to_match_exactly(self) -> None: + profile = runtime.mpfi_runtime_profile_v1() + binding = runtime.MpfiRuntimeBindingV1(profile, _limits()) + identity = runtime.runtime_binding_identity_v1(binding) + self.assertIs(type(identity), bytes) + self.assertEqual(identity, runtime.runtime_binding_identity_v1(binding)) + + with self.assertRaises(ValueError): + runtime.MpfiRuntimeBindingV1( + profile, + _limits(max_stdin_bytes=profile.max_job_bytes - 1), + ) + with self.assertRaises(ValueError): + runtime.MpfiRuntimeBindingV1( + profile, + _limits(max_stdout_bytes=profile.max_output_bytes - 1), + ) + rejected = runtime.runtime_binding_identity_v1((profile, _limits())) + self.assertIs(type(rejected), runtime.MpfiRuntimeIdentityRejectedV1) + self.assertEqual( + rejected.reason, + runtime.MpfiRuntimeProfileReasonV1.WRONG_TYPE, + ) + + def test_binding_identity_commits_every_executor_limit(self) -> None: + profile = runtime.mpfi_runtime_profile_v1() + first = runtime.MpfiRuntimeBindingV1(profile, _limits()) + second = runtime.MpfiRuntimeBindingV1( + profile, + _limits(memory_max_bytes=2 * 1024 * 1024 * 1024 - 1), + ) + first_identity = runtime.runtime_binding_identity_v1(first) + second_identity = runtime.runtime_binding_identity_v1(second) + self.assertIs(type(first_identity), bytes) + self.assertIs(type(second_identity), bytes) + self.assertNotEqual(first_identity, second_identity) + + def test_protocol_documents_the_single_binding_authority(self) -> None: + reference = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") + self.assertIn("MpfiRuntimeProfileV1", reference) + self.assertIn("MpfiRuntimeBindingV1", reference) + self.assertIn("max_stdin_bytes", reference) + self.assertIn("max_stdout_bytes", reference) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_mpfi_source_lock.py b/proof/region/v1/tests/test_mpfi_source_lock.py new file mode 100644 index 00000000..a1ec7fc5 --- /dev/null +++ b/proof/region/v1/tests/test_mpfi_source_lock.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Hostile MPFI source-lock and ordered-capability tests for proof V1.""" + +from __future__ import annotations + +import hashlib +import io +import lzma +import sys +import tarfile +import unittest +from dataclasses import replace +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import provenance # noqa: E402 +from provenance import ( # noqa: E402 + AdmittedMpfiSourcesV1, + ArchiveFormatV1, + DetachedSignaturePolicyV1, + LegalFileV1, + MpfiSourceLockV1, + ProjectPinnedArchiveDigestPolicyV1, + ProvenanceErrorV1, + ProvenanceReasonV1, + SourceReleaseLockV1, + SourceRoleV1, + admit_mpfi_sources, + admit_source_archive, + arb_source_lock_v1, + mpfi_source_lock_v1, +) + + +def sha256(value: bytes) -> bytes: + return hashlib.sha256(value).digest() + + +def fixture_archive() -> bytes: + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("fixture-1/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + for name, body in (("LICENSE", b"license"), ("value", b"data")): + member = tarfile.TarInfo(f"fixture-1/{name}") + member.mode = 0o644 + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return lzma.compress(raw.getvalue(), format=lzma.FORMAT_XZ) + + +def fixture_release( + role: SourceRoleV1, + archive: bytes, + integrity: DetachedSignaturePolicyV1 | ProjectPinnedArchiveDigestPolicyV1, +) -> SourceReleaseLockV1: + raw_tar = lzma.decompress(archive) + return SourceReleaseLockV1( + role, + "1", + "https://example.invalid/fixture-1.tar.xz", + ArchiveFormatV1.TAR_XZ, + len(archive), + sha256(archive), + len(raw_tar), + "fixture-1/", + 2, + 11, + (LegalFileV1("LICENSE", 7, sha256(b"license")),), + integrity, + ) + + +def detached_policy() -> DetachedSignaturePolicyV1: + return DetachedSignaturePolicyV1( + "https://example.invalid/fixture-1.tar.xz.sig", + 3, + sha256(b"sig"), + sha256(b"packets"), + bytes.fromhex("00112233445566778899aabbccddeeff00112233"), + ) + + +class MpfiSourceLockTests(unittest.TestCase): + def test_lane_specific_capabilities_have_no_generic_public_aggregate(self) -> None: + self.assertFalse(hasattr(provenance, "SourceClosureV1")) + self.assertFalse(hasattr(provenance, "SafeSourceClosureV1")) + + def test_exact_primary_coordinates_are_canonical_and_round_trip(self) -> None: + lock = mpfi_source_lock_v1() + self.assertEqual( + tuple(source.role for source in lock.sources), + (SourceRoleV1.GMP, SourceRoleV1.MPFR, SourceRoleV1.MPFI), + ) + + mpfi = lock.sources[2] + self.assertEqual(mpfi.version, "1.5.4") + self.assertEqual( + mpfi.archive_url, + "https://perso.ens-lyon.fr/nathalie.revol/softwares/mpfi-1.5.4.tar.xz", + ) + self.assertIs(mpfi.archive_format, ArchiveFormatV1.TAR_XZ) + self.assertEqual(mpfi.archive_length, 370_932) + self.assertEqual( + mpfi.archive_sha256.hex(), + "819e98bc7dad7cf7e67c9ddb592f44545c300de143fe30bc29ca1b422b55306a", + ) + self.assertEqual( + mpfi.identity.hex(), + "66289ae877f4526f992e4ffe5143433f6e17d73acfaeb936482ae4b797b876fb", + ) + self.assertEqual(mpfi.tar_stream_length, 3_502_080) + self.assertEqual(mpfi.root_prefix, "mpfi-1.5.4/") + self.assertEqual(mpfi.regular_file_count, 495) + self.assertEqual(mpfi.regular_file_bytes, 3_117_639) + self.assertEqual( + tuple((item.path, item.length, item.sha256.hex()) for item in mpfi.legal_files), + ( + ( + "COPYING", + 35_147, + "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903", + ), + ( + "COPYING.LESSER", + 7_651, + "da7eabb7bafdf7d3ae5e9f223aa5bdc1eece45ac569dc21b3b037520b4464768", + ), + ( + "README", + 1_336, + "dab7a52115f111ff3771dc4311a837919d45ffaa654e64c110af78bd2a003e20", + ), + ), + ) + self.assertIs(type(mpfi.integrity), ProjectPinnedArchiveDigestPolicyV1) + encoded = lock.encode() + self.assertEqual(len(encoded), 1_339) + self.assertEqual( + lock.identity.hex(), + "03636d1f8c6c4950ba74943cf148d65b596d23a078391eedf6c7606c8613f830", + ) + self.assertEqual(MpfiSourceLockV1.parse(encoded), lock) + self.assertEqual(MpfiSourceLockV1.parse(encoded).encode(), encoded) + + def test_shared_sources_have_one_declaration_but_aggregate_lanes_differ(self) -> None: + arb = arb_source_lock_v1() + mpfi = mpfi_source_lock_v1() + + self.assertEqual(arb.sources[:2], mpfi.sources[:2]) + self.assertNotEqual(arb.sources[2], mpfi.sources[2]) + self.assertNotEqual(arb.identity, mpfi.identity) + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.ArbSourceLockV1.parse(mpfi.encode()) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.NONCANONICAL_ORDER) + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1.parse(arb.encode()) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.NONCANONICAL_ORDER) + + def test_digest_only_policy_is_explicit_and_identity_bound(self) -> None: + archive = fixture_archive() + policy = ProjectPinnedArchiveDigestPolicyV1() + gmp = fixture_release(SourceRoleV1.GMP, archive, detached_policy()) + mpfr = fixture_release(SourceRoleV1.MPFR, archive, detached_policy()) + mpfi = fixture_release(SourceRoleV1.MPFI, archive, policy) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + + encoded = lock.encode() + self.assertEqual(MpfiSourceLockV1.parse(encoded), lock) + for kind, reason in ( + (1, ProvenanceReasonV1.TRUNCATED), + (2, ProvenanceReasonV1.TRUNCATED), + (255, ProvenanceReasonV1.UNKNOWN_ENUM), + ): + with self.subTest(kind=kind): + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1.parse(encoded[:-1] + bytes((kind,))) + self.assertEqual(caught.exception.reason, reason) + self.assertNotEqual( + lock.identity, + MpfiSourceLockV1((gmp, mpfr, replace(mpfi, version="2"))).identity, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + MpfiSourceLockV1((gmp, mpfr, replace(mpfi, integrity=detached_policy()))) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.INTEGRITY_KIND_MISMATCH, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.ArbSourceLockV1((gmp, mpfr, mpfi)) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.NONCANONICAL_ORDER, + ) + + def test_three_locked_sources_become_one_mpfi_capability(self) -> None: + archive = fixture_archive() + gmp = fixture_release(SourceRoleV1.GMP, archive, detached_policy()) + mpfr = fixture_release(SourceRoleV1.MPFR, archive, detached_policy()) + mpfi = fixture_release( + SourceRoleV1.MPFI, + archive, + ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(expected, archive) for expected in lock.sources + ) + + admitted = admit_mpfi_sources(lock, sources) + + self.assertIs(type(admitted), AdmittedMpfiSourcesV1) + self.assertEqual(admitted.source_lock_identity, lock.identity) + self.assertEqual(admitted.sources, sources) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_mpfi_sources(lock, (sources[1], sources[0], sources[2])) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + with self.assertRaises(TypeError): + AdmittedMpfiSourcesV1(lock.identity, sources, _token=object()) + + def test_mpfi_archive_uses_the_shared_engine_neutral_materializer(self) -> None: + archive = fixture_archive() + lock = fixture_release( + SourceRoleV1.MPFI, + archive, + ProjectPinnedArchiveDigestPolicyV1(), + ) + admitted = admit_source_archive(lock, archive) + + self.assertEqual( + provenance.materialize_admitted_source_files_v1(lock, admitted), + ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ), + ) + + def test_reference_does_not_upgrade_the_mpfi_digest_to_publisher_evidence(self) -> None: + reference = (ROOT / "PROTOCOL.md").read_text(encoding="utf-8") + + self.assertIn("ProjectPinnedArchiveDigestPolicyV1", reference) + self.assertIn("replay_materialize_admitted_source_v1", reference) + self.assertIn("bounded-decompresses", reference) + self.assertIn("не приписывает этот digest издателю", reference) + self.assertIn("не заявляет publisher authentication", reference) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/proof/region/v1/tests/test_region_proof_protocol.py b/proof/region/v1/tests/test_region_proof_protocol.py index 9acf0f00..b1e6d5a5 100644 --- a/proof/region/v1/tests/test_region_proof_protocol.py +++ b/proof/region/v1/tests/test_region_proof_protocol.py @@ -33,7 +33,7 @@ FORMULA_RELEASE_DOMAIN_V1, ComparatorBudgetV1, ComparatorKindV1, - ComparatorManifestV1, + ComparatorManifestV2, BoundaryUnprovenWitnessV1, ContextualRegionDefinitionV1, DecisionTranscriptV1, @@ -49,7 +49,7 @@ ResourceLimitWitnessV1, RunClaimV1, WitnessStoreV1, - ContentResolvedComparatorManifestV1, + ContentResolvedComparatorManifestV2, compare_dual_transcripts, encode_contextual_definition_fields_v1, ) @@ -74,16 +74,16 @@ "6e493856d3c81f0d5b12bf1221985c66210ae98c8b6c79c7c5b4aabf243c0116" ) MANIFEST_IDENTITY = bytes.fromhex( - "77373d7025d4a673db27e7ce2ca45e61f9f191f7e017f97731602997430b5739" + "805c3710b9b38189f4b9c0bb69aaf429c944637a4ee38d1ffa56ee2d72ec09d9" ) TRANSCRIPT_IDENTITY = bytes.fromhex( - "d75c7f5d1c8176fdef78cca226e42ecd0cd61bab69b636fe4397e6a763af8582" + "8de25059cf372364da4d6cea05f9a45def3a3a9edfd385443cc31e7d82b59136" ) RUN_CLAIM_IDENTITY = bytes.fromhex( - "3ffae955c59e0cffa53cfa560713fff64f4c63f7ae103eae67c62a068e124b72" + "2c2e2ea8f0737e77a456306ad15332ab1dda609b872260c6bf76229c04b1ad9b" ) COMPARISON_IDENTITY = bytes.fromhex( - "94be6dfc28c1bcc98b703f9fda6e7231984a129e22db554655d4026dae5c4ba0" + "45ee817424540eaed060fcbfc7a43aebb1e0d484759f8a88803c2ed1a2648b9f" ) @@ -102,9 +102,9 @@ def digest(label: int) -> bytes: def admit_manifest( - value: ComparatorManifestV1, -) -> ContentResolvedComparatorManifestV1: - return ContentResolvedComparatorManifestV1.admit( + value: ComparatorManifestV2, +) -> ContentResolvedComparatorManifestV2: + return ContentResolvedComparatorManifestV2.admit( value, SYNTHETIC_CONTENT.get, ) @@ -138,19 +138,19 @@ def fixture_policy() -> ProofPolicyV1: ) -def manifest(kind: ComparatorKindV1, seed: int) -> ContentResolvedComparatorManifestV1: +def manifest(kind: ComparatorKindV1, seed: int) -> ContentResolvedComparatorManifestV2: return admit_manifest( - ComparatorManifestV1( + ComparatorManifestV2( kind=kind, engine_release=digest(seed), upstream_source=digest(seed + 1), - arithmetic_closure=digest(seed + 2), + arithmetic_input_set=digest(seed + 2), wrapper_source=digest(seed + 3), evaluator_source=digest(seed + 4), build_identity=digest(seed + 5), operation_allowlist=digest(seed + 6), - test_receipt=digest(seed + 7), - license_closure=digest(seed + 8), + test_observation=digest(seed + 7), + legal_file_set=digest(seed + 8), exclusions=digest(seed + 9), ) ) @@ -671,24 +671,72 @@ class ManifestTranscriptComparisonTests(unittest.TestCase): def test_protocol_slice_cannot_name_structural_agreement_a_proof(self) -> None: self.assertFalse(hasattr(protocol, "DualProofReceiptV1")) + def test_manifest_v1_surface_is_hard_deleted(self) -> None: + for name in ( + "MANIFEST_MAGIC_V1", + "MANIFEST_ID_LABEL_V1", + "ComparatorManifestV1", + "ContentResolvedComparatorManifestV1", + ): + with self.subTest(name=name): + self.assertFalse(hasattr(protocol, name)) + + def test_manifest_v2_rejects_the_historical_v1_wire_domain(self) -> None: + coordinates = tuple(digest(index) for index in range(10, 20)) + legacy_wire = ( + b"LCMAN1\0\0" + + bytes((int(ComparatorKindV1.ARB),)) + + b"".join(coordinates) + ) + + expect_reason( + self, + ProtocolReasonV1.BAD_MAGIC, + lambda: protocol.ComparatorManifestV2.parse(legacy_wire), + ) + + def test_manifest_v2_has_a_distinct_wire_and_identity_domain(self) -> None: + current = protocol.ComparatorManifestV2( + ComparatorKindV1.ARB, + *(digest(index) for index in range(10, 20)), + ) + encoded = current.encode() + + self.assertEqual(encoded[:8], b"LCMAN2\0\0") + self.assertEqual( + current.identity, + hashlib.sha256( + b"labcolors.proof-region.comparator-manifest.v2\0" + + len(encoded).to_bytes(8, "big") + + encoded + ).digest(), + ) + legacy_wire = b"LCMAN1\0\0" + encoded[8:] + legacy_identity = hashlib.sha256( + b"labcolors.proof-region.comparator-manifest.v1\0" + + len(legacy_wire).to_bytes(8, "big") + + legacy_wire + ).digest() + self.assertNotEqual(current.identity, legacy_identity) + def test_manifest_is_content_resolved_and_each_field_changes_identity(self) -> None: base = manifest(ComparatorKindV1.ARB, 10) self.assertEqual( admit_manifest( - ComparatorManifestV1.parse(base.manifest.encode()) + ComparatorManifestV2.parse(base.manifest.encode()) ).identity, base.identity, ) for field in ( "engine_release", "upstream_source", - "arithmetic_closure", + "arithmetic_input_set", "wrapper_source", "evaluator_source", "build_identity", "operation_allowlist", - "test_receipt", - "license_closure", + "test_observation", + "legal_file_set", "exclusions", ): changed = admit_manifest( @@ -702,7 +750,7 @@ def test_manifest_is_content_resolved_and_each_field_changes_identity(self) -> N expect_reason( self, ProtocolReasonV1.INVALID_MANIFEST, - lambda coordinate=coordinate: ContentResolvedComparatorManifestV1.admit( + lambda coordinate=coordinate: ContentResolvedComparatorManifestV2.admit( base.manifest, lambda current: ( None @@ -714,7 +762,7 @@ def test_manifest_is_content_resolved_and_each_field_changes_identity(self) -> N expect_reason( self, ProtocolReasonV1.DIGEST_MISMATCH, - lambda coordinate=coordinate: ContentResolvedComparatorManifestV1.admit( + lambda coordinate=coordinate: ContentResolvedComparatorManifestV2.admit( base.manifest, lambda current: ( b"wrong" @@ -727,7 +775,7 @@ def test_manifest_is_content_resolved_and_each_field_changes_identity(self) -> N expect_reason( self, ProtocolReasonV1.UNKNOWN_RELEASE, - lambda: ComparatorManifestV1( + lambda: ComparatorManifestV2( 3, # type: ignore[arg-type] *(digest(index) for index in range(300, 310)), ), @@ -743,13 +791,13 @@ def test_manifest_is_content_resolved_and_each_field_changes_identity(self) -> N expect_reason( self, ProtocolReasonV1.INVALID_MANIFEST, - lambda: ContentResolvedComparatorManifestV1.admit( # type: ignore[arg-type] + lambda: ContentResolvedComparatorManifestV2.admit( # type: ignore[arg-type] lookalike, SYNTHETIC_CONTENT.get, ), ) with self.assertRaises(TypeError): - ContentResolvedComparatorManifestV1() # type: ignore[call-arg] + ContentResolvedComparatorManifestV2() # type: ignore[call-arg] class ForeignBytes(bytes): pass @@ -765,7 +813,7 @@ class ForeignBytes(bytes): expect_reason( self, ProtocolReasonV1.INVALID_MANIFEST, - lambda invalid_content=invalid_content: ContentResolvedComparatorManifestV1.admit( + lambda invalid_content=invalid_content: ContentResolvedComparatorManifestV2.admit( base.manifest, lambda _coordinate: invalid_content, # type: ignore[return-value] ), @@ -1242,25 +1290,25 @@ def test_synthetic_resolved_transcripts_produce_only_structural_comparison(self) ( arb.manifest, 329, - "6323e41b60305ef9cf19b4ad65450779079adc87968ed1fdf8a70952f7b39166", + "884625d5983131234d0570f90b482e0e9de2801b773f7f03e34eb2138b104c30", MANIFEST_IDENTITY, ), ( ta, 328, - "ae55a7e363a6fdd2f8cb1455ecc45b4ef937538421c017014e033e9a955c3402", + "b1bc17383b99683302d9156ef1b784a0f094e6eba4e99a346a0a3331c47db3cb", TRANSCRIPT_IDENTITY, ), ( ra, 200, - "5af9da154b5c2e6f5dbdf88b538324fe809366dab712227f95a67657046d06b2", + "4a23db54ac34d2117326d645b17fae098a49a8ec54ea1cc3955d5a1328c7053c", RUN_CLAIM_IDENTITY, ), ( candidate, 368, - "f27fb4ad6fc8e14d16f815b394f67e181d29d02099c2d640f5dec07e38e63f3d", + "017dd72a3dcf001acdd267f91a79a32926da8351874a534ce968c0cf016c0026", COMPARISON_IDENTITY, ), ): diff --git a/proof/region/v1/tests/test_source_lock.py b/proof/region/v1/tests/test_source_lock.py new file mode 100644 index 00000000..e7fffe78 --- /dev/null +++ b/proof/region/v1/tests/test_source_lock.py @@ -0,0 +1,1289 @@ +#!/usr/bin/env python3 +"""Hostile source-lock and archive-admission tests for proof tooling V1.""" + +from __future__ import annotations + +import gzip +import hashlib +import io +import lzma +import sys +import tarfile +import unittest +from dataclasses import replace +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import provenance # noqa: E402 +from provenance import ( # noqa: E402 + AdmittedArbSourcesV1, + AdmittedMpfiSourcesV1, + ArchiveFormatV1, + DetachedSignaturePolicyV1, + GitContentRelationPolicyV1, + LegalFileV1, + MpfiSourceLockV1, + ProjectPinnedArchiveDigestPolicyV1, + ProjectPinnedReleaseOnlyFileV1, + ProvenanceErrorV1, + ProvenanceReasonV1, + SourceReleaseLockV1, + SourceRoleV1, + admit_source_archive, + admit_arb_sources, + admit_mpfi_sources, + arb_source_lock_v1, +) + + +def sha256(value: bytes) -> bytes: + return hashlib.sha256(value).digest() + + +def canonical_identity(label: bytes, encoded: bytes) -> bytes: + """Independent literal oracle for identity values in hostile cache tests.""" + + return hashlib.sha256( + label + len(encoded).to_bytes(8, "big") + encoded + ).digest() + + +def admitted_closure_identity( + label: bytes, + source_lock_identity: bytes, + sources: tuple[provenance.SafeSourceArchiveV1, ...], +) -> bytes: + """Keep the cache-poisoning oracle independent of production preimages.""" + + chunks = [source_lock_identity] + for ordinal, source in enumerate(sources): + chunks.extend( + ( + bytes((ordinal,)), + source.source_lock_identity, + source.archive_sha256, + source.tree_identity, + ) + ) + return canonical_identity(label, b"".join(chunks)) + + +def _replay_source( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> provenance.ReplayedSourceMaterializationV1: + """Exercise the one source materialization contract from a fresh replay.""" + + return provenance.replay_materialize_admitted_source_v1(lock, admitted) + + +def _source_files( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> tuple[tuple[str, int, bytes], ...]: + return _replay_source(lock, admitted).files + + +def _source_coordinates( + lock: SourceReleaseLockV1, + admitted: provenance.SafeSourceArchiveV1, +) -> tuple[bytes, ...]: + return provenance.source_archive_replay_coordinates_v1(lock, admitted) + + +def tar_gz( + entries: tuple[tuple[str, bytes | None, bytes | None], ...], +) -> bytes: + """Build deterministic hostile fixtures; linkname is the third item.""" + + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + for name, body, linkname in entries: + member = tarfile.TarInfo(name) + member.mtime = 0 + member.uid = 0 + member.gid = 0 + member.uname = "" + member.gname = "" + if linkname is not None: + member.type = tarfile.SYMTYPE + member.linkname = linkname.decode("ascii") + member.mode = 0o777 + archive.addfile(member) + elif body is None: + member.type = tarfile.DIRTYPE + member.mode = 0o755 + archive.addfile(member) + else: + member.type = tarfile.REGTYPE + member.mode = 0o644 + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return gzip.compress(raw.getvalue(), compresslevel=9, mtime=0) + + +def raw_ustar( + entries: tuple[tuple[str, bytes, int], ...], +) -> bytes: + """Build a replay fixture without reusing admission's compressed input.""" + + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("fixture-1/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + for name, body, mode in entries: + member = tarfile.TarInfo(f"fixture-1/{name}") + member.mode = mode + member.size = len(body) + archive.addfile(member, io.BytesIO(body)) + return raw.getvalue() + + +def fixture_lock( + archive: bytes, + *, + root: str = "fixture-1/", + file_count: int = 2, + unpacked_bytes: int = 11, + tar_stream_bytes: int | None = None, + archive_format: ArchiveFormatV1 = ArchiveFormatV1.TAR_GZIP, +) -> SourceReleaseLockV1: + if tar_stream_bytes is None: + tar_stream_bytes = len( + gzip.decompress(archive) + if archive_format is ArchiveFormatV1.TAR_GZIP + else lzma.decompress(archive) + ) + return SourceReleaseLockV1( + role=SourceRoleV1.GMP, + version="1", + archive_url="https://example.invalid/fixture-1.tar.gz", + archive_format=archive_format, + archive_length=len(archive), + archive_sha256=sha256(archive), + tar_stream_length=tar_stream_bytes, + root_prefix=root, + regular_file_count=file_count, + regular_file_bytes=unpacked_bytes, + legal_files=(LegalFileV1("LICENSE", 7, sha256(b"license")),), + integrity=DetachedSignaturePolicyV1( + signature_url="https://example.invalid/fixture-1.tar.gz.sig", + signature_length=3, + signature_sha256=sha256(b"sig"), + public_key_packets_sha256=sha256(b"packets"), + signer_fingerprint=bytes.fromhex( + "00112233445566778899aabbccddeeff00112233" + ), + ), + ) + + +GOOD_ARCHIVE = tar_gz( + ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/value", b"data", None), + ) +) + + +class ArbSourceLockTests(unittest.TestCase): + def test_old_overclaiming_vocabulary_is_not_public(self) -> None: + for name in ( + "GeneratedFileV1", + "GitReleasePolicyV1", + "LicenseFileV1", + "OriginKindV1", + "OriginPolicyV1", + ): + with self.subTest(name=name): + self.assertFalse(hasattr(provenance, name)) + self.assertFalse( + hasattr(ProvenanceReasonV1, "LICENSE_CLOSURE_MISMATCH") + ) + self.assertFalse( + hasattr(ProvenanceReasonV1, "RELEASE_RELATION_MISMATCH") + ) + self.assertFalse( + hasattr(provenance.IntegrityKindV1, "GIT_RELEASE_RELATION") + ) + self.assertFalse(hasattr(ProvenanceReasonV1, "ORIGIN_KIND_MISMATCH")) + self.assertFalse(hasattr(provenance, "_parse_origin_policy")) + self.assertNotIn("origin", SourceReleaseLockV1.__dataclass_fields__) + self.assertIn("integrity", SourceReleaseLockV1.__dataclass_fields__) + + def test_exact_primary_coordinates_are_canonical_and_round_trip(self) -> None: + lock = arb_source_lock_v1() + self.assertEqual(tuple(item.role for item in lock.sources), ( + SourceRoleV1.GMP, + SourceRoleV1.MPFR, + SourceRoleV1.FLINT_ARB, + )) + + gmp, mpfr, flint = lock.sources + self.assertEqual(gmp.version, "6.3.0") + self.assertEqual(gmp.archive_length, 2_094_196) + self.assertEqual( + gmp.archive_sha256.hex(), + "a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898", + ) + self.assertIsInstance(gmp.integrity, DetachedSignaturePolicyV1) + self.assertEqual( + gmp.integrity.signer_fingerprint.hex(), + "343c2ff0fbee5ec2edbef399f3599ff828c67298", + ) + self.assertEqual( + gmp.integrity.public_key_packets_sha256.hex(), + "928ac84aa0e2134bbb335cd439110dc3f9b967eb04caff4a44dd5d04a3f13474", + ) + + self.assertEqual(mpfr.version, "4.2.2") + self.assertEqual( + mpfr.archive_url, + "https://www.mpfr.org/mpfr-4.2.2/mpfr-4.2.2.tar.xz", + ) + self.assertEqual(mpfr.archive_length, 1_505_596) + self.assertEqual( + mpfr.archive_sha256.hex(), + "b67ba0383ef7e8a8563734e2e889ef5ec3c3b898a01d00fa0a6869ad81c6ce01", + ) + self.assertIsInstance(mpfr.integrity, DetachedSignaturePolicyV1) + self.assertEqual( + mpfr.integrity.signer_fingerprint.hex(), + "a534be3f83e241d918280aeb5831d11a0d4db02a", + ) + self.assertEqual( + mpfr.integrity.public_key_packets_sha256.hex(), + "3fe00f68bbf3888ae185b950d4db0f708dd01b6159cb03dec77296f9045b6372", + ) + + self.assertEqual(flint.version, "3.6.0") + self.assertEqual(flint.archive_length, 9_313_139) + self.assertEqual( + flint.archive_sha256.hex(), + "b95e2c7792f5eea4a1c8d2d42c4098434756832e57a094b295eb5dfdc9b4c36b", + ) + self.assertIsInstance(flint.integrity, GitContentRelationPolicyV1) + self.assertEqual( + flint.integrity.commit.hex(), + "8d5454b96761fafe4d5a9da76a369a602f500f49", + ) + self.assertEqual( + flint.integrity.tree.hex(), + "18d57417a96227b27dd5336881403dee6fdc851b", + ) + self.assertEqual(flint.integrity.common_file_count, 10_108) + self.assertEqual(len(flint.integrity.omitted_paths), 20) + self.assertEqual( + len(flint.integrity.project_pinned_release_only_files), + 4, + ) + + encoded = lock.encode() + self.assertEqual(len(encoded), 2_286) + self.assertEqual( + lock.identity.hex(), + "a4948c57ed0f9bb066a285b17d7990415cad22ff8d03b5f91900b73da5d2b8cc", + ) + self.assertEqual(type(lock).parse(encoded).encode(), encoded) + self.assertEqual(type(lock).parse(encoded).identity, lock.identity) + + def test_every_expected_coordinate_is_identity_bound(self) -> None: + lock = arb_source_lock_v1() + seen: set[bytes] = set() + for index, source in enumerate(lock.sources): + if isinstance(source.integrity, GitContentRelationPolicyV1): + count_mutation = replace( + source, + regular_file_count=source.regular_file_count + 1, + integrity=replace( + source.integrity, + common_file_count=source.integrity.common_file_count + 1, + ), + ) + else: + count_mutation = replace( + source, + regular_file_count=source.regular_file_count + 1, + ) + source_mutations = ( + replace(source, version=source.version + "x"), + replace(source, archive_url=source.archive_url + ".invalid"), + replace(source, archive_length=source.archive_length + 1), + replace(source, archive_sha256=sha256(source.archive_sha256)), + replace(source, tar_stream_length=source.tar_stream_length + 512), + replace(source, root_prefix="x-" + source.root_prefix), + count_mutation, + replace(source, regular_file_bytes=source.regular_file_bytes + 1), + replace( + source, + legal_files=( + replace( + source.legal_files[0], + length=source.legal_files[0].length + 1, + ), + ) + + source.legal_files[1:], + ), + ) + if isinstance(source.integrity, DetachedSignaturePolicyV1): + integrity_mutations = ( + replace(source.integrity, signature_url=source.integrity.signature_url + ".invalid"), + replace(source.integrity, signature_length=source.integrity.signature_length + 1), + replace(source.integrity, signature_sha256=sha256(source.integrity.signature_sha256)), + replace( + source.integrity, + public_key_packets_sha256=sha256( + source.integrity.public_key_packets_sha256 + ), + ), + replace( + source.integrity, + signer_fingerprint=bytes( + reversed(source.integrity.signer_fingerprint) + ), + ), + ) + else: + integrity_mutations = ( + replace(source.integrity, repository_url=source.integrity.repository_url + ".invalid"), + replace(source.integrity, tag=source.integrity.tag + "x"), + replace(source.integrity, commit=bytes(reversed(source.integrity.commit))), + replace(source.integrity, tree=bytes(reversed(source.integrity.tree))), + replace( + source.integrity, + omitted_paths=(source.integrity.omitted_paths[0] + "x",) + + source.integrity.omitted_paths[1:], + ), + replace( + source.integrity, + project_pinned_release_only_files=( + replace( + source.integrity.project_pinned_release_only_files[0], + sha256=sha256( + source.integrity.project_pinned_release_only_files[0].sha256 + ), + ), + ) + + source.integrity.project_pinned_release_only_files[1:], + ), + ) + for mutation in ( + *source_mutations, + *(replace(source, integrity=value) for value in integrity_mutations), + ): + sources = list(lock.sources) + sources[index] = mutation + changed = type(lock)(tuple(sources)) + self.assertNotEqual(changed.identity, lock.identity) + self.assertNotIn(changed.identity, seen) + seen.add(changed.identity) + + def test_parser_rejects_malleability_and_arbitrary_order(self) -> None: + lock = arb_source_lock_v1() + encoded = lock.encode() + for hostile in (encoded[:-1], encoded + b"\0", b"wrong!!!" + encoded[8:]): + with self.assertRaises(ProvenanceErrorV1): + type(lock).parse(hostile) + with self.assertRaises(ProvenanceErrorV1) as caught: + type(lock)((lock.sources[1], lock.sources[0], lock.sources[2])) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.NONCANONICAL_ORDER) + + with self.assertRaises(ProvenanceErrorV1) as caught: + type(lock).parse(encoded[:10]) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.TRUNCATED) + + def test_malformed_url_is_a_typed_input_failure(self) -> None: + for url in ( + "https://[invalid/signature", + "https://example.invalid:bad/signature", + "https://example.invalid/\nsignature", + ): + with self.subTest(url=url): + with self.assertRaises(ProvenanceErrorV1) as caught: + DetachedSignaturePolicyV1( + url, + 3, + sha256(b"sig"), + sha256(b"packets"), + bytes.fromhex("00112233445566778899aabbccddeeff00112233"), + ) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.INVALID_FIELD, + ) + + def test_constructor_cardinality_limits_match_the_wire_parser(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + with self.assertRaises(ProvenanceErrorV1) as caught: + replace(lock, legal_files=lock.legal_files * 4_097) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.INVALID_FIELD) + + flint_integrity = arb_source_lock_v1().sources[2].integrity + self.assertIsInstance(flint_integrity, GitContentRelationPolicyV1) + with self.assertRaises(ProvenanceErrorV1) as caught: + replace( + flint_integrity, + omitted_paths=flint_integrity.omitted_paths * 205, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.INVALID_FIELD) + + +class SafeArchiveAdmissionTests(unittest.TestCase): + def test_three_locked_sources_become_one_ordered_capability(self) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + flint = replace( + gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + lock = provenance.ArbSourceLockV1((gmp, mpfr, flint)) + sources = tuple( + admit_source_archive(expected, GOOD_ARCHIVE) + for expected in lock.sources + ) + + admitted = admit_arb_sources(lock, sources) + + self.assertIs(type(admitted), AdmittedArbSourcesV1) + self.assertEqual(admitted.source_lock_identity, lock.identity) + self.assertEqual(admitted.sources, sources) + self.assertEqual(len(admitted.identity), 32) + with self.assertRaises((ProvenanceErrorV1, TypeError)): + admit_arb_sources(lock, (sources[1], sources[0], sources[2])) + with self.assertRaises(TypeError): + AdmittedArbSourcesV1( + lock.identity, + sources, + _token=object(), + ) + + def test_aggregate_admission_replays_each_source_before_it_owns_the_closure( + self, + ) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + arb_third = replace( + gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + mpfi_third = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + + class CounterfeitDigest(bytes): + def __eq__(self, _other: object) -> bool: + return True + + def __ne__(self, _other: object) -> bool: + return False + + cases = ( + ( + provenance.ArbSourceLockV1((gmp, mpfr, arb_third)), + admit_arb_sources, + ), + ( + MpfiSourceLockV1((gmp, mpfr, mpfi_third)), + admit_mpfi_sources, + ), + ) + for lock, admit in cases: + with self.subTest(lock_type=type(lock).__name__): + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + original = sources[0].archive_sha256 + object.__setattr__( + sources[0], + "archive_sha256", + CounterfeitDigest(b"\x92" * 32), + ) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + admit(lock, sources) + finally: + object.__setattr__(sources[0], "archive_sha256", original) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + + fresh = admit(lock, sources) + self.assertIsNot(fresh.sources[0], sources[0]) + + def test_archive_is_hash_checked_then_scanned_without_extracting(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + self.assertEqual(admitted.source_lock_identity, lock.identity) + self.assertEqual(admitted.regular_file_count, 2) + self.assertEqual(admitted.regular_file_bytes, 11) + self.assertEqual(tuple(item.path for item in admitted.files), ("LICENSE", "value")) + self.assertEqual(admitted.files[0].sha256, sha256(b"license")) + self.assertIs(admitted.archive_bytes, GOOD_ARCHIVE) + + with self.assertRaises(TypeError): + provenance.SafeSourceArchiveV1( + lock.identity, + lock.archive_sha256, + b"t" * 32, + 2, + 11, + admitted.files, + GOOD_ARCHIVE, + _token=object(), + ) + + for changed in ( + replace(lock, archive_length=lock.archive_length + 1), + replace(lock, archive_sha256=sha256(b"other")), + replace(lock, tar_stream_length=lock.tar_stream_length + 512), + replace(lock, root_prefix="other/"), + replace(lock, regular_file_count=3), + replace(lock, regular_file_bytes=12), + replace( + lock, + legal_files=(LegalFileV1("LICENSE", 7, sha256(b"wrong")),), + ), + ): + with self.assertRaises(ProvenanceErrorV1): + admit_source_archive(changed, GOOD_ARCHIVE) + + changed_legal_file = replace( + lock, + legal_files=(LegalFileV1("LICENSE", 7, sha256(b"wrong")),), + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_source_archive(changed_legal_file, GOOD_ARCHIVE) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.LEGAL_FILES_MISMATCH, + ) + + def test_derived_identities_ignore_injected_instance_caches(self) -> None: + poison = bytes.fromhex("a5" * 32) + release = fixture_lock(GOOD_ARCHIVE) + release_identity = canonical_identity( + b"labcolors.proof-region.source-release-lock.v1\0", + release.encode(), + ) + release.__dict__["identity"] = poison + + self.assertEqual(release.identity, release_identity) + admitted_release = admit_source_archive(release, GOOD_ARCHIVE) + self.assertEqual(admitted_release.source_lock_identity, release_identity) + replay = _replay_source(release, admitted_release) + self.assertEqual(replay.source.source_lock_identity, release_identity) + self.assertEqual( + provenance.source_archive_replay_coordinates_v1( + release, + admitted_release, + )[2], + release_identity, + ) + self.assertEqual( + replay.files, + (("LICENSE", 0o644, b"license"), ("value", 0o644, b"data")), + ) + + arb_gmp = fixture_lock(GOOD_ARCHIVE) + arb_mpfr = replace(arb_gmp, role=SourceRoleV1.MPFR) + arb_flint = replace( + arb_gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + arb_lock = provenance.ArbSourceLockV1((arb_gmp, arb_mpfr, arb_flint)) + arb_lock_identity = canonical_identity( + b"labcolors.proof-region.source-lock.v1\0", + arb_lock.encode(), + ) + arb_lock.__dict__["identity"] = poison + arb_sources = tuple( + admit_source_archive(source, GOOD_ARCHIVE) + for source in arb_lock.sources + ) + arb_admitted = admit_arb_sources(arb_lock, arb_sources) + self.assertIs(type(arb_admitted), AdmittedArbSourcesV1) + self.assertEqual(arb_admitted.source_lock_identity, arb_lock_identity) + arb_admitted_identity = admitted_closure_identity( + b"labcolors.proof-region.admitted-arb-sources.v1\0", + arb_lock_identity, + arb_sources, + ) + arb_admitted.__dict__["identity"] = poison + self.assertEqual(arb_admitted.identity, arb_admitted_identity) + + mpfi_gmp = fixture_lock(GOOD_ARCHIVE) + mpfi_mpfr = replace(mpfi_gmp, role=SourceRoleV1.MPFR) + mpfi_release = replace( + mpfi_gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + mpfi_lock = MpfiSourceLockV1((mpfi_gmp, mpfi_mpfr, mpfi_release)) + mpfi_lock_identity = canonical_identity( + b"labcolors.proof-region.source-lock.v1\0", + mpfi_lock.encode(), + ) + mpfi_lock.__dict__["identity"] = poison + mpfi_sources = tuple( + admit_source_archive(source, GOOD_ARCHIVE) + for source in mpfi_lock.sources + ) + mpfi_admitted = admit_mpfi_sources(mpfi_lock, mpfi_sources) + self.assertIs(type(mpfi_admitted), AdmittedMpfiSourcesV1) + self.assertEqual(mpfi_admitted.source_lock_identity, mpfi_lock_identity) + mpfi_admitted_identity = admitted_closure_identity( + b"labcolors.proof-region.admitted-mpfi-sources.v1\0", + mpfi_lock_identity, + mpfi_sources, + ) + mpfi_admitted.__dict__["identity"] = poison + self.assertEqual(mpfi_admitted.identity, mpfi_admitted_identity) + + def test_operation_owned_source_coordinates_have_no_public_projection(self) -> None: + self.assertFalse(hasattr(provenance, "materialized_source_coordinates_v1")) + + def test_replay_rejects_an_instance_encode_shadow(self) -> None: + """A frozen dataclass can still shadow a method through ``__dict__``.""" + + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + foreign_lock = replace(lock, version="2") + foreign_admitted = admit_source_archive(foreign_lock, GOOD_ARCHIVE) + lock.__dict__["encode"] = lambda: SourceReleaseLockV1.encode(foreign_lock) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_materialize_admitted_source_v1( + lock, + foreign_admitted, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + replay = provenance.replay_materialize_admitted_source_v1(lock, admitted) + self.assertEqual(replay.source_lock.version, "1") + finally: + del lock.__dict__["encode"] + + def test_replay_rejects_a_nested_encoder_shadow(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + foreign_legal_file = LegalFileV1("value", 4, sha256(b"data")) + foreign_lock = replace(lock, legal_files=(foreign_legal_file,)) + foreign_admitted = admit_source_archive(foreign_lock, GOOD_ARCHIVE) + legal_file = lock.legal_files[0] + legal_file.__dict__["encode"] = lambda: LegalFileV1.encode(foreign_legal_file) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_materialize_admitted_source_v1( + lock, + foreign_admitted, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + replay = provenance.replay_materialize_admitted_source_v1(lock, admitted) + self.assertEqual(replay.source_lock.legal_files[0].path, "LICENSE") + finally: + del legal_file.__dict__["encode"] + + def test_metadata_replays_do_not_materialize_file_bodies(self) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + mpfi = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + + with mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + ) as materialize: + provenance.source_archive_replay_coordinates_v1(gmp, sources[0]) + admitted = admit_mpfi_sources(lock, sources) + + materialize.assert_not_called() + self.assertIs(type(admitted), AdmittedMpfiSourcesV1) + + def test_closure_snapshot_replays_metadata_without_materializing_bodies( + self, + ) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + flint = replace( + gmp, + role=SourceRoleV1.FLINT_ARB, + integrity=GitContentRelationPolicyV1( + "https://example.invalid/fixture.git", + "v1", + bytes.fromhex("11" * 20), + bytes.fromhex("22" * 20), + 1, + ("missing",), + ( + ProjectPinnedReleaseOnlyFileV1( + "value", + 0o644, + 4, + sha256(b"data"), + ), + ), + ), + ) + lock = provenance.ArbSourceLockV1((gmp, mpfr, flint)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + admitted = admit_arb_sources(lock, sources) + real_admit = provenance._admit_source_archive_once + real_materialize = provenance._materialize_replayed_source_files_v1 + + with ( + mock.patch.object( + provenance, + "_admit_source_archive_once", + wraps=real_admit, + ) as replay, + mock.patch.object( + provenance, + "_materialize_replayed_source_files_v1", + wraps=real_materialize, + ) as materialize, + ): + snapshot = provenance.snapshot_admitted_source_closure_v1(lock, admitted) + + self.assertIs(type(snapshot), AdmittedArbSourcesV1) + self.assertEqual(snapshot.identity, admitted.identity) + self.assertEqual(replay.call_count, 3) + self.assertEqual(materialize.call_count, 0) + self.assertTrue( + all( + snapshot_source is not retained_source + for snapshot_source, retained_source in zip( + snapshot.sources, + admitted.sources, + strict=True, + ) + ) + ) + + flint_source = admitted.sources[2] + original_files = flint_source.files + for replacement in ( + list(original_files), + (replace(original_files[0], path="LICENSE-FORGED"), *original_files[1:]), + ): + with self.subTest(retained_manifest=type(replacement).__name__): + object.__setattr__(flint_source, "files", replacement) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.snapshot_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(flint_source, "files", original_files) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + original_archive = flint_source.archive_bytes + corrupted_archive = bytes((original_archive[0] ^ 1,)) + original_archive[1:] + object.__setattr__(flint_source, "_archive_bytes", corrupted_archive) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.snapshot_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(flint_source, "_archive_bytes", original_archive) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.ARCHIVE_DIGEST_MISMATCH, + ) + + def test_public_closure_replay_totalizes_a_mutated_source_tuple(self) -> None: + gmp = fixture_lock(GOOD_ARCHIVE) + mpfr = replace(gmp, role=SourceRoleV1.MPFR) + mpfi = replace( + gmp, + role=SourceRoleV1.MPFI, + integrity=ProjectPinnedArchiveDigestPolicyV1(), + ) + lock = MpfiSourceLockV1((gmp, mpfr, mpfi)) + sources = tuple( + admit_source_archive(release, GOOD_ARCHIVE) + for release in lock.sources + ) + admitted = admit_mpfi_sources(lock, sources) + original = admitted.sources + object.__setattr__(admitted, "sources", object()) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.replay_admitted_source_closure_v1(lock, admitted) + finally: + object.__setattr__(admitted, "sources", original) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + def test_shared_materializer_replays_only_the_exact_admitted_archive(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + + self.assertEqual( + _source_files(lock, admitted), + ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ), + ) + + mutations = ( + ("source_lock_identity", sha256(b"foreign-lock")), + ("archive_sha256", sha256(b"foreign-archive")), + ("tree_identity", sha256(b"foreign-tree")), + ("regular_file_count", admitted.regular_file_count + 1), + ("regular_file_bytes", admitted.regular_file_bytes + 1), + ("files", tuple(reversed(admitted.files))), + ) + for field_name, replacement in mutations: + with self.subTest(retained_coordinate=field_name): + original = getattr(admitted, field_name) + object.__setattr__(admitted, field_name, replacement) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _source_files(lock, admitted) + finally: + object.__setattr__(admitted, field_name, original) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + + original_archive_bytes = admitted.archive_bytes + object.__setattr__(admitted, "_archive_bytes", GOOD_ARCHIVE[:-1]) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _source_files(lock, admitted) + finally: + object.__setattr__(admitted, "_archive_bytes", original_archive_bytes) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.ARCHIVE_LENGTH_MISMATCH, + ) + + original_role = lock.role + object.__setattr__(lock, "role", 999) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _source_files(lock, admitted) + finally: + object.__setattr__(lock, "role", original_role) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + for hostile_lock, hostile_admitted in ((object(), admitted), (lock, object())): + with self.subTest(hostile=type(hostile_lock).__name__): + with self.assertRaises(TypeError): + _source_files(hostile_lock, hostile_admitted) + + def test_replay_boundary_totalizes_hostile_nominal_coordinates(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + + class ExplodingCoordinate: + def __ne__(self, _other: object) -> bool: + raise RuntimeError("hostile coordinate comparison") + + original_length = lock.archive_length + object.__setattr__(lock, "archive_length", ExplodingCoordinate()) + try: + for name, operation in ( + ("atomic-source-snapshot", lambda: _replay_source(lock, admitted)), + ): + with self.subTest(operation=name): + with self.assertRaises(ProvenanceErrorV1) as caught: + operation() + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + finally: + object.__setattr__(lock, "archive_length", original_length) + + class InterruptedCoordinate: + def to_bytes(self, _length: int, _order: str) -> bytes: + raise KeyboardInterrupt("source lock interruption") + + object.__setattr__(lock, "archive_length", InterruptedCoordinate()) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _replay_source(lock, admitted) + finally: + object.__setattr__(lock, "archive_length", original_length) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + def test_replay_rejects_counterfeit_retained_coordinates_before_equality( + self, + ) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + + class CounterfeitDigest(bytes): + def __eq__(self, _other: object) -> bool: + return True + + def __ne__(self, _other: object) -> bool: + return False + + originals = { + field_name: getattr(admitted, field_name) + for field_name in ( + "source_lock_identity", + "archive_sha256", + "tree_identity", + ) + } + for field_name, original in originals.items(): + with self.subTest(retained_coordinate=field_name): + object.__setattr__( + admitted, + field_name, + CounterfeitDigest(b"\xa5" * 32), + ) + try: + for operation in (lambda: _replay_source(lock, admitted),): + with self.assertRaises(ProvenanceErrorV1) as caught: + operation() + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.FOREIGN_BINDING, + ) + finally: + object.__setattr__(admitted, field_name, original) + + original_file = admitted.files[0] + original_digest = original_file.sha256 + object.__setattr__( + original_file, + "sha256", + CounterfeitDigest(b"\x91" * 32), + ) + try: + with self.assertRaises(ProvenanceErrorV1) as caught: + _replay_source(lock, admitted) + finally: + object.__setattr__(original_file, "sha256", original_digest) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.FOREIGN_BINDING) + + def test_replay_coordinates_keep_one_lock_snapshot_across_reentrancy(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + original_root_prefix = lock.root_prefix + real_admit = provenance._admit_source_archive_once + mutated = False + + def admit_then_mutate( + expected: SourceReleaseLockV1, + archive: bytes, + ) -> tuple[provenance.SafeSourceArchiveV1, bytes]: + nonlocal mutated + replayed = real_admit(expected, archive) + object.__setattr__(lock, "root_prefix", "other/") + mutated = True + return replayed + + try: + with mock.patch.object( + provenance, + "_admit_source_archive_once", + side_effect=admit_then_mutate, + ): + coordinates = _source_coordinates(lock, admitted) + finally: + object.__setattr__(lock, "root_prefix", original_root_prefix) + self.assertTrue(mutated) + self.assertEqual( + canonical_identity( + b"labcolors.proof-region.source-release-lock.v1\0", + coordinates[1], + ), + coordinates[2], + ) + + def test_shared_materializer_is_invariant_under_regular_member_permutation(self) -> None: + expected = ( + ("LICENSE", 0o644, b"license"), + ("value", 0o644, b"data"), + ) + for entries in ( + ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/value", b"data", None), + ), + ( + ("fixture-1/", None, None), + ("fixture-1/value", b"data", None), + ("fixture-1/LICENSE", b"license", None), + ), + ): + with self.subTest(member_order=entries): + archive = tar_gz(entries) + lock = fixture_lock(archive) + admitted = admit_source_archive(lock, archive) + self.assertEqual( + provenance.materialize_admitted_source_files_v1(lock, admitted), + expected, + ) + + def test_shared_materializer_rechecks_the_replayed_tar_before_returning_files(self) -> None: + lock = fixture_lock(GOOD_ARCHIVE) + admitted = admit_source_archive(lock, GOOD_ARCHIVE) + replayed, _ = provenance.replay_admitted_source_archive_v1(lock, admitted) + cases = ( + ( + "body", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"evil", 0o644), + ) + ), + ProvenanceReasonV1.FILE_CONTENT_MISMATCH, + ), + ( + "mode", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"data", 0o755), + ) + ), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ( + "duplicate", + raw_ustar( + ( + ("LICENSE", b"license", 0o644), + ("value", b"data", 0o644), + ("value", b"data", 0o644), + ) + ), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ( + "missing", + raw_ustar((("LICENSE", b"license", 0o644),)), + ProvenanceReasonV1.FOREIGN_BINDING, + ), + ) + for name, raw_tar, reason in cases: + with self.subTest(mutation=name): + with mock.patch.object( + provenance, + "_admit_source_archive_once", + return_value=(replayed, raw_tar), + ): + with self.assertRaises(ProvenanceErrorV1) as caught: + provenance.materialize_admitted_source_files_v1(lock, admitted) + self.assertEqual(caught.exception.reason, reason) + + def test_unsafe_member_kinds_and_paths_are_rejected(self) -> None: + fixtures = ( + (ProvenanceReasonV1.UNSAFE_PATH, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/../escape", b"data", None), + )), + (ProvenanceReasonV1.UNSAFE_PATH, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/bad\\path", b"data", None), + )), + (ProvenanceReasonV1.ABSOLUTE_PATH, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("/absolute", b"data", None), + )), + (ProvenanceReasonV1.UNSAFE_LINK, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/link", None, b"../escape"), + )), + (ProvenanceReasonV1.CASE_COLLISION, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/license", b"data", None), + )), + (ProvenanceReasonV1.DUPLICATE_PATH, ( + ("fixture-1/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/LICENSE", b"data", None), + )), + (ProvenanceReasonV1.UNSAFE_PATH, ( + ("fixture-1/", None, None), + ("fixture-1/a/b/", None, None), + ("fixture-1/LICENSE", b"license", None), + ("fixture-1/value", b"data", None), + )), + ) + for expected, entries in fixtures: + with self.subTest(expected=expected): + archive = tar_gz(entries) + lock = fixture_lock( + archive, + file_count=2, + unpacked_bytes=11, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_source_archive(lock, archive) + self.assertEqual(caught.exception.reason, expected) + + def test_special_member_and_noncanonical_compressed_stream_are_rejected(self) -> None: + for member_type, reason in ( + (tarfile.FIFOTYPE, ProvenanceReasonV1.UNSAFE_MEMBER_TYPE), + (tarfile.CHRTYPE, ProvenanceReasonV1.UNSAFE_MEMBER_TYPE), + (tarfile.BLKTYPE, ProvenanceReasonV1.UNSAFE_MEMBER_TYPE), + (tarfile.LNKTYPE, ProvenanceReasonV1.UNSAFE_LINK), + ): + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w", format=tarfile.USTAR_FORMAT) as archive: + root = tarfile.TarInfo("fixture-1/") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + license_member = tarfile.TarInfo("fixture-1/LICENSE") + license_member.size = 7 + license_member.mode = 0o644 + archive.addfile(license_member, io.BytesIO(b"license")) + hostile = tarfile.TarInfo("fixture-1/hostile") + hostile.type = member_type + hostile.mode = 0o644 + hostile.linkname = "fixture-1/LICENSE" + archive.addfile(hostile) + special = gzip.compress(raw.getvalue(), mtime=0) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_source_archive( + fixture_lock(special, file_count=1, unpacked_bytes=7), + special, + ) + self.assertEqual(caught.exception.reason, reason) + + concatenated = GOOD_ARCHIVE + gzip.compress(b"trailing", mtime=0) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_source_archive( + fixture_lock( + concatenated, + tar_stream_bytes=len(gzip.decompress(GOOD_ARCHIVE)), + ), + concatenated, + ) + self.assertEqual(caught.exception.reason, ProvenanceReasonV1.TRAILING_COMPRESSED_DATA) + + def test_xz_uses_the_same_bounded_archive_law(self) -> None: + raw_tar = gzip.decompress(GOOD_ARCHIVE) + archive = lzma.compress(raw_tar, format=lzma.FORMAT_XZ) + lock = fixture_lock(archive, archive_format=ArchiveFormatV1.TAR_XZ) + admitted = admit_source_archive(lock, archive) + self.assertEqual(admitted.regular_file_count, 2) + + def test_compressed_expansion_cannot_cross_the_locked_tar_bound(self) -> None: + raw_tar = gzip.decompress(GOOD_ARCHIVE) + locked_length = len(raw_tar) - 512 + for archive_format, archive in ( + ( + ArchiveFormatV1.TAR_GZIP, + gzip.compress(raw_tar, compresslevel=9, mtime=0), + ), + ( + ArchiveFormatV1.TAR_XZ, + lzma.compress(raw_tar, format=lzma.FORMAT_XZ), + ), + ): + with self.subTest(archive_format=archive_format): + lock = fixture_lock( + archive, + tar_stream_bytes=locked_length, + archive_format=archive_format, + ) + with self.assertRaises(ProvenanceErrorV1) as caught: + admit_source_archive(lock, archive) + self.assertEqual( + caught.exception.reason, + ProvenanceReasonV1.TAR_STREAM_LENGTH_MISMATCH, + ) + + def test_encoded_mutations_never_reenter_as_the_same_lock(self) -> None: + lock = arb_source_lock_v1() + encoded = lock.encode() + accepted = 0 + for offset in range(len(encoded)): + mutated = encoded[:offset] + bytes((encoded[offset] ^ 1,)) + encoded[offset + 1 :] + try: + parsed = type(lock).parse(mutated) + except ProvenanceErrorV1: + continue + accepted += 1 + self.assertNotEqual(parsed.identity, lock.identity) + self.assertEqual(parsed.encode(), mutated) + self.assertGreater(accepted, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index d32d4698..5b899a3b 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -14,7 +14,7 @@ export const DEFAULT_BUDGET = resolve( "packages/colors/bench/wasm.json", ); export const WASM_BUDGET_FILE_SHA256 = - "5d61b976f770a5dd46075d7571a9f4368f07c41f6b97c844df9493edda805d0e"; + "e39d32e035deb6a878746744da7d2c0f4e4d5bdb015445e6874c60c9b3258c39"; const SCHEMA_VERSION = 2; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm";