Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ remain necessary alongside these byte-oriented targets.

`scripts/quality/qualify.py` records committed source, worktree status,
platform/toolchains and hashes of explicitly supplied executables before and
after one check. Tracked files marked `assume-unchanged` or `skip-worktree`
after one check. For every supplied executable it also records the exact
binary-seeded FNV value and `proto:` separator selected by protobuf-go's
`internal/detrand`. That text is normally diagnostic, but a governance proposal
can persist a dynamic-message decoding error in `failed_reason`; a binary hash
alone does not make this otherwise hidden consensus input reviewable. Tracked
files marked `assume-unchanged` or `skip-worktree`
can hide modified contents from Git status. Qualification rejects those root
index flags, even if their files currently match HEAD, and records index flags
in source evidence so flags added during a check invalidate its result. It does
Expand Down
19 changes: 17 additions & 2 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -1105,10 +1105,25 @@ Open:
for the owner to file (re-check the ICA allow list whenever mainnet
governance changes it).

- Go itself: protobuf-go deliberately selects `proto: ` or `proto:\u00a0`
from a hash of the program binary. A passed governance proposal can persist
such a dynamic-message decoding error in `failed_reason`, so Go validators
built as different binaries can split among themselves if governance passes
a crafted proposal. The authenticated qualification binaries for v10.6.0 and
v9.0.8 select ASCII, which is the deterministic behavior Rust currently
matches. The scheduled long-run workflow rebuilt both binaries with different
bytes that select U+00A0; v9-upgrade seed 62203 then demonstrated the persisted
`MsgAnnounceValidator.storage_location` error and app-hash split. That run is
not exact-candidate evidence. Official standalone release binaries do not
remove the ambiguity: the v10.6.0 asset selects U+00A0 while the v9.0.8 asset
selects ASCII. Exact-source qualification records this hidden binary input for
every supplied oracle. The general halt policy above still applies. Mainnet
reachability requires governance to pass the crafted proposal; ordinary
rejected transactions expose the same difference only in unhashed logs.

- Error text (logs; not hashed, code and gas match) that still differs
from Go, judged infeasible or not worth emulating: Go panics log a
stack (goroutine ids, addresses); protobuf-go randomizes a space in its
errors (`proto:` followed by a space or U+00A0, chosen per binary);
stack (goroutine ids, addresses);
`Simulate`'s error carries `cosmossdk.io/errors`' `%v` source frame
(`[celestiaorg/cosmos-sdk@v0.52.12/x/auth/ante/sigverify.go:290]`, see
"Log parity" above); gogoproto's `wrong wireType` names the Go field.
Expand Down
39 changes: 39 additions & 0 deletions scripts/quality/qualify.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
"""

import argparse
import binascii
import hashlib
import json
from pathlib import Path
import platform
import struct
import subprocess
import sys
import time
Expand Down Expand Up @@ -41,6 +43,39 @@ def digest(path):
return hashlib.file_digest(stream, "sha256").hexdigest()


def protobuf_go_detrand(path):
"""Classify protobuf-go's binary-seeded error prefix for this executable.

google.golang.org/protobuf/internal/detrand hashes the executable size and
eight 64-byte samples with FNV-1. Its low bit selects the ordinary or
nonbreaking space after ``proto:``. The choice can become consensus-visible
when governance persists a dynamic-message decoding error.
"""
size = path.stat().st_size
offset_basis = 14695981039346656037
prime = 1099511628211
value = offset_basis
with path.open("rb") as stream:
chunks = [struct.pack("<Q", size)]
for index in range(8):
stream.seek(index * size // 8)
sample = stream.read(64)
# Go returns seed zero if ReadAt cannot fill a complete sample.
if len(sample) != 64:
value = 0
break
chunks.append(sample)
else:
for byte in b"".join(chunks):
value = value * prime & ((1 << 64) - 1)
value ^= byte
return {
"seed_hex": f"0x{value:016x}",
"separator": "nonbreaking-space" if value & 1 else "space",
"separator_utf8_hex": binascii.hexlify("\u00a0".encode() if value & 1 else b" ").decode(),
}


def tool(*command):
try:
result = subprocess.run(command, text=True, capture_output=True, check=False)
Expand All @@ -60,6 +95,9 @@ def qualify(root, out, command, binaries):
# Never overwrite evidence from an earlier run or turn a rerun into its pass.
out.mkdir(parents=True, exist_ok=False)
inputs = {str(path.resolve()): digest(path) for path in binaries}
protobuf_prefixes = {
str(path.resolve()): protobuf_go_detrand(path) for path in binaries
}
evidence = {
"schema_version": 1,
"source": before,
Expand All @@ -70,6 +108,7 @@ def qualify(root, out, command, binaries):
"toolchains": {"rustc": tool("rustc", "-Vv"), "cargo": tool("cargo", "-V"),
"go": tool("go", "version")},
"binary_sha256": inputs,
"protobuf_go_detrand": protobuf_prefixes,
"status": "running",
"scope": "one check only; remaining F1 gates require separate evidence",
}
Expand Down
31 changes: 31 additions & 0 deletions scripts/quality/test_qualify.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ def test_mutating_source_or_binary_invalidates_successful_command(self):
self.assertEqual(evidence["exit_code"], 0)
self.assertEqual(evidence["status"], "failed")

def test_records_protobuf_go_binary_seeded_error_prefix(self):
with tempfile.TemporaryDirectory() as folder:
root = self.repo(folder)
binary = Path(folder) / "binary"
binary.write_bytes(bytes(range(256)) * 4)
result, evidence = self.run_check(
root, Path(folder) / "out", "pass", [binary]
)
self.assertTrue(result)
self.assertEqual(
evidence["protobuf_go_detrand"][str(binary.resolve())],
{
"seed_hex": "0x0ce7a0bfde0748e1",
"separator": "nonbreaking-space",
"separator_utf8_hex": "c2a0",
},
)

def test_short_binary_uses_protobuf_go_zero_seed_fallback(self):
with tempfile.TemporaryDirectory() as folder:
binary = Path(folder) / "binary"
binary.write_bytes(b"short")
self.assertEqual(
MODULE.protobuf_go_detrand(binary),
{
"seed_hex": "0x0000000000000000",
"separator": "space",
"separator_utf8_hex": "20",
},
)

def test_untracked_source_prevents_qualification(self):
with tempfile.TemporaryDirectory() as folder:
root = self.repo(folder)
Expand Down
Loading