From 7df8ef19eb74226bb70b957d979b5614bf9b07ee Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:01:07 +0800 Subject: [PATCH] Build the documented GNU fixture and reuse qualified PCRE package bytes --- .../publish-ubuntu-security-backports.yml | 13 +- .../bundle/installer/README.md | 7 +- .../installer/consumer-fixture.Dockerfile | 16 +- .../bundle/qualify-native-packages.py | 154 ++++++++++++++---- .../bundle/source-inputs.lock.json | 88 +++++++++- .../bundle/test_qualify_native_packages.py | 43 ++++- 6 files changed, 269 insertions(+), 52 deletions(-) diff --git a/.github/workflows/publish-ubuntu-security-backports.yml b/.github/workflows/publish-ubuntu-security-backports.yml index 2c4c505ff7..a6d2826ac0 100644 --- a/.github/workflows/publish-ubuntu-security-backports.yml +++ b/.github/workflows/publish-ubuntu-security-backports.yml @@ -93,7 +93,7 @@ jobs: path: pcre2-source persist-credentials: false - - name: Build only the released PCRE2 producer and static installer + - name: Verify released PCRE2 source and build the current static installer working-directory: source env: WORK: ${{ steps.inputs.outputs.work }} @@ -102,17 +102,6 @@ jobs: qualifier=server/security/ubuntu-backports/bundle/qualify-native-packages.py python3 -B "$qualifier" --check-pcre2-source mkdir "$WORK/publisher" - python3 - <<'PY' - import os - from pathlib import Path - original = Path('../pcre2-source/server/Dockerfile.web-compose-release').read_bytes() - appended = b'\nFROM scratch AS pcre2_bundle_inputs\nCOPY --from=runtime_security_packages /out/pcre2/ /pcre2/\n' - Path(os.environ['WORK'], 'Dockerfile.pcre2').write_bytes(original + appended) - PY - docker buildx build --platform linux/amd64 --progress plain --no-cache \ - --file "$WORK/Dockerfile.pcre2" --target pcre2_bundle_inputs \ - --output "type=local,dest=$WORK/pcre2-export" ../pcre2-source/server \ - 2>&1 | tee "$WORK/publisher/pcre2-build.log" docker buildx build --platform linux/amd64 --progress plain --no-cache \ --file server/security/ubuntu-backports/bundle/Dockerfile.bundle --target installer_export \ --output "type=local,dest=$WORK/installer-export" server/security/ubuntu-backports/bundle \ diff --git a/server/security/ubuntu-backports/bundle/installer/README.md b/server/security/ubuntu-backports/bundle/installer/README.md index 310d9b5f7a..d60344b071 100644 --- a/server/security/ubuntu-backports/bundle/installer/README.md +++ b/server/security/ubuntu-backports/bundle/installer/README.md @@ -55,5 +55,8 @@ read-only qualified bundle bytes, restore deliberately path-excluded docs, perform actual install/verify, preserve package set and provider, and exercise the installed glibc loader/cache, jq, id, p11-kit, PCRE grep and NSS getent. `result.json` and the console/install/verify receipts are exported. These are -isolated consumer fixtures, with normal official package/provider setup and no -force flags. They do not replace acceptance in each actual product rootfs. +isolated consumer fixtures. GNU baseline construction follows Ubuntu's documented +provider transition, including its required `--allow-remove-essential` option; +this is confined to test setup, before the backport installer runs. The installer +does not invoke apt or switch providers. These fixtures do not replace acceptance +in each actual product rootfs. diff --git a/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile index 2a9e6a82a8..5275adb14f 100644 --- a/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile +++ b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile @@ -34,22 +34,26 @@ APT # files. The installer must enumerate missing docs and restore exact paths. printf 'path-exclude=/usr/share/doc/*\n' >/etc/dpkg/dpkg.cfg.d/01fixture-doc-exclusions apt-get update +# Construct the explicitly selected GNU fixture using Ubuntu's documented +# provider transition. This is baseline setup, before the backport installer; +# the installer itself must preserve whichever provider was selected here. +# https://ubuntu.com/server/docs/reference/other-tools/sudo-rs/ +if [[ "$PROVIDER" == gnu ]]; then + apt-get install -y --no-install-recommends --allow-remove-essential \ + coreutils-from-gnu=0.0.0~ubuntu25 coreutils-from-uutils- rust-coreutils- +fi set -- \ libpcre2-8-0=10.46-1build1 libpcre2-posix3=10.46-1build1 \ libc6=2.43-2ubuntu2.4 libc-bin=2.43-2ubuntu2.4 libc-gconv-modules-extra=2.43-2ubuntu2.4 \ jq=1.8.1-4ubuntu2 libjq1=1.8.1-4ubuntu2 \ p11-kit=0.26.2-2 p11-kit-modules=0.26.2-2 libp11-kit0=0.26.2-2 \ - 'libxrender1=1:0.9.12-1build1' gnu-coreutils=9.7-3ubuntu2 ca-certificates grep + 'libxrender1=1:0.9.12-1build1' gnu-coreutils=9.7-3ubuntu2.1 ca-certificates grep case "$PROVIDER" in gnu) set -- "$@" 'coreutils-from-gnu=0.0.0~ubuntu25' ;; uutils) set -- "$@" 'coreutils-from-uutils=0.0.0~ubuntu25' 'rust-coreutils=0.10.0-1ubuntu2~26.04.1' ;; esac -# Baseline construction uses the official provider transition without force or -# protected-package overrides. A refused transition fails the fixture. +# All baseline runtime versions are explicit; no dependency force is used. apt-get install --no-install-recommends --reinstall -y "$@" -if [[ "$PROVIDER" == gnu ]]; then - apt-get remove -y rust-coreutils -fi rm -rf /var/lib/apt/lists/* /tmp/ca-bootstrap /tmp/ca-certificates.deb SH COPY consumer-smoke.sh /consumer-smoke.sh diff --git a/server/security/ubuntu-backports/bundle/qualify-native-packages.py b/server/security/ubuntu-backports/bundle/qualify-native-packages.py index 927a1ffd34..48babfae9c 100644 --- a/server/security/ubuntu-backports/bundle/qualify-native-packages.py +++ b/server/security/ubuntu-backports/bundle/qualify-native-packages.py @@ -41,6 +41,14 @@ 'p11-kit': ['libp11-kit0', 'p11-kit', 'p11-kit-modules'], 'libxrender': ['libxrender1'], } BASE = 'server/security/ubuntu-backports/' +PCRE2_STAGE_STEPS = [ + {'number': 8, 'name': 'Build only the released PCRE2 producer and static installer', 'conclusion': 'success'}, + {'number': 9, 'name': 'Qualify package bytes, native receipts, payload metadata, SBOM and exact-package VEX', 'conclusion': 'success'}, + {'number': 10, 'name': 'Require real installation in both isolated Ubuntu provider fixtures', 'conclusion': 'failure'}, + {'number': 11, 'name': 'Publish the new build-only OCI data artifact', 'conclusion': 'skipped'}, +] +PCRE2_STAGE_RECEIPTS = {name: 'context/out/' + name for name in + ['manifest.json', 'qualification.json', 'source-inputs.lock.json']} def require(ok, message): @@ -118,13 +126,32 @@ def load_lock(ready=False): require(re.fullmatch('[0-9a-f]{64}', package['sha256']) and safe_name(package['file']).name == package['file'] and package['source'] == family and package['version'] == item['packages'][0]['version'], 'Unpinned/unsafe deb') - require(lock['pcre2']['kind'] == 'fresh-production-target' + require(lock['pcre2']['kind'] == 'qualified-production-stage' and lock['pcre2']['runtimePackages'] == FAMILIES['pcre2'] and lock['pcre2']['target'] == 'runtime_security_packages' and lock['pcre2']['sourceTag'] == 'v1.6.551' and re.fullmatch('[0-9a-f]{40}', lock['pcre2']['sourceCommit']) and re.fullmatch('[0-9a-f]{64}', lock['pcre2']['lockSha256']) and re.fullmatch('[0-9a-f]{64}', lock['pcre2']['copyrightSha256']), 'Wrong PCRE2 build contract') + stage = lock['pcre2']['stageInput'] + require(stage['status'] == 'pcre2-qualified-before-consumer-failure' + and re.fullmatch('[0-9a-f]{40}', stage['publisherCommit']) + and all(type(stage[key]) is int for key in ['runId', 'jobId', 'artifactId']) + and re.fullmatch('[0-9a-f]{64}', stage['zipSha256']) + and stage['runConclusion'] == stage['jobConclusion'] == 'failure' + and stage['stepOutcomes'] == PCRE2_STAGE_STEPS + and set(stage['metadataSha256']) == set(PCRE2_STAGE_RECEIPTS) + and set(stage['sourceFiles']) == {'.github/workflows/publish-ubuntu-security-backports.yml', + BASE + 'bundle/qualify-native-packages.py', BASE + 'bundle/source-inputs.lock.json'} + and stage['metadataSha256']['source-inputs.lock.json'] == stage['sourceFiles'][BASE + 'bundle/source-inputs.lock.json'] + and [p['name'] for p in stage['packages']] == FAMILIES['pcre2'], 'PCRE2 stage evidence is not precisely pinned') + require(all(re.fullmatch('[0-9a-f]{64}', digest) for digest in + [*stage['metadataSha256'].values(), *stage['sourceFiles'].values(), *stage['evidence'].values(), stage['buildLogSha256']]), + 'PCRE2 stage source/evidence SHA pin is malformed') + for p in stage['packages']: + require(p['source'] == 'pcre2' and p['version'] == lock['pcre2']['version'] + and safe_name(p['file']).name == p['file'] and re.fullmatch('[0-9a-f]{64}', p['sha256']), + 'Unexpected pinned PCRE2 package') load_rust_source_audit(lock['nativeInputs']['rust-coreutils']) return lock @@ -227,10 +254,17 @@ def check_current_source(lock): check_source_files(ROOT, {name: proof['currentSha256']}) original = subprocess.check_output(['git', 'show', item['sourceCommit'] + ':' + name], cwd=ROOT) require(sha(original) == proof['nativeSha256'], 'Native CI source pin changed') + stage = lock['pcre2']['stageInput'] + for name, digest in stage['sourceFiles'].items(): + safe_name(name) + require(sha(command(['git', 'show', stage['publisherCommit'] + ':' + name])) == digest, + 'Original PCRE2 build/qualification recipe changed: ' + name) def ensure_native_sources(lock): - for commit in sorted({item['sourceCommit'] for item in lock['nativeInputs'].values()}): + commits = {item['sourceCommit'] for item in lock['nativeInputs'].values()} + commits.add(lock['pcre2']['stageInput']['publisherCommit']) + for commit in sorted(commits): require(re.fullmatch('[0-9a-f]{40}', commit), 'Native producer must be an exact commit SHA') probe = ['git', 'cat-file', '-e', commit + '^{commit}'] if subprocess.run(probe, cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode: @@ -267,14 +301,34 @@ def verify_api(family, item, run, job, artifact): and artifact['workflow_run']['head_sha'] == item['sourceCommit'], 'Artifact API ownership mismatch') +def verify_pcre2_api(stage, run, job, artifact): + require(run['id'] == stage['runId'] and run['status'] == 'completed' and run['conclusion'] == 'failure' + and run['head_sha'] == stage['publisherCommit'] and run['event'] == 'workflow_dispatch' + and run['path'] == '.github/workflows/publish-ubuntu-security-backports.yml', 'PCRE2 publisher run mismatch') + require(job['id'] == stage['jobId'] and job['run_id'] == stage['runId'] and job['name'] == 'publish' + and job['status'] == 'completed' and job['conclusion'] == 'failure' + and job['head_sha'] == stage['publisherCommit'], 'Original publisher job failure must be retained') + for expected in PCRE2_STAGE_STEPS: + matches = [s for s in job['steps'] if s['name'] == expected['name']] + require(len(matches) == 1 and matches[0]['number'] == expected['number'] + and matches[0]['status'] == 'completed' and matches[0]['conclusion'] == expected['conclusion'], + 'PCRE2/qualification/consumer/publication step outcome mismatch: ' + expected['name']) + require(artifact['id'] == stage['artifactId'] and not artifact['expired'] + and artifact['name'] == stage['artifactName'] and artifact['digest'] == 'sha256:' + stage['zipSha256'] + and artifact['workflow_run']['id'] == stage['runId'] + and artifact['workflow_run']['head_sha'] == stage['publisherCommit'], 'PCRE2 stage artifact ownership mismatch') + + def fetch_native(lock): require(not WORK.exists(), 'Bundle staging directory must be new') (WORK / 'inputs').mkdir(parents=True) - for family, item in lock['nativeInputs'].items(): + inputs = list(lock['nativeInputs'].items()) + [('pcre2', lock['pcre2']['stageInput'])] + for family, item in inputs: run = gh_json('actions/runs/' + str(item['runId'])) job = gh_json('actions/jobs/' + str(item['jobId'])) artifact = gh_json('actions/artifacts/' + str(item['artifactId'])) - verify_api(family, item, run, job, artifact) + if family == 'pcre2': verify_pcre2_api(item, run, job, artifact) + else: verify_api(family, item, run, job, artifact) write_json(WORK / 'inputs' / (family + '-api.json'), {'run': run, 'job': job, 'artifact': artifact}) with (WORK / 'inputs' / (family + '.zip')).open('xb') as stream: subprocess.run(['gh', 'api', 'repos/' + REPO + '/actions/artifacts/' + @@ -452,6 +506,67 @@ def command(args): return subprocess.check_output(args, cwd=ROOT, stderr=subprocess.PIPE) +def pcre2_evidence(stage): + return dict(stage['evidence'], **{'production-stage-' + name: digest + for name, digest in stage['metadataSha256'].items()}, **{'production-stage-build.log': stage['buildLogSha256']}) + + +def validate_pcre2(item, archive, api, test_directory): + stage = item['stageInput']; verify_pcre2_api(stage, api['run'], api['job'], api['artifact']) + contents = read_zip(archive, stage['zipSha256']) + documents = {} + for name, path in PCRE2_STAGE_RECEIPTS.items(): + require(sha(contents[path]) == stage['metadataSha256'][name], 'Original PCRE2 qualification metadata changed') + documents[name] = json.loads(contents[path]) + original = documents['source-inputs.lock.json']['pcre2'] + require(original['kind'] == 'fresh-production-target' and all(original[key] == item[key] for key in + ['sourceCommit', 'sourceTag', 'target', 'version', 'sourceFiles', 'lockSha256', 'copyrightSha256', + 'runtimePackages', 'fixedVulnerabilities']), 'Original PCRE2 released551 source/recipe closure mismatch') + manifest, qualification = documents['manifest.json'], documents['qualification.json'] + require(manifest['schemaVersion'] == qualification['schemaVersion'] == 1 + and manifest['platform'] == qualification['platform'] == PLATFORM + and manifest['qualification'] == {'path': 'qualification.json', 'sha256': stage['metadataSha256']['qualification.json']} + and qualification['status'] == 'qualified' and 'consumerFixtures' not in qualification + and qualification['sourceLockSha256'] == stage['metadataSha256']['source-inputs.lock.json'], + 'PCRE2 stage was not qualified before the failed consumer fixture') + families = [f for f in qualification['families'] if f['name'] == 'pcre2'] + require(len(families) == 1 and families[0]['status'] == 'qualified' and families[0]['nativeInput'] == original + and families[0]['provenanceSha256'] == stage['evidence']['provenance.json'], 'Original PCRE2 family qualification mismatch') + prefix = 'context/out/provenance/pcre2/' + files = {name.removeprefix(prefix): data for name, data in contents.items() if name.startswith(prefix)} + require(set(files) == set(stage['evidence']) and all(sha(files[n]) == h for n, h in stage['evidence'].items()), + 'PCRE2 native evidence differs from pinned stage bytes') + require(sha(files['pcre2.lock.json']) == item['lockSha256'] and sha(files['COPYRIGHT']) == item['copyrightSha256'], + 'PCRE2 production source lock/COPYRIGHT mismatch') + provenance = json.loads(files['provenance.json']); source_lock = json.loads(files['pcre2.lock.json']) + require(provenance['sourceLockSha256'] == item['lockSha256'] and provenance['sourceFiles'] == source_lock['sourceFiles'] + and provenance['patches'] == source_lock['patches'] and provenance['fixedCves'] == item['fixedVulnerabilities'] + and provenance['debianPackagingInputSha256']['debian/copyright'] == item['copyrightSha256'], + 'PCRE2 native source/packaging proof changed') + records = provenance['runtimePackages']; selected = stage['packages'] + require([r['package'] for r in records] == item['runtimePackages'], 'Unexpected original PCRE2 runtime catalog') + actual = [p for p in manifest['packages'] if p['family'] == 'pcre2'] + require([p['name'] for p in actual] == item['runtimePackages'], 'Original manifest PCRE2 catalog mismatch') + for p, record, entry in zip(selected, records, actual): + require(record['package'] == entry['name'] == p['name'] and record['version'] == entry['version'] == p['version'] + and record['architecture'] == entry['architecture'] == 'amd64' and entry['source'] == 'pcre2' + and record['file'] == p['file'] and entry['archive'] == {'path': 'debs/' + p['file'], 'sha256': p['sha256']} + and record['sha256'] == p['sha256'], 'PCRE2 archive/provenance/original manifest mismatch') + files[p['file']] = contents['context/out/debs/' + p['file']] + require(sha(files[p['file']]) == p['sha256'], 'PCRE2 deb differs from the reviewed stage pin') + tests = check_owning_tests('pcre2', files, provenance, test_directory) + require(families[0]['owningChecks'] == tests, 'Original PCRE2 owning receipt was not qualified') + for name, path in PCRE2_STAGE_RECEIPTS.items(): files['production-stage-' + name] = contents[path] + files['production-stage-build.log'] = contents['publisher/pcre2-build.log'] + require(sha(files['production-stage-build.log']) == stage['buildLogSha256'] + and ('PCRE2_BACKPORT_BUILT packages=2 tests=3 lock=' + item['lockSha256']).encode() + in files['production-stage-build.log'], 'Original native PCRE2 build completion receipt is missing') + tests['originalPublisherScope'] = {'runConclusion': 'failure', 'jobConclusion': 'failure', + 'consumerConclusion': 'failure', 'publicationConclusion': 'skipped', + 'scope': 'PCRE2 source/package/owning-test qualification only'} + return files, provenance, tests + + def package_payload(archive, expected, directory): fields = {key: command(['dpkg-deb', '--field', str(archive), key]).decode().strip() for key in ['Package', 'Version', 'Architecture', 'Source']} @@ -559,30 +674,11 @@ def qualify(lock): families, packages, qualifications = [], [], [] for family in FAMILIES: if family == 'pcre2': - item = lock['pcre2']; directory = WORK / 'pcre2-export' / 'pcre2' - require(directory.is_dir(), 'Fresh production PCRE2 native output is missing') - files = {file.name: file.read_bytes() for file in directory.iterdir() if file.is_file()} - require(sha(files['pcre2.lock.json']) == item['lockSha256'] - and sha(files['COPYRIGHT']) == item['copyrightSha256'], 'PCRE2 production input/COPYRIGHT mismatch') - provenance = json.loads(files['provenance.json']) - require(provenance['sourceLockSha256'] == item['lockSha256'], 'PCRE2 native lock mismatch') - tests = check_owning_tests(family, files, provenance, WORK / 'owning-replay' / family) - selected = [dict(name=r['package'], version=r['version'], source='pcre2', file=r['file'], sha256=r['sha256']) - for r in provenance['runtimePackages']] - require([p['name'] for p in selected] == FAMILIES[family], 'Unexpected fresh PCRE2 runtime catalog') - require(all(p['version'] == item['version'] and re.fullmatch('[0-9a-f]{64}', p['sha256']) - for p in selected) and {name for name in files if name.endswith('.deb')} == - {p['file'] for p in selected}, 'Unexpected fresh PCRE2 version/package bytes') - source_lock = json.loads(files['pcre2.lock.json']) - require(provenance['sourceFiles'] == source_lock['sourceFiles'] - and provenance['patches'] == source_lock['patches'] - and provenance['fixedCves'] == item['fixedVulnerabilities'] - and provenance['debianPackagingInputSha256']['debian/copyright'] == item['copyrightSha256'], - 'Fresh PCRE2 source/packaging proof differs from approved producer') - evidence = {name: sha(data) for name, data in files.items() - if name.endswith(('.json', '.trs', '.log', '.buildinfo', '.changes', '.tsv', '.md')) - and name != 'build.log'} - evidence['COPYRIGHT'] = item['copyrightSha256'] + item = lock['pcre2'] + api = json.loads((WORK / 'inputs' / 'pcre2-api.json').read_text()) + files, provenance, tests = validate_pcre2(item, WORK / 'inputs' / 'pcre2.zip', api, + WORK / 'owning-replay' / family) + selected, evidence = item['stageInput']['packages'], pcre2_evidence(item['stageInput']) else: item = lock['nativeInputs'][family] api = json.loads((WORK / 'inputs' / (family + '-api.json')).read_text()) @@ -593,8 +689,8 @@ def qualify(lock): for name, digest in evidence.items(): require(safe_name(name).name == name and sha(files[name]) == digest, 'Unsafe/mutated output evidence') (destination / name).write_bytes(files[name]) + write_json(destination / 'artifact-api.json', api) if family != 'pcre2': - write_json(destination / 'artifact-api.json', api) raw = read_zip(WORK / 'inputs' / (family + '.zip'), item['zipSha256']) (destination / 'native-result.json').write_bytes(raw['export/result.json']) if family == 'rust-coreutils': diff --git a/server/security/ubuntu-backports/bundle/source-inputs.lock.json b/server/security/ubuntu-backports/bundle/source-inputs.lock.json index 1dfaa8eb01..3502e3135d 100644 --- a/server/security/ubuntu-backports/bundle/source-inputs.lock.json +++ b/server/security/ubuntu-backports/bundle/source-inputs.lock.json @@ -1,6 +1,13 @@ { "bundleVersion": "0.1.0", "consumerFixture": { + "gnuBaselineProviderSetup": { + "scope": "Disposable GNU fixture construction only; the backport installer must preserve the selected provider.", + "method": "Ubuntu documented coreutils-from-gnu transition, with explicit coreutils-from-uutils and rust-coreutils removal and the required --allow-remove-essential option.", + "officialDocumentation": "https://ubuntu.com/server/docs/reference/other-tools/sudo-rs/", + "gnuCoreutilsVersion": "9.7-3ubuntu2.1", + "versionSource": "server/security/ubuntu-security-packages.lock" + }, "baselineMainIndexEvidence": { "inReleaseSha256": "45f95ce276cdba3e41870516a130e03c58b8b7a79e9546b0efe9e526d255740c", "index": { @@ -613,7 +620,7 @@ "CVE-2026-89161", "CVE-2026-89162" ], - "kind": "fresh-production-target", + "kind": "qualified-production-stage", "lockSha256": "c9bddd7006761549e30843a83c097fa4019b1a58262755a1c2dc23e10ba0aeb5", "runtimePackages": [ "libpcre2-8-0", @@ -628,9 +635,86 @@ "server/security/ubuntu-security-packages.lock": "8e43275a0b7b13a2a5d825eea2b15c68ec75a27f50dd6c0f497aadba164c6704" }, "sourceTag": "v1.6.551", + "stageInput": { + "artifactId": 12574478015, + "artifactName": "ubuntu-security-backports-v0.1.0-321d2d9e326eafcbcc5b550f453c80aaa029dc3d-40130336365", + "buildLogSha256": "55ec792cc3ba93aa22e659ad35a19626c56c2d97c6029c8a1a3a5840addeba54", + "evidence": { + "COPYRIGHT": "e915cde4251f96ec6af107ecd2e194440a4e69440893b0c982802f8a1e4ae40a", + "RunGrepTest.log": "dc97e8c6438464025fffc1d844e3ab25f23a12d3e8aef667cd7f7f8a3ab0a719", + "RunGrepTest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229", + "RunTest.log": "857c6b1d1eafd0c813e9550048119123241e026132b7a03818859123ba16e3d7", + "RunTest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229", + "builder-packages.tsv": "f1225c1a65e414d42b170d3e86caf865f73c8357b7b9c31ac634ebd14ca469ac", + "manifest.tsv": "50dd0216082b4140d1a3352ec419fed9d2405849365dc4bdee2b336a54c01ca2", + "pcre2-10.49-SUPPORT-LIFECYCLE.md": "64a3e10be90d7b3c218d05f21c53aa2511b9100e01f98c77c199c473b33f1856", + "pcre2.lock.json": "c9bddd7006761549e30843a83c097fa4019b1a58262755a1c2dc23e10ba0aeb5", + "pcre2_10.46-1build1+pasturestack.1_amd64.buildinfo": "fffade5dd49f9570f15c36da86081645cb7d9eb9d45db9b3bad78a303634b998", + "pcre2_10.46-1build1+pasturestack.1_amd64.changes": "a5d2219f582c285489d6294e1723b228e4131322c063562a692706b73e9563d0", + "pcre2_jit_test.log": "6d6278bdb724b2b3c62415ee4c3ede9ddd7823522c477e9bb68c8ed396144341", + "pcre2_jit_test.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229", + "provenance.json": "44a8d639712557438bfb51c26822c91772d74463ad558e06147fbf06f2706c21" + }, + "jobConclusion": "failure", + "jobId": 120980581742, + "metadataSha256": { + "manifest.json": "8103b82ac0d712e631d8199cb69caf652dcb4739da954eaac86df5d7af3d2ef4", + "qualification.json": "0eba611048025af32a8832b7d073a5aa7367b8ec8109682641e4219931f78d70", + "source-inputs.lock.json": "db1ec64f796c7014b8e367f1f7fa11a80bed0c3aa77ea415a4701bb1f9d6921b" + }, + "packages": [ + { + "file": "libpcre2-8-0_10.46-1build1+pasturestack.1_amd64.deb", + "name": "libpcre2-8-0", + "sha256": "b15853575ae72e0cfe19358a5642d1fa54a52bad7c725253c229ed85211b6a1f", + "source": "pcre2", + "version": "10.46-1build1+pasturestack.1" + }, + { + "file": "libpcre2-posix3_10.46-1build1+pasturestack.1_amd64.deb", + "name": "libpcre2-posix3", + "sha256": "f90ae28d8b8fb3defc8636cb9de7770de75735c0d73241dc81e705fc8f73a7cb", + "source": "pcre2", + "version": "10.46-1build1+pasturestack.1" + } + ], + "publisherCommit": "321d2d9e326eafcbcc5b550f453c80aaa029dc3d", + "runConclusion": "failure", + "runId": 40130336365, + "scope": "Only the original released551 PCRE package bytes, native owning receipts and completed qualification stage are reused. The whole publisher job and consumer fixture failed; no image was published. Installer/consumer tests must run again.", + "sourceFiles": { + ".github/workflows/publish-ubuntu-security-backports.yml": "783d24812c2d9e30f9d34c23758404796be3093c8ce0b6a0f1e997705e1bb624", + "server/security/ubuntu-backports/bundle/qualify-native-packages.py": "8796eb0243fad227882c30a34b3eba6926eafe08d3984f8ead70b3e0135ccf29", + "server/security/ubuntu-backports/bundle/source-inputs.lock.json": "db1ec64f796c7014b8e367f1f7fa11a80bed0c3aa77ea415a4701bb1f9d6921b" + }, + "status": "pcre2-qualified-before-consumer-failure", + "stepOutcomes": [ + { + "conclusion": "success", + "name": "Build only the released PCRE2 producer and static installer", + "number": 8 + }, + { + "conclusion": "success", + "name": "Qualify package bytes, native receipts, payload metadata, SBOM and exact-package VEX", + "number": 9 + }, + { + "conclusion": "failure", + "name": "Require real installation in both isolated Ubuntu provider fixtures", + "number": 10 + }, + { + "conclusion": "skipped", + "name": "Publish the new build-only OCI data artifact", + "number": 11 + } + ], + "zipSha256": "8db99edc031b7476a954d4f5a57cd975015d6a5675ce691fda37c1c2d7825244" + }, "target": "runtime_security_packages", "version": "10.46-1build1+pasturestack.1", - "vexScope": "Fresh build of the exact released Server v1.6.551 runtime_security_packages producer and its pinned official release/pcre2-10.46 backports. All three original owning suites must PASS; exact generated runtime package hashes are in the bundle manifest." + "vexScope": "Exact PCRE package bytes and three original native owning suites from the released Server v1.6.551 runtime_security_packages producer, qualified in publisher run40130336365 before its consumer fixture failed. The failed whole run/job is preserved, not treated as consumer or publication PASS. Only pinned PCRE bytes/receipts are reused; current installer and real consumer gates remain mandatory." }, "platform": { "architecture": "amd64", diff --git a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py index 5dd81fe1de..f09e552869 100644 --- a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py +++ b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py @@ -31,6 +31,18 @@ def fixture_api(item, family='jq'): 'workflow_run': {'id': item['runId'], 'head_sha': item['sourceCommit']}}} +def pcre2_api(stage): + return {'run': {'id': stage['runId'], 'head_sha': stage['publisherCommit'], 'status': 'completed', + 'conclusion': 'failure', 'event': 'workflow_dispatch', + 'path': '.github/workflows/publish-ubuntu-security-backports.yml'}, + 'job': {'id': stage['jobId'], 'run_id': stage['runId'], 'name': 'publish', 'head_sha': stage['publisherCommit'], + 'status': 'completed', 'conclusion': 'failure', + 'steps': [dict(row, status='completed') for row in Q.PCRE2_STAGE_STEPS]}, + 'artifact': {'id': stage['artifactId'], 'name': stage['artifactName'], 'expired': False, + 'digest': 'sha256:' + stage['zipSha256'], + 'workflow_run': {'id': stage['runId'], 'head_sha': stage['publisherCommit']}}} + + class Boundaries(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory(prefix='bundle-unit-') @@ -58,7 +70,8 @@ def test_pending_glibc_is_not_publishable(self): Q.load_lock(ready=True) def test_missing_native_producer_is_fetched_by_exact_sha_without_changing_head(self): - commits = sorted({item['sourceCommit'] for item in self.lock['nativeInputs'].values()}) + commits = sorted({item['sourceCommit'] for item in self.lock['nativeInputs'].values()} | + {self.lock['pcre2']['stageInput']['publisherCommit']}) with patch.object(Q.subprocess, 'run', return_value=SimpleNamespace(returncode=0)) as run: Q.ensure_native_sources(self.lock) self.assertEqual(len(run.call_args_list), len(commits)) @@ -70,6 +83,34 @@ def test_missing_native_producer_is_fetched_by_exact_sha_without_changing_head(s fetches = [call.args[0] for call in run.call_args_list if call.args[0][1] != 'cat-file'] self.assertEqual(fetches, [['git', 'fetch', '--no-tags', 'origin', commits[0]]]) + def test_pcre2_input_keeps_failed_job_and_requires_exact_completed_stage_outcomes(self): + stage = self.lock['pcre2']['stageInput']; api = pcre2_api(stage) + Q.verify_pcre2_api(stage, **api) + for mutate in [lambda a: a['run'].update(conclusion='success'), + lambda a: a['job'].update(conclusion='success'), + lambda a: a['job']['steps'][0].update(conclusion='failure'), + lambda a: a['job']['steps'][1].update(conclusion='failure'), + lambda a: a['job']['steps'][2].update(conclusion='success'), + lambda a: a['job']['steps'][3].update(conclusion='success'), + lambda a: a['job']['steps'].pop(), + lambda a: a['artifact']['workflow_run'].update(head_sha='0' * 40)]: + altered = copy.deepcopy(api); mutate(altered) + with self.assertRaises(ValueError): Q.verify_pcre2_api(stage, **altered) + + def test_pcre2_input_has_no_fresh_build_fallback_or_recipe_waiver(self): + for mutate in [lambda x: x['pcre2'].update(kind='fresh-production-target'), + lambda x: x['pcre2']['stageInput']['sourceFiles'].pop('.github/workflows/publish-ubuntu-security-backports.yml'), + lambda x: x['pcre2']['stageInput']['stepOutcomes'][1].update(conclusion='failure')]: + lock = copy.deepcopy(self.lock); mutate(lock) + file = self.root / 'lock.json'; Q.write_json(file, lock) + with patch.object(Q, 'LOCK_FILE', file), self.assertRaises(ValueError): Q.load_lock() + workflow = (Q.ROOT / '.github/workflows/publish-ubuntu-security-backports.yml').read_text(encoding='utf-8') + self.assertNotIn('pcre2_bundle_inputs', workflow) + self.assertNotIn('pcre2-build.log', workflow) + self.assertIn('--check-pcre2-source', workflow) + self.assertIn('--target installer_export', workflow) + self.assertIn('go test -mod=readonly ./...', (HERE / 'Dockerfile.bundle').read_text(encoding='utf-8')) + def test_no_extra_runtime_family_or_deb(self): for mutate in [lambda x: x['nativeInputs'].update(tar={}), lambda x: x['nativeInputs']['jq']['runtimePackages'].append('libjq-dev')]: