diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index c621ca809a..848d6a6459 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,5 +1,16 @@ # Compatibility Contract +## Server v1.6.550 prepared, not published + +The source candidate preserves all component versions and operator settings from +549 while rebuilding the two PCRE2 runtime packages with the upstream's official +10.46 backports. Original Ubuntu packaging, SONAMEs, JIT and POSIX interfaces are +retained. First-party package version `10.46-1build1+pasturestack.1` distinguishes +this rebuild from Canonical binaries. Native upstream tests, exact package +integrity, ELF hashes and actual consumers are mandatory gates. The published +installation remains 549 until a new immutable image passes publication. +See [the candidate note](docs/releases/server-1.6.550.md). + ## Server v1.6.549 published diff --git a/README.md b/README.md index 69a7bdb1f2..c7f9469dab 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ See the [549 release note](docs/releases/server-1.6.549.md) and [compatibility contract](COMPATIBILITY.md#server-v16549-web-console-sbom) for exact identities, attachment recovery and acceptance boundaries. +Source candidate `v1.6.550` adds the official PCRE2 10.46 security backports with +original Ubuntu packaging, ABI checks and native regression tests. It is not yet +published; installation examples continue to use the verified release above. +See the [candidate note](docs/releases/server-1.6.550.md). + Personal API Keys remain available without an accessible environment. See the [Host guide](docs/hosts/README.md) before enrolling a host. ## API Keys diff --git a/docs/releases/server-1.6.550.md b/docs/releases/server-1.6.550.md new file mode 100644 index 0000000000..12c691568c --- /dev/null +++ b/docs/releases/server-1.6.550.md @@ -0,0 +1,48 @@ +# Server v1.6.550 candidate + +Prepared source only; no Server image or release has been published. + +## Change + +Backport the twelve official PCRE2 `release/pcre2-10.46` commits through +`947809f11e9c9744aa0974b4b630a1ae461733dd` onto the original Ubuntu 26.04 +`10.46-1build1` source package. The source, original packaging and every patch +are SHA-256 pinned. This is a first-party rebuild, not a Canonical release. +It avoids unrelated regex, Unicode, JIT or packaging changes. + +The two packages `libpcre2-8-0` and `libpcre2-posix3` use version +`10.46-1build1+pasturestack.1`. The eight fixed vulnerabilities are +CVE-2026-103111, CVE-2026-86145, CVE-2026-89156, CVE-2026-89157, +CVE-2026-89158, CVE-2026-89160, CVE-2026-89161 and CVE-2026-89162. +The [official supported backport list](https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md) +identifies this exact series. Ubuntu having no updated binary package does not +mean upstream has no accepted fix. + +## Verification and boundaries + +The build runs the unchanged Ubuntu packaging tests: `pcre2_jit_test`, `RunTest` +and `RunGrepTest`, and requires all three PASS records. The output retains source +locks, build logs, native test logs, compiler flags, package identity and ABI / +hardening evidence. Runtime checks require exact package versions, zero dpkg +integrity differences, both library hashes, resolved linkage from grep, Git, +MariaDB, libselinux and the POSIX wrapper, and C / C.utf8 regex controls. + +The inherited image's documentation exclusion is overridden only while installing +the package-owned `README.Debian`; no integrity exception is added. Raw scanner +results remain intact. The fixed VEX entries apply only to the exact rebuilt +package PURLs; the other ten pending Server CVEs remain tracked. + +Native library and layered-candidate checks have passed on the QA host. These do +not establish publication or startup/restart acceptance of the final image. +Those results and the immutable digest will be recorded after the release gates. +Web Console 1.6.195, its runtime SBOM and all other component pins are unchanged. +Unrelated role, UI, OIDC, MFA and feature-matrix PASS evidence is carried forward. + +## Upgrade and rollback + +Until publication, continue using the numeric `v1.6.549` installation examples. +Do not overwrite an existing image tag or apply library patches to a running +container. Upgrade with a new immutable image after verified backups, preserving +all existing volumes, configuration and the previous container/image for rollback. +No database migration or operator setting change is introduced by this patch. +No production deployment is part of this release task. diff --git a/scripts/check-server-api-explorer-patch.sh b/scripts/check-server-api-explorer-patch.sh index da0f52f6d5..9b85034012 100755 --- a/scripts/check-server-api-explorer-patch.sh +++ b/scripts/check-server-api-explorer-patch.sh @@ -570,7 +570,7 @@ python3 scripts/test-web-console-sbom.py python3 server/security/test_web_console_sbom_scan.py require_marker "$release_dockerfile" \ - 'ARG SERVER_RELEASE_TAG=v1.6.549' \ + 'ARG SERVER_RELEASE_TAG=v1.6.550' \ SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING require_marker "$release_dockerfile" \ 'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"' \ @@ -1138,10 +1138,10 @@ require_marker "$build_script" \ '--file server/Dockerfile.web-compose-release' \ SERVER_INCREMENTAL_RELEASE_BUILD_PATH_MISSING require_marker "$build_script" \ - 'image=${IMAGE:-pasturestack-validation/server:v1.6.549}' \ + 'image=${IMAGE:-pasturestack-validation/server:v1.6.550}' \ SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING require_marker "$build_script" \ - 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.549}' \ + 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.550}' \ SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING require_marker "$build_script" \ 'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"' \ @@ -1691,10 +1691,10 @@ fi if grep -Fxq '## Current source candidate' README.md; then for candidate_readme_marker in \ '' \ - 'Server `v1.6.549` is a prepared source candidate, not a published image.' \ + 'Server `v1.6.550` is a prepared source candidate, not a published image.' \ 'run them only after its official release' \ 'This candidate is not yet available' \ - 'ghcr.io/pasturestack/server:v1.6.549'; do + 'ghcr.io/pasturestack/server:v1.6.550'; do require_marker README.md "$candidate_readme_marker" \ SERVER_CURRENT_CANDIDATE_README_MISSING done @@ -2300,11 +2300,11 @@ done jq -e ' ["CVE-2026-107928","CVE-2026-107929","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"] as $curl_fixed_cves | .["@context"] == "https://openvex.dev/ns/v0.2.0" - and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.549" - and (.statements | length) == 48 + and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.550" + and (.statements | length) == 56 and ([.statements[].vulnerability.name] | length == (unique | length)) and ([.statements[] | select(.status == "fixed") | .vulnerability.name] | sort) - == ["CVE-2024-52005","CVE-2026-107928","CVE-2026-107929","CVE-2026-12087","CVE-2026-13221","CVE-2026-18798","CVE-2026-27171","CVE-2026-56391","CVE-2026-57432","CVE-2026-57433","CVE-2026-75803","CVE-2026-85091","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-8932","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"] + == ["CVE-2024-52005","CVE-2026-103111","CVE-2026-107928","CVE-2026-107929","CVE-2026-12087","CVE-2026-13221","CVE-2026-18798","CVE-2026-27171","CVE-2026-56391","CVE-2026-57432","CVE-2026-57433","CVE-2026-75803","CVE-2026-85091","CVE-2026-86145","CVE-2026-86409","CVE-2026-86410","CVE-2026-86412","CVE-2026-86414","CVE-2026-87862","CVE-2026-87863","CVE-2026-87864","CVE-2026-87865","CVE-2026-89156","CVE-2026-89157","CVE-2026-89158","CVE-2026-89160","CVE-2026-89161","CVE-2026-89162","CVE-2026-8932","CVE-2026-92386","CVE-2026-92387","CVE-2026-92388","CVE-2026-92389","CVE-2026-92390","CVE-2026-92391","CVE-2026-92392","CVE-2026-94266"] and ([.statements[] | select(.status == "not_affected") | .vulnerability.name] | sort) == ["CVE-2024-2236","CVE-2024-56433","CVE-2025-1352","CVE-2025-1376","CVE-2025-66382","CVE-2026-13757","CVE-2026-18374","CVE-2026-18477","CVE-2026-18508","CVE-2026-27456","CVE-2026-3184","CVE-2026-40228","CVE-2026-53910","CVE-2026-54371","CVE-2026-56392","CVE-2026-56855","CVE-2026-57062","CVE-2026-78662","GO-2026-5932"] and ([.statements[] | select(.status == "under_investigation") | .vulnerability.name] | sort) @@ -2338,7 +2338,7 @@ jq -r ' | @tsv ' "$runtime_vendor_pending" | LC_ALL=C sort -u >"$vendor_pending_fixture" bash "$vendor_pending_validator" "$runtime_vendor_pending" \ - "$vendor_pending_fixture" v1.6.549 >/dev/null + "$vendor_pending_fixture" v1.6.550 >/dev/null bash scripts/test-vendor-pending-findings.sh >/dev/null rm -f "$vendor_pending_fixture" trap - EXIT @@ -2427,4 +2427,4 @@ for release_readback_contract in \ fi done -printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.549 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 webhook_automation_service=0.10.5 authentication_service=0.4.46 curl=8.18.0-1ubuntu2.8 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n' +printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.550 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 webhook_automation_service=0.10.5 authentication_service=0.4.46 curl=8.18.0-1ubuntu2.8 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n' diff --git a/scripts/check-server-source-gates.sh b/scripts/check-server-source-gates.sh index 37adb9dcd2..33bf69106f 100755 --- a/scripts/check-server-source-gates.sh +++ b/scripts/check-server-source-gates.sh @@ -28,6 +28,8 @@ run_gate catalog_card_brand scripts/check-server-catalog-card-brand-patch.sh run_gate dockerfile_artifact_downloads scripts/check-server-dockerfile-artifact-downloads.sh run_gate production_build_context python3 scripts/test-server-build-context.py run_gate offline_ubuntu_security_updates python3 scripts/test-offline-ubuntu-security-updates.py +run_gate pcre2_official_backports python3 server/security/ubuntu-backports/test_pcre2_backport.py +run_gate pcre2_runtime_verifier python3 scripts/test-pcre2-runtime-verifier.py run_gate engine_archive_contract python3 scripts/test-engine-archive-contract.py run_gate runtime_contract_readback python3 scripts/test-runtime-contract-readback.py run_gate server_dev_dockerfile_downloads scripts/check-server-dev-dockerfile-downloads.sh diff --git a/scripts/test-current-server-packaging-contract.py b/scripts/test-current-server-packaging-contract.py index b348757060..92fe6c3293 100644 --- a/scripts/test-current-server-packaging-contract.py +++ b/scripts/test-current-server-packaging-contract.py @@ -42,7 +42,7 @@ ENGINE_SHA = 'f4889bfb8759a7664f07524bb129d0cda19f35604a06c877b6ca7033246df6fa' ENGINE_SOURCE = 'dc5a8d7bc44131f609f72a6504483b692d47ce16' OLD_COORDINATES = { - 'v1.6.549': 'v1.6.521', + 'v1.6.550': 'v1.6.521', '1.6.195': '1.6.181', WEB_SHA: '9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03', WEB_SOURCE: '7f2bdad0c90a3241693c2ddd4475202ababba550', @@ -66,7 +66,7 @@ } EXPECTED = { COMPATIBILITY_CODE: (COMPATIBILITY, PUBLISHED_508), - 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.549'), + 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.550'), 'SERVER_INCREMENTAL_RELEASE_VERSION_MISSING': (DOCKER, 'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"'), 'SERVER_INCREMENTAL_RELEASE_RUNTIME_VERSION_MISSING': (DOCKER, 'ENV CATTLE_RANCHER_SERVER_VERSION=${SERVER_RELEASE_TAG}'), 'SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.195'), @@ -81,8 +81,8 @@ 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_HASH_MISSING': (BUILD, 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-' + WEB_SHA + '}'), 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_LOCK_HASH_MISSING': (BUILD, 'web_console_lock_sha256=${WEB_CONSOLE_LOCK_SHA256:-' + WEB_LOCK_SHA + '}'), 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_SBOM_HASH_MISSING': (BUILD, 'web_console_runtime_sbom_sha256=${WEB_CONSOLE_RUNTIME_SBOM_SHA256:-' + WEB_SBOM_SHA + '}'), - 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.549}'), - 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.549}'), + 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.550}'), + 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.550}'), 'SERVER_INCREMENTAL_RELEASE_BUILD_RUNTIME_VERSION_MISSING': (BUILD, 'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"'), 'SERVER_WEB_CONSOLE_RUNTIME_VERSION_GATE_MISSING': (BUILD, 'test "$(cat "${web_root}/VERSION.txt")" = "${WEB_CONSOLE_RELEASE_TAG}"'), 'SERVER_VOLUME_NATIVE_READ_ONLY_BUILD_GATE_MISSING': (DOCKER, 'grep -F \'"volume.isNative" : "r"\' >/dev/null'), @@ -600,15 +600,15 @@ def verify(files, gate=GATE): pipeline = r'(?:^|[;\n])\s*' + re.escape(marker) + r'(?=\s*(?:;|\n|$))' if len(re.findall(pipeline, source)) != 1: raise AssertionError('CURRENT_CATALOG_HASH_BYPASS') - if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.549"' not in gate: + if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.550"' not in gate: raise AssertionError('CURRENT_VEX_GATE_PIN_MISMATCH') - if '"$vendor_pending_fixture" v1.6.549 >/dev/null' not in gate: + if '"$vendor_pending_fixture" v1.6.550 >/dev/null' not in gate: raise AssertionError('CURRENT_VENDOR_GATE_PIN_MISMATCH') - if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.549': + if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.550': raise AssertionError('CURRENT_VEX_RELEASE_MISMATCH') - if json.loads(files[VENDOR]).get('release') != 'v1.6.549': + if json.loads(files[VENDOR]).get('release') != 'v1.6.550': raise AssertionError('CURRENT_VENDOR_RELEASE_MISMATCH') - if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.549 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 ' not in gate: + if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.550 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 ' not in gate: raise AssertionError('CURRENT_SUMMARY_MISMATCH') verify_incremental_contract(files, gate) verify_load_balancer_contract(files, gate) @@ -623,11 +623,11 @@ def previous_gate(gate=GATE): if stale(marker) != marker: replacements["'" + marker + "' "] = "'" + stale(marker) + "' " replacements.update({ - 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.549"': + 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.550"': 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.521"', - '"$vendor_pending_fixture" v1.6.549 >/dev/null': + '"$vendor_pending_fixture" v1.6.550 >/dev/null': '"$vendor_pending_fixture" v1.6.521 >/dev/null', - 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.549 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 ': + 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.550 base=v1.6.460 engine=0.183.354 web_console=1.6.195 catalog_service=0.20.15 ': 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.521 base=v1.6.460 engine=0.183.340 web_console=1.6.181 catalog_service=0.20.12 ', }) for marker, stale_marker in replacements.items(): @@ -1311,7 +1311,7 @@ def test_historical_engine340_cannot_be_packaged(self): def test_engine352_actual_war_producer_and_previous351_350_boundary(self): candidate = (REPO / 'server/build-component-candidate.sh').read_text(encoding='utf-8') - self.assertIn('export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.549}', candidate) + self.assertIn('export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.550}', candidate) self.assertIn('export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.354}', candidate) for old in ( {'0.183.354': '0.183.351', @@ -1619,7 +1619,7 @@ def test_machine_actual_binary_package_and_retired_boundary_are_verified(self): def test_current_readme_status_specific_prose_does_not_block_publication_transition(self): common = GATE.split('for current_readme_marker in ', 1)[1].split('; do', 1)[0] self.assertNotIn('candidate', common) - self.assertNotIn('v1.6.549', common) + self.assertNotIn('v1.6.550', common) self.assertIn("grep -Fqi 'keep all three named volumes' README.md", GATE) status = GATE.split("if grep -Fxq '## Current source candidate' README.md; then", 1)[1] candidate, published = status.split("elif grep -Fxq '## Current release' README.md; then", 1) @@ -1628,7 +1628,7 @@ def test_current_readme_status_specific_prose_does_not_block_publication_transit self.assertIn('This candidate is not yet available', candidate) self.assertIn('SERVER_CURRENT_PUBLISHED_README_MISSING', published) self.assertNotIn('candidate', published) - self.assertNotIn('v1.6.549', published) + self.assertNotIn('v1.6.550', published) if __name__ == '__main__': diff --git a/scripts/test-pcre2-runtime-verifier.py b/scripts/test-pcre2-runtime-verifier.py new file mode 100644 index 0000000000..7908215196 --- /dev/null +++ b/scripts/test-pcre2-runtime-verifier.py @@ -0,0 +1,64 @@ +"""Source guards for the final-image verifier; native execution belongs to QA.""" +from pathlib import Path +import re +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / 'server/artifacts/verify-pcre2-runtime.sh' + + +class Pcre2RuntimeVerifier(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.text = SCRIPT.read_text(encoding='utf-8') + + def test_exact_first_party_versions_and_dpkg_integrity(self): + self.assertIn('expected_version=10.46-1build1+pasturestack.1', self.text) + self.assertIn('for package in libpcre2-8-0 libpcre2-posix3;', self.text) + self.assertIn("${db:Status-Status}\\t${Version}\\n", self.text) + self.assertIn('dpkg --verify libpcre2-8-0 libpcre2-posix3', self.text) + self.assertRegex(self.text, r'if test -s "\$temporary/dpkg-verify"; then\s+' + r'cat "\$temporary/dpkg-verify" >&2\s+exit 1\s+fi') + # The installer must retain package files; this verifier never ignores + # missing documentation or filters a real ELF discrepancy. + self.assertNotIn('README.Debian', self.text) + + def test_manifest_is_exact_and_binds_resolved_runtime_files(self): + for required in ['manifest=/usr/share/pasturestack/security/pcre2-runtime.sha256', + 'NF != 2', 'length($1) != 64', '$1 ~ /[^0-9a-f]/', 'NR != 2', + 'test "$count_pcre2:$count_posix" = 1:1', + 'sha256sum --check "$manifest"', + 'resolved=$(readlink -f -- "$file")']: + self.assertIn(required, self.text) + + def test_all_five_consumers_resolve_the_verified_pcre2(self): + match = re.search(r'for consumer in (.*?);\s*do\b', self.text, re.S) + self.assertIsNotNone(match) + consumers = match[1].replace('\\\n', ' ').split() + self.assertEqual(['/usr/bin/grep', '/usr/bin/git', '/usr/sbin/mariadbd', + '/usr/lib/x86_64-linux-gnu/libselinux.so.1', + '/usr/lib/x86_64-linux-gnu/libpcre2-posix.so.3'], consumers) + self.assertIn('ldd -r "$consumer"', self.text) + self.assertIn('not found|undefined symbol', self.text) + self.assertIn('if (count != 1) exit 1', self.text) + self.assertIn('test "$(readlink -f -- "$linked")" = "$pcre2"', self.text) + + def test_minimal_real_grep_git_controls_cover_both_locales_and_interpreter(self): + for required in ['for test_locale in C C.utf8;', "for prefix in '' '(*NO_JIT)';", + '${prefix}^ASCII$', '${prefix}(*UTF)^漢字$', '${prefix}^CRLF\\r$', + "/usr/bin/git -C /usr/lib grep --no-index -P '^ID=ubuntu$' -- os-release", + 'cmp "$temporary/os-release" "$temporary/actual"']: + self.assertIn(required, self.text) + self.assertIn("printf 'ASCII\\n漢字\\nCRLF\\r\\n'", self.text) + + def test_readback_never_installs_tools_or_opens_database(self): + self.assertNotRegex(self.text, r'(?m)^\s*(?:apt(?:-get)?|pip[0-9]*|python[0-9]*|readelf|nm)\b') + self.assertNotIn('dpkg --install', self.text) + self.assertNotRegex(self.text, r'(?m)^\s*/usr/sbin/mariadbd\b') + self.assertIn('mktemp -d /tmp/pcre2-runtime.XXXXXXXX', self.text) + self.assertIn('set -eu', self.text) + self.assertTrue(self.text.rstrip().endswith('echo PCRE2_RUNTIME_BACKPORT_OK')) + + +if __name__ == '__main__': + unittest.main() diff --git a/server/Dockerfile.web-compose-release b/server/Dockerfile.web-compose-release index 3cab787143..1b191db590 100644 --- a/server/Dockerfile.web-compose-release +++ b/server/Dockerfile.web-compose-release @@ -536,6 +536,7 @@ RUN set -eux; \ FROM ${UBUNTU_SECURITY_IMAGE} AS runtime_security_packages ARG UBUNTU_RUNTIME_SECURITY_SNAPSHOT=20261002T000000Z +ARG PCRE2_PACKAGE_VERSION=10.46-1build1+pasturestack.1 ARG CURL_PACKAGE_VERSION=8.18.0-1ubuntu2.8 ARG OPENSSL_PACKAGE_VERSION=3.5.5-1ubuntu3.7 ARG LIBDBI_PERL_PACKAGE_VERSION=1.647-1ubuntu0.26.04.4 @@ -546,6 +547,7 @@ ENV DEBIAN_FRONTEND=noninteractive WORKDIR /build COPY scripts/download-ubuntu-security-updates.sh /usr/local/libexec/pasturestack/download-ubuntu-security-updates.sh COPY security/ubuntu-security-packages.lock /build/ubuntu-security-packages.lock +COPY security/ubuntu-backports/build-pcre2-backport.py security/ubuntu-backports/pcre2.lock.json /build/ubuntu-backports/ ADD --checksum=sha256:5b6326f1932cfd7be57130561dc78192f09ed9f58bfbe6606ed0c50510c3b6cd \ https://archive.ubuntu.com/ubuntu/pool/main/c/ca-certificates/ca-certificates_20260816ubuntu0.26.04.1_all.deb \ /tmp/ca-certificates.deb @@ -654,6 +656,24 @@ RUN set -eux; \ RUN sh /usr/local/libexec/pasturestack/download-ubuntu-security-updates.sh \ /build/ubuntu-security-packages.lock /etc/ssl/certs/ca-certificates.crt /out/security-updates +# Keep Canonical binaries and first-party source rebuilds separate. The latter +# retains Ubuntu's packaging/ABI and applies only upstream's approved backports. +RUN set -eux; \ + apt-get install -y --no-install-recommends build-essential debhelper dpkg-dev dh-autoreconf fakeroot patch binutils python3; \ + python3 /build/ubuntu-backports/build-pcre2-backport.py \ + --output-dir /out/pcre2 --work-dir /build/pcre2-backport-work --jobs 2; \ + mkdir -p /tmp/pcre2-runtime; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + archive="/out/pcre2/${package}_${PCRE2_PACKAGE_VERSION}_amd64.deb"; \ + test "$(dpkg-deb -f "${archive}" Package)" = "${package}"; \ + test "$(dpkg-deb -f "${archive}" Version)" = "${PCRE2_PACKAGE_VERSION}"; \ + dpkg-deb --extract "${archive}" /tmp/pcre2-runtime; \ + done; \ + cd /tmp/pcre2-runtime; \ + sha256sum usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 usr/lib/x86_64-linux-gnu/libpcre2-posix.so.3 > /out/pcre2-runtime.sha256; \ + cd /out; \ + sha256sum pcre2/*.deb pcre2/provenance.json pcre2/pcre2.lock.json pcre2-runtime.sha256 >> SHA256SUMS + FROM ${BASE_IMAGE} RUN rm -rf /opt/java/openjdk @@ -671,7 +691,7 @@ ARG PER_HOST_SUBNET_VERSION=0.2.5 ARG PER_HOST_SUBNET_PRODUCER_COMMIT=e102e0bb38d84b0a9cbf87e5458cf1bc74d01904 ARG PER_HOST_SUBNET_ARCHIVE_SHA256=7237e83e96a64e7f3de64eb86bff9721c4e879c35cda9e9de1e4cb1ba225f298 ARG PER_HOST_SUBNET_PROVENANCE_SHA256=afa2db0e0755c6bd3ee28e111937c521a6d84e6d71d563e99202cf4693000eaf -ARG SERVER_RELEASE_TAG=v1.6.549 +ARG SERVER_RELEASE_TAG=v1.6.550 ARG API_EXPLORER_RELEASE_TAG=v1.1.20 ARG API_EXPLORER_ARTIFACT_SHA256=c989ee12a518f6d9d2b23b1f17f48d781aacf6ec31fb65291dd7de9b1661badb ARG API_EXPLORER_COMMIT=a586f0dc0aa50df579049a8d4bf26f04400fdf75 @@ -695,6 +715,7 @@ ARG LIBPNG_PACKAGE_VERSION=1.6.57-1ubuntu0.1 ARG CA_CERTIFICATES_PACKAGE_VERSION=20260816ubuntu0.26.04.1 ARG GLIBC_PACKAGE_VERSION=2.43-2ubuntu2.4 ARG PERL_PACKAGE_VERSION=5.40.1-7ubuntu0.3 +ARG PCRE2_PACKAGE_VERSION=10.46-1build1+pasturestack.1 ARG WEB_CONSOLE_RELEASE_TAG=1.6.195 ARG WEB_CONSOLE_ARTIFACT_SHA256=26cbe1f3c057722c6682c43316021687508b7d9dd58c28c32713512d75238bb3 ARG WEB_CONSOLE_COMMIT=f905bc12daae18d98a6ac65942625562fff2a424 @@ -747,20 +768,30 @@ ARG MACHINE_DRIVER_BINARY_SHA256=9d379214de116542cb0e2b1a09c17409fd59fd2b0109428 ARG PACKET_DRIVER_BINARY_SHA256=b3e5c71928fcebe1bb648f79ebbf38af9ad32d6f6dbb87587e1b92c6c331e100 COPY --from=runtime_security_packages /out/ /tmp/pasturestack-runtime-security/ -COPY scripts/install-ubuntu-security-updates.sh security/ubuntu-security-packages.lock /tmp/pasturestack-security-input/ +COPY scripts/install-ubuntu-security-updates.sh security/ubuntu-security-packages.lock artifacts/verify-pcre2-runtime.sh /tmp/pasturestack-security-input/ # Keep the official zlib backport active: ldconfig otherwise prefers the old # upstream 1.3.2 library, which is affected by USN-8954-1 / CVE-2026-85091. # Retain the base image's removed, unused Git DAV/Expat surface after updating. RUN set -eux; \ printf '%s\n' "${WEB_CONSOLE_COMMIT}" | grep -Eq '^[0-9a-f]{40}$'; \ cd /tmp/pasturestack-runtime-security; \ + install -D -m 0755 /tmp/pasturestack-security-input/verify-pcre2-runtime.sh /usr/local/libexec/pasturestack/verify-pcre2-runtime.sh; \ sha256sum -c SHA256SUMS; \ install -m 0755 tar /usr/bin/tar; \ dpkg -i packages/*.deb; \ install -D -m 0644 /tmp/pasturestack-security-input/ubuntu-security-packages.lock /usr/share/pasturestack/security/ubuntu-security-packages.lock; \ sh /tmp/pasturestack-security-input/install-ubuntu-security-updates.sh /usr/share/pasturestack/security/ubuntu-security-packages.lock /tmp/pasturestack-runtime-security/security-updates; \ + dpkg --path-include=/usr/share/doc/libpcre2-8-0/README.Debian -i pcre2/*.deb; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + test "$(dpkg-query -W -f='${Version}' "${package}")" = "${PCRE2_PACKAGE_VERSION}"; \ + test -z "$(dpkg --verify "${package}")"; \ + done; \ + install -D -m 0644 pcre2/provenance.json /usr/share/pasturestack/security/pcre2-backport-provenance.json; \ + install -D -m 0644 pcre2/pcre2.lock.json /usr/share/pasturestack/security/pcre2-backport.lock.json; \ + install -D -m 0644 pcre2-runtime.sha256 /usr/share/pasturestack/security/pcre2-runtime.sha256; \ rm -f /usr/lib/x86_64-linux-gnu/libz.so.1.3.2; \ ldconfig; \ + sh /usr/local/libexec/pasturestack/verify-pcre2-runtime.sh; \ test "$(readlink -f /usr/lib/x86_64-linux-gnu/libz.so.1)" = /usr/lib/x86_64-linux-gnu/libz.so.1.3.1; \ zlib_integrity="$(dpkg --verify zlib1g)"; \ test -z "${zlib_integrity}"; \ diff --git a/server/artifacts/verify-pcre2-runtime.sh b/server/artifacts/verify-pcre2-runtime.sh new file mode 100644 index 0000000000..e99209dd00 --- /dev/null +++ b/server/artifacts/verify-pcre2-runtime.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Read-only candidate-container verification; fixture files stay in /tmp. +set -eu +test "$#" -eq 0 +export LC_ALL=C +cd / +expected_version=10.46-1build1+pasturestack.1 +manifest=/usr/share/pasturestack/security/pcre2-runtime.sha256 +temporary=$(mktemp -d /tmp/pcre2-runtime.XXXXXXXX) +trap 'rm -f -- "$temporary/"*; rmdir -- "$temporary"' EXIT HUP INT TERM + +for package in libpcre2-8-0 libpcre2-posix3; do + actual=$(dpkg-query --show --showformat='${db:Status-Status}\t${Version}\n' "$package") + test "$actual" = "$(printf 'installed\t%s' "$expected_version")" +done +if ! dpkg --verify libpcre2-8-0 libpcre2-posix3 > "$temporary/dpkg-verify" 2>&1; then + cat "$temporary/dpkg-verify" >&2 + exit 1 +fi +if test -s "$temporary/dpkg-verify"; then + cat "$temporary/dpkg-verify" >&2 + exit 1 +fi + +pcre2=$(readlink -f /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0) +posix=$(readlink -f /usr/lib/x86_64-linux-gnu/libpcre2-posix.so.3) +test -f "$pcre2" && test -f "$posix" && test "$pcre2" != "$posix" +test -s "$manifest" +awk 'NF != 2 || length($1) != 64 || $1 ~ /[^0-9a-f]/ {exit 1} + END {if (NR != 2) exit 1}' "$manifest" +count_pcre2=0 count_posix=0 +while read -r expected_sha file; do + resolved=$(readlink -f -- "$file") + case "$resolved" in + "$pcre2") count_pcre2=$((count_pcre2 + 1));; + "$posix") count_posix=$((count_posix + 1));; + *) echo 'Unexpected file in PCRE2 runtime hash manifest' >&2; exit 1;; + esac +done < "$manifest" +test "$count_pcre2:$count_posix" = 1:1 +sha256sum --check "$manifest" + +for consumer in /usr/bin/grep /usr/bin/git /usr/sbin/mariadbd \ + /usr/lib/x86_64-linux-gnu/libselinux.so.1 \ + /usr/lib/x86_64-linux-gnu/libpcre2-posix.so.3; do + test -f "$consumer" + if ! ldd -r "$consumer" > "$temporary/ldd" 2>&1; then + cat "$temporary/ldd" >&2 + exit 1 + fi + if grep -Eiq 'not found|undefined symbol' "$temporary/ldd"; then + cat "$temporary/ldd" >&2 + exit 1 + fi + linked=$(awk '$1 == "libpcre2-8.so.0" && $2 == "=>" {print $3; count++} + END {if (count != 1) exit 1}' "$temporary/ldd") + test "$(readlink -f -- "$linked")" = "$pcre2" + printf 'PCRE2_CONSUMER_LINK_OK consumer=%s resolved=%s\n' "$consumer" "$pcre2" +done + +locale -a > "$temporary/locales" +grep -Fx C "$temporary/locales" > /dev/null +grep -Fx C.utf8 "$temporary/locales" > /dev/null +printf 'ASCII\n漢字\nCRLF\r\n' > "$temporary/input" +printf 'ASCII\n' > "$temporary/ascii" +printf '漢字\n' > "$temporary/han" +printf 'CRLF\r\n' > "$temporary/crlf" +printf 'os-release:ID=ubuntu\n' > "$temporary/os-release" +for test_locale in C C.utf8; do + for prefix in '' '(*NO_JIT)'; do + LC_ALL="$test_locale" /usr/bin/grep -P -- "${prefix}^ASCII$" "$temporary/input" > "$temporary/actual" + cmp "$temporary/ascii" "$temporary/actual" + LC_ALL="$test_locale" /usr/bin/grep -P -- "${prefix}(*UTF)^漢字$" "$temporary/input" > "$temporary/actual" + cmp "$temporary/han" "$temporary/actual" + LC_ALL="$test_locale" /usr/bin/grep -P -- "${prefix}^CRLF\r$" "$temporary/input" > "$temporary/actual" + cmp "$temporary/crlf" "$temporary/actual" + done + LC_ALL="$test_locale" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ + /usr/bin/git -C /usr/lib grep --no-index -P '^ID=ubuntu$' -- os-release > "$temporary/actual" + cmp "$temporary/os-release" "$temporary/actual" +done +echo PCRE2_RUNTIME_BACKPORT_OK diff --git a/server/build-api-explorer-patch-image.sh b/server/build-api-explorer-patch-image.sh index 6c2c44ab66..f9608ee2b4 100755 --- a/server/build-api-explorer-patch-image.sh +++ b/server/build-api-explorer-patch-image.sh @@ -14,7 +14,7 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then fi revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)} -server_release_tag=${SERVER_RELEASE_TAG:-v1.6.549} +server_release_tag=${SERVER_RELEASE_TAG:-v1.6.550} source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)} base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403} orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download} @@ -130,7 +130,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-6d4 usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-90ed65f689c91801abd0cf482211c96a01e7c63ef695e4faecdf4713eff730be} supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0' newest_docker_version=v29.8.0 -image=${IMAGE:-pasturestack-validation/server:v1.6.549} +image=${IMAGE:-pasturestack-validation/server:v1.6.550} build_options=() [[ "$revision" =~ ^[0-9a-f]{40}$ ]] @@ -1025,6 +1025,7 @@ EOF test "$(dpkg-query -W -f='"'"'${Version}'"'"' libpng16-16t64)" = 1.6.57-1ubuntu0.1 test "$(wc -l < /usr/share/pasturestack/security/libpng-runtime.sha256)" -eq 1 sha256sum -c /usr/share/pasturestack/security/libpng-runtime.sha256 + sh /usr/local/libexec/pasturestack/verify-pcre2-runtime.sh libpng_linkage=$(ldd -r /usr/lib/x86_64-linux-gnu/libpng16.so.16 2>&1) require_resolved_linkage "$libpng_linkage" test "$(readlink -f /usr/lib/x86_64-linux-gnu/libfreetype.so.6)" = /usr/lib/x86_64-linux-gnu/libfreetype.so.6.20.5 diff --git a/server/build-component-candidate.sh b/server/build-component-candidate.sh index 717710eaa2..76ee284545 100755 --- a/server/build-component-candidate.sh +++ b/server/build-component-candidate.sh @@ -18,7 +18,7 @@ for name in "${required[@]}"; do case "$name" in *_COMMIT) [[ ${!name} =~ ^[0-9a-f]{40}$ ]] ;; *) [[ ${!name} =~ ^[0-9a-f]{64}$ ]] ;; esac export "$name" done -export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.549} +export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.550} export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.354} export ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.195} diff --git a/server/security/openvex.json b/server/security/openvex.json index 667d89eee2..aad6b9272d 100644 --- a/server/security/openvex.json +++ b/server/security/openvex.json @@ -1,9 +1,9 @@ { "@context": "https://openvex.dev/ns/v0.2.0", - "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.549", + "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.550", "author": "PastureStack Security", - "timestamp": "2026-10-23T19:28:00Z", - "version": 62, + "timestamp": "2026-10-25T00:00:00Z", + "version": 63, "statements": [ { "vulnerability": { @@ -827,6 +827,126 @@ "status": "not_affected", "justification": "vulnerable_code_not_present", "impact_statement": "The official libexpat1 2.7.4-1ubuntu0.2 package update retains the base image runtime surface: git-http-push, libexpat.so.1 and libexpat.so.1.11.2 are absent. Post-build validation checks both files, the linker cache and native executable dependencies. This is component absence, not an upstream fix; Canonical still defers CVE-2025-66382 for Ubuntu 26.04." + }, + { + "vulnerability": { + "name": "CVE-2026-103111" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-103111 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-86145" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-86145 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89156" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89156 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89157" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89157 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89158" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89158 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89160" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89160 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89161" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89161 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." + }, + { + "vulnerability": { + "name": "CVE-2026-89162" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/libpcre2-8-0@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libpcre2-posix3@10.46-1build1%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "CVE-2026-89162 is fixed by the official PCRE2 release/pcre2-10.46 backports through 947809f11e9c9744aa0974b4b630a1ae461733dd (https://github.com/PCRE2Project/pcre2/blob/pcre2-10.49/SUPPORT-LIFECYCLE.md). This first-party 10.46-1build1+pasturestack.1 rebuild retains original Ubuntu source packaging and ABI; it is not issued by Canonical. All twelve exact SHA-256-pinned official backports, the original three native test suites, package metadata, ELF integrity and five runtime consumers are mandatory build/publication gates. Raw scanner findings remain available; this disposition applies only to the two exact patched package PURLs." } ] } diff --git a/server/security/ubuntu-backports/build-pcre2-backport.py b/server/security/ubuntu-backports/build-pcre2-backport.py new file mode 100644 index 0000000000..0ab0d046f6 --- /dev/null +++ b/server/security/ubuntu-backports/build-pcre2-backport.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +"""Build first-party PCRE2 .debs with Ubuntu packaging and pinned upstream fixes. + +Run in a disposable Ubuntu 26.04 amd64 builder with the original Build-Depends, +build-essential, fakeroot, patch, binutils, Python 3 and trusted CA certificates. +This helper never installs packages. Only the two selected runtime .debs leave +the build directory; the unchanged Debian rules run all upstream tests first. +""" +import argparse +import datetime +import email.utils +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import urllib.request + +HERE = Path(__file__).resolve().parent +LOCK_FILE = HERE / 'pcre2.lock.json' +PACKAGES = ['libpcre2-8-0', 'libpcre2-posix3'] +SOURCE_FILES = {'pcre2_10.46-1build1.dsc', 'pcre2_10.46.orig.tar.gz', + 'pcre2_10.46-1build1.diff.gz'} +TESTS = ['pcre2_jit_test', 'RunTest', 'RunGrepTest'] +CVES = ['CVE-2026-103111', 'CVE-2026-86145', 'CVE-2026-89156', 'CVE-2026-89157', + 'CVE-2026-89158', 'CVE-2026-89160', 'CVE-2026-89161', 'CVE-2026-89162'] + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def digest(file): + return hashlib.sha256(file.read_bytes()).hexdigest() + + +def job_option(jobs): + # Select a literal argv token; never synthesize commands from CLI input. + if jobs == 2: + return '-j2' + if jobs == 4: + return '-j4' + raise ValueError('Build jobs must be 2 or 4') + + +def load_lock(file): + lock = json.loads(file.read_text(encoding='utf-8')) + expected = {'schemaVersion': 1, 'sourcePackage': 'pcre2', + 'ubuntuSourceVersion': '10.46-1build1', + 'customVersion': '10.46-1build1+pasturestack.1', + 'distribution': 'resolute', 'osVersion': '26.04', + 'architecture': 'amd64', 'runtimePackages': PACKAGES, 'fixedCves': CVES} + require(all(lock.get(key) == value for key, value in expected.items()), + 'Unsupported PCRE2 lock identity') + require(type(lock.get('sourceDateEpoch')) is int and lock['sourceDateEpoch'] > 0, + 'SOURCE_DATE_EPOCH must be a positive integer') + sources, patches = lock['sourceFiles'], lock['patches'] + require(len(sources) == 3 and {item['name'] for item in sources} == SOURCE_FILES, + 'Exactly the Ubuntu .dsc, original tarball and Debian diff are required') + require(len(patches) == 12 and len({item['name'] for item in patches}) == 12 + and len({item['commit'] for item in patches}) == 12, + 'Exactly twelve distinct approved backports are required') + upstream = lock['upstream'] + require(upstream['baseTag'] == 'pcre2-10.46' + and upstream['baseCommit'] == 'b2bd4254b379b9d7dc9a3dda060a7e27009ccdff' + and upstream['backportBranch'] == 'release/pcre2-10.46' + and upstream['backportCommit'] == '947809f11e9c9744aa0974b4b630a1ae461733dd' + and patches[-1]['commit'] == upstream['backportCommit'], + 'Unexpected upstream release/backport boundary') + for item in sources + patches: + require(Path(item['name']).name == item['name'] and '/' not in item['name'] + and '\\' not in item['name'], 'Unsafe input filename') + require(re.fullmatch('[0-9a-f]{64}', item['sha256']) is not None, + 'Every source and patch needs a SHA-256 pin') + require(type(item['size']) is int and 0 < item['size'] < 8 * 1024 * 1024, + 'Invalid input size') + if item in sources: + expected_url = 'https://archive.ubuntu.com/ubuntu/pool/main/p/pcre2/' + item['name'] + else: + require(re.fullmatch('[0-9a-f]{40}', item['commit']) is not None, + 'Every patch needs a complete commit pin') + expected_url = 'https://github.com/PCRE2Project/pcre2/commit/' + item['commit'] + '.patch' + require(item['url'] == expected_url, 'Unexpected source or patch URL') + require(upstream['lifecycleUrl'] == 'https://raw.githubusercontent.com/PCRE2Project/pcre2/' + 'pcre2-10.49/SUPPORT-LIFECYCLE.md' + and re.fullmatch('[0-9a-f]{64}', upstream['lifecycleSha256']) is not None, + 'The official supported-backport list must be pinned') + return lock + + +class HttpsRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, request, file, code, message, headers, new_url): + require(new_url.startswith('https://'), 'Refusing HTTPS downgrade') + return super().redirect_request(request, file, code, message, headers, new_url) + + +def verify_input(item, file): + require(file.is_file() and digest(file) == item['sha256'], + 'SHA-256 mismatch: ' + item['name']) + if 'size' in item: + require(file.stat().st_size == item['size'], 'Size mismatch: ' + item['name']) + + +def download(item, directory, cache): + destination = directory / item['name'] + cached = cache / item['name'] if cache else None + if cached and cached.exists(): + # A poisoned cache fails closed instead of silently fetching other bytes. + verify_input(item, cached) + shutil.copyfile(cached, destination) + else: + opener = urllib.request.build_opener(HttpsRedirect()) + with opener.open(item['url'], timeout=60) as response, destination.open('xb') as output: + require(response.url.startswith('https://'), 'Refusing non-HTTPS input') + size = 0 + while True: + chunk = response.read(65536) + if not chunk: + break + size += len(chunk) + require(size < 8 * 1024 * 1024, 'Input exceeds download limit') + output.write(chunk) + verify_input(item, destination) + if cached: + shutil.copyfile(destination, cached) + verify_input(item, destination) + return destination + + +def verify_source_control(lock, directory): + text = (directory / 'pcre2_10.46-1build1.dsc').read_text(encoding='utf-8') + require(re.search(r'^Source: pcre2$', text, re.M) + and re.search(r'^Version: 10\.46-1build1$', text, re.M) + and re.search(r'^Format: 1\.0$', text, re.M), 'Unexpected Ubuntu source control') + block = re.search(r'^Checksums-Sha256:\n((?: .+\n)+)', text, re.M) + require(block is not None, 'Source control has no SHA-256 manifest') + actual = {name: (sha, int(size)) for sha, size, name in + re.findall(r'^ ([0-9a-f]{64}) (\d+) (\S+)$', block[1], re.M)} + expected = {item['name']: (item['sha256'], item['size']) for item in lock['sourceFiles'] + if not item['name'].endswith('.dsc')} + require(actual == expected, '.dsc source manifest does not match the lock') + + +def verify_supported_patches(lock, lifecycle): + section = re.search(r'(?s)### PCRE2 10\.46 .*?(?=### PCRE2)', lifecycle) + require(section is not None, 'Official lifecycle has no PCRE2 10.46 section') + names = re.findall(r'`patches/([^`]+)`', section[0]) + require(names == [item['name'] for item in lock['patches']], + 'Patch set/order differs from the official supported 10.46 backports') + + +def verify_patch(item, file): + text = file.read_text(encoding='utf-8') + require(text.startswith('From ' + item['commit'] + ' '), 'Patch commit identity mismatch') + paths = re.findall(r'^diff --git a/(\S+) b/(\S+)$', text, re.M) + require(paths and all(left == right and left.startswith(('src/', 'testdata/')) + and '..' not in Path(left).parts for left, right in paths), + 'Approved patches may only change upstream source and regression tests') + + +def build_environment(lock, jobs): + # Inherited DEB_BUILD_OPTIONS, compiler overrides or build profiles must not + # suppress tests, hardening, widths or JIT. No runtime APT is performed here. + return {'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + 'HOME': os.environ.get('HOME', '/tmp'), 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', + 'TZ': 'UTC', 'SOURCE_DATE_EPOCH': str(lock['sourceDateEpoch']), + 'DEB_BUILD_OPTIONS': 'parallel=' + str(jobs), + 'DEB_BUILD_MAINT_OPTIONS': 'hardening=+all', 'DEB_BUILD_PROFILES': '', + 'DPKG_COLORS': 'never'} + + +def run(command, directory, environment, log): + with log.open('ab') as stream: + stream.write(('\n$ ' + json.dumps(command) + '\n').encode()) + stream.flush() + process = subprocess.Popen(command, cwd=directory, env=environment, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for line in process.stdout: + stream.write(line) + stream.flush() + sys.stdout.buffer.write(line) + sys.stdout.buffer.flush() + require(process.wait() == 0, 'Command failed; see ' + str(log)) + + +def capture(command, directory, environment): + return subprocess.check_output(command, cwd=directory, env=environment, + text=True, encoding='utf-8').strip() + + +def packaging_snapshot(source): + return {file.relative_to(source).as_posix(): digest(file) + for file in sorted((source / 'debian').rglob('*')) + if file.is_file() and file.name != 'changelog'} + + +def test_receipt(source): + results = [] + for name in TESTS: + reports = list(source.rglob(name + '.trs')) + require(len(reports) == 1, 'Missing or ambiguous native test result: ' + name) + result = reports[0].read_text(encoding='utf-8') + require(re.search(r'^:test-result: PASS$', result, re.M) is not None, + 'Native test did not PASS: ' + name) + results.append({'test': name, 'status': 'PASS', + 'path': reports[0].relative_to(source).as_posix(), + 'sha256': digest(reports[0])}) + return results + + +def deb_field(file, field, directory, environment): + return capture(['dpkg-deb', '--field', str(file), field], directory, environment) + + +def require_separate_paths(output, work, cache): + for directory in [work, cache]: + if directory: + require(output != directory and output not in directory.parents + and directory not in output.parents, + 'Output must be separate from build/cache directories') + + +def verify_debs(lock, work, source, environment, log): + selected = [] + extracted = work / 'verify-runtime' + extracted.mkdir() + for package, soname in zip(PACKAGES, ['libpcre2-8.so.0', 'libpcre2-posix.so.3']): + matches = list(work.glob(package + '_' + lock['customVersion'] + '_amd64.deb')) + require(len(matches) == 1, 'Missing or ambiguous runtime .deb: ' + package) + deb = matches[0] + for field, expected in [('Package', package), ('Version', lock['customVersion']), + ('Architecture', 'amd64')]: + require(deb_field(deb, field, work, environment) == expected, + 'Unexpected .deb metadata: ' + field) + source_identity = deb_field(deb, 'Source', work, environment) + require(source_identity in ('pcre2', 'pcre2 (' + lock['customVersion'] + ')'), + 'Unexpected .deb Source identity') + run(['dpkg-deb', '--extract', str(deb), str(extracted)], work, environment, log) + library = extracted / 'usr/lib/x86_64-linux-gnu' / soname + require(library.is_file(), 'Expected runtime SONAME is absent: ' + soname) + dynamic = capture(['readelf', '--dynamic', str(library)], work, environment) + headers = capture(['readelf', '--program-headers', str(library)], work, environment) + require('[' + soname + ']' in dynamic, 'Runtime SONAME changed') + require('BIND_NOW' in dynamic or re.search(r'FLAGS_1.*NOW', dynamic), + 'Runtime library lacks immediate binding hardening') + require('GNU_RELRO' in headers, 'Runtime library lacks RELRO hardening') + copyright_file = extracted / 'usr/share/doc' / package / 'copyright' + require(copyright_file.is_file() and digest(copyright_file) == digest(source / 'debian/copyright'), + 'Original copyright must remain in each runtime package') + selected.append({'package': package, 'version': lock['customVersion'], + 'architecture': 'amd64', 'file': deb.name, 'sha256': digest(deb), + 'soname': soname, 'hardening': ['RELRO', 'BIND_NOW']}) + symbols = capture(['nm', '--dynamic', '--defined-only', + str(extracted / 'usr/lib/x86_64-linux-gnu/libpcre2-posix.so.3')], work, environment) + require(all(re.search(r'\bpcre2_' + name + r'(?:@@?\S+)?$', symbols, re.M) + for name in ['regcomp', 'regexec', 'regfree', 'regerror']), + 'Ubuntu POSIX wrapper ABI is missing') + return selected + + +def build(args, lock): + os_release = dict(re.findall(r'^(\w+)=["\']?([^"\'\n]+)', + Path('/etc/os-release').read_text(encoding='utf-8'), re.M)) + require(os_release.get('ID') == 'ubuntu' and os_release.get('VERSION_ID') == '26.04', + 'Use an isolated Ubuntu 26.04 builder') + environment = build_environment(lock, args.jobs) + for command in ['dpkg', 'dpkg-source', 'dpkg-buildpackage', 'dpkg-deb', + 'dpkg-buildflags', 'dpkg-checkbuilddeps', 'dpkg-query', 'patch', 'readelf', 'nm', 'fakeroot']: + require(shutil.which(command, path=environment['PATH']), 'Builder dependency missing: ' + command) + require(capture(['dpkg', '--print-architecture'], HERE, environment) == 'amd64', + 'Use an amd64 builder') + output = args.output_dir.resolve() + require(not output.exists(), 'Output directory must be new') + cache = args.cache_dir.resolve() if args.cache_dir else None + require_separate_paths(output, args.work_dir.resolve() if args.work_dir else None, cache) + if args.work_dir: + work = args.work_dir.resolve() + work.mkdir(parents=True, exist_ok=False) + else: + work = Path(tempfile.mkdtemp(prefix='pcre2-ubuntu-backport-')) + print('PCRE2_BACKPORT_WORK_DIR=' + str(work), flush=True) + inputs = work / 'inputs' + inputs.mkdir() + if cache: + cache.mkdir(parents=True, exist_ok=True) + for item in lock['sourceFiles'] + lock['patches']: + download(item, inputs, cache) + lifecycle = {'name': 'pcre2-10.49-SUPPORT-LIFECYCLE.md', + 'url': lock['upstream']['lifecycleUrl'], + 'sha256': lock['upstream']['lifecycleSha256']} + lifecycle_file = download(lifecycle, inputs, cache) + verify_supported_patches(lock, lifecycle_file.read_text(encoding='utf-8')) + verify_source_control(lock, inputs) + log = work / 'build.log' + source = work / 'pcre2-source' + run(['dpkg-source', '--extract', str(inputs / 'pcre2_10.46-1build1.dsc'), str(source)], + work, environment, log) + packaging = packaging_snapshot(source) + for item in lock['patches']: + patch = inputs / item['name'] + verify_input(item, patch) + verify_patch(item, patch) + run(['patch', '-p1', '--fuzz=0', '--batch', '--forward', '--input', str(patch)], + source, environment, log) + require(packaging_snapshot(source) == packaging, 'Upstream patches changed Debian packaging') + changelog = source / 'debian/changelog' + date = email.utils.format_datetime(datetime.datetime.fromtimestamp( + lock['sourceDateEpoch'], datetime.timezone.utc)) + entry = ('pcre2 (' + lock['customVersion'] + ') resolute; urgency=high\n\n' + ' * First-party PastureStack rebuild; not issued by Canonical.\n' + ' * Apply all twelve approved upstream 10.46 backports through\n' + ' 947809f11e9c9744aa0974b4b630a1ae461733dd, with regression tests.\n\n' + ' -- PastureStack maintainers ' + date + '\n\n') + changelog.write_text(entry + changelog.read_text(encoding='utf-8'), encoding='utf-8') + run(['dpkg-checkbuilddeps'], source, environment, log) + run(['dpkg-buildpackage', '-b', '-us', '-uc', job_option(args.jobs)], source, environment, log) + require({name: digest(source / name) for name in packaging} == packaging, + 'Build changed Debian packaging inputs') + tests = test_receipt(source) + selected = verify_debs(lock, work, source, environment, log) + output.mkdir(parents=True) + for item in selected: + shutil.copyfile(work / item['file'], output / item['file']) + shutil.copyfile(log, output / 'build.log') + shutil.copyfile(LOCK_FILE, output / 'pcre2.lock.json') + shutil.copyfile(lifecycle_file, output / lifecycle['name']) + shutil.copyfile(source / 'debian/copyright', output / 'COPYRIGHT') + for result in tests: + report = source / result['path'] + shutil.copyfile(report, output / (result['test'] + '.trs')) + test_log = report.with_suffix('.log') + require(test_log.is_file(), 'Native test output is missing') + shutil.copyfile(test_log, output / (result['test'] + '.log')) + for pattern in ['pcre2_*.buildinfo', 'pcre2_*.changes']: + files = list(work.glob(pattern)) + require(len(files) == 1, 'Native package build receipt is absent or ambiguous') + shutil.copyfile(files[0], output / files[0].name) + (output / 'builder-packages.tsv').write_text(capture( + ['dpkg-query', '--show', '--showformat=${binary:Package}\t${Version}\n'], + work, environment) + '\n', encoding='utf-8') + provenance = {'schemaVersion': 1, 'identity': lock['identity'], 'sourceLockSha256': digest(LOCK_FILE), + 'sourceDateEpoch': lock['sourceDateEpoch'], 'sourceFiles': lock['sourceFiles'], + 'upstream': lock['upstream'], 'patches': lock['patches'], 'fixedCves': lock['fixedCves'], + 'debianPackagingInputSha256': packaging, 'buildEnvironment': environment, + 'buildCommand': ['dpkg-buildpackage', '-b', '-us', '-uc', job_option(args.jobs)], + 'tests': tests, 'runtimePackages': selected, + 'scope': 'Ubuntu packaging built all configured widths/JIT; only two runtime .debs exported', + 'signaturePolicy': 'Source bytes are SHA-256 pinned from official Ubuntu HTTPS; no Canonical signing claim for custom outputs.'} + (output / 'provenance.json').write_text(json.dumps(provenance, indent=2) + '\n', encoding='utf-8') + (output / 'manifest.tsv').write_text('lock-sha256\t' + digest(LOCK_FILE) + '\n' + ''.join( + '\t'.join([item['package'], item['version'], item['architecture'], item['sha256'], item['file']]) + '\n' + for item in selected), encoding='utf-8') + (output / 'SHA256SUMS').write_text(''.join(digest(file) + ' ' + file.name + '\n' + for file in sorted(output.iterdir()) if file.is_file()), encoding='utf-8') + print('PCRE2_BACKPORT_BUILT packages=2 tests=3 lock=' + digest(LOCK_FILE), flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--validate-lock', action='store_true') + parser.add_argument('--output-dir', type=Path) + parser.add_argument('--work-dir', type=Path) + parser.add_argument('--cache-dir', type=Path) + parser.add_argument('--jobs', type=int, choices=(2, 4), default=2) + args = parser.parse_args() + lock = load_lock(LOCK_FILE) + if args.validate_lock: + print('PCRE2_BACKPORT_LOCK_OK sha256=' + digest(LOCK_FILE)) + return + require(args.output_dir is not None, '--output-dir is required for a native build') + build(args, lock) + + +if __name__ == '__main__': + try: + main() + except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error: + print('PCRE2_BACKPORT_FAILED: ' + str(error), file=sys.stderr) + sys.exit(1) diff --git a/server/security/ubuntu-backports/pcre2.lock.json b/server/security/ubuntu-backports/pcre2.lock.json new file mode 100644 index 0000000000..dce2e6c169 --- /dev/null +++ b/server/security/ubuntu-backports/pcre2.lock.json @@ -0,0 +1,141 @@ +{ + "schemaVersion": 1, + "sourcePackage": "pcre2", + "ubuntuSourceVersion": "10.46-1build1", + "customVersion": "10.46-1build1+pasturestack.1", + "distribution": "resolute", + "osVersion": "26.04", + "architecture": "amd64", + "sourceDateEpoch": 1792857600, + "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18107131", + "sourceFiles": [ + { + "name": "pcre2_10.46-1build1.dsc", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/pcre2/pcre2_10.46-1build1.dsc", + "sha256": "0e10c366d10e72b6940fcf37b61c2309d0136e05edff7adb1843030c0765f155", + "size": 2221 + }, + { + "name": "pcre2_10.46.orig.tar.gz", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/pcre2/pcre2_10.46.orig.tar.gz", + "sha256": "8d28d7f2c3b970c3a4bf3776bcbb5adfc923183ce74bc8df1ebaad8c1985bd07", + "size": 2718545 + }, + { + "name": "pcre2_10.46-1build1.diff.gz", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/pcre2/pcre2_10.46-1build1.diff.gz", + "sha256": "3534c69dec410207e74bf9fd6ed334ad5827fb6de1d0715175b7a14257c5bc72", + "size": 8804 + } + ], + "upstream": { + "repository": "https://github.com/PCRE2Project/pcre2", + "baseTag": "pcre2-10.46", + "baseCommit": "b2bd4254b379b9d7dc9a3dda060a7e27009ccdff", + "backportBranch": "release/pcre2-10.46", + "backportCommit": "947809f11e9c9744aa0974b4b630a1ae461733dd", + "lifecycleUrl": "https://raw.githubusercontent.com/PCRE2Project/pcre2/pcre2-10.49/SUPPORT-LIFECYCLE.md", + "lifecycleSha256": "64a3e10be90d7b3c218d05f21c53aa2511b9100e01f98c77c199c473b33f1856" + }, + "patches": [ + { + "name": "pcre2-10.47-Fix-for-callback.patch", + "commit": "16c1b6fb2a45c8bebcc46aa22e7c0b26759b77a0", + "url": "https://github.com/PCRE2Project/pcre2/commit/16c1b6fb2a45c8bebcc46aa22e7c0b26759b77a0.patch", + "sha256": "a7893d9a54fccd89c9eb2da0aead0cfae11bd3e95565f490dd57eba75c03f408", + "size": 6067 + }, + { + "name": "pcre2-10.48-Write-serialization-padding.patch", + "commit": "efeabb2dc9adfb91abf8feecd8daccb2a38ea94f", + "url": "https://github.com/PCRE2Project/pcre2/commit/efeabb2dc9adfb91abf8feecd8daccb2a38ea94f.patch", + "sha256": "ccb97e38f86bddc4944d091a4db18fe3dd8585421bd58fef92ab48b957a3facf", + "size": 3159 + }, + { + "name": "pcre2-10.48-Fix-character-list-generator.patch", + "commit": "a4318173dab0c332ab7d859f37ac71099326a0f2", + "url": "https://github.com/PCRE2Project/pcre2/commit/a4318173dab0c332ab7d859f37ac71099326a0f2.patch", + "sha256": "605e1c8e0a1baf7542d4d4833afe6721c80ab0b6a199b92d10748f04c3338a92", + "size": 5462 + }, + { + "name": "pcre2-10.48-Fix-scan-prefix-repeat-reset.patch", + "commit": "f945e356e0ba203a974717af2cff0baee01f1db4", + "url": "https://github.com/PCRE2Project/pcre2/commit/f945e356e0ba203a974717af2cff0baee01f1db4.patch", + "sha256": "e71e2b92a1cf76e42c08134055826911f7d5cd3398242dffee5b04cd57a49f78", + "size": 1436 + }, + { + "name": "pcre2-10.48-Fix-JIT-STR_END.patch", + "commit": "0aa5bbca3ac70e6c22562488ead6cb2032447fc5", + "url": "https://github.com/PCRE2Project/pcre2/commit/0aa5bbca3ac70e6c22562488ead6cb2032447fc5.patch", + "sha256": "d835898636b4063101d1cb546e6e044f888cb82d1e2f564bdd8230fd767528fa", + "size": 1573 + }, + { + "name": "pcre2-10.48-Fix-JIT-match-context-reuse.patch", + "commit": "e5b9232fc4cd6a0df312ae25568e72cd337cafa2", + "url": "https://github.com/PCRE2Project/pcre2/commit/e5b9232fc4cd6a0df312ae25568e72cd337cafa2.patch", + "sha256": "708a2cfe0a786411952a5de364a054f4a3ab1027aa9a8b1fc4a87d8adf780df7", + "size": 6423 + }, + { + "name": "pcre2-10.48-Check-JIT-mode-before-validation.patch", + "commit": "ab7097fbde778e0e4d17752ba2a14a7430d35385", + "url": "https://github.com/PCRE2Project/pcre2/commit/ab7097fbde778e0e4d17752ba2a14a7430d35385.patch", + "sha256": "76e4fdd573ecc3ee51dc822c33808bb8a74ce77884f29fbfc181b634662d7f56", + "size": 9222 + }, + { + "name": "pcre2-10.48-Fix-allocation-byte-sizing.patch", + "commit": "31ec59526d641b85108c726fe201effc5dca8627", + "url": "https://github.com/PCRE2Project/pcre2/commit/31ec59526d641b85108c726fe201effc5dca8627.patch", + "sha256": "437013bddd46cc860784b9261dbc94b024c6c4ea22d3f235aadd33f22e591167", + "size": 2799 + }, + { + "name": "pcre2-10.48-Fix-invalid-UTF-backwards-scans.patch", + "commit": "e1fd2e68fe364d14a01ec42bf07201d9820eee8e", + "url": "https://github.com/PCRE2Project/pcre2/commit/e1fd2e68fe364d14a01ec42bf07201d9820eee8e.patch", + "sha256": "72d1621c28afa0265e7bb01f6876eea43eb5d968b3ce8fe5b4f3fe78c5e5a893", + "size": 6835 + }, + { + "name": "pcre2-10.48-Fix-compiler-integer-overflows.patch", + "commit": "983158a222ec27a93ce678778f9343f7dfa35845", + "url": "https://github.com/PCRE2Project/pcre2/commit/983158a222ec27a93ce678778f9343f7dfa35845.patch", + "sha256": "1968d664b3a2f02d6256886d2049ba802c07cc797877765d1c2daac678c1363b", + "size": 6693 + }, + { + "name": "pcre2-10.48-Fix-DFA-workspace-overflows.patch", + "commit": "bcaf1ba40748a27e1e666491a0e9887f0f5f2965", + "url": "https://github.com/PCRE2Project/pcre2/commit/bcaf1ba40748a27e1e666491a0e9887f0f5f2965.patch", + "sha256": "1e906da52268ecdd03459d1086922af24ac010f8672a26d6638fc22e395bd6bb", + "size": 5636 + }, + { + "name": "pcre2-10.49-Fix-large-stack-allocation.patch", + "commit": "947809f11e9c9744aa0974b4b630a1ae461733dd", + "url": "https://github.com/PCRE2Project/pcre2/commit/947809f11e9c9744aa0974b4b630a1ae461733dd.patch", + "sha256": "cdd5e0241e5dd3a7a3e3f773c10f20c8f353063fcc8f737f6a391503cb6bd037", + "size": 4535 + } + ], + "runtimePackages": [ + "libpcre2-8-0", + "libpcre2-posix3" + ], + "fixedCves": [ + "CVE-2026-103111", + "CVE-2026-86145", + "CVE-2026-89156", + "CVE-2026-89157", + "CVE-2026-89158", + "CVE-2026-89160", + "CVE-2026-89161", + "CVE-2026-89162" + ], + "identity": "First-party PastureStack rebuild of official Ubuntu source with approved upstream backports; not a Canonical-issued package." +} diff --git a/server/security/ubuntu-backports/test_pcre2_backport.py b/server/security/ubuntu-backports/test_pcre2_backport.py new file mode 100644 index 0000000000..c4f0dd21d1 --- /dev/null +++ b/server/security/ubuntu-backports/test_pcre2_backport.py @@ -0,0 +1,192 @@ +"""Focused, network-free checks for the pinned PCRE2 source-build boundary.""" +import copy +import hashlib +import importlib.util +import json +import os +import subprocess +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock +import urllib.request + +HERE = Path(__file__).resolve().parent +sys.dont_write_bytecode = True +SPEC = importlib.util.spec_from_file_location('pcre2_backport', HERE / 'build-pcre2-backport.py') +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class Pcre2Backport(unittest.TestCase): + def test_job_flags_are_literal_and_untrusted_lock_override_is_rejected(self): + self.assertEqual('-j2', MODULE.job_option(2)) + self.assertEqual('-j4', MODULE.job_option(4)) + for value in [0, 1, 32, '2', '-j2;false']: + with self.subTest(value=value), self.assertRaises(ValueError): + MODULE.job_option(value) + result = subprocess.run([sys.executable, str(HERE / 'build-pcre2-backport.py'), + '--validate-lock', '--lock', str(HERE / 'pcre2.lock.json')], + capture_output=True, text=True) + self.assertEqual(2, result.returncode) + self.assertIn('unrecognized arguments: --lock', result.stderr) + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix='pcre2-backport-contract-') + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.lock = MODULE.load_lock(HERE / 'pcre2.lock.json') + + def load(self, value): + file = self.root / 'fixture.lock.json' + file.write_text(json.dumps(value), encoding='utf-8') + return MODULE.load_lock(file) + + def test_locked_official_sources_and_complete_approved_boundary(self): + self.assertEqual('0e10c366d10e72b6940fcf37b61c2309d0136e05edff7adb1843030c0765f155', + self.lock['sourceFiles'][0]['sha256']) + self.assertEqual(12, len(self.lock['patches'])) + self.assertEqual(8, len(self.lock['fixedCves'])) + self.assertEqual(MODULE.PACKAGES, self.lock['runtimePackages']) + self.assertEqual('947809f11e9c9744aa0974b4b630a1ae461733dd', + self.lock['patches'][-1]['commit']) + + def test_foreign_source_version_architecture_or_package_set_fails(self): + for field, bad in [('ubuntuSourceVersion', '10.49-1'), ('customVersion', '10.46-1build1'), + ('architecture', 'arm64'), ('runtimePackages', ['libpcre2-8-0']), + ('fixedCves', ['CVE-2026-103111']), + ('sourceDateEpoch', True)]: + with self.subTest(field=field): + altered = copy.deepcopy(self.lock) + altered[field] = bad + with self.assertRaises(ValueError): self.load(altered) + + def test_missing_duplicate_or_unpinned_patch_is_rejected(self): + for mutation in ['missing', 'duplicate', 'hash', 'commit', 'path', 'url']: + with self.subTest(mutation=mutation): + altered = copy.deepcopy(self.lock) + if mutation == 'missing': altered['patches'].pop(0) + if mutation == 'duplicate': altered['patches'][1] = altered['patches'][0] + if mutation == 'hash': altered['patches'][0]['sha256'] = '' + if mutation == 'commit': altered['patches'][0]['commit'] = '16c1b6f' + if mutation == 'path': altered['patches'][0]['name'] = '../outside.patch' + if mutation == 'url': altered['patches'][0]['url'] = 'http://untrusted.invalid/patch' + with self.assertRaises(ValueError): self.load(altered) + + def test_source_set_and_source_url_cannot_be_replaced(self): + for mutation in ['missing', 'url', 'duplicate']: + with self.subTest(mutation=mutation): + altered = copy.deepcopy(self.lock) + if mutation == 'missing': altered['sourceFiles'].pop() + if mutation == 'duplicate': altered['sourceFiles'][1] = altered['sourceFiles'][0] + if mutation == 'url': altered['sourceFiles'][0]['url'] = 'https://example.invalid/source.dsc' + with self.assertRaises(ValueError): self.load(altered) + + def test_cached_bytes_are_verified_and_tamper_never_fetches(self): + cache, output = self.root / 'cache', self.root / 'input' + cache.mkdir(); output.mkdir() + data = b'official fixture bytes\n' + item = {'name': 'input.patch', 'url': 'https://example.invalid/never-requested', + 'sha256': hashlib.sha256(data).hexdigest(), 'size': len(data)} + cached = cache / item['name']; cached.write_bytes(data) + with mock.patch.object(urllib.request, 'build_opener') as network: + self.assertEqual(data, MODULE.download(item, output, cache).read_bytes()) + cached.write_bytes(data + b'tampered') + with self.assertRaises(ValueError): MODULE.download(item, output, cache) + network.assert_not_called() + + def test_https_redirect_cannot_downgrade(self): + request = urllib.request.Request('https://github.com/official/patch') + with self.assertRaises(ValueError): + MODULE.HttpsRedirect().redirect_request(request, None, 302, 'Found', {}, + 'http://example.invalid/patch') + + def source_control(self): + inputs = [item for item in self.lock['sourceFiles'] if not item['name'].endswith('.dsc')] + return ('Format: 1.0\nSource: pcre2\nVersion: 10.46-1build1\nChecksums-Sha256:\n' + + ''.join(' {sha256} {size} {name}\n'.format(**item) for item in inputs)) + + def test_dsc_binds_original_and_debian_source_checksums(self): + file = self.root / 'pcre2_10.46-1build1.dsc' + original = self.source_control() + file.write_text(original, encoding='utf-8') + MODULE.verify_source_control(self.lock, self.root) + for altered in [original.replace('10.46-1build1\n', '10.49-1\n'), + original.replace(self.lock['sourceFiles'][1]['sha256'], '0' * 64), + original.replace('Source: pcre2', 'Source: other'), + original.rsplit('\n', 2)[0] + '\n']: + with self.subTest(altered=altered): + file.write_text(altered, encoding='utf-8') + with self.assertRaises(ValueError): MODULE.verify_source_control(self.lock, self.root) + + def test_lifecycle_set_and_order_must_match(self): + section = ('### PCRE2 10.46 release\nBackports to apply:\n' + ''.join( + '* `patches/' + item['name'] + '`\n' for item in self.lock['patches']) + '### PCRE2 10.47\n') + MODULE.verify_supported_patches(self.lock, section) + with self.assertRaises(ValueError): + MODULE.verify_supported_patches(self.lock, section.replace( + self.lock['patches'][0]['name'], 'unapproved.patch')) + reversed_lock = copy.deepcopy(self.lock) + reversed_lock['patches'].reverse() + with self.assertRaises(ValueError): MODULE.verify_supported_patches(reversed_lock, section) + + def test_patch_identity_and_packaging_boundary(self): + item = self.lock['patches'][0] + file = self.root / 'input.patch' + original = ('From ' + item['commit'] + ' Mon Sep 17 00:00:00 2001\n' + 'diff --git a/src/pcre2_match.c b/src/pcre2_match.c\n') + file.write_text(original, encoding='utf-8') + MODULE.verify_patch(item, file) + for altered in [original.replace(item['commit'], '0' * 40), + original.replace('src/pcre2_match.c', 'debian/rules'), + original.replace('src/pcre2_match.c', 'src/../../outside')]: + file.write_text(altered, encoding='utf-8') + with self.assertRaises(ValueError): MODULE.verify_patch(item, file) + + def test_inherited_environment_cannot_skip_tests_or_replace_flags(self): + with mock.patch.dict(os.environ, {'DEB_BUILD_OPTIONS': 'nocheck noopt', + 'DEB_BUILD_PROFILES': 'nocheck', 'CFLAGS': '-O0', + 'DEB_CFLAGS_MAINT_APPEND': '-Dunsafe=1'}): + environment = MODULE.build_environment(self.lock, 2) + self.assertEqual('parallel=2', environment['DEB_BUILD_OPTIONS']) + self.assertEqual('', environment['DEB_BUILD_PROFILES']) + self.assertEqual('hardening=+all', environment['DEB_BUILD_MAINT_OPTIONS']) + self.assertEqual(str(self.lock['sourceDateEpoch']), environment['SOURCE_DATE_EPOCH']) + self.assertNotIn('CFLAGS', environment) + self.assertNotIn('DEB_CFLAGS_MAINT_APPEND', environment) + + def test_native_pass_receipt_requires_every_real_test_report(self): + for name in MODULE.TESTS: + (self.root / (name + '.trs')).write_text(':test-result: PASS\n', encoding='utf-8') + self.assertEqual(3, len(MODULE.test_receipt(self.root))) + file = self.root / 'RunTest.trs' + for result in ['FAIL', 'SKIP', 'XFAIL', 'ERROR']: + file.write_text(':test-result: ' + result + '\n', encoding='utf-8') + with self.assertRaises(ValueError): MODULE.test_receipt(self.root) + file.unlink() + with self.assertRaises(ValueError): MODULE.test_receipt(self.root) + + def test_original_packaging_snapshot_excludes_only_changelog(self): + debian = self.root / 'debian'; debian.mkdir() + (debian / 'rules').write_bytes(b'original rules\n') + (debian / 'copyright').write_bytes(b'original copyright\n') + (debian / 'changelog').write_bytes(b'original changelog\n') + before = MODULE.packaging_snapshot(self.root) + (debian / 'changelog').write_bytes(b'first-party rebuild entry\n') + self.assertEqual(before, MODULE.packaging_snapshot(self.root)) + (debian / 'rules').write_bytes(b'--disable-jit\n') + self.assertNotEqual(before, MODULE.packaging_snapshot(self.root)) + + def test_output_cannot_overlap_build_or_download_cache(self): + MODULE.require_separate_paths(self.root / 'out', self.root / 'build', self.root / 'cache') + for other in [self.root / 'out', self.root / 'out/nested', self.root]: + with self.subTest(other=other): + with self.assertRaises(ValueError): + MODULE.require_separate_paths(self.root / 'out', other, None) + with self.assertRaises(ValueError): + MODULE.require_separate_paths(self.root / 'out', None, other) + + +if __name__ == '__main__': + unittest.main() diff --git a/server/security/vendor-pending.json b/server/security/vendor-pending.json index 09fe071597..097ec72aca 100644 --- a/server/security/vendor-pending.json +++ b/server/security/vendor-pending.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "release": "v1.6.549", - "generatedAt": "2026-10-23T03:05:10Z", + "release": "v1.6.550", + "generatedAt": "2026-10-25T00:00:00Z", "policy": { "allowedSeverities": [ "LOW", @@ -43,26 +43,6 @@ "verifiedAt": "2026-10-20T01:35:18Z", "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-102474.json" }, - { - "vulnerabilityId": "CVE-2026-103111", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Vulnerable", - "source": "https://ubuntu.com/security/CVE-2026-103111", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-103111.json" - }, { "vulnerabilityId": "CVE-2026-107161", "severity": "MEDIUM", @@ -99,25 +79,6 @@ "verifiedAt": "2026-10-20T01:35:18Z", "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-67693.json" }, - { - "vulnerabilityId": "CVE-2026-86145", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "source": "https://ubuntu.com/security/CVE-2026-86145", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-86145.json" - }, { "vulnerabilityId": "CVE-2026-88647", "severity": "MEDIUM", @@ -173,125 +134,6 @@ "verifiedAt": "2026-10-20T01:35:18Z", "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89092.json" }, - { - "vulnerabilityId": "CVE-2026-89156", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Vulnerable", - "source": "https://ubuntu.com/security/CVE-2026-89156", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89156.json" - }, - { - "vulnerabilityId": "CVE-2026-89157", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Vulnerable", - "source": "https://ubuntu.com/security/CVE-2026-89157", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89157.json" - }, - { - "vulnerabilityId": "CVE-2026-89158", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Vulnerable", - "source": "https://ubuntu.com/security/CVE-2026-89158", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89158.json" - }, - { - "vulnerabilityId": "CVE-2026-89160", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Vulnerable", - "source": "https://ubuntu.com/security/CVE-2026-89160", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89160.json" - }, - { - "vulnerabilityId": "CVE-2026-89161", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "source": "https://ubuntu.com/security/CVE-2026-89161", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89161.json" - }, - { - "vulnerabilityId": "CVE-2026-89162", - "severity": "MEDIUM", - "vendorStatus": "needs-evaluation", - "upstreamVendorStatus": "Needs evaluation", - "source": "https://ubuntu.com/security/CVE-2026-89162", - "reviewAfter": "2026-10-27", - "packages": [ - { - "name": "libpcre2-8-0", - "installedVersion": "10.46-1build1" - }, - { - "name": "libpcre2-posix3", - "installedVersion": "10.46-1build1" - } - ], - "verifiedAt": "2026-10-20T01:35:18Z", - "vendorEvidence": "https://raw.githubusercontent.com/canonical/ubuntu-security-notices/main/osv/cve/2026/UBUNTU-CVE-2026-89162.json" - }, { "vulnerabilityId": "CVE-2026-95184", "severity": "MEDIUM",