Verify prepared runtime IDs at launch (Refs #93)

This commit is contained in:
2026-10-09 19:18:54 -04:00
parent 596b160be3
commit d1f581d6a8
5 changed files with 135 additions and 17 deletions
+20 -7
View File
@@ -35,7 +35,7 @@ import urllib.parse
import urllib.request
import uuid
VERSION = "1.8.6"
VERSION = "1.8.7"
LABEL = "io.electric-cherry"
MAX_FILE = 32 * 1024 * 1024
MANIFEST_MAX_ENTRIES = 10000
@@ -1643,7 +1643,20 @@ class Agent:
raise AgentError("Invalid runtime environment")
# Runtime download is a separate preparation operation. A deployment
# uses the already prepared image even during a registry outage.
self.docker.call("image", "inspect", image)
image_id = self.docker.call("image", "inspect", "--format", "{{.Id}}", image).strip()
if not re.fullmatch(r"sha256:[a-f0-9]{64}", image_id):
raise AgentError("Docker did not report a valid runtime image ID")
try:
raw = managed_read(self.root / "agent", "runtime-images.json")
prepared = json.loads(raw) if raw is not None else {}
except (OSError, ValueError):
raise AgentError("Runtime preparation records are unavailable") from None
if not isinstance(prepared, dict):
raise AgentError("Runtime preparation records are invalid")
# The controller also authorizes its configured default without a pull
# record. Whenever preparation was recorded, enforce that exact image.
if image in prepared and prepared[image] != image_id:
raise AgentError("Prepared runtime image has changed. An admin must prepare it again before launching.")
files = payload.get("files", [])
if not isinstance(files, list) or len(files) > 10000:
raise Rejected("Invalid initial file list")
@@ -1656,7 +1669,7 @@ class Agent:
total += len(self.decode_file(item))
if total > 64 * 1024 * 1024:
raise Rejected("Initial files exceed the 64 MiB operation limit")
return game, library, image, port, bind, env
return game, library, image, image_id, port, bind, env
@staticmethod
def plutonium_manifest(data):
@@ -1890,7 +1903,7 @@ class Agent:
raise AgentError("Instance already exists with a different specification")
return existing
self.operation_phase = "validating-create-spec"
game, library, image, port, bind, env = self.validate_spec(payload)
game, library, image, image_id, port, bind, env = self.validate_spec(payload)
app = directory / "app"
self.operation_phase = "preparing-create-directory"
app.mkdir(parents=True, exist_ok=True)
@@ -1905,15 +1918,15 @@ class Agent:
atomic_bytes(env_path, "".join(f"{key}={value}\n" for key, value in env.items()).encode())
name = self.container_name(instance_id, generation)
spec_sha = digest(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
# Do not let Docker fetch a replacement if the prepared image disappears
# between validation and container creation.
# Use the validated immutable ID so retagging after validation cannot
# substitute another image. A deleted image must not trigger a pull.
args = ["container", "create", "--pull=never", "--name", name, "--restart", "unless-stopped", "--label", LABEL + ".managed=true", "--label", LABEL + ".instance=" + instance_id, "--label", LABEL + ".generation=" + str(generation), "--label", LABEL + ".operation=" + (self.executing_id or "direct"), "--label", LABEL + ".spec-sha256=" + spec_sha, "--mount", f"type=bind,src={app},dst=/home/plutainer/app", "--mount", f"type=bind,src={library / 'content'},dst=/home/plutainer/gamefiles,readonly", "--env-file", str(env_path)]
if self.config.get("hostId"):
args.extend(["--label", LABEL + ".host=" + identifier(self.config["hostId"])])
bind_arg = "[" + bind + "]" if ":" in bind else bind
for protocol in ("udp", "tcp"):
args.extend(["--publish", f"{bind_arg}:{port}:{port}/{protocol}"])
args.append(image)
args.append(image_id)
self.check_creation_cancellation()
self.operation_phase = "creating-container"
self.checkpoint({"phase": "creating", "generation": generation, "specSha256": spec_sha})
+2 -2
View File
@@ -1,8 +1,8 @@
{
"version": "1.8.6",
"version": "1.8.7",
"protocolVersion": 1,
"platform": "debian-amd64",
"sha256": "79702dc8cf80d2521153bfda9241f7240285014fe746fc921f0a7793dd81212b",
"sha256": "e773c907141c97eab2055b354306973b248a67109a51337f1f1d76d80aaa8701",
"updaterProtocolVersion": 1,
"updaterSha256": "066bb896726600ece9ce72697ce523c9a707cc9a621505931ba0892dec59d3c8"
}