Files

144 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Verify an immutable bundle or launch its fixed-duration Linux diagnostic."""
import argparse
import json
from pathlib import Path
import re
import stat
import subprocess
import sys
import time
import uuid
import xml.etree.ElementTree as ET
sys.dont_write_bytecode = True
from analyze import CONTAINER_SECONDS, DOCKER_LABEL, PROTOCOL, container_command, evidence_hashes, inspect_run, require
from bundle import context_identity, image_identity, verify_bundle
def remove_container(context, run_id):
docker = ["docker", "--host", context["endpoint"]]
def containers():
result = subprocess.check_output([*docker, "ps", "--all", "--quiet", "--no-trunc",
"--filter", f"label={DOCKER_LABEL}={run_id}"], text=True, timeout=10)
ids = result.split()
require(all(re.fullmatch(r"[0-9a-f]{64}", value) for value in ids), "Unexpected container ID")
return ids
ids = containers()
if ids:
subprocess.run([*docker, "rm", "--force", *ids], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
require(not containers(), "The task-owned container is still present")
def write(path, value):
with path.open("x", encoding="utf-8") as stream:
json.dump(value, stream, indent=2, allow_nan=False)
stream.write("\n")
def verify_local_image(bundle):
context = bundle["dockerContext"]
require(context_identity(context["name"]) == context, "Reviewed Docker context endpoint changed")
require(image_identity(bundle["image"], context) == bundle["imageIdentity"], "Wrong cached image identity")
def create_task_output(output, bundle_root):
require(not output.is_symlink(), "Run output cannot be a symbolic link")
output = output.resolve()
require(not output.is_relative_to(bundle_root), "Run output must be outside the immutable bundle")
output.mkdir(mode=0o700, parents=True, exist_ok=False)
temporary = output / "tmp"
temporary.mkdir(mode=0o700, exist_ok=False)
for directory in (output, temporary):
mode = directory.lstat().st_mode
require(stat.S_ISDIR(mode) and stat.S_IMODE(mode) == 0o700, f"Expected private directory mode 0700 for {directory}")
return output
def launch(root, bundle, bundle_hash, output):
require(Path(__file__).resolve() == root / "tools/launch.py", "Invoke the reviewed bundled launcher")
output = create_task_output(output, root)
run_id = uuid.uuid4().hex
invocation = container_command(root, output, bundle, run_id)
provenance = {"kind": "launch", "protocol": PROTOCOL, "acceptance": False, "runId": run_id,
"bundleReceiptSha256": bundle_hash, "sourceRevision": bundle["sourceRevision"], "image": bundle["image"],
"coreClrSha256": bundle["files"]["app/libcoreclr.so"], "command": invocation,
"dockerContext": bundle["dockerContext"], "bundlePath": str(root), "outputPath": str(output),
"containerTimeoutSeconds": CONTAINER_SECONDS}
write(output / "launch.json", provenance)
errors = []
removed = False
started = time.monotonic()
try:
with (output / "container.log").open("x", encoding="utf-8") as log:
process = subprocess.run(invocation, stdout=log, stderr=subprocess.STDOUT, timeout=CONTAINER_SECONDS)
status = process.returncode
except subprocess.TimeoutExpired:
status = 124
errors.append("The container reached the fixed outer deadline")
except KeyboardInterrupt:
status = 130
errors.append("Container execution was interrupted")
except OSError as error:
status = 125
errors.append(str(error))
finally:
wall = time.monotonic() - started
try:
remove_container(bundle["dockerContext"], run_id)
removed = True
except (OSError, subprocess.SubprocessError, ValueError) as error:
errors.append(f"Container cleanup failed: {error}")
(output / "container-exit-status.txt").write_text(f"{status}\n", encoding="ascii")
receipt = {**provenance, "kind": "run", "containerExitStatus": status, "wallSeconds": wall,
"containerRemoved": removed, "diagnosticCompleted": False}
try:
verify_bundle(root, bundle_hash)
verify_local_image(bundle)
receipt["evidenceSha256"] = evidence_hashes(output)
inspected = inspect_run(output, provenance, wall)
for field in ("diagnosticOutcome", "testExitStatus", "descriptorTests", "experimentTests"):
receipt[field] = inspected[field]
except (OSError, KeyError, TypeError, ValueError, ET.ParseError, subprocess.SubprocessError) as error:
errors.append(f"Evidence validation failed: {error}")
receipt["diagnosticCompleted"] = not errors
if errors:
receipt["validationErrors"] = errors
# Return the original container status when the test rethrows its first resource cap.
receipt["launcherExitStatus"] = (128 - status if status < 0 else status) or int(bool(errors))
write(output / "run-receipt.json", receipt)
print(json.dumps(receipt, indent=2, allow_nan=False))
return receipt["launcherExitStatus"]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bundle", type=Path, required=True)
parser.add_argument("--receipt-sha256", required=True)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--verify-only", action="store_true")
action.add_argument("--output", type=Path)
options = parser.parse_args()
try:
root = options.bundle.resolve(strict=True)
bundle = verify_bundle(root, options.receipt_sha256)
verify_local_image(bundle)
if options.verify_only:
print(json.dumps({"protocol": PROTOCOL, "sourceRevision": bundle["sourceRevision"],
"receiptSha256": options.receipt_sha256, "verified": True,
"acceptance": False, "workloadLaunched": False}, indent=2))
return 0
return launch(root, bundle, options.receipt_sha256, options.output)
except (OSError, KeyError, TypeError, ValueError, subprocess.SubprocessError) as error:
print(f"Late-slope launcher rejected the request: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())