315 lines
13 KiB
Python
315 lines
13 KiB
Python
# #region Tooling.DrRecovery.Git [C:4] [TYPE Module] [SEMANTICS dr,recovery,git,closure]
|
|
# @BRIEF Verify actual bundle closure/commit/blob bytes in owned scratch, then independently inspect declared stand roots.
|
|
# @RELATION DEPENDS_ON -> [Tooling.DrRecovery.Git.Runner]
|
|
# @INVARIANT No transport, inherited Git config, authentication, replacement objects or another object source is used.
|
|
# @SIDE_EFFECT Writes only a caller-owned temporary bare repository; deletes only that temporary directory.
|
|
# @RATIONALE Importing the declared bundle into an empty ODB proves actual available objects, unlike ls-remote/header metadata.
|
|
# @REJECTED Satisfying bundle prerequisites from a source clone would hide an incomplete backup.
|
|
import configparser
|
|
from hashlib import sha256
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess as subprocess
|
|
import tempfile
|
|
|
|
from dr_recovery_io import Refused, require, read, pinned, BUNDLE_LIMIT, BLOB_LIMIT
|
|
|
|
|
|
from dr_recovery_git_command import git
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.Refs [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def refs(source):
|
|
values = {item.name: item.commit_sha for item in source.refs}
|
|
require(len(values) == len(source.refs), "DR_RECOVERY_CONTRACT_INVALID")
|
|
return values
|
|
# #endregion Tooling.DrRecovery.Git.Refs
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.ExactCommit [C:3] [TYPE Function]
|
|
# @POST The literal claim resolves to exactly itself and is a commit object; neither prefix resolution nor tag peeling supplies identity.
|
|
# @RATIONALE A tag or abbreviated SHA can resolve to valid publication bytes while naming a different object than the receipt claims.
|
|
# @REJECTED SHA^{commit} and cat-file commit alone peel annotated tags and do not establish literal commit identity.
|
|
def exact_commit(root, identity):
|
|
actual = (
|
|
git(
|
|
root,
|
|
["rev-parse", "--verify", "--end-of-options", identity],
|
|
"DR_RECOVERY_GIT_OBJECT_UNAVAILABLE",
|
|
)
|
|
.decode("ascii")
|
|
.strip()
|
|
)
|
|
require(actual == identity, "DR_RECOVERY_GIT_OBJECT_UNAVAILABLE")
|
|
kind = git(
|
|
root, ["cat-file", "-t", identity], "DR_RECOVERY_GIT_OBJECT_UNAVAILABLE"
|
|
).strip()
|
|
require(kind == b"commit", "DR_RECOVERY_GIT_OBJECT_UNAVAILABLE")
|
|
|
|
|
|
# #endregion Tooling.DrRecovery.Git.ExactCommit
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.RequiredRoots [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def required_roots(root, source, operations):
|
|
# Current publication contract requires one explicit nonnull reviewed parent; no parent is synthesized.
|
|
roots = {ref.commit_sha for ref in source.refs}
|
|
roots.update(operation.commit_sha for operation in operations)
|
|
roots.update(operation.expected_branch_head for operation in operations)
|
|
for identity in roots:
|
|
exact_commit(root, identity)
|
|
return sorted(roots)
|
|
# #endregion Tooling.DrRecovery.Git.RequiredRoots
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.Inspect [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def inspect(root, source, operations, render_bytes):
|
|
for name, commit in refs(source).items():
|
|
actual = (
|
|
git(root, ["rev-parse", "--verify", name], "DR_RECOVERY_GIT_REFS_MISMATCH")
|
|
.decode()
|
|
.strip()
|
|
)
|
|
require(actual == commit, "DR_RECOVERY_GIT_REFS_MISMATCH")
|
|
exact_commit(root, commit)
|
|
for operation in operations:
|
|
commit = operation.commit_sha
|
|
exact_commit(root, commit)
|
|
provenance(root, source, operation)
|
|
path = commit + ":" + operation.destination.path
|
|
size = git(root, ["cat-file", "-s", path], "DR_RECOVERY_GIT_OBJECT_UNAVAILABLE")
|
|
require(size.strip().isdigit() and int(size) <= BLOB_LIMIT, "DR_RECOVERY_LIMIT")
|
|
content = git(
|
|
root,
|
|
["cat-file", "blob", path],
|
|
"DR_RECOVERY_GIT_OBJECT_UNAVAILABLE",
|
|
BLOB_LIMIT,
|
|
)
|
|
require(
|
|
content == render_bytes[operation.id]
|
|
and sha256(content).hexdigest() == operation.render_sha256,
|
|
"DR_RECOVERY_GIT_RENDER_MISMATCH",
|
|
)
|
|
# #endregion Tooling.DrRecovery.Git.Inspect
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.Provenance [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def provenance(root, source, operation):
|
|
# Reuse the publication owner's exact parent+marker rule over actual local commit bytes.
|
|
try:
|
|
from src.services.dashboard_testing.execution.publication_worker import (
|
|
_observe_own_commit,
|
|
)
|
|
except Exception:
|
|
raise Refused("DR_RECOVERY_GIT_OWNER_UNAVAILABLE") from None
|
|
raw = git(
|
|
root,
|
|
["cat-file", "commit", operation.commit_sha],
|
|
"DR_RECOVERY_GIT_OBJECT_UNAVAILABLE",
|
|
BLOB_LIMIT,
|
|
)
|
|
headers, message = raw.split(b"\n\n", 1)
|
|
parents = [
|
|
{"sha": line[7:].decode("ascii")}
|
|
for line in headers.splitlines()
|
|
if line.startswith(b"parent ")
|
|
]
|
|
payload = {
|
|
"sha": operation.commit_sha,
|
|
"parents": parents,
|
|
"commit": {"message": message.decode("utf-8")},
|
|
}
|
|
# #region Tooling.DrRecovery.Git.Provenance.Localresponse [C:3] [TYPE Class] [SEMANTICS dr,recovery]
|
|
class LocalResponse:
|
|
status_code = 200
|
|
|
|
# #region Tooling.DrRecovery.Git.Provenance.Localresponse.Json [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def json(self):
|
|
return payload
|
|
# #endregion Tooling.DrRecovery.Git.Provenance.Localresponse.Json
|
|
# #endregion Tooling.DrRecovery.Git.Provenance.Localresponse
|
|
|
|
# #region Tooling.DrRecovery.Git.Provenance.Localreader [C:3] [TYPE Class] [SEMANTICS dr,recovery]
|
|
class LocalReader:
|
|
# #region Tooling.DrRecovery.Git.Provenance.Localreader.Get [C:3] [TYPE Function] [SEMANTICS dr,recovery]
|
|
def get(self, *args, **kwargs):
|
|
return LocalResponse()
|
|
# #endregion Tooling.DrRecovery.Git.Provenance.Localreader.Get
|
|
# #endregion Tooling.DrRecovery.Git.Provenance.Localreader
|
|
|
|
reader = LocalReader()
|
|
require(
|
|
_observe_own_commit(
|
|
reader, source.repository, operation.commit_sha, operation
|
|
).matches,
|
|
"DR_RECOVERY_GIT_PROVENANCE_MISMATCH",
|
|
)
|
|
# #endregion Tooling.DrRecovery.Git.Provenance
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.Bundle [C:4] [TYPE Function]
|
|
# @INVARIANT Install every declared root in owned scratch before full closure validation.
|
|
def bundle(root_fd, source, operations, render_bytes):
|
|
data = read(root_fd, source.bundle.path, BUNDLE_LIMIT, source.bundle)
|
|
boundary = data.find(b"\n\n", 0, 128 * 1024)
|
|
require(boundary >= 0, "DR_RECOVERY_GIT_BUNDLE_INVALID")
|
|
header = data[:boundary].splitlines()
|
|
require(
|
|
header[0] in (b"# v2 git bundle", b"# v3 git bundle"),
|
|
"DR_RECOVERY_GIT_BUNDLE_INVALID",
|
|
)
|
|
prerequisites = [
|
|
line[1:].split(b" ", 1)[0].decode("ascii")
|
|
for line in header
|
|
if line.startswith(b"-")
|
|
]
|
|
require(
|
|
set(prerequisites) == set(source.prerequisites)
|
|
and len(prerequisites) == len(source.prerequisites),
|
|
"DR_RECOVERY_GIT_PREREQUISITES_MISMATCH",
|
|
)
|
|
require(not prerequisites, "DR_RECOVERY_GIT_NOT_SELF_CONTAINED")
|
|
with tempfile.TemporaryDirectory(prefix="ss-dr-git-check-") as directory:
|
|
scratch = Path(directory)
|
|
archive = scratch / "catalog.bundle"
|
|
archive.write_bytes(data)
|
|
odb = scratch / "objects.git"
|
|
git(
|
|
odb,
|
|
["init", "--bare", "--object-format=" + source.object_format, str(odb)],
|
|
"DR_RECOVERY_GIT_UNAVAILABLE",
|
|
)
|
|
advertised = git(
|
|
odb,
|
|
["bundle", "list-heads", str(archive)],
|
|
"DR_RECOVERY_GIT_BUNDLE_INVALID",
|
|
)
|
|
actual = {}
|
|
for line in advertised.decode("ascii").splitlines():
|
|
commit, name = line.split(" ", 1)
|
|
require(name not in actual, "DR_RECOVERY_GIT_REFS_MISMATCH")
|
|
actual[name] = commit
|
|
require(actual == refs(source), "DR_RECOVERY_GIT_REFS_MISMATCH")
|
|
git(odb, ["bundle", "verify", str(archive)], "DR_RECOVERY_GIT_BUNDLE_INVALID")
|
|
git(odb, ["bundle", "unbundle", str(archive)], "DR_RECOVERY_GIT_BUNDLE_INVALID")
|
|
roots = required_roots(odb, source, operations)
|
|
# unbundle imports ODB objects but does not install refs; only this owned scratch is written.
|
|
for name, commit in actual.items():
|
|
git(odb, ["update-ref", name, commit], "DR_RECOVERY_GIT_BUNDLE_INVALID")
|
|
git(
|
|
odb,
|
|
["fsck", "--full", "--strict", "--no-reflogs", *roots],
|
|
"DR_RECOVERY_GIT_BUNDLE_INVALID",
|
|
)
|
|
inspect(odb, source, operations, render_bytes)
|
|
|
|
|
|
# #endregion Tooling.DrRecovery.Git.Bundle
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.Stand [C:3] [TYPE Function]
|
|
def stand(path, source, operations, render_bytes):
|
|
path = Path(path)
|
|
require(path.is_absolute() and not path.is_symlink(), "DR_RECOVERY_GIT_ROOT_UNSAFE")
|
|
with pinned(path) as descriptor:
|
|
stand_snapshot(descriptor, source, operations, render_bytes)
|
|
|
|
|
|
# #endregion Tooling.DrRecovery.Git.Stand
|
|
|
|
|
|
# #region Tooling.DrRecovery.Git.StandSnapshot [C:4] [TYPE Function]
|
|
# @INVARIANT Config and all ODB/ref files use the same pinned root; a root rename cannot select another repo.
|
|
def stand_snapshot(descriptor, source, operations, render_bytes):
|
|
raw = read(descriptor, "config", 64 * 1024)
|
|
config = configparser.ConfigParser()
|
|
try:
|
|
config.read_string(raw.decode())
|
|
allowed = {
|
|
"core": {"repositoryformatversion", "bare", "filemode", "logallrefupdates"},
|
|
"extensions": {"objectformat"},
|
|
}
|
|
require(
|
|
not config.defaults()
|
|
and all(
|
|
section in allowed and set(config[section]) <= allowed[section]
|
|
for section in config.sections()
|
|
),
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
require(config.get("core", "bare") == "true", "DR_RECOVERY_GIT_ROOT_UNSAFE")
|
|
if source.object_format == "sha1":
|
|
require(
|
|
config.get("core", "repositoryformatversion") == "0"
|
|
and not config.has_option("extensions", "objectformat"),
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
else:
|
|
require(
|
|
config.get("extensions", "objectformat") == "sha256"
|
|
and config.get("core", "repositoryformatversion") == "1",
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
except (configparser.Error, UnicodeError):
|
|
raise Refused("DR_RECOVERY_GIT_ROOT_UNSAFE") from None
|
|
# Git itself never opens the untrusted root or its config. Copy only pinned regular ODB/ref
|
|
# files into an owned bounded snapshot; a directory/pack swap cannot redirect the byte read.
|
|
with tempfile.TemporaryDirectory(prefix="ss-dr-stand-check-") as directory:
|
|
snapshot = Path(directory) / "stand.git"
|
|
git(
|
|
snapshot,
|
|
[
|
|
"init",
|
|
"--bare",
|
|
"--object-format=" + source.object_format,
|
|
str(snapshot),
|
|
],
|
|
"DR_RECOVERY_GIT_UNAVAILABLE",
|
|
)
|
|
total = count = 0
|
|
anchor = Path("/proc/self/fd") / str(descriptor)
|
|
for current, directories, files in os.walk(anchor, followlinks=False):
|
|
relative = Path(current).relative_to(anchor)
|
|
if relative == Path("."):
|
|
directories[:] = [
|
|
name for name in directories if name in ("objects", "refs")
|
|
]
|
|
require(
|
|
"shallow" not in files and "commondir" not in files,
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
files = [name for name in files if name in ("HEAD", "packed-refs")]
|
|
count += len(directories) + len(files)
|
|
require(count <= 200000, "DR_RECOVERY_LIMIT")
|
|
require(
|
|
not any(
|
|
(Path(current) / name).is_symlink()
|
|
for name in (*directories, *files)
|
|
),
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
for name in files:
|
|
entry = (relative / name).as_posix()
|
|
require(
|
|
entry != "objects/info/alternates"
|
|
and not name.endswith(".promisor"),
|
|
"DR_RECOVERY_GIT_ROOT_UNSAFE",
|
|
)
|
|
data = read(descriptor, entry, BUNDLE_LIMIT)
|
|
total += len(data)
|
|
require(total <= 128 * 1024 * 1024, "DR_RECOVERY_LIMIT")
|
|
destination = snapshot / entry
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_bytes(data)
|
|
roots = required_roots(snapshot, source, operations)
|
|
git(
|
|
snapshot,
|
|
["fsck", "--full", "--strict", "--no-reflogs", *roots],
|
|
"DR_RECOVERY_GIT_OBJECT_UNAVAILABLE",
|
|
)
|
|
inspect(snapshot, source, operations, render_bytes)
|
|
|
|
|
|
# #endregion Tooling.DrRecovery.Git.StandSnapshot
|
|
# #endregion Tooling.DrRecovery.Git
|