From 6cdfb98275b65a63a0acf5831c33fa2d4ba35404 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:01:00 +0000 Subject: [PATCH 1/2] Retain bounded command failures in published activity evidence --- scripts/ci/published-activity-acceptance.md | 3 + scripts/ci/published-activity-acceptance.py | 33 ++++++++++- tests/test_published_activity_acceptance.py | 65 +++++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/scripts/ci/published-activity-acceptance.md b/scripts/ci/published-activity-acceptance.md index 21f6d4c..a719242 100644 --- a/scripts/ci/published-activity-acceptance.md +++ b/scripts/ci/published-activity-acceptance.md @@ -41,6 +41,9 @@ six-minute execution bound, followed by independent cleanup of each captured ID and then the network. Label-bound reconciliation covers uncertain create responses. A foreign replacement is preserved. A reserved result descriptor retains a terminal failure even if the output path refuses further use. +Failed subprocesses retain up to 64 KiB of stdout and stderr, the full stream +lengths and hashes, and the exit code or timeout. Losing an optional diagnostic +file cannot disable cleanup or turn the terminal failure into a pass. The seven-day artifact contains the selected tuple, complete wheel hashes, dependency versions, SDK inventory, before/after container and network facts, diff --git a/scripts/ci/published-activity-acceptance.py b/scripts/ci/published-activity-acceptance.py index 7e7bb2b..57040f5 100644 --- a/scripts/ci/published-activity-acceptance.py +++ b/scripts/ci/published-activity-acceptance.py @@ -35,6 +35,7 @@ OWNER_VALUE = "=sdk-python" LABEL_FILTER = "label=" ID_FORMAT = "{{.ID}}" +DIAGNOSTIC_LIMIT = 64 * 1024 # No inherited context, registry credentials, proxy, SSH or remote Docker endpoint. # This kernel directory is private to each CLI process and contains no config.json. DOCKER_ENVIRONMENT = { @@ -54,6 +55,35 @@ def cleanup_call(*arguments, check=True): return subprocess.run(arguments, check=check, capture_output=True, text=True, timeout=10, env=DOCKER_ENVIRONMENT) +def record_failure(files, report, error): + report["failure"] = type(error).__name__ + ": " + str(error)[:1000] + report["complete"] = False + if not isinstance(error, subprocess.CalledProcessError | subprocess.TimeoutExpired): + return + arguments = list(error.cmd) if isinstance(error.cmd, tuple | list) else [error.cmd] + report["command_failure"] = { + "program": arguments[0], + "operation": arguments[1:4], + "argv_sha256": hashlib.sha256(json.dumps(arguments).encode()).hexdigest(), + "exit_code": getattr(error, "returncode", None), + "timeout_seconds": getattr(error, "timeout", None), + "streams": {}, + } + for name, value in (("stdout", error.stdout), ("stderr", error.stderr)): + data = value.encode() if isinstance(value, str) else value or b"" + retained = data[:DIAGNOSTIC_LIMIT] + report["command_failure"]["streams"][name] = { + "bytes": len(data), + "retained_bytes": len(retained), + "truncated": len(data) > len(retained), + "sha256": hashlib.sha256(data).hexdigest(), + } + try: + files.write("command-" + name + ".txt", retained) + except (OSError, ValueError) as retention_error: + report.setdefault("diagnostic_retention_failures", {})[name] = type(retention_error).__name__ + + def deadline(_signum, _frame): raise TimeoutError("published cell reached its six-minute execution bound") @@ -463,8 +493,7 @@ def main(): if engine() != report["engine"]: raise ValueError("Docker engine authority changed") except Exception as error: - report["failure"] = type(error).__name__ + ": " + str(error)[:1000] - report["complete"] = False + record_failure(files, report, error) finally: signal.alarm(0) owned.cleanup() diff --git a/tests/test_published_activity_acceptance.py b/tests/test_published_activity_acceptance.py index 8407b9f..be3f816 100644 --- a/tests/test_published_activity_acceptance.py +++ b/tests/test_published_activity_acceptance.py @@ -541,3 +541,68 @@ def test_platform_image_binding_refuses_an_ambiguous_or_foreign_local_object(mut del platform["Descriptor"] with pytest.raises(ValueError): runtime.bind_local_image(index, platform, index_digest, child_digest, config_digest) + + +def test_failed_command_retains_diagnostics_and_cleanup_in_the_terminal_failure(monkeypatch, tmp_path): + output = tmp_path / "result" + monkeypatch.setattr(runner, "OUTPUT", output) + error = runner.subprocess.CalledProcessError( + 1, ["docker", "buildx", "imagetools", "inspect"], output="captured stdout\n", stderr="metadata reader failed\n" + ) + + def fail_source(_report): + raise error + + cleaned = [] + monkeypatch.setattr(runner, "capture_source", fail_source) + monkeypatch.setattr(runner.OwnedDocker, "cleanup", lambda self: cleaned.append(self.nonce)) + with pytest.raises(RuntimeError, match="acceptance or cleanup is incomplete"): + runner.main() + result = json.loads((output / "result.json").read_text()) + assert result["complete"] is False + assert result["command_failure"]["exit_code"] == 1 + assert (output / "command-stdout.txt").read_text() == "captured stdout\n" + assert (output / "command-stderr.txt").read_text() == "metadata reader failed\n" + assert len(cleaned) == 1 + + +def test_failed_command_streams_are_bounded_and_keep_full_lengths_and_hashes(tmp_path): + files = runner.output_files.Output(tmp_path / "result") + data = b"x" * (runner.DIAGNOSTIC_LIMIT + 123) + error = runner.subprocess.TimeoutExpired(["docker", "buildx"], 5, output=data, stderr=b"timeout detail") + report = {"complete": True} + try: + runner.record_failure(files, report, error) + stream = report["command_failure"]["streams"]["stdout"] + assert stream["bytes"] == len(data) + assert stream["retained_bytes"] == runner.DIAGNOSTIC_LIMIT + assert stream["truncated"] is True + assert stream["sha256"] == runner.hashlib.sha256(data).hexdigest() + assert (files.path / "command-stdout.txt").stat().st_size == runner.DIAGNOSTIC_LIMIT + assert report["command_failure"]["timeout_seconds"] == 5 + assert report["complete"] is False + finally: + files.close() + + +def test_diagnostic_path_refusal_does_not_prevent_cleanup_or_the_held_failure_receipt(monkeypatch, tmp_path): + output = tmp_path / "result" + original = tmp_path / "original" + monkeypatch.setattr(runner, "OUTPUT", output) + + def fail_source(_report): + output.rename(original) + output.mkdir() + raise runner.subprocess.CalledProcessError(1, ["docker", "buildx"], stderr="retained failure") + + cleaned = [] + monkeypatch.setattr(runner, "capture_source", fail_source) + monkeypatch.setattr(runner.OwnedDocker, "cleanup", lambda self: cleaned.append(self.nonce)) + with pytest.raises(RuntimeError, match="acceptance or cleanup is incomplete"): + runner.main() + result = json.loads((original / "result.json").read_text()) + assert result["complete"] is False + assert result["output_failure"] == "identity_changed" + assert result["diagnostic_retention_failures"] == {"stdout": "ValueError", "stderr": "ValueError"} + assert list(output.iterdir()) == [] + assert len(cleaned) == 1 From d4be6e12f065bdf30d3a1330b2ad7bd9f201a88d Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:13:27 +0000 Subject: [PATCH 2/2] Give read-only Buildx lookups separate private tool state --- scripts/ci/activity_acceptance_buildx.py | 50 +++++++++++++++ scripts/ci/published-activity-acceptance.md | 5 ++ scripts/ci/published-activity-acceptance.py | 8 +++ tests/test_published_activity_acceptance.py | 68 +++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 scripts/ci/activity_acceptance_buildx.py diff --git a/scripts/ci/activity_acceptance_buildx.py b/scripts/ci/activity_acceptance_buildx.py new file mode 100644 index 0000000..af03bac --- /dev/null +++ b/scripts/ci/activity_acceptance_buildx.py @@ -0,0 +1,50 @@ +"""Fresh private Buildx tool state; Docker credential authority stays separate.""" + +from __future__ import annotations + +import os +import shutil +import stat +import tempfile +from pathlib import Path + + +class BuildxState: + def __init__(self, environment, parent=None): + self.path = Path(tempfile.mkdtemp(prefix="published-activity-buildx-", dir=parent)) + self.directory = os.open(self.path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + self.identity = self.shape(os.fstat(self.directory)) + self.environment = {**environment, "BUILDX_CONFIG": str(self.path)} + + @staticmethod + def shape(info): + return info.st_dev, info.st_ino, info.st_uid, info.st_gid, info.st_mode + + def check(self): + current = self.path.lstat() + held = os.fstat(self.directory) + if ( + self.shape(current) != self.identity + or self.shape(held) != self.identity + or not stat.S_ISDIR(current.st_mode) + or current.st_uid != os.getuid() + or stat.S_IMODE(current.st_mode) != 0o700 + ): + raise ValueError("private Buildx state directory changed") + + def __enter__(self): + self.check() + return self.environment + + def __exit__(self, _error_type, error, _traceback): + try: + self.check() + shutil.rmtree(self.path) + except Exception as state_error: + if error is None: + raise + # Preserve the command's captured streams when both operations fail. + error.buildx_state_failure = type(state_error).__name__ + finally: + os.close(self.directory) + return False diff --git a/scripts/ci/published-activity-acceptance.md b/scripts/ci/published-activity-acceptance.md index a719242..fb275ac 100644 --- a/scripts/ci/published-activity-acceptance.md +++ b/scripts/ci/published-activity-acceptance.md @@ -44,6 +44,11 @@ retains a terminal failure even if the output path refuses further use. Failed subprocesses retain up to 64 KiB of stdout and stderr, the full stream lengths and hashes, and the exit code or timeout. Losing an optional diagnostic file cannot disable cleanup or turn the terminal failure into a pass. +Buildx receives fresh owner-only tool state for each manifest lookup, checked +against a held directory identity and removed afterward. Its writable state is +separate from Docker's empty credential configuration and never enters a +container or artifact. Other Docker operations and cleanup keep their original +credential and engine authority. The seven-day artifact contains the selected tuple, complete wheel hashes, dependency versions, SDK inventory, before/after container and network facts, diff --git a/scripts/ci/published-activity-acceptance.py b/scripts/ci/published-activity-acceptance.py index 57040f5..0931ac5 100644 --- a/scripts/ci/published-activity-acceptance.py +++ b/scripts/ci/published-activity-acceptance.py @@ -19,6 +19,7 @@ try: from scripts.ci import activity_acceptance_output as output_files from scripts.ci import activity_acceptance_runtime as runtime + from scripts.ci.activity_acceptance_buildx import BuildxState from scripts.ci.activity_acceptance_session import Session except ModuleNotFoundError as error: if error.name != "scripts": @@ -26,6 +27,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from scripts.ci import activity_acceptance_output as output_files from scripts.ci import activity_acceptance_runtime as runtime + from scripts.ci.activity_acceptance_buildx import BuildxState from scripts.ci.activity_acceptance_session import Session ROOT = Path(__file__).resolve().parents[2] @@ -48,6 +50,10 @@ def call(*arguments, check=True, **kwargs): if arguments[0] == "docker": kwargs["env"] = DOCKER_ENVIRONMENT + if arguments[:2] == ("docker", "buildx"): + with BuildxState(DOCKER_ENVIRONMENT) as environment: + kwargs["env"] = environment + return subprocess.run(arguments, check=check, capture_output=True, text=True, timeout=180, **kwargs) return subprocess.run(arguments, check=check, capture_output=True, text=True, timeout=180, **kwargs) @@ -69,6 +75,8 @@ def record_failure(files, report, error): "timeout_seconds": getattr(error, "timeout", None), "streams": {}, } + if hasattr(error, "buildx_state_failure"): + report["command_failure"]["buildx_state_failure"] = error.buildx_state_failure for name, value in (("stdout", error.stdout), ("stderr", error.stderr)): data = value.encode() if isinstance(value, str) else value or b"" retained = data[:DIAGNOSTIC_LIMIT] diff --git a/tests/test_published_activity_acceptance.py b/tests/test_published_activity_acceptance.py index be3f816..31078ab 100644 --- a/tests/test_published_activity_acceptance.py +++ b/tests/test_published_activity_acceptance.py @@ -606,3 +606,71 @@ def fail_source(_report): assert result["diagnostic_retention_failures"] == {"stdout": "ValueError", "stderr": "ValueError"} assert list(output.iterdir()) == [] assert len(cleaned) == 1 + + +def test_buildx_has_private_tool_state_without_changing_docker_credential_authority(monkeypatch, tmp_path): + original = runner.BuildxState + monkeypatch.setattr(runner, "BuildxState", lambda environment: original(environment, parent=tmp_path)) + monkeypatch.setenv("DOCKER_CONFIG", "/foreign/credentials") + observed = [] + + def run(arguments, **kwargs): + environment = kwargs["env"] + state = Path(environment["BUILDX_CONFIG"]) + assert environment["DOCKER_CONFIG"] == "/proc/self/fd" + assert environment["DOCKER_HOST"] == "unix:///var/run/docker.sock" + assert state.is_dir() + assert state.stat().st_mode & 0o777 == 0o700 + assert list(state.iterdir()) == [] + (state / "current").write_text("synthetic tool state") + observed.append(state) + return SimpleNamespace(stdout="manifest", returncode=0) + + monkeypatch.setattr(runner.subprocess, "run", run) + result = runner.call("docker", "buildx", "imagetools", "inspect", "--raw", "public-image") + assert result.stdout == "manifest" + assert len(observed) == 1 + assert not observed[0].exists() + + +def test_regular_docker_and_cleanup_calls_do_not_inherit_buildx_state(monkeypatch): + observed = [] + monkeypatch.setattr(runner, "BuildxState", lambda *_args: pytest.fail("unexpected Buildx state")) + monkeypatch.setattr(runner.subprocess, "run", lambda _arguments, **kwargs: observed.append(kwargs["env"])) + runner.call("docker", "image", "inspect", "selected-image") + runner.cleanup_call("docker", "ps", "-a") + assert observed == [runner.DOCKER_ENVIRONMENT, runner.DOCKER_ENVIRONMENT] + assert "BUILDX_CONFIG" not in runner.DOCKER_ENVIRONMENT + + +def test_buildx_state_path_replacement_preserves_the_foreign_directory(tmp_path): + state = runner.BuildxState(runner.DOCKER_ENVIRONMENT, parent=tmp_path) + original = tmp_path / "original" + state.__enter__() + state.path.rename(original) + state.path.mkdir() + (state.path / "foreign").write_text("preserved") + with pytest.raises(ValueError, match="state directory changed"): + state.__exit__(None, None, None) + assert (state.path / "foreign").read_text() == "preserved" + assert original.is_dir() + + +def test_buildx_state_refusal_preserves_the_command_error_and_its_stderr(tmp_path): + state = runner.BuildxState(runner.DOCKER_ENVIRONMENT, parent=tmp_path) + original = tmp_path / "original" + error = runner.subprocess.CalledProcessError(1, ["docker", "buildx"], stderr="original metadata failure") + + def fail_with_replaced_state(): + with state: + state.path.rename(original) + state.path.mkdir() + (state.path / "foreign").write_text("preserved") + raise error + + with pytest.raises(runner.subprocess.CalledProcessError) as captured: + fail_with_replaced_state() + assert captured.value is error + assert captured.value.stderr == "original metadata failure" + assert captured.value.buildx_state_failure == "ValueError" + assert (state.path / "foreign").read_text() == "preserved"