From 5203a5cb2982d3e3709cdf64373ef83f769b78b9 Mon Sep 17 00:00:00 2001 From: AHMED YOUNIS Date: Fri, 9 Oct 2026 00:39:55 +0300 Subject: [PATCH 1/5] fix(privacy): suppress trial subprocess output at public CI boundary --- scripts/p8_a3_external_trial_capture.py | 36 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/scripts/p8_a3_external_trial_capture.py b/scripts/p8_a3_external_trial_capture.py index cd9cf08e..10cce99c 100644 --- a/scripts/p8_a3_external_trial_capture.py +++ b/scripts/p8_a3_external_trial_capture.py @@ -41,6 +41,22 @@ def run_text(command: list[str], cwd: Path | None = None) -> tuple[int, str]: return proc.returncode, proc.stdout.strip() +def run_privacy_bounded(command: list[str], cwd: Path) -> subprocess.CompletedProcess[bytes]: + """Run an untrusted trial stage without copying child output into public CI logs. + + Never retain stdout/stderr or command text in the trial's shared artifacts. + Exit codes remain available for stage-level, non-sensitive diagnostics. + DEVNULL avoids unbounded PIPE memory and unbounded artifact creation. + """ + return subprocess.run( + command, + cwd=cwd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + + def git_head(path: Path) -> str: rc, out = run_text(["git", "rev-parse", "HEAD"], cwd=path) if rc != 0 or len(out) != 40: @@ -235,15 +251,17 @@ def main() -> None: summary["stages"]["preflight"] = "pass" write_json(summary_path, summary) - build = subprocess.run( + build = run_privacy_bounded( ["cargo", "build", "--locked", "-p", "execsurface"], cwd=target_source, - check=False, ) summary["build_exit_code"] = build.returncode if build.returncode != 0: summary["stages"]["build"] = "fail" - raise TrialError("build-failed", "frozen target source did not build") + raise TrialError( + "build-failed", + f"frozen target source did not build (exit code {build.returncode}); inspect privately", + ) if not tracked_clean(target_source): raise TrialError( "target-worktree-mutated-by-build", @@ -256,15 +274,18 @@ def main() -> None: if not binary.is_file(): raise TrialError("binary-missing-after-build", "execsurface binary not found") - doctor = subprocess.run([str(binary), "doctor"], cwd=workdir, check=False) + doctor = run_privacy_bounded([str(binary), "doctor"], cwd=workdir) summary["doctor_exit_code"] = doctor.returncode summary["stages"]["doctor"] = "pass" if doctor.returncode == 0 else "fail" write_json(summary_path, summary) if doctor.returncode != 0: - raise TrialError("doctor-failed", "execsurface doctor did not pass") + raise TrialError( + "doctor-failed", + f"execsurface doctor did not pass (exit code {doctor.returncode}); inspect privately", + ) evidence = output_dir / EVIDENCE_NAME - observed = subprocess.run( + observed = run_privacy_bounded( [ str(binary), "observe", @@ -274,7 +295,6 @@ def main() -> None: *command, ], cwd=workdir, - check=False, ) summary["observe_exit_code"] = observed.returncode summary["stages"]["observe"] = "pass" if observed.returncode == 0 else "fail" @@ -284,7 +304,7 @@ def main() -> None: if observed.returncode != 0: raise TrialError( "observe-failed", - "experimental observe/evidence generation returned non-zero; preserve this initial result", + f"experimental observe/evidence generation returned exit code {observed.returncode}; preserve this initial result", ) if not evidence.is_file(): raise TrialError("evidence-missing", "observe returned success without evidence file") From 0a5d0bc5c72c75ceca746538af2532e353bc4f15 Mon Sep 17 00:00:00 2001 From: AHMED YOUNIS Date: Fri, 9 Oct 2026 00:40:16 +0300 Subject: [PATCH 2/5] test(privacy): adversarial stdout/stderr, exit, signal and volume controls --- scripts/tests/test_p8_a3_trial_log_privacy.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts/tests/test_p8_a3_trial_log_privacy.py diff --git a/scripts/tests/test_p8_a3_trial_log_privacy.py b/scripts/tests/test_p8_a3_trial_log_privacy.py new file mode 100644 index 00000000..65650460 --- /dev/null +++ b/scripts/tests/test_p8_a3_trial_log_privacy.py @@ -0,0 +1,74 @@ +"""Adversarial, stdlib-only privacy tests for the P8-A3.5 external trial harness. + +Synthetic content only: do not insert real credentials or private paths. +""" +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parents[1] +WRAPPER = """ +import sys +sys.path.insert(0, sys.argv[1]) +from p8_a3_external_trial_capture import run_privacy_bounded +result = run_privacy_bounded([sys.executable, "-c", sys.argv[2]], cwd=__import__("pathlib").Path(sys.argv[3])) +raise SystemExit(result.returncode) +""" + + +class PrivacyBoundaryTests(unittest.TestCase): + def run_wrapped(self, child_code: str) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as workdir: + return subprocess.run( + [sys.executable, "-c", WRAPPER, str(SCRIPT_DIR), child_code, workdir], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def assert_no_spill(self, result: subprocess.CompletedProcess[str]) -> None: + # Never put the child output in assertion messages (CI may be public). + self.assertFalse(result.stdout, "child stdout escaped public trial harness") + self.assertFalse(result.stderr, "child stderr escaped public trial harness") + + def test_success_suppresses_both_streams(self) -> None: + result = self.run_wrapped( + 'import sys; print("SYNTHETIC_STDOUT_SECRET"); ' + 'sys.stderr.write("SYNTHETIC_STDERR_SECRET\\n")' + ) + self.assertEqual(result.returncode, 0) + self.assert_no_spill(result) + + def test_failure_keeps_exact_nonzero_status_without_streams(self) -> None: + result = self.run_wrapped( + 'import sys; print("SYNTHETIC_STDOUT_SECRET"); ' + 'sys.stderr.write("SYNTHETIC_STDERR_SECRET\\n"); ' + 'sys.exit(19)' + ) + self.assertEqual(result.returncode, 19) + self.assert_no_spill(result) + + def test_large_child_output_does_not_block_or_escape(self) -> None: + result = self.run_wrapped( + 'import os; os.write(1, b"S" * 2000000); ' + 'os.write(2, b"E" * 2000000)' + ) + self.assertEqual(result.returncode, 0) + self.assert_no_spill(result) + + def test_signal_termination_is_not_normalized(self) -> None: + if sys.platform == "win32": + self.skipTest("POSIX signal return semantics") + result = self.run_wrapped('import os, signal; os.kill(os.getpid(), signal.SIGTERM)') + self.assertEqual(result.returncode, -15, "POSIX signal exit must remain negative") + self.assert_no_spill(result) + + +if __name__ == "__main__": + unittest.main() From 3cbb4f39718d266a5d57054688a391e9763853d3 Mon Sep 17 00:00:00 2001 From: AHMED YOUNIS Date: Fri, 9 Oct 2026 00:40:33 +0300 Subject: [PATCH 3/5] test(privacy): prove exact signal status without shell exit-code translation --- scripts/tests/test_p8_a3_trial_log_privacy.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/tests/test_p8_a3_trial_log_privacy.py b/scripts/tests/test_p8_a3_trial_log_privacy.py index 65650460..5091aa55 100644 --- a/scripts/tests/test_p8_a3_trial_log_privacy.py +++ b/scripts/tests/test_p8_a3_trial_log_privacy.py @@ -17,15 +17,18 @@ sys.path.insert(0, sys.argv[1]) from p8_a3_external_trial_capture import run_privacy_bounded result = run_privacy_bounded([sys.executable, "-c", sys.argv[2]], cwd=__import__("pathlib").Path(sys.argv[3])) +if len(sys.argv) > 4 and sys.argv[4] == "safe-status": + print(f"CHILD_STATUS={result.returncode}") + raise SystemExit(0) raise SystemExit(result.returncode) """ class PrivacyBoundaryTests(unittest.TestCase): - def run_wrapped(self, child_code: str) -> subprocess.CompletedProcess[str]: + def run_wrapped(self, child_code: str, safe_status: bool = False) -> subprocess.CompletedProcess[str]: with tempfile.TemporaryDirectory() as workdir: return subprocess.run( - [sys.executable, "-c", WRAPPER, str(SCRIPT_DIR), child_code, workdir], + [sys.executable, "-c", WRAPPER, str(SCRIPT_DIR), child_code, workdir, "safe-status" if safe_status else "exit"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -65,9 +68,10 @@ def test_large_child_output_does_not_block_or_escape(self) -> None: def test_signal_termination_is_not_normalized(self) -> None: if sys.platform == "win32": self.skipTest("POSIX signal return semantics") - result = self.run_wrapped('import os, signal; os.kill(os.getpid(), signal.SIGTERM)') - self.assertEqual(result.returncode, -15, "POSIX signal exit must remain negative") - self.assert_no_spill(result) + result = self.run_wrapped('import os, signal; os.kill(os.getpid(), signal.SIGTERM)', safe_status=True) + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout.strip(), "CHILD_STATUS=-15", "POSIX signal exit must stay negative") + self.assertFalse(result.stderr, "signal-path error output escaped") if __name__ == "__main__": From 000193bcc6046faba96127b207322ef91273cf4d Mon Sep 17 00:00:00 2001 From: AHMED YOUNIS Date: Fri, 9 Oct 2026 00:40:49 +0300 Subject: [PATCH 4/5] ci(privacy): run secret-stream and exact-exit red-team tests in packet qualification --- .github/workflows/p8-a3-5-external-trial-pack.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/p8-a3-5-external-trial-pack.yml b/.github/workflows/p8-a3-5-external-trial-pack.yml index 49063e5c..600cd594 100644 --- a/.github/workflows/p8-a3-5-external-trial-pack.yml +++ b/.github/workflows/p8-a3-5-external-trial-pack.yml @@ -10,6 +10,7 @@ on: - "docs/external/P8_A3_TYPED_EVIDENCE_TRIAL_MANIFEST.json" - "docs/external/P8_A3_TYPED_EVIDENCE_TRIAL_REPORT_TEMPLATE.md" - "scripts/p8_a3_external_trial_capture.py" + - "scripts/tests/test_p8_a3_trial_log_privacy.py" - "scripts/p8_a3_typed_evidence_reference_consumer.py" - "scripts/p8_a3_typed_evidence_mutation_redteam.py" @@ -38,6 +39,7 @@ jobs: run: | set -euo pipefail python3 -m py_compile scripts/p8_a3_external_trial_capture.py scripts/p8_a3_typed_evidence_reference_consumer.py scripts/p8_a3_typed_evidence_mutation_redteam.py + python3 -m unittest discover -s scripts/tests -p 'test_p8_a3_trial_log_privacy.py' -v python3 - <<'PY' import json from pathlib import Path From 9ed749776a21870eeb17e3feccbe19bea171f7b4 Mon Sep 17 00:00:00 2001 From: AHMED YOUNIS Date: Fri, 9 Oct 2026 00:41:04 +0300 Subject: [PATCH 5/5] docs(security): warn historic trial users about public CI log privacy gap --- docs/external/P8_A3_TYPED_EVIDENCE_EXTERNAL_TRIAL.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/external/P8_A3_TYPED_EVIDENCE_EXTERNAL_TRIAL.md b/docs/external/P8_A3_TYPED_EVIDENCE_EXTERNAL_TRIAL.md index aa553f14..e979bb20 100644 --- a/docs/external/P8_A3_TYPED_EVIDENCE_EXTERNAL_TRIAL.md +++ b/docs/external/P8_A3_TYPED_EVIDENCE_EXTERNAL_TRIAL.md @@ -29,6 +29,14 @@ For the cleanest independence record: A failed build, unsupported environment, ERROR, incomplete result, verifier rejection or no-fit result is useful evidence. +## Security notice — historical trial packet revisions + +The original packet revision referenced in Issue #157 has a confirmed **public CI log privacy gap**: child-process stdout/stderr from the experimental `observe` stage may escape into shared runner logs before evaluator review. This is distinct from the reported technical trial outcome; the historical result and its initial qualification remain unchanged. + +**Do not run that original revision with sensitive workloads in public CI.** Upgrade to a separately qualified harness revision with quiet build/doctor/observe stages before using a shared CI runner. Run any older pinned packet only in a private environment with separately reviewed log handling. Re-running is not a substitute for the original independent result, and should be declared a follow-up. + +The repaired harness deliberately sends child stdout/stderr to a discard sink rather than retaining them in logs or artifacts; bounded diagnostics retain stage and exit status. For detailed compiler or observer diagnostics, reproduce privately in a controlled environment. The raw typed-evidence artifact can still contain paths and must be inspected prior to external sharing. See [privacy defect and independent reporter](https://github.com/AETHERXGLOBAL/execsurface/issues/164) for the original report, verification and test gate. + ## Privacy boundary The capture harness deliberately does **not** save: