From b33d0cc0f1ce249963698b19741733b967985af8 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:55:45 +0000 Subject: [PATCH 1/8] Protect the selected hosted Python before binding its private pip runtime --- .../actions/bootstrap-patched-pip/action.yml | 4 + .../protect-hosted-runtime.py | 109 +++++++++++++ .github/workflows/ci.yml | 18 ++ tests/test_ci_trust.py | 5 +- tests/test_hosted_runtime_parent.py | 154 ++++++++++++++++++ 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 .github/actions/bootstrap-patched-pip/protect-hosted-runtime.py create mode 100644 tests/test_hosted_runtime_parent.py diff --git a/.github/actions/bootstrap-patched-pip/action.yml b/.github/actions/bootstrap-patched-pip/action.yml index 3b75fe8..4d19e28 100644 --- a/.github/actions/bootstrap-patched-pip/action.yml +++ b/.github/actions/bootstrap-patched-pip/action.yml @@ -37,6 +37,10 @@ outputs: runs: using: composite steps: + - name: Protect the hosted toolcache parent + if: runner.environment == 'github-hosted' && runner.os == 'Linux' + shell: bash + run: /usr/bin/sudo --non-interactive /usr/bin/python3 -I -S -B "$GITHUB_ACTION_PATH/protect-hosted-runtime.py" "$pythonLocation" - name: Seed or verify the exact job-private pip runtime id: runtime shell: bash diff --git a/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py new file mode 100644 index 0000000..a1b5f85 --- /dev/null +++ b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py @@ -0,0 +1,109 @@ +"""Protect the shared parent of the selected GitHub-hosted Python runtime.""" + +import os +import re +import stat +import sys + + +def protect_directory(path): + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + before = os.fstat(descriptor) + mode = stat.S_IMODE(before.st_mode) + if before.st_uid != 0 or mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Hosted runtime parent must be an ordinary root-owned directory") + if not mode & 0o022 or mode & stat.S_ISVTX: + return + + # Hosted /opt is shared and writable. Sticky ownership prevents other + # users from replacing its existing toolcache entry while preserving + # the runner's ability to create tools below this directory. + expected = mode | stat.S_ISVTX + os.fchmod(descriptor, expected) + after = os.fstat(descriptor) + if ((before.st_dev, before.st_ino, before.st_uid) + != (after.st_dev, after.st_ino, after.st_uid) + or stat.S_IMODE(after.st_mode) != expected): + raise ValueError("Hosted runtime parent protection was not established") + finally: + os.close(descriptor) + + +def seal_tree(descriptor, runner_uid, budget=None): + """Remove other-user writes from only the selected runtime, without following links.""" + budget = [0] if budget is None else budget + metadata = os.fstat(descriptor) + if metadata.st_uid not in {0, runner_uid} or metadata.st_mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Selected runtime has an unsupported owner or special mode") + os.fchmod(descriptor, stat.S_IMODE(metadata.st_mode) & ~0o022) + for name in os.listdir(descriptor): + budget[0] += 1 + if budget[0] > 200000: + raise ValueError("Selected runtime exceeds the metadata walk limit") + entry = os.stat(name, dir_fd=descriptor, follow_symlinks=False) + if entry.st_uid not in {0, runner_uid}: + raise ValueError("Selected runtime entry has an unsupported owner") + if stat.S_ISLNK(entry.st_mode): + continue + directory = stat.S_ISDIR(entry.st_mode) + if not directory and (not stat.S_ISREG(entry.st_mode) or entry.st_nlink != 1): + raise ValueError("Selected runtime entry is not a private regular file or directory") + child = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK + | (os.O_DIRECTORY if directory else 0), dir_fd=descriptor) + try: + held = os.fstat(child) + if ((held.st_dev, held.st_ino, held.st_uid, held.st_mode, held.st_nlink) + != (entry.st_dev, entry.st_ino, entry.st_uid, entry.st_mode, entry.st_nlink)): + raise ValueError("Selected runtime entry changed during protection") + if directory: + seal_tree(child, runner_uid, budget) + else: + if held.st_mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Selected runtime file has a special mode") + expected = stat.S_IMODE(held.st_mode) & ~0o022 + os.fchmod(child, expected) + if stat.S_IMODE(os.fstat(child).st_mode) != expected: + raise ValueError("Selected runtime file protection was not established") + finally: + os.close(child) + after = os.fstat(descriptor) + if ((metadata.st_dev, metadata.st_ino, metadata.st_uid) + != (after.st_dev, after.st_ino, after.st_uid) + or stat.S_IMODE(after.st_mode) != stat.S_IMODE(metadata.st_mode) & ~0o022): + raise ValueError("Selected runtime directory protection was not established") + + +def protect_hosted_python(location, runner_uid): + if re.fullmatch(r"/opt/hostedtoolcache/Python/\d+\.\d+\.\d+/(?:x64|arm64)", location) is None: + raise ValueError("Selected runtime is outside the fixed hosted Python location") + # Hold each ancestor and never follow a directory symlink. Root-owned + # shared parents get sticky ownership; runner-owned parents lose other-user + # writes. Other versions and languages are never traversed. + descriptor = os.open("/", os.O_RDONLY | os.O_DIRECTORY) + try: + for part in location.split("/")[1:]: + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=descriptor) + os.close(descriptor) + descriptor = child + metadata = os.fstat(descriptor) + mode = stat.S_IMODE(metadata.st_mode) + if metadata.st_uid not in {0, runner_uid} or mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Hosted runtime ancestry has an unsupported owner or mode") + expected = ((mode | stat.S_ISVTX) if metadata.st_uid == 0 and mode & 0o022 + else mode & ~0o022) + if mode != expected: + os.fchmod(descriptor, expected) + if stat.S_IMODE(os.fstat(descriptor).st_mode) != expected: + raise ValueError("Hosted runtime ancestor protection was not established") + seal_tree(descriptor, runner_uid) + finally: + os.close(descriptor) + + +if __name__ == "__main__": + if (len(sys.argv) != 2 or os.geteuid() != 0 + or re.fullmatch(r"[1-9][0-9]*", os.environ.get("SUDO_UID", "")) is None): + raise SystemExit("Hosted runtime parent protection requires its fixed privileged invocation") + protect_hosted_python(sys.argv[1], int(os.environ["SUDO_UID"])) + print("Hosted runtime parent protection verified.") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b984645..e6b39eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,24 @@ concurrency: cancel-in-progress: true jobs: + hosted-python-runtime: + name: Hosted Python runtime protection + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - name: Protect and qualify the selected hosted runtime + uses: ./.github/actions/bootstrap-patched-pip + with: + clients: pip,python + - name: Verify the effective private pip runtime + run: python -I -m pip --version + contracts: name: Shared contract tests runs-on: ubuntu-latest diff --git a/tests/test_ci_trust.py b/tests/test_ci_trust.py index 750fb7b..c7e6eaf 100644 --- a/tests/test_ci_trust.py +++ b/tests/test_ci_trust.py @@ -18,7 +18,8 @@ test_php_linux_recipe, test_php_linux_archive, test_setup_php_linux, test_php_linux_qualification, test_php_linux_qualification_workflow, test_bookworm_apt_source, test_private_pip_runtime, - test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives) + test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives, + test_hosted_runtime_parent) ROOT = Path(__file__).resolve().parents[1] @@ -37,7 +38,7 @@ def load_tests(loader, standard_tests, pattern): for module in (test_fixed_apt, test_php_linux_bootstrap, test_php_linux_tools, test_php_linux_recipe, test_php_linux_archive, test_setup_php_linux, test_php_linux_qualification, test_php_linux_qualification_workflow, - test_bookworm_apt_source, test_private_pip_runtime, + test_bookworm_apt_source, test_private_pip_runtime, test_hosted_runtime_parent, test_php_linux_normalize, test_php_linux_archive_metadata, test_php_linux_alternatives): standard_tests.addTests(loader.loadTestsFromModule(module)) return standard_tests diff --git a/tests/test_hosted_runtime_parent.py b/tests/test_hosted_runtime_parent.py new file mode 100644 index 0000000..191ade2 --- /dev/null +++ b/tests/test_hosted_runtime_parent.py @@ -0,0 +1,154 @@ +"""Controls for the fixed hosted /opt repair; never change the host's /opt.""" + +import importlib.util +import os +from pathlib import Path +import stat +import tempfile +import unittest +from unittest.mock import patch + +from tests.test_private_pip_runtime import runtime + + +SOURCE = (Path(__file__).resolve().parents[1] + / ".github/actions/bootstrap-patched-pip/protect-hosted-runtime.py") +SPEC = importlib.util.spec_from_file_location("protect_hosted_runtime", SOURCE) +protection = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(protection) + + +class HostedRuntimeParentTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.directory = Path(self.temporary.name) / "opt" + self.directory.mkdir() + self.original_fstat = os.fstat + self.original_stat = os.stat + self.identity = self.directory.stat() + + def root_metadata(self, metadata): + if (metadata.st_dev, metadata.st_ino) == (self.identity.st_dev, self.identity.st_ino): + fields = list(metadata) + fields[4] = 0 + return os.stat_result(fields) + return metadata + + def root_owned(self, descriptor): + return self.root_metadata(self.original_fstat(descriptor)) + + def root_stat(self, *arguments, **options): + return self.root_metadata(self.original_stat(*arguments, **options)) + + def test_writable_parent_is_refused_before_and_accepted_only_after_sticky_protection(self): + self.directory.chmod(0o777) + with patch.object(os, "fstat", side_effect=self.root_owned), \ + patch.object(os, "stat", side_effect=self.root_stat): + with self.assertRaisesRegex(ValueError, "Runtime directory refused"): + with runtime.BoundDirectory(self.directory): + pass + protection.protect_directory(self.directory) + self.assertEqual(0o1777, stat.S_IMODE(self.directory.stat().st_mode)) + with runtime.BoundDirectory(self.directory): + pass + + def test_protected_directory_permissions_are_preserved_without_a_write(self): + for mode in (0o755, 0o1777): + with self.subTest(mode=mode): + self.directory.chmod(mode) + with patch.object(os, "fstat", side_effect=self.root_owned), \ + patch.object(os, "fchmod") as change: + protection.protect_directory(self.directory) + change.assert_not_called() + self.assertEqual(mode, stat.S_IMODE(self.directory.stat().st_mode)) + + def test_foreign_owner_and_special_modes_refuse_without_mutation(self): + self.directory.chmod(0o777) + with patch.object(os, "fchmod") as change: + with self.assertRaisesRegex(ValueError, "root-owned"): + protection.protect_directory(self.directory) + change.assert_not_called() + self.directory.chmod(0o2777) + with patch.object(os, "fstat", side_effect=self.root_owned), \ + patch.object(os, "fchmod") as change: + with self.assertRaisesRegex(ValueError, "root-owned"): + protection.protect_directory(self.directory) + change.assert_not_called() + + def test_link_and_regular_file_refuse_without_following_or_mutation(self): + link = self.directory.parent / "link" + link.symlink_to(self.directory) + file = self.directory.parent / "file" + file.touch() + for path in (link, file): + with self.subTest(path=path.name), patch.object(os, "fchmod") as change: + with self.assertRaises(OSError): + protection.protect_directory(path) + change.assert_not_called() + + def test_failed_or_ineffective_permission_change_cannot_pass(self): + self.directory.chmod(0o777) + with patch.object(os, "fstat", side_effect=self.root_owned): + with patch.object(os, "fchmod", side_effect=PermissionError): + with self.assertRaises(PermissionError): + protection.protect_directory(self.directory) + with patch.object(os, "fchmod"): + with self.assertRaisesRegex(ValueError, "not established"): + protection.protect_directory(self.directory) + with self.assertRaisesRegex(ValueError, "Runtime directory refused"): + with runtime.BoundDirectory(self.directory): + pass + + def test_selected_tree_preserves_contents_and_execute_bits_but_closes_writable_entries(self): + library = self.directory / "lib" + library.mkdir() + binary = library / "python" + binary.write_bytes(b"inert selected-runtime fixture") + binary.chmod(0o777) + library.chmod(0o777) + self.directory.chmod(0o777) + other = self.directory.parent / "other-version" + other.mkdir() + other.chmod(0o777) + link = library / "outside" + link.symlink_to(other, target_is_directory=True) + descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) + try: + protection.seal_tree(descriptor, os.getuid()) + finally: + os.close(descriptor) + self.assertEqual(b"inert selected-runtime fixture", binary.read_bytes()) + for path in (self.directory, library, binary): + self.assertEqual(0o755, stat.S_IMODE(path.stat().st_mode)) + self.assertTrue(link.is_symlink()) + self.assertEqual(0o777, stat.S_IMODE(other.stat().st_mode)) + + def test_hardlinks_and_special_files_refuse_without_modifying_outside_contents(self): + outside = self.directory.parent / "outside" + outside.write_bytes(b"outside") + outside.chmod(0o666) + os.link(outside, self.directory / "hardlink") + descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) + try: + with self.assertRaisesRegex(ValueError, "private regular"): + protection.seal_tree(descriptor, os.getuid()) + finally: + os.close(descriptor) + self.assertEqual(0o666, stat.S_IMODE(outside.stat().st_mode)) + (self.directory / "hardlink").unlink() + os.mkfifo(self.directory / "fifo") + descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) + try: + with self.assertRaisesRegex(ValueError, "private regular"): + protection.seal_tree(descriptor, os.getuid()) + finally: + os.close(descriptor) + + def test_noncanonical_hosted_location_refuses_before_opening_any_path(self): + for location in ("/tmp/python", "/opt/hostedtoolcache/Python/3.13.18/../../other", + "/opt/hostedtoolcache/Python/3.13.18/x86", "//opt/hostedtoolcache/Python/3.13.18/x64"): + with self.subTest(location=location), patch.object(os, "open") as opened: + with self.assertRaisesRegex(ValueError, "fixed hosted"): + protection.protect_hosted_python(location, os.getuid()) + opened.assert_not_called() From 793a7091b59676dd2145d239574695a7ce07fd92 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:00:29 +0000 Subject: [PATCH 2/8] Stage hosted qualification without changing the protected live workflow --- .../protect-hosted-runtime.py | 56 ++++---- .github/workflows/ci.yml | 18 --- .../fixtures/ci-hosted-python-protection.yml | 134 ++++++++++++++++++ 3 files changed, 160 insertions(+), 48 deletions(-) create mode 100644 tests/fixtures/ci-hosted-python-protection.yml diff --git a/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py index a1b5f85..11684b0 100644 --- a/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py +++ b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py @@ -9,30 +9,32 @@ def protect_directory(path): descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) try: - before = os.fstat(descriptor) - mode = stat.S_IMODE(before.st_mode) - if before.st_uid != 0 or mode & (stat.S_ISUID | stat.S_ISGID): - raise ValueError("Hosted runtime parent must be an ordinary root-owned directory") - if not mode & 0o022 or mode & stat.S_ISVTX: - return - - # Hosted /opt is shared and writable. Sticky ownership prevents other - # users from replacing its existing toolcache entry while preserving - # the runner's ability to create tools below this directory. - expected = mode | stat.S_ISVTX - os.fchmod(descriptor, expected) - after = os.fstat(descriptor) - if ((before.st_dev, before.st_ino, before.st_uid) - != (after.st_dev, after.st_ino, after.st_uid) - or stat.S_IMODE(after.st_mode) != expected): - raise ValueError("Hosted runtime parent protection was not established") + protect_parent(descriptor, 0) finally: os.close(descriptor) -def seal_tree(descriptor, runner_uid, budget=None): +def protect_parent(descriptor, runner_uid): + before = os.fstat(descriptor) + mode = stat.S_IMODE(before.st_mode) + if before.st_uid not in {0, runner_uid} or mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Hosted runtime parent must be root-owned or runner-owned without special modes") + expected = ((mode | stat.S_ISVTX) if before.st_uid == 0 and mode & 0o022 + else mode & ~0o022) + if mode != expected: + os.fchmod(descriptor, expected) + after = os.fstat(descriptor) + if ((before.st_dev, before.st_ino, before.st_uid) + != (after.st_dev, after.st_ino, after.st_uid) + or stat.S_IMODE(after.st_mode) != expected): + raise ValueError("Hosted runtime parent protection was not established") + + +def seal_tree(descriptor, runner_uid, budget=None, depth=0): """Remove other-user writes from only the selected runtime, without following links.""" budget = [0] if budget is None else budget + if depth > 64: + raise ValueError("Selected runtime exceeds the metadata depth limit") metadata = os.fstat(descriptor) if metadata.st_uid not in {0, runner_uid} or metadata.st_mode & (stat.S_ISUID | stat.S_ISGID): raise ValueError("Selected runtime has an unsupported owner or special mode") @@ -57,13 +59,16 @@ def seal_tree(descriptor, runner_uid, budget=None): != (entry.st_dev, entry.st_ino, entry.st_uid, entry.st_mode, entry.st_nlink)): raise ValueError("Selected runtime entry changed during protection") if directory: - seal_tree(child, runner_uid, budget) + seal_tree(child, runner_uid, budget, depth + 1) else: if held.st_mode & (stat.S_ISUID | stat.S_ISGID): raise ValueError("Selected runtime file has a special mode") expected = stat.S_IMODE(held.st_mode) & ~0o022 os.fchmod(child, expected) - if stat.S_IMODE(os.fstat(child).st_mode) != expected: + after = os.fstat(child) + if ((held.st_dev, held.st_ino, held.st_uid, held.st_nlink) + != (after.st_dev, after.st_ino, after.st_uid, after.st_nlink) + or stat.S_IMODE(after.st_mode) != expected): raise ValueError("Selected runtime file protection was not established") finally: os.close(child) @@ -86,16 +91,7 @@ def protect_hosted_python(location, runner_uid): child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=descriptor) os.close(descriptor) descriptor = child - metadata = os.fstat(descriptor) - mode = stat.S_IMODE(metadata.st_mode) - if metadata.st_uid not in {0, runner_uid} or mode & (stat.S_ISUID | stat.S_ISGID): - raise ValueError("Hosted runtime ancestry has an unsupported owner or mode") - expected = ((mode | stat.S_ISVTX) if metadata.st_uid == 0 and mode & 0o022 - else mode & ~0o022) - if mode != expected: - os.fchmod(descriptor, expected) - if stat.S_IMODE(os.fstat(descriptor).st_mode) != expected: - raise ValueError("Hosted runtime ancestor protection was not established") + protect_parent(descriptor, runner_uid) seal_tree(descriptor, runner_uid) finally: os.close(descriptor) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6b39eb..b984645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,24 +15,6 @@ concurrency: cancel-in-progress: true jobs: - hosted-python-runtime: - name: Hosted Python runtime protection - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.13' - - name: Protect and qualify the selected hosted runtime - uses: ./.github/actions/bootstrap-patched-pip - with: - clients: pip,python - - name: Verify the effective private pip runtime - run: python -I -m pip --version - contracts: name: Shared contract tests runs-on: ubuntu-latest diff --git a/tests/fixtures/ci-hosted-python-protection.yml b/tests/fixtures/ci-hosted-python-protection.yml new file mode 100644 index 0000000..e6b39eb --- /dev/null +++ b/tests/fixtures/ci-hosted-python-protection.yml @@ -0,0 +1,134 @@ +name: Repository checks + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: repository-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + hosted-python-runtime: + name: Hosted Python runtime protection + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - name: Protect and qualify the selected hosted runtime + uses: ./.github/actions/bootstrap-patched-pip + with: + clients: pip,python + - name: Verify the effective private pip runtime + run: python -I -m pip --version + + contracts: + name: Shared contract tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.13" + + - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python,composer,curl,git,npm,cargo + + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.3' + tools: composer:2.10.3, phpunit:11.5.57 + coverage: none + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.22.1 + cache: npm + + - uses: dtolnay/rust-toolchain@4d407b29186a635f0cc27475ef0bc0ae605a8866 # 1.86.0 + with: + toolchain: 1.86.0 + + - uses: ./.github/actions/verify-composer-trust # local + + - name: Install Python test dependencies + run: >- + python -m pip install --only-binary :all: jsonschema==4.23.0 PyYAML==6.0.2 + + - name: Test retained shared contracts + run: >- + python -m unittest + tests.test_qualification_policy + tests.test_protected_policy_context + tests.test_protected_policy_check + tests.test_protected_policy_inputs + tests.test_protected_policy_issue + tests.test_native_policy_check + tests.test_native_policy_workflow + tests.test_regression_corpus + tests.test_regression_evidence_schema + tests.test_server_codec_counterfactual_schema + tests.test_visual_evidence + tests.test_temporal_runtime + tests.test_cancellation_workflow_policy + tests.test_sdk_metrics_contract + tests.test_ci_trust + tests.test_pip_effective_trust + tests.test_cli_effective_trust + tests.test_composer_trust_contract + tests.test_composer_container_trust + tests.test_composer_container_effective_trust + tests.test_php_linux_bootstrap + tests.test_php_linux_archive + tests.test_php_linux_normalize + tests.test_php_linux_recipe + tests.test_php_linux_tools + tests.test_php_linux_alternatives + tests.test_setup_php_linux + tests.test_php_linux_qualification + tests.test_php_linux_qualification_workflow + tests.test_php_linux_configuration + + - name: Test actual Composer trust selection + run: COMPOSER_BINARY="$(command -v composer)" phpunit --configuration phpunit-composer-trust.xml --do-not-cache-result + + - name: Install Node dependencies + run: npm ci --ignore-scripts + + - name: Test browser evidence boundary + run: npm run test:visual-capture + + protected-parser-install: + name: Protected parser installer controls + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - name: Create a disposable installer runtime + run: | + python -I -m venv "$RUNNER_TEMP/protected-parser-install" + printf '%s\n' "$RUNNER_TEMP/protected-parser-install/bin" >> "$GITHUB_PATH" + - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + with: + clients: pip,python + - name: Exercise the actual isolated hash-locked installer + run: python -I tests/test_policy_dependency_install.py From 32aa8526de798b76ebf870ff86dc26b344efe9e8 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:05:26 +0000 Subject: [PATCH 3/8] Cover the complete hosted ancestry and refuse unsafe entry metadata --- .../protect-hosted-runtime.py | 74 ++++++++------ tests/test_hosted_runtime_parent.py | 99 ++++++++++++++++++- 2 files changed, 139 insertions(+), 34 deletions(-) diff --git a/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py index 11684b0..17e9731 100644 --- a/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py +++ b/.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py @@ -43,35 +43,7 @@ def seal_tree(descriptor, runner_uid, budget=None, depth=0): budget[0] += 1 if budget[0] > 200000: raise ValueError("Selected runtime exceeds the metadata walk limit") - entry = os.stat(name, dir_fd=descriptor, follow_symlinks=False) - if entry.st_uid not in {0, runner_uid}: - raise ValueError("Selected runtime entry has an unsupported owner") - if stat.S_ISLNK(entry.st_mode): - continue - directory = stat.S_ISDIR(entry.st_mode) - if not directory and (not stat.S_ISREG(entry.st_mode) or entry.st_nlink != 1): - raise ValueError("Selected runtime entry is not a private regular file or directory") - child = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK - | (os.O_DIRECTORY if directory else 0), dir_fd=descriptor) - try: - held = os.fstat(child) - if ((held.st_dev, held.st_ino, held.st_uid, held.st_mode, held.st_nlink) - != (entry.st_dev, entry.st_ino, entry.st_uid, entry.st_mode, entry.st_nlink)): - raise ValueError("Selected runtime entry changed during protection") - if directory: - seal_tree(child, runner_uid, budget, depth + 1) - else: - if held.st_mode & (stat.S_ISUID | stat.S_ISGID): - raise ValueError("Selected runtime file has a special mode") - expected = stat.S_IMODE(held.st_mode) & ~0o022 - os.fchmod(child, expected) - after = os.fstat(child) - if ((held.st_dev, held.st_ino, held.st_uid, held.st_nlink) - != (after.st_dev, after.st_ino, after.st_uid, after.st_nlink) - or stat.S_IMODE(after.st_mode) != expected): - raise ValueError("Selected runtime file protection was not established") - finally: - os.close(child) + seal_entry(descriptor, name, runner_uid, budget, depth) after = os.fstat(descriptor) if ((metadata.st_dev, metadata.st_ino, metadata.st_uid) != (after.st_dev, after.st_ino, after.st_uid) @@ -79,6 +51,42 @@ def seal_tree(descriptor, runner_uid, budget=None, depth=0): raise ValueError("Selected runtime directory protection was not established") +def seal_entry(descriptor, name, runner_uid, budget, depth): + entry = os.stat(name, dir_fd=descriptor, follow_symlinks=False) + if entry.st_uid not in {0, runner_uid}: + raise ValueError("Selected runtime entry has an unsupported owner") + if stat.S_ISLNK(entry.st_mode): + return + directory = stat.S_ISDIR(entry.st_mode) + if not directory and (not stat.S_ISREG(entry.st_mode) or entry.st_nlink != 1): + raise ValueError("Selected runtime entry is not a private regular file or directory") + child = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK + | (os.O_DIRECTORY if directory else 0), dir_fd=descriptor) + try: + held = os.fstat(child) + if ((held.st_dev, held.st_ino, held.st_uid, held.st_mode, held.st_nlink) + != (entry.st_dev, entry.st_ino, entry.st_uid, entry.st_mode, entry.st_nlink)): + raise ValueError("Selected runtime entry changed during protection") + if directory: + seal_tree(child, runner_uid, budget, depth + 1) + else: + seal_file(child, held) + finally: + os.close(child) + + +def seal_file(descriptor, before): + if before.st_mode & (stat.S_ISUID | stat.S_ISGID): + raise ValueError("Selected runtime file has a special mode") + expected = stat.S_IMODE(before.st_mode) & ~0o022 + os.fchmod(descriptor, expected) + after = os.fstat(descriptor) + if ((before.st_dev, before.st_ino, before.st_uid, before.st_nlink) + != (after.st_dev, after.st_ino, after.st_uid, after.st_nlink) + or stat.S_IMODE(after.st_mode) != expected): + raise ValueError("Selected runtime file protection was not established") + + def protect_hosted_python(location, runner_uid): if re.fullmatch(r"/opt/hostedtoolcache/Python/\d+\.\d+\.\d+/(?:x64|arm64)", location) is None: raise ValueError("Selected runtime is outside the fixed hosted Python location") @@ -97,9 +105,13 @@ def protect_hosted_python(location, runner_uid): os.close(descriptor) -if __name__ == "__main__": +def main(): if (len(sys.argv) != 2 or os.geteuid() != 0 - or re.fullmatch(r"[1-9][0-9]*", os.environ.get("SUDO_UID", "")) is None): + or re.fullmatch(r"[1-9]\d*", os.environ.get("SUDO_UID", "")) is None): raise SystemExit("Hosted runtime parent protection requires its fixed privileged invocation") protect_hosted_python(sys.argv[1], int(os.environ["SUDO_UID"])) print("Hosted runtime parent protection verified.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_hosted_runtime_parent.py b/tests/test_hosted_runtime_parent.py index 191ade2..d1165f9 100644 --- a/tests/test_hosted_runtime_parent.py +++ b/tests/test_hosted_runtime_parent.py @@ -2,8 +2,10 @@ import importlib.util import os +import io from pathlib import Path import stat +import sys import tempfile import unittest from unittest.mock import patch @@ -26,6 +28,8 @@ def setUp(self): self.directory.mkdir() self.original_fstat = os.fstat self.original_stat = os.stat + self.original_open = os.open + self.runner_uid = os.getuid() self.identity = self.directory.stat() def root_metadata(self, metadata): @@ -132,7 +136,7 @@ def test_hardlinks_and_special_files_refuse_without_modifying_outside_contents(s descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) try: with self.assertRaisesRegex(ValueError, "private regular"): - protection.seal_tree(descriptor, os.getuid()) + protection.seal_tree(descriptor, self.runner_uid) finally: os.close(descriptor) self.assertEqual(0o666, stat.S_IMODE(outside.stat().st_mode)) @@ -141,7 +145,7 @@ def test_hardlinks_and_special_files_refuse_without_modifying_outside_contents(s descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) try: with self.assertRaisesRegex(ValueError, "private regular"): - protection.seal_tree(descriptor, os.getuid()) + protection.seal_tree(descriptor, self.runner_uid) finally: os.close(descriptor) @@ -150,5 +154,94 @@ def test_noncanonical_hosted_location_refuses_before_opening_any_path(self): "/opt/hostedtoolcache/Python/3.13.18/x86", "//opt/hostedtoolcache/Python/3.13.18/x64"): with self.subTest(location=location), patch.object(os, "open") as opened: with self.assertRaisesRegex(ValueError, "fixed hosted"): - protection.protect_hosted_python(location, os.getuid()) + protection.protect_hosted_python(location, self.runner_uid) opened.assert_not_called() + + def sandbox_open(self, path, flags, **options): + if path == "/" and "dir_fd" not in options: + path = self.directory.parent + return self.original_open(path, flags, **options) + + def hosted_tree(self): + selected = self.directory / "hostedtoolcache/Python/3.13.18/x64" + selected.mkdir(parents=True) + file = selected / "python" + file.write_bytes(b"selected interpreter fixture") + file.chmod(0o777) + for directory in (selected, *selected.parents): + if directory == self.directory.parent: + break + directory.chmod(0o777) + return selected, file + + def test_complete_hosted_ancestry_and_selected_tree_are_protected_without_touching_other_versions(self): + selected, file = self.hosted_tree() + other = selected.parent / "arm64" + other.mkdir(mode=0o777) + other.chmod(0o777) + with patch.object(os, "open", side_effect=self.sandbox_open): + protection.protect_hosted_python("/opt/hostedtoolcache/Python/3.13.18/x64", self.runner_uid) + self.assertEqual(b"selected interpreter fixture", file.read_bytes()) + for directory in (selected, *selected.parents): + if directory == self.directory.parent: + break + self.assertEqual(0o755, stat.S_IMODE(directory.stat().st_mode)) + self.assertEqual(0o755, stat.S_IMODE(file.stat().st_mode)) + self.assertEqual(0o777, stat.S_IMODE(other.stat().st_mode)) + + def test_linked_ancestry_refuses_before_traversing_selected_runtime(self): + selected, file = self.hosted_tree() + python = selected.parent.parent + moved = python.with_name("other-python") + python.rename(moved) + python.symlink_to(moved, target_is_directory=True) + with patch.object(os, "open", side_effect=self.sandbox_open): + with self.assertRaises(OSError): + protection.protect_hosted_python("/opt/hostedtoolcache/Python/3.13.18/x64", self.runner_uid) + self.assertEqual(0o777, stat.S_IMODE(file.stat().st_mode)) + + def test_metadata_limits_refuse_without_traversing_a_large_or_deep_tree(self): + (self.directory / "file").touch() + descriptor = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY) + try: + with self.assertRaisesRegex(ValueError, "walk limit"): + protection.seal_tree(descriptor, self.runner_uid, [200000]) + with self.assertRaisesRegex(ValueError, "depth limit"): + protection.seal_tree(descriptor, self.runner_uid, depth=65) + finally: + os.close(descriptor) + + def test_special_mode_and_ineffective_file_protection_refuse(self): + file = self.directory / "file" + file.touch() + descriptor = os.open(file, os.O_RDONLY) + try: + file.chmod(0o4777) + special = os.fstat(descriptor) + with self.assertRaisesRegex(ValueError, "special mode"): + protection.seal_file(descriptor, special) + file.chmod(0o777) + before = os.fstat(descriptor) + with patch.object(os, "fchmod"): + with self.assertRaisesRegex(ValueError, "not established"): + protection.seal_file(descriptor, before) + finally: + os.close(descriptor) + + def test_cli_requires_sudo_identity_and_passes_only_the_selected_location(self): + location = "/opt/hostedtoolcache/Python/3.13.18/x64" + for arguments, uid, sudo_uid in ((["tool"], 0, "1000"), (["tool", location], 1000, "1000"), + (["tool", location], 0, ""), (["tool", location], 0, "0")): + with self.subTest(arguments=arguments, uid=uid, sudo_uid=sudo_uid), \ + patch.object(sys, "argv", arguments), patch.object(os, "geteuid", return_value=uid), \ + patch.dict(os.environ, {"SUDO_UID": sudo_uid}), \ + patch.object(protection, "protect_hosted_python") as protect: + with self.assertRaises(SystemExit): + protection.main() + protect.assert_not_called() + with patch.object(sys, "argv", ["tool", location]), patch.object(os, "geteuid", return_value=0), \ + patch.dict(os.environ, {"SUDO_UID": "1000"}), \ + patch.object(protection, "protect_hosted_python") as protect, \ + patch("sys.stdout", new_callable=io.StringIO): + protection.main() + protect.assert_called_once_with(location, 1000) From 52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:09:56 +0000 Subject: [PATCH 4/8] Admit the exact console script generated by the pinned pip wheel --- .../bootstrap-patched-pip/pip_runtime.py | 9 +- .../fixtures/ci-hosted-python-protection.yml | 134 ------------------ tests/test_private_pip_runtime.py | 18 +++ 3 files changed, 26 insertions(+), 135 deletions(-) delete mode 100644 tests/fixtures/ci-hosted-python-protection.yml diff --git a/.github/actions/bootstrap-patched-pip/pip_runtime.py b/.github/actions/bootstrap-patched-pip/pip_runtime.py index 883a9a5..5165569 100644 --- a/.github/actions/bootstrap-patched-pip/pip_runtime.py +++ b/.github/actions/bootstrap-patched-pip/pip_runtime.py @@ -639,12 +639,19 @@ def _seed_runtime(root, interpreter, manager, *, expected_identity, execute): sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) sys.exit(main()) ''' +SCRIPT_BODY_PIP_26 = '''import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = sys.argv[0].removesuffix('.exe') + sys.exit(main()) +''' def _entrypoint(path, private_python): with held_file(path, maximum=8192, owned=True, executable=True) as (contents, identity): header = ("#!" + str(private_python) + "\n").encode() - expected = {header + source.encode() for source in (SCRIPT_BODY_OLD, SCRIPT_BODY_CURRENT)} + expected = {header + source.encode() + for source in (SCRIPT_BODY_OLD, SCRIPT_BODY_CURRENT, SCRIPT_BODY_PIP_26)} if contents not in expected: raise ValueError("Private pip entrypoint is not the bound stock script/shebang") return contents, identity diff --git a/tests/fixtures/ci-hosted-python-protection.yml b/tests/fixtures/ci-hosted-python-protection.yml deleted file mode 100644 index e6b39eb..0000000 --- a/tests/fixtures/ci-hosted-python-protection.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Repository checks - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: repository-checks-${{ github.ref }} - cancel-in-progress: true - -jobs: - hosted-python-runtime: - name: Hosted Python runtime protection - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.13' - - name: Protect and qualify the selected hosted runtime - uses: ./.github/actions/bootstrap-patched-pip - with: - clients: pip,python - - name: Verify the effective private pip runtime - run: python -I -m pip --version - - contracts: - name: Shared contract tests - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: "3.13" - - - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - with: - clients: pip,python,composer,curl,git,npm,cargo - - - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 - with: - php-version: '8.3' - tools: composer:2.10.3, phpunit:11.5.57 - coverage: none - - - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version: 24.22.1 - cache: npm - - - uses: dtolnay/rust-toolchain@4d407b29186a635f0cc27475ef0bc0ae605a8866 # 1.86.0 - with: - toolchain: 1.86.0 - - - uses: ./.github/actions/verify-composer-trust # local - - - name: Install Python test dependencies - run: >- - python -m pip install --only-binary :all: jsonschema==4.23.0 PyYAML==6.0.2 - - - name: Test retained shared contracts - run: >- - python -m unittest - tests.test_qualification_policy - tests.test_protected_policy_context - tests.test_protected_policy_check - tests.test_protected_policy_inputs - tests.test_protected_policy_issue - tests.test_native_policy_check - tests.test_native_policy_workflow - tests.test_regression_corpus - tests.test_regression_evidence_schema - tests.test_server_codec_counterfactual_schema - tests.test_visual_evidence - tests.test_temporal_runtime - tests.test_cancellation_workflow_policy - tests.test_sdk_metrics_contract - tests.test_ci_trust - tests.test_pip_effective_trust - tests.test_cli_effective_trust - tests.test_composer_trust_contract - tests.test_composer_container_trust - tests.test_composer_container_effective_trust - tests.test_php_linux_bootstrap - tests.test_php_linux_archive - tests.test_php_linux_normalize - tests.test_php_linux_recipe - tests.test_php_linux_tools - tests.test_php_linux_alternatives - tests.test_setup_php_linux - tests.test_php_linux_qualification - tests.test_php_linux_qualification_workflow - tests.test_php_linux_configuration - - - name: Test actual Composer trust selection - run: COMPOSER_BINARY="$(command -v composer)" phpunit --configuration phpunit-composer-trust.xml --do-not-cache-result - - - name: Install Node dependencies - run: npm ci --ignore-scripts - - - name: Test browser evidence boundary - run: npm run test:visual-capture - - protected-parser-install: - name: Protected parser installer controls - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.13' - - name: Create a disposable installer runtime - run: | - python -I -m venv "$RUNNER_TEMP/protected-parser-install" - printf '%s\n' "$RUNNER_TEMP/protected-parser-install/bin" >> "$GITHUB_PATH" - - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust - with: - clients: pip,python - - name: Exercise the actual isolated hash-locked installer - run: python -I tests/test_policy_dependency_install.py diff --git a/tests/test_private_pip_runtime.py b/tests/test_private_pip_runtime.py index 382a678..16d54b3 100644 --- a/tests/test_private_pip_runtime.py +++ b/tests/test_private_pip_runtime.py @@ -60,6 +60,24 @@ def setUp(self): self.environment.start() self.addCleanup(self.environment.stop) + def test_pinned_pip26_generated_script_binds_the_exact_interpreter_and_complete_body(self): + observed_body = b"import sys\nfrom pip._internal.cli.main import main\nif __name__ == '__main__':\n sys.argv[0] = sys.argv[0].removesuffix('.exe')\n sys.exit(main())\n" + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + interpreter = root / "bin/python" + script = root / "pip" + header = ("#!" + str(interpreter) + "\n").encode() + script.write_bytes(header + observed_body) + script.chmod(0o700) + self.assertEqual(header + observed_body, runtime._entrypoint(script, interpreter)[0]) + for contents in (("#!/other/python\n").encode() + observed_body, + header + observed_body + b"print('unexpected')\n", + header + observed_body.replace(b".removesuffix('.exe')", b".removesuffix('.py')")): + with self.subTest(contents=contents): + script.write_bytes(contents) + with self.assertRaisesRegex(ValueError, "bound stock script"): + runtime._entrypoint(script, interpreter) + def test_full_private_bootstrap_and_read_only_reuse_have_no_ambient_or_network_fallback(self): with tempfile.TemporaryDirectory() as temporary: runner = Path(temporary) From adc9f99e5541fdd70fac675b604af5962f88428f Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:11:35 +0000 Subject: [PATCH 5/8] Exercise the actual hosted bootstrap through its ordinary source checks --- .github/workflows/hosted-pip.yml | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/hosted-pip.yml diff --git a/.github/workflows/hosted-pip.yml b/.github/workflows/hosted-pip.yml new file mode 100644 index 0000000..5bf78ff --- /dev/null +++ b/.github/workflows/hosted-pip.yml @@ -0,0 +1,36 @@ +name: Hosted pip bootstrap + +on: + push: + branches: [main] + paths: + - '.github/actions/bootstrap-patched-pip/**' + - '.github/workflows/hosted-pip.yml' + pull_request: + paths: + - '.github/actions/bootstrap-patched-pip/**' + - '.github/workflows/hosted-pip.yml' + +permissions: + contents: read + +concurrency: + group: hosted-pip-${{ github.ref }} + cancel-in-progress: true + +jobs: + bootstrap: + name: Bootstrap on hosted Python 3.13 + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: '3.13' + - uses: ./.github/actions/bootstrap-patched-pip # local + with: + clients: pip,python + - run: python -I -m pip --version From c4339aa6ea39ce44c38fe4fe7e496f8f094e6b80 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:13:03 +0000 Subject: [PATCH 6/8] Use canonical explicit bootstrap paths in the protected source workflow --- .github/workflows/hosted-pip.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hosted-pip.yml b/.github/workflows/hosted-pip.yml index 5bf78ff..bd62022 100644 --- a/.github/workflows/hosted-pip.yml +++ b/.github/workflows/hosted-pip.yml @@ -4,11 +4,17 @@ on: push: branches: [main] paths: - - '.github/actions/bootstrap-patched-pip/**' + - '.github/actions/bootstrap-patched-pip/action.yml' + - '.github/actions/bootstrap-patched-pip/pip_runtime.py' + - '.github/actions/bootstrap-patched-pip/export-trust.py' + - '.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py' - '.github/workflows/hosted-pip.yml' pull_request: paths: - - '.github/actions/bootstrap-patched-pip/**' + - '.github/actions/bootstrap-patched-pip/action.yml' + - '.github/actions/bootstrap-patched-pip/pip_runtime.py' + - '.github/actions/bootstrap-patched-pip/export-trust.py' + - '.github/actions/bootstrap-patched-pip/protect-hosted-runtime.py' - '.github/workflows/hosted-pip.yml' permissions: From e61ca4a34f8a8b6b48fb1355ac5043c07a956791 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:14:17 +0000 Subject: [PATCH 7/8] Catalog the exact hosted-qualified bootstrap revision --- qualification/policy.json | 1 + 1 file changed, 1 insertion(+) diff --git a/qualification/policy.json b/qualification/policy.json index 13d814f..093671d 100644 --- a/qualification/policy.json +++ b/qualification/policy.json @@ -83,6 +83,7 @@ "durable-workflow/.github": { "18783be6bef737cc778e4c7282a8797e39daa61a": "fixed-apt", "242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5": "clean-trust", + "52283e2e8ea9a2cf7e3865ee30f2a72a38b098a2": "clean-trust", "54cce4c6a814c8137dafbee594a1e0bdcf0ba60c": "clean-trust", "699d09d602fc0353160bdecaa2b23f07ef4c7c21": "clean-trust", "75587fc33ff40c34f32ef777293dad56bb6f48c7": "clean-trust", From cb3f548d408ccad44a453ccada90aba22b0ddc4d Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:16:12 +0000 Subject: [PATCH 8/8] Catalog reviewed Docker login and Sonar patch updates for the pending consumer --- qualification/policy.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qualification/policy.json b/qualification/policy.json index 093671d..a05b6a6 100644 --- a/qualification/policy.json +++ b/qualification/policy.json @@ -63,7 +63,8 @@ "docker/login-action": { "af1e73f918a031802d376d3c8bbc3fe56130a9b0": "v4", "dbcb813823bdd20940b903addbd779551569679f": "v4.6.0", - "81c6f93d4bc991e510b342a00b419b3bb4efc5c6": "v4.7.0" + "81c6f93d4bc991e510b342a00b419b3bb4efc5c6": "v4.7.0", + "8aa5831fb8d48d8f76594c7ddde61ae58019f550": "v4.8.0" }, "docker/metadata-action": { "dc802804100637a589fabce1cb79ff13a1411302": "v6" @@ -116,7 +117,8 @@ }, "sonarsource/sonarqube-scan-action": { "d209202bc7d53ff1cc128f7f907dac145c9d6ae9": "v8.3.0", - "4d1dc0c91ee400dcf8a6dddf2f705ac36542e304": "v8.4.0" + "4d1dc0c91ee400dcf8a6dddf2f705ac36542e304": "v8.4.0", + "e805d875275f728fac1885b413934be6d7c63f65": "v8.4.1" } }, "supported_javascript_runtimes": [